diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml
index b8c96916..4face8e8 100644
--- a/.github/workflows/docker-build-push.yml
+++ b/.github/workflows/docker-build-push.yml
@@ -81,8 +81,10 @@ jobs:
# VITE_* values are baked into the bundle at build time. VITE_POWERGRID_SIMU
# is the same-origin base the frontend posts "apply action" to; without it the
# POST lands on / and nginx answers 405. Blank lines are ignored by the action.
+ # Secrets must NOT be passed here: a build-arg ends up readable in the public
+ # bundle. The cognitive API token is a runtime env var on the container instead
+ # (COGNITIVE_TOKEN, injected into the nginx proxy by start-webui.sh).
build-args: |
- ${{ matrix.service == 'cab-standalone-frontend' && format('VITE_COGNITIVE_TOKEN={0}', secrets.VITE_COGNITIVE_TOKEN) || '' }}
${{ matrix.service == 'cab-standalone-frontend' && 'VITE_POWERGRID_SIMU=/powergrid-simu' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max
diff --git a/.gitignore b/.gitignore
index ae6f754d..286604b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -197,4 +197,5 @@ go.sh
# Frontend
!frontend/.vscode
!frontend/env/
-!frontend/env/.env
\ No newline at end of file
+!frontend/env/.env
+hmisurveys/
\ No newline at end of file
diff --git a/.secrets.tmp b/.secrets.tmp
new file mode 100644
index 00000000..e69de29b
diff --git a/HANDOVER.md b/HANDOVER.md
new file mode 100644
index 00000000..1452547b
--- /dev/null
+++ b/HANDOVER.md
@@ -0,0 +1,66 @@
+# Railway Use Case — Deployment Handover
+
+## What was built
+A Flatland railway dispatcher training tool integrated into InteractiveAI (SystemX).
+Two scenarios (Kreuzungskonflikt, Fahrt auf Sichtweite) with Recommendation and Co-Learning modes.
+
+## New services (not in original docker-compose)
+
+### 1. Flask Railway Brain (`railway-brain`)
+- **Source:** `usecases_examples/Railway/`
+- **Dockerfile:** `usecases_examples/Railway/Railway.Dockerfile`
+- **Port:** 5001
+- **Runs:** Flatland simulation + scenario player + card publishing
+
+### 2. ZWL Angular Frontend (`zwl-frontend`)
+- **Source:** `flatland-hmi-hack4rail/frontend/`
+- **Dockerfile:** `flatland-hmi-hack4rail/zwl.Dockerfile`
+- **Port:** 4200
+- **Shows:** Kartenansicht (map) + ZWL Diagramm (Marey)
+
+## To deploy on server
+
+1. Copy `.env.example` → `.env` and fill in:
+ ```
+ VITE_RAILWAY_SIMU=http://:5001
+ RL_AGENT_API_URL=http://railway-brain:5001/recommendations
+ ```
+
+2. Run:
+ ```bash
+ docker compose \
+ -f config/dev/cab-standalone/docker-compose.yml \
+ -f config/dev/cab-standalone/docker-compose-railway.yml \
+ up --build
+ ```
+
+3. On first start, run the MongoDB perimeter init manually (timing issue with auto-init):
+ ```bash
+ docker exec cab-standalone-mongodb-1 mongo operator-fabric \
+ -u root -p password --authenticationDatabase admin \
+ /docker-entrypoint-initdb.d/01-cabprocess.js
+ ```
+
+## Decisions for SystemX developer
+
+- **Port 5001 public access:** Flask brain must be reachable from the browser.
+ Options: expose port directly, or proxy via nginx at `/railway-api/`.
+ If proxied, update all `BACKEND_URL` in the Angular services + Vue frontend.
+
+- **Authentication:** `AUTH_DISABLED=true` is set everywhere.
+ Enable auth when deploying to production.
+
+- **ZWL build output path:** Check `angular.json` `outputPath` — the Dockerfile
+ assumes `dist/frontend/browser`. Adjust if different.
+
+- **Maps volume:** `usecases_examples/Railway/maps/` contains JSON map files
+ needed at runtime. Mount as volume or bake into Docker image.
+
+## Files changed from original InteractiveAI repo
+- `frontend/src/entities/Railway/CAB/` — all CAB Vue components
+- `usecases_examples/Railway/app.py` — Flask brain (heavily extended)
+- `usecases_examples/Railway/ScenarioPlayer.py` — new file
+- `usecases_examples/Railway/FlatlandMapLoader.py` — new file
+- `usecases_examples/Railway/ExperimentLogger.py` — new file
+- `experiment_scenarios/` — new directory with scenario definitions
+- `flatland-hmi-hack4rail/frontend/src/app/` — ZWL Angular components
diff --git a/README.md b/README.md
index 4f9394b1..c7a76dfb 100644
--- a/README.md
+++ b/README.md
@@ -84,11 +84,15 @@ Key variables (see `.secrets.example` for all options and per-environment values
- `VITE_POWERGRID_SIMU` — the frontend's simulator endpoint. Use the same-origin proxy
value `/powergrid-simu` (avoids CORS); set it to `false` to disable the PowerGrid UI.
`VITE_RAILWAY_SIMU` / `VITE_ATM_SIMU` are the equivalents for the other use cases.
-- `POWERGRID_SIMU_UPSTREAM` — where nginx actually forwards `/powergrid-simu/`. This is the
- only value that changes per environment:
+- `POWERGRID_SIMU_UPSTREAM` — where nginx actually forwards `/powergrid-simu/`:
- Local dev : `http://host.docker.internal:5122/` (simulator container on the host)
- LAN : `http://192.168.208.61:5100/`
- - Public/k8s: handled by `nginx-kubernetes.conf` via the helm chart (not this variable)
+ - Public/k8s: same variable, set as an env var on the **frontend pod** (see
+ `deploy-chart/values.ovh.yaml`).
+- `COGNITIVE_TOKEN` — bearer token for the INESCTEC cognitive API. nginx attaches it to
+ every `/cognitive-api/` request, so the frontend never sees it. It used to be
+ `VITE_COGNITIVE_TOKEN`, a build-time value inlined into the public JS bundle; that meant
+ any visitor could read it and rotating it required a full image rebuild.
- `RL_AGENT_API_URL` / `RL_AGENT_API_TOKEN` — the deep expert agent that powers PowerGrid
recommendations (see [The PowerGrid expert agent API](#the-powergrid-expert-agent-api) below to
install it). A token is required in every mode:
@@ -104,6 +108,52 @@ Key variables (see `.secrets.example` for all options and per-environment values
> in [InteractiveAI/usecases_examples/PowerGrid/](/usecases_examples/PowerGrid/README.md).
>
>
+### How runtime nginx configuration works
+
+`POWERGRID_SIMU_UPSTREAM` and `COGNITIVE_TOKEN` are **runtime** values, not
+build-time ones. They appear in the nginx config as `__NAME__` placeholders, and
+`frontend/start-webui.sh` substitutes them from the matching env var when the container
+starts. Changing one is: update the env var (or the k8s secret) and restart the
+frontend — no image rebuild.
+
+To add another: give it a default in `start-webui.sh`, append its name to `SUBST_VARS`, and
+use `__NAME__` in the config. If a placeholder survives substitution the container exits
+with the name of the missing variable, and the generated config is checked with `nginx -t`
+before the daemon starts — so a misconfiguration fails loudly at startup instead of
+producing a silently broken proxy.
+
+`REQUIRED_VARS` (space- or comma-separated) lists the variables that must be **non-empty**;
+an empty one aborts startup. It is opt-in because an absent value is not always wrong —
+local dev runs the whole stack with no cognitive token and just loses that panel — whereas
+on a public deploy an empty token means nginx sends `Bearer ` with nothing after it and
+every `/cognitive-api/` call 401s while the pod still reports itself healthy.
+`deploy-chart/values.ovh.yaml` therefore sets `REQUIRED_VARS=COGNITIVE_TOKEN`, so the pod
+crashloops with the reason in its log and k8s keeps the previous pod serving.
+
+Two ordering rules follow from all of this, and breaking the first is what silently broke
+`/cognitive-api/` once already:
+
+- **Never push a conf ahead of the pod that has to substitute it.** A placeholder the
+ running image does not know is left in the config *literally* and goes out in the proxied
+ request. `deploy-chart/apply-nginx-conf.sh` now refuses to push in that case: it checks
+ every `__NAME__` in the conf against the deployment's env, and resolves `secretKeyRef`s
+ to confirm the secret and key actually exist.
+- **Verify the config nginx loaded, not the ConfigMap.** nginx runs with an explicit
+ `-c /personal-conf/nginx.conf`; a bare `nginx -T` re-reads `/etc/nginx/nginx.conf` and the
+ raw ConfigMap mount, where `proxy_pass __POWERGRID_SIMU_UPSTREAM__;` is not a valid URL —
+ so it exits non-zero and prints nothing, which reads as a missing location.
+
+Two things to keep in mind:
+
+- **In k8s the config does not come from the image.** The `cab-assistant-platform-config`
+ ConfigMap is mounted over `/etc/nginx/conf.d` and **overrides** the `default.conf` baked
+ into the image, so every placeholder and every `location` must be present in the ConfigMap
+ too (`deploy-chart/apply-nginx-conf.sh` pushes just that key). A missing
+ `/powergrid-simu/` location, for instance, lets the apply POST fall through to the static
+ `location /`, and nginx answers 405.
+- **nginx reads `conf.d` only at startup**, so restart the frontend after any change:
+ `kubectl -n cab rollout restart deploy/cab-frontend`.
+
2. **Run InteractiveAI assistant**
```sh
cd config/dev/cab-standalone
@@ -219,3 +269,85 @@ Contributions to the InteractiveAI Assistant Platform are welcome! To contribute
# Docs
A postman collection is under docs/postman_collections.
You can also check the openapi through the URL http://localhost:[Service port]/docs
+
+
+---
+
+## Railway Use Case (FHNW / AI4REALNET)
+
+The Railway use case adds a Flatland train simulation with interactive scenario-based dispatcher training. It requires **two additional services** beyond the main Docker stack.
+
+> See also `HANDOVER.md` for deployment decisions.
+
+### Additional Prerequisites
+
+- **Python 3.10** (exact version required for `flatland-rl`)
+- **Node.js 18+**
+
+### Step-by-step Local Setup
+
+After completing the standard InteractiveAI setup above, add `VITE_RAILWAY_SIMU=http://localhost:5001` to your `.secrets` file, then:
+
+**1. Install Python dependencies (first time only)**
+```bash
+cd usecases_examples/Railway
+python3.10 -m venv .venv
+source .venv/bin/activate # Linux/Mac
+# .venv\Scripts\activate # Windows
+pip install -r requirements.txt
+cd ../..
+```
+
+**2. Start Flask Railway brain** *(new terminal)*
+```bash
+cd usecases_examples/Railway
+source .venv/bin/activate
+python app.py
+```
+Ready when: `Running on http://0.0.0.0:5001`
+
+**3. Start ZWL Angular frontend** *(new terminal)*
+```bash
+cd flatland-hmi-hack4rail/frontend
+npm install # first time only
+npm start
+```
+Ready when: `Local: http://localhost:4200`
+
+**4. MongoDB perimeter setup** *(required after every Docker restart)*
+```bash
+docker exec cab-standalone-mongodb-1 mongo operator-fabric \
+ -u root -p password --authenticationDatabase admin \
+ --eval 'db.perimeter.updateOne({_id:"cabProcess"},{$set:{process:"cabProcess",stateRights:[{state:"messageState",right:"ReceiveAndWrite"}]}},{upsert:true}); db.group.updateOne({_id:"Planner"},{$addToSet:{perimeters:"cabProcess"}}); db.group.updateOne({_id:"Dispatcher"},{$addToSet:{perimeters:"cabProcess"}}); print("done")'
+```
+
+Open `http://localhost:3200/cab/Railway` and log in as `railway_user` / `test`.
+
+### Scenarios
+
+| ID | Name | Description |
+|----|------|-------------|
+| `scenario1` | Kreuzungskonflikt | Single-track crossing conflict |
+| `scenario2` | Fahrt auf Sichtweite | Speed restriction causing dispatch conflict |
+| `scenario3` | Zugreihenfolge | Multiple delays disrupting train order |
+
+### Server Deployment (Docker)
+
+Dockerfiles are provided to containerise Flask and the ZWL frontend:
+
+```bash
+docker compose \
+ -f config/dev/cab-standalone/docker-compose.yml \
+ -f config/dev/cab-standalone/docker-compose-railway.yml \
+ up --build
+```
+
+Set `VITE_RAILWAY_SIMU=http://:5001` in `.secrets` before building. See `HANDOVER.md` for open deployment decisions (port exposure, auth, nginx proxy).
+
+### Troubleshooting
+
+| Problem | Solution |
+|---------|----------|
+| Kartenansicht: "cannot connect to localhost:4200" | ZWL Angular not running — run step 3 |
+| No train data | Flask not running — run step 2 |
+| No notification cards | Re-run step 4 (MongoDB command) |
diff --git a/backend/recommendation-service/resources/Railway/manager.py b/backend/recommendation-service/resources/Railway/manager.py
index 687472ec..372eab46 100644
--- a/backend/recommendation-service/resources/Railway/manager.py
+++ b/backend/recommendation-service/resources/Railway/manager.py
@@ -1,85 +1,51 @@
# backend/recommendation-service/resources/Railway/manager.py
-import json
-from api.manager.base_manager import BaseRecommendationManager
-from .mockRecommendations.mockRecommendations import RECOMMENDATION_CATALOG
-from .sncf_recommender import SNCF_RECO3, SNCF_deontic, SNCF_risk, SNCF_risk_tie_break
-
-import logging
-logger = logging.getLogger(__name__)
+import os
+import requests
+from api.manager.base_manager import BaseRecommendationManager
+from settings import logger
class RailwayManager(BaseRecommendationManager):
def __init__(self):
+ # URL of our Flask brain's /recommendations endpoint
+ # Set RL_AGENT_API_URL in .env to point at the Flask brain
+ self.agent_api_url = os.environ.get(
+ "RL_AGENT_API_URL",
+ "http://host.docker.internal:5001/recommendations",
+ )
+ self.agent_api_token = os.environ.get("RL_AGENT_API_TOKEN", "")
super().__init__()
- def _transform_recommendation(self, reco_json):
- """Transform a recommendation from catalog format to API output format."""
- reco = json.loads(reco_json)
- return {
- "title": reco["data"]["title"],
- "description": reco["data"]["description"],
- "use_case": reco["data"]["use_case"],
- "agent_type": reco["data"]["agent_type"],
- "actions": [{}],
- "kpis": reco["data"]["kpis"],
- }
-
def get_recommendation(self, request_data):
"""
- Return recommendations for a Railway event.
-
- Supports four modes controlled by request_data["event"]["mode"]:
-
- - "basic" (default): best-first ordering via SNCF_RECO3
- - "deontic": filter by KPI threshold, then sort ascending via SNCF_deontic
- requires: sort_type ("passengers"|"delay"|"cost"|"total_cost")
- threshold_value (int, or delay string e.g. "1h30" for delay)
- - "risk": sort all recommendations by KPI ascending via SNCF_risk
- requires: sort_type
- - "risk_tie_break": sort by primary KPI, break ties with secondary via SNCF_risk_tie_break
- requires: sort_type
- optional: tie_breaker (same values as sort_type)
+ Calls our Flask brain's /recommendations endpoint and returns
+ the result in the format InteractiveAI expects.
"""
- event_data = request_data.get("event", {})
- context_data = request_data.get("context", {})
-
- # Ensure id_event has a fallback so catalog lookup always has a key
- event_for_sncf = {**event_data, "id_event": str(event_data.get("id_event", "1"))}
-
- # Wrap into the structure expected by SNCF functions
- event_json = json.dumps({"data": event_for_sncf})
- context_json = json.dumps({"data": context_data})
-
- mode = event_data.get("mode", "deontic")
- logger.info(f"Railway recommendation — event_id: {event_for_sncf['id_event']}, mode: {mode}")
-
- if mode == "deontic":
- sort_type = event_data.get("sort_type", "cost")
- threshold_value = event_data.get("threshold_value")
- recommendations = SNCF_deontic(
- event_json, context_json, RECOMMENDATION_CATALOG,
- type=sort_type, threshold_value=threshold_value,
- )
- elif mode == "risk":
- sort_type = event_data.get("sort_type", "cost")
- recommendations = SNCF_risk(
- event_json, context_json, RECOMMENDATION_CATALOG,
- type=sort_type,
+ headers = {"Content-Type": "application/json"}
+ if self.agent_api_token:
+ headers["Authorization"] = "Bearer " + self.agent_api_token
+
+ try:
+ response = requests.post(
+ self.agent_api_url,
+ json=request_data,
+ headers=headers,
+ timeout=10,
+ verify=False,
)
- elif mode == "risk_tie_break":
- sort_type = event_data.get("sort_type", "cost")
- tie_breaker = event_data.get("tie_breaker")
- recommendations = SNCF_risk_tie_break(
- event_json, context_json, RECOMMENDATION_CATALOG,
- type=sort_type, tie_breaker=tie_breaker,
- )
- else: # "basic" or any unrecognised mode
- recommendations = SNCF_RECO3(event_json, context_json, RECOMMENDATION_CATALOG)
-
- # Surface catalog errors as an empty list rather than crashing downstream
- if recommendations and isinstance(recommendations[0], dict) and "error" in recommendations[0]:
- logger.error(f"Recommendation error: {recommendations[0]['error']}")
- return []
-
- return [self._transform_recommendation(reco) for reco in recommendations]
+ recommendations = response.json()
+ logger.info("Railway recommendations received: " + str(len(recommendations)))
+ return recommendations
+
+ except Exception as e:
+ logger.error("Failed to get Railway recommendations: " + str(e))
+ # Return a fallback so the UI doesn't break
+ return [{
+ "title": "No recommendations available",
+ "description": "Could not reach the simulation brain. Please try again.",
+ "use_case": "Railway",
+ "agent_type": "AI",
+ "actions": [{}],
+ "kpis": {},
+ }]
diff --git a/config/dev/cab-standalone/.env.example b/config/dev/cab-standalone/.env.example
index 825583ac..f969d319 100644
--- a/config/dev/cab-standalone/.env.example
+++ b/config/dev/cab-standalone/.env.example
@@ -23,6 +23,13 @@ RL_AGENT_API_TOKEN=
# not from this variable.)
POWERGRID_SIMU_UPSTREAM=http://host.docker.internal:5122/
+# Bearer token for the INESCTEC cognitive API, injected into the nginx /cognitive-api/
+# proxy at container start. Runtime, not build-time: restart the container to change it.
+COGNITIVE_TOKEN=
+# Leaving it empty is fine here: the stack starts and only the cognitive panel is
+# unavailable. Public deploys set REQUIRED_VARS=COGNITIVE_TOKEN instead, which makes
+# an empty value abort container start rather than serve an unauthenticated proxy.
+
# VITE build-time variables (also required in frontend/env/.env.local for local dev)
VITE_POWERGRID_SIMU=false
VITE_ATM_SIMU=false
diff --git a/config/dev/cab-standalone/.secrets.example b/config/dev/cab-standalone/.secrets.example
index 2b2d2668..3ca7c1dc 100644
--- a/config/dev/cab-standalone/.secrets.example
+++ b/config/dev/cab-standalone/.secrets.example
@@ -20,7 +20,14 @@ export RL_AGENT_API_TOKEN=
# export RL_AGENT_API_TOKEN=
export VITE_POWERGRID_SIMU=/powergrid-simu
-export VITE_COGNITIVE_TOKEN=
+
+# Bearer token for the INESCTEC cognitive API. Consumed at RUNTIME by the frontend
+# container: start-webui.sh injects it into the nginx /cognitive-api/ proxy, so it stays
+# out of the JS bundle. Changing it needs a container restart, not a rebuild.
+export COGNITIVE_TOKEN=
+# Leaving it empty is fine here: the stack starts and only the cognitive panel is
+# unavailable. Public deploys set REQUIRED_VARS=COGNITIVE_TOKEN instead, which makes
+# an empty value abort container start rather than serve an unauthenticated proxy.
# PowerGrid simulator upstream for the frontend's /powergrid-simu/ proxy.
# Local dev : http://host.docker.internal:5122/ (default)
diff --git a/config/dev/cab-standalone/docker-compose-railway.yml b/config/dev/cab-standalone/docker-compose-railway.yml
new file mode 100644
index 00000000..24d4d973
--- /dev/null
+++ b/config/dev/cab-standalone/docker-compose-railway.yml
@@ -0,0 +1,51 @@
+# docker-compose override — Railway use case services
+# Usage: docker compose -f config/dev/cab-standalone/docker-compose.yml \
+# -f config/dev/cab-standalone/docker-compose-railway.yml up
+
+version: '3.5'
+services:
+
+ # Flask Railway brain (app.py)
+ railway-brain:
+ container_name: railway-brain
+ build:
+ context: ../../../usecases_examples/Railway
+ dockerfile: Railway.Dockerfile
+ restart: unless-stopped
+ ports:
+ - '5001:5001' # exposed so browser can reach it directly
+ volumes:
+ - railway_logs:/app/experiment_logs
+ - railway_maps:/app/maps
+ environment:
+ - PYTHONUNBUFFERED=1
+ # Cards-publication reachable via Docker network
+ depends_on:
+ - cards-publication
+
+ # ZWL Angular frontend (Kartenansicht + Marey diagram)
+ zwl-frontend:
+ container_name: zwl-frontend
+ build:
+ context: ../../../flatland-hmi-hack4rail
+ dockerfile: zwl.Dockerfile
+ args:
+ # Must be reachable FROM THE BROWSER — use server public IP/hostname
+ # Local dev: http://localhost:5001
+ # Deployment: http://:5001
+ RAILWAY_SIMU_URL: ${VITE_RAILWAY_SIMU:-http://localhost:5001}
+ restart: unless-stopped
+ ports:
+ - '4200:80' # same port as local npm start
+
+ # MongoDB with auto-perimeter init for cabProcess cards
+ mongodb:
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: root
+ MONGO_INITDB_ROOT_PASSWORD: password
+ volumes:
+ - ../../../config/dev/cab-standalone/mongo-init.js:/docker-entrypoint-initdb.d/01-cabprocess.js:ro
+
+volumes:
+ railway_logs:
+ railway_maps:
diff --git a/config/dev/cab-standalone/docker-compose.sh b/config/dev/cab-standalone/docker-compose.sh
index 6bb0ef20..2826d66a 100755
--- a/config/dev/cab-standalone/docker-compose.sh
+++ b/config/dev/cab-standalone/docker-compose.sh
@@ -52,7 +52,11 @@ echo "RL_AGENT_API_URL=${RL_AGENT_API_URL:-https://interactiveagent.passerelle.i
echo "RL_AGENT_API_TOKEN=${RL_AGENT_API_TOKEN:-}" >> .env
echo "VITE_POWERGRID_SIMU=${VITE_POWERGRID_SIMU:-/powergrid-simu}" >> .env
echo "POWERGRID_SIMU_UPSTREAM=${POWERGRID_SIMU_UPSTREAM:-http://host.docker.internal:5122/}" >> .env
-echo "VITE_COGNITIVE_TOKEN=${VITE_COGNITIVE_TOKEN:-}" >> .env
+echo "COGNITIVE_TOKEN=${COGNITIVE_TOKEN:-}" >> .env
+
+# Echo the generated .env for confidence, with credential values masked: this file now
+# carries RL_AGENT_API_TOKEN and COGNITIVE_TOKEN, and a plain `cat` put both in the
+# terminal (and in any CI log that runs this script).
+sed -E 's/^([A-Z_]*(TOKEN|SECRET|PASSWORD)[A-Z_]*)=(.+)$/\1=/; s/^([A-Z_]*(TOKEN|SECRET|PASSWORD)[A-Z_]*)=$/\1=/' .env
-cat .env
docker compose up -d
diff --git a/config/dev/cab-standalone/docker-compose.yml b/config/dev/cab-standalone/docker-compose.yml
index a3137056..077f4b77 100644
--- a/config/dev/cab-standalone/docker-compose.yml
+++ b/config/dev/cab-standalone/docker-compose.yml
@@ -138,7 +138,6 @@ services:
VITE_POWERGRID_SIMU: ${VITE_POWERGRID_SIMU}
VITE_ATM_SIMU: ${VITE_ATM_SIMU}
VITE_RAILWAY_SIMU: ${VITE_RAILWAY_SIMU}
- VITE_COGNITIVE_TOKEN: ${VITE_COGNITIVE_TOKEN}
restart: unless-stopped
# Lets nginx proxy /powergrid-simu/ to a simulator running on the host (local dev).
extra_hosts:
@@ -150,6 +149,9 @@ services:
# PowerGrid simulator upstream for the /powergrid-simu/ nginx proxy:
# local -> http://host.docker.internal:5122/ LAN -> http://192.168.208.61:5100/
- POWERGRID_SIMU_UPSTREAM=${POWERGRID_SIMU_UPSTREAM}
+ # Injected into the nginx /cognitive-api/ proxy at container start, so the token
+ # is never baked into the public JS bundle. Restart the container to change it.
+ - COGNITIVE_TOKEN=${COGNITIVE_TOKEN}
volumes:
- './ui-config:/usr/share/nginx/html/opfab'
- './nginx-cors-permissive.conf:/etc/nginx/conf.d/default.conf'
diff --git a/config/dev/cab-standalone/mongo-init.js b/config/dev/cab-standalone/mongo-init.js
new file mode 100644
index 00000000..4a82621f
--- /dev/null
+++ b/config/dev/cab-standalone/mongo-init.js
@@ -0,0 +1,30 @@
+// MongoDB init script — runs once when the container is first created
+// Sets up the cabProcess perimeter so Railway notification cards work
+
+db = db.getSiblingDB('operator-fabric');
+
+db.perimeter.updateOne(
+ { _id: 'cabProcess' },
+ { $set: {
+ process: 'cabProcess',
+ stateRights: [{ state: 'messageState', right: 'ReceiveAndWrite' }]
+ }},
+ { upsert: true }
+);
+
+db.group.updateOne(
+ { _id: 'Planner' },
+ { $addToSet: { perimeters: 'cabProcess' } }
+);
+
+db.group.updateOne(
+ { _id: 'Dispatcher' },
+ { $addToSet: { perimeters: 'cabProcess' } }
+);
+
+db.group.updateOne(
+ { _id: 'ReadOnly' },
+ { $addToSet: { perimeters: 'cabProcess' } }
+);
+
+print('cabProcess perimeter initialized');
diff --git a/config/dev/cab-standalone/nginx-cors-permissive.conf b/config/dev/cab-standalone/nginx-cors-permissive.conf
index 2b4dc8bd..f99158f4 100644
--- a/config/dev/cab-standalone/nginx-cors-permissive.conf
+++ b/config/dev/cab-standalone/nginx-cors-permissive.conf
@@ -372,8 +372,11 @@ server {
return 204;
}
+ # The bearer token is attached here, not by the browser: __COGNITIVE_TOKEN__ is
+ # substituted from $COGNITIVE_TOKEN at container start (see start-webui.sh).
proxy_set_header Host wesenss.inesctec.pt;
proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header Authorization "Bearer __COGNITIVE_TOKEN__";
proxy_ssl_server_name on;
proxy_ssl_verify off;
proxy_pass https://wesenss.inesctec.pt/api/v1/;
diff --git a/config/dev/cab-standalone/nginx-kubernetes.conf b/config/dev/cab-standalone/nginx-kubernetes.conf
index b0a3e022..11f75cc3 100644
--- a/config/dev/cab-standalone/nginx-kubernetes.conf
+++ b/config/dev/cab-standalone/nginx-kubernetes.conf
@@ -205,6 +205,23 @@ server {
proxy_set_header X-Forwarded-For $remote_addr;
}
+ # PowerGrid (grid2op) simulator "apply action" proxy. Keeps the browser POST
+ # same-origin (no CORS): the bundle is built with VITE_POWERGRID_SIMU=/powergrid-simu
+ # and posts to /powergrid-simu/api/v1/recommendations. Without this location the POST
+ # falls through to the static `location /` and nginx answers 405.
+ # __POWERGRID_SIMU_UPSTREAM__ is substituted at container start by start-webui.sh from
+ # the POWERGRID_SIMU_UPSTREAM env var of the frontend pod, e.g.
+ # https://interactivepowergrid.passerelle.irt-systemx.fr/
+ # No explicit Host header: nginx defaults it to $proxy_host (the authority from
+ # proxy_pass), which is what an ip:port upstream AND a vhost upstream both need.
+ # Forwarding $http_host instead would make the passerelle route to the wrong vhost.
+ location /powergrid-simu/ {
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_ssl_server_name on;
+ proxy_ssl_verify off;
+ proxy_pass __POWERGRID_SIMU_UPSTREAM__;
+ }
+
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
diff --git a/deploy-chart/apply-nginx-conf.sh b/deploy-chart/apply-nginx-conf.sh
new file mode 100755
index 00000000..71851087
--- /dev/null
+++ b/deploy-chart/apply-nginx-conf.sh
@@ -0,0 +1,221 @@
+#!/usr/bin/env bash
+# Push ONLY the nginx.conf key of configmap-assistant-platform.yaml to the live cluster,
+# then restart the frontend so nginx re-reads it (nginx reads conf.d at startup only).
+#
+# Why a patch and not `kubectl apply -f configmap-assistant-platform.yaml`: that file is a
+# dump of the whole ConfigMap (web-ui.json, users.yml, ...). Applying it wholesale would
+# also revert any of those keys that drifted in the cluster since the dump was taken.
+#
+# Usage: ./deploy-chart/apply-nginx-conf.sh [namespace] [deployment]
+set -euo pipefail
+
+NS="${1:-cab}"
+DEPLOY="${2:-cab-frontend}"
+CM=cab-assistant-platform-config
+SRC="$(dirname "$0")/configmap-assistant-platform.yaml"
+
+command -v kubectl >/dev/null || { echo "kubectl not found" >&2; exit 1; }
+[ -f "$SRC" ] || { echo "missing $SRC" >&2; exit 1; }
+
+# Repo -> JSON merge patch carrying just the one key.
+PATCH=$(python3 - "$SRC" <<'PY'
+import json, sys
+try:
+ import yaml
+except ImportError:
+ sys.exit('PyYAML missing on this machine: pip install pyyaml')
+
+path = sys.argv[1]
+doc = yaml.safe_load(open(path))
+if not isinstance(doc, dict) or not isinstance(doc.get('data'), dict):
+ sys.exit(
+ f'{path} is not a ConfigMap manifest with a data: mapping '
+ f'(parsed as {type(doc).__name__}). Is this file up to date? '
+ 'It must be the version carrying the /powergrid-simu/ block.'
+ )
+conf = doc['data'].get('nginx.conf')
+if not conf:
+ sys.exit(f'{path} has no data."nginx.conf" key; found: {sorted(doc["data"])}')
+if '/powergrid-simu/' not in conf:
+ sys.exit(
+ f'{path} nginx.conf has no /powergrid-simu/ location - this copy predates the fix. '
+ 'Pull the latest revision of the repo.'
+ )
+if '__COGNITIVE_TOKEN__' not in conf:
+ sys.exit(
+ f'{path} nginx.conf does not inject __COGNITIVE_TOKEN__ into /cognitive-api/ - this '
+ 'copy predates the move of the token out of the frontend bundle. Pull the latest '
+ 'revision of the repo.'
+ )
+print(json.dumps({'data': {'nginx.conf': conf}}))
+PY
+)
+
+echo "--- diff (live -> repo) for $NS/$CM nginx.conf"
+kubectl -n "$NS" get cm "$CM" -o jsonpath='{.data.nginx\.conf}' > /tmp/nginx.conf.live || true
+python3 -c "import json,sys;sys.stdout.write(json.loads(sys.argv[1])['data']['nginx.conf'])" "$PATCH" > /tmp/nginx.conf.repo
+diff -u /tmp/nginx.conf.live /tmp/nginx.conf.repo || true
+
+# ---------------------------------------------------------------------------
+# Pre-flight: never push a conf ahead of the pod that has to substitute it.
+#
+# Every __NAME__ placeholder in the conf is substituted at container start by
+# start-webui.sh from the matching env var on the pod. Pushing a conf whose
+# placeholders the running deployment cannot satisfy breaks the proxy silently,
+# and how it breaks depends on the image:
+# - an image whose start-webui.sh knows the name but gets no value substitutes
+# EMPTY - "Bearer " with nothing after it;
+# - an older image that never heard of the name leaves the placeholder to go out
+# LITERALLY in the proxied request.
+# nginx starts cleanly either way, so nothing surfaces it but a 401 in the browser.
+#
+# This is exactly how /cognitive-api/ broke: the ConfigMap gained __COGNITIVE_TOKEN__
+# while the pod still ran an image that only knew POWERGRID_SIMU_UPSTREAM, and
+# proxy_set_header replaced the real token the bundle was still sending with the
+# literal string "__COGNITIVE_TOKEN__". Set ALLOW_MISSING_ENV=1 to push anyway.
+# ---------------------------------------------------------------------------
+echo "--- pre-flight: placeholders in the conf vs env on deploy/$DEPLOY"
+
+placeholders=$(grep -o '__[A-Z_][A-Z_]*__' /tmp/nginx.conf.repo | sort -u || true)
+if [ -z "$placeholders" ]; then
+ echo " (none)"
+fi
+
+env_names=$(kubectl -n "$NS" get "deploy/$DEPLOY" \
+ -o go-template='{{range .spec.template.spec.containers}}{{range .env}}{{.name}}{{"\n"}}{{end}}{{end}}')
+
+problems=""
+for ph in $placeholders; do
+ name=${ph#__}; name=${name%__}
+
+ if ! printf '%s\n' "$env_names" | grep -qx "$name"; then
+ problems="${problems} ${name}: no env var of that name on deploy/${DEPLOY}"$'\n'
+ echo " $name: MISSING from the deployment"
+ continue
+ fi
+
+ # Declared is not the same as resolvable: a secretKeyRef to a secret or key that does
+ # not exist leaves the pod in CreateContainerConfigError, and the OLD pod keeps serving.
+ ref=$(kubectl -n "$NS" get "deploy/$DEPLOY" -o go-template="{{range .spec.template.spec.containers}}{{range .env}}{{if eq .name \"${name}\"}}{{with .valueFrom}}{{with .secretKeyRef}}{{.name}}/{{.key}}{{end}}{{end}}{{end}}{{end}}{{end}}")
+
+ if [ -n "$ref" ]; then
+ sname=${ref%%/*}; skey=${ref##*/}
+ if ! keys=$(kubectl -n "$NS" get secret "$sname" -o go-template='{{range $k,$v := .data}}{{$k}}{{"\n"}}{{end}}' 2>/dev/null); then
+ problems="${problems} ${name}: secretKeyRef -> secret '${sname}' does not exist in namespace ${NS}"$'\n'
+ echo " $name: <- secret $sname/$skey (SECRET NOT FOUND)"
+ elif ! printf '%s\n' "$keys" | grep -qx "$skey"; then
+ problems="${problems} ${name}: secret '${sname}' exists but has no key '${skey}' (keys: $(printf '%s ' $keys))"$'\n'
+ echo " $name: <- secret $sname/$skey (KEY NOT FOUND)"
+ else
+ echo " $name: <- secret $sname/$skey (ok)"
+ fi
+ continue
+ fi
+
+ # A literal value. Never echo one that is named like a credential.
+ val=$(kubectl -n "$NS" get "deploy/$DEPLOY" -o go-template="{{range .spec.template.spec.containers}}{{range .env}}{{if eq .name \"${name}\"}}{{.value}}{{end}}{{end}}{{end}}")
+ case "$name" in
+ *TOKEN*|*SECRET*|*PASSWORD*) echo " $name: set inline (${#val} chars)" ;;
+ *) echo " $name: $val" ;;
+ esac
+done
+
+if [ -n "$problems" ]; then
+ echo >&2
+ echo "REFUSING to push: the conf carries placeholders this deployment cannot substitute." >&2
+ printf '%s' "$problems" >&2
+ echo "Fix the deployment first (add the env var, or create the secret/key), then re-run." >&2
+ echo "Pushing now would leave nginx serving a silently broken proxy - it starts fine and" >&2
+ echo "the failure only shows up as a 401 from the upstream." >&2
+ if [ "${ALLOW_MISSING_ENV:-0}" != "1" ]; then
+ echo "Override with ALLOW_MISSING_ENV=1 if you really mean to." >&2
+ exit 1
+ fi
+ echo "ALLOW_MISSING_ENV=1 - continuing anyway." >&2
+fi
+
+echo
+read -r -p "Apply to $NS/$CM and restart $DEPLOY? [y/N] " ans
+case "$ans" in
+ y|Y|yes|YES|Yes) ;;
+ *) echo "ABORTED - nothing was applied."; exit 0 ;;
+esac
+
+kubectl -n "$NS" patch cm "$CM" --type merge -p "$PATCH"
+kubectl -n "$NS" rollout restart "deploy/$DEPLOY"
+kubectl -n "$NS" rollout status "deploy/$DEPLOY" --timeout=180s
+
+# ---------------------------------------------------------------------------
+# Verify against the config nginx ACTUALLY loaded.
+#
+# start-webui.sh runs `nginx -c /personal-conf/nginx.conf`, and that tree is built at
+# startup from the ConfigMap with the placeholders substituted. A bare `nginx -T` ignores
+# it and re-reads the default /etc/nginx/nginx.conf, which pulls in the raw ConfigMap
+# mount - where `proxy_pass __POWERGRID_SIMU_UPSTREAM__;` is not a valid URL. nginx then
+# exits non-zero and prints no dump at all, which reads as "the location is missing".
+# That false negative is why this script reported FAILED on a healthy pod.
+# ---------------------------------------------------------------------------
+NGINX_CONF=/personal-conf/nginx.conf
+
+# Target one Running pod, not deploy/... : during a rollout that can select the pod being
+# terminated and report on the config we just replaced.
+SELECTOR=$(kubectl -n "$NS" get "deploy/$DEPLOY" \
+ -o go-template='{{range $k,$v := .spec.selector.matchLabels}}{{$k}}={{$v}},{{end}}' | sed 's/,$//')
+POD=$(kubectl -n "$NS" get pods -l "$SELECTOR" --field-selector=status.phase=Running \
+ --sort-by=.metadata.creationTimestamp -o name | tail -1)
+
+if [ -z "$POD" ]; then
+ echo "FAILED - no Running pod matching '$SELECTOR' in namespace $NS." >&2
+ kubectl -n "$NS" get pods -l "$SELECTOR" >&2
+ exit 1
+fi
+echo "--- verify: the config loaded by $POD ($NGINX_CONF)"
+
+# Keep stderr: if nginx rejects the config, its reason is the whole point.
+if ! dump=$(kubectl -n "$NS" exec "$POD" -- nginx -T -c "$NGINX_CONF" 2>&1); then
+ echo "FAILED - nginx could not dump $NGINX_CONF in $POD:" >&2
+ printf '%s\n' "$dump" >&2
+ exit 1
+fi
+
+rc=0
+
+for loc in /powergrid-simu/ /cognitive-api/; do
+ if printf '%s\n' "$dump" | grep -q "location $loc"; then
+ echo "OK - location $loc is live."
+ else
+ echo "FAIL - location $loc is NOT in the running config." >&2
+ rc=1
+ fi
+done
+
+# The failure this script exists to catch: a placeholder that reached the running config
+# unsubstituted. It means the image predates the variable, or the env var never arrived.
+leftover=$(printf '%s\n' "$dump" | grep -o '__[A-Z_][A-Z_]*__' | sort -u || true)
+if [ -n "$leftover" ]; then
+ echo "FAIL - unsubstituted placeholders in the RUNNING config:" >&2
+ printf ' %s\n' $leftover >&2
+ echo " The pod's image cannot substitute these. Check that its start-webui.sh lists them" >&2
+ echo " in SUBST_VARS - an image older than the conf is the usual cause:" >&2
+ kubectl -n "$NS" get "$POD" -o jsonpath='{.spec.containers[*].image}{"\n"}' >&2
+ rc=1
+else
+ echo "OK - no unsubstituted placeholders in the running config."
+fi
+
+# Never print the dump itself - it now carries the substituted token.
+echo "--- /cognitive-api/ as loaded (token redacted):"
+printf '%s\n' "$dump" | grep -A8 'location /cognitive-api/' |
+ sed -E 's/(Authorization "Bearer )[^"]*/\1/'
+
+if [ "$rc" -ne 0 ]; then
+ echo >&2
+ echo " ConfigMap occurrences of powergrid-simu in the cluster:" >&2
+ kubectl -n "$NS" get cm "$CM" -o jsonpath='{.data.nginx\.conf}' | grep -c powergrid-simu >&2 || true
+ echo " (0 above = the patch did not stick, e.g. ArgoCD self-heal reverted it)" >&2
+ echo " Pods (a crashlooping new pod leaves the OLD one serving):" >&2
+ kubectl -n "$NS" get pods -l "$SELECTOR" >&2
+ exit 1
+fi
+
+echo "--- all checks passed."
diff --git a/deploy-chart/configmap-assistant-platform.yaml b/deploy-chart/configmap-assistant-platform.yaml
index 3a06da54..6ba86d93 100644
--- a/deploy-chart/configmap-assistant-platform.yaml
+++ b/deploy-chart/configmap-assistant-platform.yaml
@@ -265,27 +265,38 @@ data:
# PowerGrid (grid2op) simulator "apply action" proxy. Keeps the browser POST
# same-origin (no CORS) — the frontend is built with
# VITE_POWERGRID_SIMU=/powergrid-simu and posts to /powergrid-simu/api/v1/recommendations.
- # Host must be the upstream vhost, not $http_host: the passerelle routes by hostname.
+ # The proxy_pass placeholder below is substituted at container start by start-webui.sh
+ # from the POWERGRID_SIMU_UPSTREAM env var of the frontend pod.
+ # No explicit Host header: nginx defaults it to $proxy_host (the authority from
+ # proxy_pass), so the passerelle gets the right vhost whatever the upstream is.
+ # Forwarding $http_host instead would make it route to the wrong vhost.
location /powergrid-simu/ {
add_header Cache-Control "no-cache";
- proxy_set_header Host interactivepowergrid.passerelle.irt-systemx.fr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_ssl_server_name on;
proxy_ssl_verify off;
- proxy_pass https://interactivepowergrid.passerelle.irt-systemx.fr/;
+ proxy_pass __POWERGRID_SIMU_UPSTREAM__;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
+ # Proxy for the INESCTEC cognitive API (avoids browser CORS restrictions).
+ # The bearer token is attached here, not by the browser: the placeholder on the
+ # Authorization line is substituted at container start by start-webui.sh from the
+ # COGNITIVE_TOKEN env var of the frontend pod, so the token stays out of the public
+ # JS bundle and is rotated by updating the secret and restarting the deployment.
+ # The placeholder is deliberately not named in this comment: substitution is a
+ # plain sed, so naming it would write the token into the comment as well.
location /cognitive-api/ {
- add_header Cache-Control "no-cache";
- proxy_set_header Host wesenss.inesctec.pt;
- proxy_set_header X-Fordwarded-For $remote_addr;
- proxy_ssl_server_name on;
- proxy_ssl_verify off;
- proxy_pass https://wesenss.inesctec.pt/api/v1/;
+ add_header Cache-Control "no-cache";
+ proxy_set_header Host wesenss.inesctec.pt;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header Authorization "Bearer __COGNITIVE_TOKEN__";
+ proxy_ssl_server_name on;
+ proxy_ssl_verify off;
+ proxy_pass https://wesenss.inesctec.pt/api/v1/;
}
}
ui-menu.json: |
@@ -726,11 +737,8 @@ kind: ConfigMap
metadata:
annotations:
argocd.argoproj.io/tracking-id: cab:/ConfigMap:cab/cab-assistant-platform-config
- kubectl.kubernetes.io/last-applied-configuration: |
- {"apiVersion":"v1","data":{"businessconfig.yml":"spring:\n application:\n name: businessconfig\noperatorfabric.businessconfig:\n storage:\n path: \"/businessconfig-storage\"\n","cards-consultation.yml":"spring:\n application:\n name: cards-consultation\ncheckIfUserIsAlreadyConnected: true\n","cards-publication.yml":"spring:\n application:\n name: cards-publication\n deserializer:\n value:\n delegate:\n class: io.confluent.kafka.serializers.KafkaAvroDeserializer\n serializer:\n value:\n delegate:\n class: io.confluent.kafka.serializers.KafkaAvroSerializer\nopfab:\n kafka:\n topics:\n card:\n topicname: opfab\n response-card:\n topicname: opfab-response\n schema:\n registry:\n url: http://localhost:8081\ncheckAuthenticationForCardSending: false\n","common.yml":"management:\n endpoints:\n web:\n exposure:\n include: '*'\nspring:\n rabbitmq:\n host: cab-rabbitmq.cab.svc.cluster.local\n port: 5672\n username: ${RABBITMQ_USERNAME}\n password: ${RABBITMQ_PASSWORD}\n security:\n provider-url: https://keycloak.irtsysx.fr/auth\n oauth2:\n resourceserver:\n jwt:\n jwk-set-uri: ${spring.security.provider-url}/realms/interactiveai/protocol/openid-connect/certs\n data:\n mongodb:\n database: ${MONGODB_DB}\n uri: mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@cab-mongodb.cab.svc.cluster.local:27017/${MONGODB_DB}?authSource=admin\u0026authMode=scram-sha1\nserver:\n forward-headers-strategy: framework\noperatorfabric:\n servicesUrls:\n users: \"http://cab-users.cab.svc.cluster.local:8080\"\n businessconfig: \"http://cab-businessconfig.cab.svc.cluster.local:8080\"\n","nginx.conf":"# docker-compose DNS used to resolved users service\n# resolver 127.0.0.11 ipv6=off;\n\n# Log format to have msec in time + request processing time\nmap \"$time_local:$msec\" $time_local_ms {\n ~(^\\S+)(\\s+\\S+):\\d+\\.(\\d+)$ $1.$3$2;\n}\nlog_format opfab-log '$remote_addr - $time_local_ms'\n'\"$request\" $status $request_time $body_bytes_sent ';\n\nlog_format upstreamlog '[$time_local] $remote_addr - $remote_user - $server_name $host to: $upstream_addr: $request $status upstream_response_time $upstream_response_time msec $msec request_time $request_time';\n\nserver {\n listen 8080;\n server_name localhost demo.interactiveai.irt-systemx.fr;\n error_log /var/log/nginx/error.log debug;\n access_log /var/log/nginx/access.log opfab-log;\n\n ### CUSTOMIZATION - BEGIN\n # Url of the Authentication provider\n set $KeycloakBaseUrl \"https://keycloak.irtsysx.fr\";\n # Realm associated to OperatorFabric within the Authentication provider\n set $OperatorFabricRealm \"interactiveai\";\n # base64 encoded pair of authentication in the form of 'client-id:secret-id'\n set $ClientPairOFAuthentication \"b3BmYWItY2xpZW50Oklxdkh4VzBEdENDNXVMVEVTcFhyN0sxdkhtSHViOGdE\" ;\n\n ### CUSTOMIZATION - END\n\n ### OPFAB GENERIC CONFIGURATION ###\n ### BE CAREFUL WHEN MODIFYING ###\n set $BasicValue \"Basic $ClientPairOFAuthentication\";\n set $KeycloakOpenIdConnect $KeycloakBaseUrl/auth/realms/$OperatorFabricRealm/protocol/openid-connect;\n gzip on;\n gzip_types application/javascript text/css;\n\n location / {\n add_header Cache-Control \"no-cache\";\n alias /usr/share/nginx/html/;\n try_files $uri $uri/ /index.html;\n }\n location = /external/ {\n add_header Cache-Control \"no-cache\";\n alias /usr/share/nginx/html/external/;\n index index.html index.htm;\n }\n location /ui/ {\n add_header Cache-Control \"no-cache\";\n alias /usr/share/nginx/html/;\n index index.html index.htm;\n }\n location /auth/check_token {\n add_header Cache-Control \"no-cache\";\n proxy_set_header Host keycloak.irtsysx.fr;\n proxy_set_header Authorization $BasicValue ;\n proxy_pass $KeycloakOpenIdConnect/token/introspect;\n }\n location /auth/token {\n add_header Cache-Control \"no-cache\";\n proxy_set_header Host keycloak.irtsysx.fr;\n proxy_set_header Authorization $BasicValue ;\n proxy_pass $KeycloakOpenIdConnect/token;\n }\n location /auth/code/ {\n add_header Cache-Control \"no-cache\";\n proxy_set_header Host keycloak.irtsysx.fr;\n proxy_set_header Authorization $BasicValue ;\n proxy_pass $KeycloakOpenIdConnect/auth?response_type=code\u0026client_id=opfab-client\u0026$args;\n }\n\n location /auth {\n add_header Cache-Control \"no-cache\";\n proxy_set_header X-Forwarded-For $proxy_protocol_addr;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header Host keycloak.irtsysx.fr;\n proxy_pass $KeycloakBaseUrl/auth;\n }\n\n # To be sure new files are downloaded when version change\n # we set no-cache for json config files and for i18n files\n location /config/web-ui.json {\n add_header Cache-Control \"no-cache\";\n alias /usr/share/nginx/html/opfab/web-ui.json;\n }\n location /config/ui-menu.json {\n add_header Cache-Control \"no-cache\";\n alias /usr/share/nginx/html/opfab/ui-menu.json;\n }\n location /ui/assets/i18n/ {\n add_header Cache-Control \"no-cache\";\n alias /usr/share/nginx/html/assets/i18n/;\n }\n\n location /businessconfig {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-businessconfig.cab.svc.cluster.local:8080;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location ~ \"^/users/internal/(.*)\" {\n return 404;\n }\n\n location ~ \"^/users/(.*)\" {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-users.cab.svc.cluster.local:8080/$1$is_args$args;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /users {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-users.cab.svc.cluster.local:8080/users;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /perimeters {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-users.cab.svc.cluster.local:8080/perimeters;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /cards/ {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cardsconsultation.cab.svc.cluster.local:8080/;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n ### !!!! SECURITY WARNING !!!!\n ### The following configuration is suitable only if you set checkAuthenticationForCardSending to true\n ### which is the default configuration\n ###\n\n location /cardspub/cards {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cardspublication.cab.svc.cluster.local:8080/cards;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n ### if you set checkAuthenticationForCardSending to false\n ### you MUST not permit to access cards endpoint via nginx\n ### and replace the previous configuration by the following conf\n\n #location /cardspub/cards/user {\n # proxy_pass http://cards-publication:8080/cards/user;\n # proxy_set_header X-Forwarded-For $remote_addr;\n #}\n #location /cardspub/cards/translateCardField {\n # proxy_pass http://cards-publication:8080/cards/translateCardField;\n # proxy_set_header X-Forwarded-For $remote_addr;\n #}\n ###\n ### !!! END SECURITY WARNING !!!\n location /archives {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cardsconsultation.cab.svc.cluster.local:8080;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n location /externaldevices {\n add_header Cache-Control \"no-cache\";\n set $externaldevices http://external-devices.cab.svc.cluster.local:8080;\n proxy_pass $externaldevices;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n location ~ \"^/externaldevices/(.*)\" {\n add_header Cache-Control \"no-cache\";\n set $externaldevices http://external-devices.cab.svc.cluster.local:8080;\n proxy_pass $externaldevices/$1;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /cabcontext/ {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cabcontext.cab.svc.cluster.local:5000/;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /cab_event/ {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cabevent.cab.svc.cluster.local:5000/;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /cab_recommendation/ {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cabrecommendation.cab.svc.cluster.local:5000/;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /cabhistoric/ {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cabhistoric.cab.svc.cluster.local:5000/;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n location /cab_capitalization/ {\n add_header Cache-Control \"no-cache\";\n proxy_pass http://cab-cabcapitalization.cab.svc.cluster.local:5000/;\n proxy_set_header X-Forwarded-For $remote_addr;\n }\n\n error_page 500 502 503 504 /50x.html;\n location = /50x.html {\n root /usr/share/nginx/html;\n }\n}\n","ui-menu.json":"{\n \"coreMenusConfiguration\":\n [\n {\n \"id\": \"feed\",\n \"visible\": true\n },\n {\n \"id\": \"archives\",\n \"visible\": true\n },\n {\n \"id\": \"monitoring\",\n \"visible\": true\n },\n {\n \"id\": \"logging\",\n \"visible\": true\n },\n {\n \"id\": \"usercard\",\n \"visible\": true\n },\n {\n \"id\": \"calendar\",\n \"visible\": true\n },\n {\n \"id\": \"admin\",\n \"visible\": true,\n \"showOnlyForGroups\": [\"ADMIN\"]\n },\n {\n \"id\": \"settings\",\n \"visible\": true\n },\n {\n \"id\": \"activityarea\",\n \"visible\": true\n },\n {\n \"id\": \"feedconfiguration\",\n \"visible\": true\n },\n {\n \"id\": \"realtimeusers\",\n \"visible\": true\n },\n {\n \"id\": \"externaldevicesconfiguration\",\n \"visible\": true,\n \"showOnlyForGroups\": [\"ADMIN\"]\n },\n {\n \"id\": \"nightdaymode\",\n \"visible\": true\n },\n {\n \"id\": \"about\",\n \"visible\": true\n },\n {\n \"id\": \"changepassword\",\n \"visible\": true\n },\n {\n \"id\": \"logout\",\n \"visible\": true\n }\n ],\n \"menus\": [\n {\n \"id\": \"menu1\",\n \"label\": \"title.single\",\n \"entries\": [\n {\n \"id\": \"uid_test_0\",\n \"url\": \"https://en.wikipedia.org/w/index.php\",\n \"label\": \"entry.single\",\n \"linkType\": \"BOTH\"\n }\n ]\n },\n {\n \"id\": \"menu2\",\n \"label\": \"title.multi\",\n \"entries\": [\n {\n \"id\": \"uid_test_1\",\n \"url\": \"https://opfab.github.io/\",\n \"label\": \"entry.entry1\",\n \"linkType\": \"BOTH\",\n \"showOnlyForGroups\": [\"ReadOnly\",\"Planner\"]\n },\n {\n \"id\": \"uid_test_2\",\n \"url\": \"https://www.wikipedia.org/\",\n \"label\": \"entry.entry2\",\n \"linkType\": \"BOTH\",\n \"showOnlyForGroups\": [\"Dispatcher\"]\n },\n {\n \"id\": \"uid_test_3\",\n \"url\": \"http://localhost:2002/external/appExample/\",\n \"label\": \"entry.entry3\",\n \"linkType\": \"BOTH\"\n }\n ]\n },\n {\n \"id\": \"adminmenu\",\n \"label\": \"title.admin\",\n \"entries\": [\n {\n \"id\": \"uid_test_3\",\n \"url\": \"https://opfab.github.io/\",\n \"label\": \"entry.admin\",\n \"linkType\": \"BOTH\",\n \"showOnlyForGroups\": [\"ADMIN\"]\n }\n ]\n }\n ],\n \"locales\": [\n {\n \"language\": \"en\",\n \"i18n\": {\n \"menu1\": {\n \"title\": {\n \"single\": \"First menu\"\n },\n \"entry\": {\n \"single\": \"Single menu entry\"\n }\n },\n \"menu2\": {\n \"title\": {\n \"multi\": \"Second menu\"\n },\n \"entry\": {\n \"entry1\": \"First menu entry\",\n \"entry2\": \"Second menu entry\",\n \"entry3\": \"External application\"\n }\n },\n \"adminmenu\": {\n \"title\": {\n \"admin\": \"Admin menu\"\n },\n \"entry\": {\n \"admin\": \"Admin menu entry\"\n }\n }\n }\n },\n {\n \"language\": \"fr\",\n \"i18n\": {\n \"menu1\": {\n \"title\": {\n \"single\": \"Premier menu\"\n },\n \"entry\": {\n \"single\": \"Unique élément\"\n }\n },\n \"menu2\": {\n \"title\": {\n \"multi\": \"Deuxième menu\"\n },\n \"entry\": {\n \"entry1\": \"Premier élément\",\n \"entry2\": \"Deuxième élément\",\n \"entry3\": \"Application externe\"\n }\n },\n \"adminmenu\": {\n \"title\": {\n \"admin\": \"Admin menu\"\n },\n \"entry\": {\n \"admin\": \"Admin menu entry\"\n }\n }\n }\n },\n {\n \"language\": \"nl\",\n \"i18n\": {\n \"menu1\": {\n \"title\": {\n \"single\": \"Eerste menu\"\n },\n \"entry\": {\n \"single\": \"Enkel menu-item\"\n }\n },\n \"menu2\": {\n \"title\": {\n \"multi\": \"Tweede menu\"\n },\n \"entry\": {\n \"entry1\": \"Eerste menu-item\",\n \"entry2\": \"Tweede menu-item\",\n \"entry3\": \"TExterne applicatie\"\n }\n },\n \"adminmenu\": {\n \"title\": {\n \"admin\": \"Admin menu\"\n },\n \"entry\": {\n \"admin\": \"Admin menu-item\"\n }\n }\n }\n }\n ]\n}\n","users.yml":"\n# POPULATE THE USER DATABASE ON INIT\n# !!!! WARNING: VALUES SHOULD BE CHANGED FOR PRODUCTION MODE !!!!\nspring:\n application:\n name: users\noperatorfabric.users.default:\n users:\n - login: admin\n groups: [\"ADMIN\"]\n entities: [\"Railway\", \"ATM\", \"PowerGrid\"]\n - login: ilyes\n firstname : Ilyes\n lastname : KAANICH\n groups: [\"Dispatcher\",\"ReadOnly\"]\n entities: [\"IRT_MAIN\"]\n - login: railway_user\n groups: [\"Planner\", \"ReadOnly\"]\n entities: [\"Railway\"]\n - login: orange_user\n groups: [ \"PowerGrid\",\"ADMIN\",\"ReadOnly\",\"Dispatcher\"]\n entities: [ \"ORANGE\" ]\n - login: atm_user\n groups: [\"ReadOnly\",\"Dispatcher\"]\n entities: [\"ATM\"]\n - login: PowerGrid_user\n groups: [ \"PowerGrid\",\"ADMIN\",\"ReadOnly\",\"Dispatcher\"]\n entities: [ \"PowerGrid\" ]\n groups:\n - id: ADMIN\n name: ADMINISTRATORS\n description: The admin group\n - id: PowerGrid\n name: RTE France\n description: RTE TSO Group\n realtime: false\n - id: Dispatcher\n name: Dispatcher\n description: Dispatcher Group\n realtime: true\n - id: Planner\n name: Planner\n description: Planner Group\n realtime: true\n - id: Supervisor\n name: Supervisor\n description: Supervisor Group\n realtime: true\n - id: Manager\n name: Manager\n description: Manager Group\n realtime: false\n - id: ReadOnly\n name: ReadOnly\n description: ReadOnly Group\n realtime: false\n entities:\n - id: Railway\n name: National society of French railroads\n description: National society of French railroads\n parents : [\"IRT_MAIN\"]\n - id: ORANGE\n name: Orange\n description: Orange\n parents : [\"IRT_MAIN\"]\n - id: ATM\n name: ATM usecase\n description: ATM usecase\n parents : [\"IRT_MAIN\"]\n - id: PowerGrid\n name: Electricity Transmission Network\n description: Electricity Transmission Network\n parents : [\"IRT_MAIN\"]\n - id: IRT_MAIN\n name: IRT Control Centers\n description: IRT Control Centers\n entityAllowedToSendCard: false\n","web-ui.json":"{\n \"environmentName\": \"KUBERNETES ENV\",\n \"environmentColor\": \"blue\",\n \"checkIfUrlIsLocked\": true,\n \"showUserEntitiesOnTopRightOfTheScreen\": true,\n \"externalDevicesEnabled\": true,\n \"selectActivityAreaOnLogin\": false,\n \"archive\": {\n \"filters\": {\n \"page\": {\n \"size\": [\n \"10\"\n ]\n },\n \"tags\": {\n \"list\": [\n {\n \"label\": \"Label for tag 1\",\n \"value\": \"tag1\"\n },\n {\n \"label\": \"Label for tag 2\",\n \"value\": \"tag2\"\n }\n ]\n }\n }\n },\n \"logging\": {\n \"filters\": {\n \"tags\": {\n \"list\": [\n {\n \"label\": \"Label for tag 1\",\n \"value\": \"tag1\"\n }\n ]\n }\n }\n },\n \"feed\": {\n \"defaultSorting\": \"unread\",\n \"defaultAcknowledgmentFilter\": \"notack\",\n \"card\": {\n \"hideTimeFilter\": false,\n \"time\": {\n \"display\": \"BUSINESS\"\n },\n \"hideResponseFilter\": false,\n \"hideApplyFiltersToTimeLineChoice\": false,\n \"secondsBeforeLttdForClockDisplay\": 3700,\n \"hideAckAllCardsFeature\": false,\n \"titleUpperCase\": true\n },\n \"timeline\": {\n \"domains\": [\n \"TR\",\n \"J\",\n \"7D\",\n \"W\",\n \"M\",\n \"Y\"\n ]\n },\n \"geomap\": {\n \"enableMap\": false,\n \"defaultDataProjection\": \"EPSG:4326\",\n \"initialLongitude\": 5.3255,\n \"initialLatitude\": 52.1845,\n \"initialZoom\": 6,\n \"zoomLevelWhenZoomToLocation\": 14,\n \"maxZoom\": 11,\n \"zoomDuration\": 500\n },\n \"enableGroupedCards\": false\n },\n \"i18n\": {\n \"supported\": {\n \"locales\": [\n \"en\",\n \"fr\",\n \"nl\"\n ]\n }\n },\n \"security\": {\n \"jwt\": {\n \"expire-claim\": \"exp\",\n \"login-claim\": \"preferred_username\"\n },\n \"logout-url\": \"https://keycloak.irtsysx.fr/auth/realms/interactiveai/protocol/openid-connect/logout?redirect_uri=https://demo.interactiveai.irt-systemx.fr/\",\n \"oauth2\": {\n \"client-id\": \"opfab-client\",\n \"flow\": {\n \"delegate-url\": \"https://keycloak.irtsysx.fr/auth/realms/interactiveai/protocol/openid-connect/auth?response_type=code\u0026client_id=opfab-client\",\n \"mode\": \"PASSWORD\",\n \"provider\": \"Opfab Keycloak\"\n }\n },\n \"provider-realm\": \"interactiveai\",\n \"provider-url\": \"https://keycloak.irtsysx.fr/auth\",\n \"changePasswordUrl\": \"https://keycloak.irtsysx.fr/auth/realms/interactiveai/account/#/security/signingin\"\n },\n \"settings\": {\n \"locale\": \"en\",\n \"dateTimeFormat\": \"HH:mm DD/MM/YYYY\",\n \"dateFormat\": \"DD/MM/YYYY\",\n \"styleWhenNightDayModeDesactivated\": \"NIGHT\",\n \"replayInterval\" : 10,\n \"replayEnabled\" : true\n\n },\n \"settingsScreen\": {\n \"hiddenSettings\": [\"description\"]\n },\n \"about\": {\n \"firstapplication\": {\n \"name\": \"First application\",\n \"rank\": 1,\n \"version\": \"v12.34.56\"\n },\n \"keycloack\": {\n \"name\": \"Keycloak\",\n \"rank\": 2,\n \"version\": \"6.0.1\"\n },\n \"lastapplication\": {\n \"name\": \"Wonderful Solution\",\n \"version\": \"0.1.2-RELEASE\"\n }\n },\n \"usercard\": {\n \"useDescriptionFieldForEntityList\": false\n }\n}\n"},"kind":"ConfigMap","metadata":{"annotations":{"argocd.argoproj.io/tracking-id":"cab:/ConfigMap:cab/cab-assistant-platform-config"},"labels":{"app.kubernetes.io/instance":"cab","app.kubernetes.io/managed-by":"Helm","app.kubernetes.io/name":"interactiveai-platform","app.kubernetes.io/version":"0.0.1","helm.sh/chart":"interactiveai-platform-0.2.2"},"name":"cab-assistant-platform-config","namespace":"cab"}}
meta.helm.sh/release-name: cab
meta.helm.sh/release-namespace: cab
- creationTimestamp: "2026-01-30T10:53:28Z"
labels:
app.kubernetes.io/instance: cab
app.kubernetes.io/managed-by: Helm
@@ -739,5 +747,3 @@ metadata:
helm.sh/chart: interactiveai-platform-0.2.2
name: cab-assistant-platform-config
namespace: cab
- resourceVersion: "21095247065"
- uid: 96bf811c-fdd7-453e-a97e-4179f061c469
\ No newline at end of file
diff --git a/deploy-chart/values.ovh.yaml b/deploy-chart/values.ovh.yaml
index 9da2074b..1011af9a 100644
--- a/deploy-chart/values.ovh.yaml
+++ b/deploy-chart/values.ovh.yaml
@@ -2,25 +2,25 @@ cabcontext:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-context
pullPolicy: Always
- tag: "1.3.7"
+ tag: "1.4.1"
cabevent:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-event
pullPolicy: Always
- tag: "1.3.7"
+ tag: "1.4.1"
cabhistoric:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-historic
pullPolicy: Always
- tag: "1.3.7"
+ tag: "1.4.1"
cabrecommendation:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-recommendation
pullPolicy: Always
- tag: "1.3.7"
+ tag: "1.4.1"
extraEnv:
- name: RL_AGENT_API_URL
value: "https://interactiveagent.passerelle.irt-systemx.fr/api/v1/recommendation"
@@ -34,20 +34,29 @@ cabcapitalization:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-capitalization
pullPolicy: Always
- tag: "1.3.7"
+ tag: "1.4.1"
frontend:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-standalone-frontend
pullPolicy: Always
- tag: "1.3.7"
+ tag: "1.4.1"
extraEnv:
- name: VITE_POWERGRID_SIMU
value: "/powergrid-simu"
- name: POWERGRID_SIMU_UPSTREAM
value: "https://interactivepowergrid.passerelle.irt-systemx.fr/"
- - name: VITE_COGNITIVE_TOKEN
+ # Read at container start by start-webui.sh and injected into the nginx
+ # /cognitive-api/ proxy. Rotating it is: update the secret, then
+ # `kubectl -n cab rollout restart deploy/cab-frontend` (nginx reads conf.d once,
+ # at startup). No image rebuild - it is no longer a build-time VITE_* value.
+ - name: COGNITIVE_TOKEN
valueFrom:
secretKeyRef:
name: cab-frontend
- key: vite-cognitive-token
+ key: cognitive-token
+ # Makes an empty COGNITIVE_TOKEN fatal at container start instead of letting nginx
+ # serve /cognitive-api/ with an empty bearer token - which looks healthy and 401s.
+ # The pod crashloops, k8s keeps the previous one serving, and the reason is in the log.
+ - name: REQUIRED_VARS
+ value: "COGNITIVE_TOKEN"
diff --git a/flatland-hmi-hack4rail/.gitignore b/flatland-hmi-hack4rail/.gitignore
new file mode 100644
index 00000000..d5a42098
--- /dev/null
+++ b/flatland-hmi-hack4rail/.gitignore
@@ -0,0 +1,3 @@
+
+# node_modules directory
+node_modules
\ No newline at end of file
diff --git a/flatland-hmi-hack4rail/.prettierrc.json b/flatland-hmi-hack4rail/.prettierrc.json
new file mode 100644
index 00000000..780b1bb7
--- /dev/null
+++ b/flatland-hmi-hack4rail/.prettierrc.json
@@ -0,0 +1,20 @@
+{
+ "braceStyle": "1tbs",
+ "bracketSameLine": false,
+ "bracketSpacing": true,
+ "phpVersion": "8.1",
+ "printWidth": 120,
+ "proseWrap": "preserve",
+ "semi": false,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "useTabs": false,
+ "overrides": [
+ {
+ "files": "*.html",
+ "options": {
+ "parser": "angular"
+ }
+ }
+ ]
+}
diff --git a/flatland-hmi-hack4rail/LICENSE b/flatland-hmi-hack4rail/LICENSE
new file mode 100644
index 00000000..712805b5
--- /dev/null
+++ b/flatland-hmi-hack4rail/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Flatland Association
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/flatland-hmi-hack4rail/README.md b/flatland-hmi-hack4rail/README.md
new file mode 100644
index 00000000..9db736a0
--- /dev/null
+++ b/flatland-hmi-hack4rail/README.md
@@ -0,0 +1,56 @@
+# Flatland HMI
+
+
+
+A simple prototype demonstrating how to create a Human-Machine Interface (HMI) that can interact with a Flatland simulation environment. This repository showcases the integration between a web-based frontend and a Python backend running Flatland railway simulations.
+
+## Demo
+
+https://github.com/user-attachments/assets/6cec2f96-a897-462d-9cb6-9f79216a0436
+
+
+## Overview
+
+This project consists of:
+
+- **Frontend**: An Angular application that provides a visual interface for viewing and controlling Flatland simulations
+- **Backend**: A FastAPI server that manages the Flatland environment and exposes REST APIs for interaction
+
+The HMI allows users to visualize railway networks, observe train movements, and control the simulation through step-by-step execution or continuous playback. The interface provides comprehensive information about actual train runs through real-time tracking and historical data visualization. Additionally, it displays alternative route variants that can be selected when critical decisions need to be made, such as during train malfunctions, equipment failures, or unexpected delays.
+
+## Quick Start
+
+### Backend
+
+```bash
+cd backend
+pip install -r requirements.txt
+uvicorn main:app --reload
+```
+
+### Frontend
+
+```bash
+cd frontend
+npm install
+npm run start
+```
+
+Open your browser and navigate to `http://localhost:4200` to interact with the Flatland simulation.
+
+## Features
+
+- Real-time visualization of railway environments with train movement tracking
+- Interactive Marey diagram showing train trajectories over time and distance
+- Train movement tracking with directional indicators and malfunction detection
+- Dynamic route planning with selectable variants for decision support
+- Interactive simulation controls (step, play, pause, reset)
+- Alternative route selection interface for handling disruptions and malfunctions
+- Multiple policy implementations (random, deadlock avoidance)
+- RESTful API for environment interaction
+
+## Technologies
+
+- **Frontend**: Angular, TypeScript, SCSS
+- **Backend**: FastAPI, Python, Flatland-RL
+- **Communication**: HTTP REST APIs with CORS support
diff --git a/flatland-hmi-hack4rail/backend/.gitignore b/flatland-hmi-hack4rail/backend/.gitignore
new file mode 100644
index 00000000..7a8c1b76
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/.gitignore
@@ -0,0 +1,11 @@
+
+# Compiled output
+__pycache__
+
+# Python
+/venv
+
+# Trajectory output
+/env_states
+
+.mypy_cache
\ No newline at end of file
diff --git a/flatland-hmi-hack4rail/backend/.python-version b/flatland-hmi-hack4rail/backend/.python-version
new file mode 100644
index 00000000..2c073331
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/flatland-hmi-hack4rail/backend/README.md b/flatland-hmi-hack4rail/backend/README.md
new file mode 100644
index 00000000..275ec3f3
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/README.md
@@ -0,0 +1,38 @@
+# FastAPI Backend
+
+This is the backend of the Flatland Interactive project, built using FastAPI.
+
+## Prerequisites
+
+- Python 3.10 or higher
+- pip (Python package manager)
+
+## Installation
+
+2. Install the required dependencies:
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+## Running the Application
+
+To run the FastAPI application using Uvicorn, execute the following command:
+
+```bash
+uvicorn main:app --reload
+```
+
+- `main` refers to the `main.py` file.
+- `app` is the FastAPI instance created in `main.py`.
+- `--reload` enables auto-reloading for development purposes.
+
+The application will be accessible at:
+
+```
+http://127.0.0.1:8000
+```
+
+## Additional Information
+
+- FastAPI documentation: [https://fastapi.tiangolo.com/](https://fastapi.tiangolo.com/)
+- Uvicorn documentation: [https://www.uvicorn.org/](https://www.uvicorn.org/)
diff --git a/flatland-hmi-hack4rail/backend/app/__init__.py b/flatland-hmi-hack4rail/backend/app/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/flatland-hmi-hack4rail/backend/app/env.py b/flatland-hmi-hack4rail/backend/app/env.py
new file mode 100644
index 00000000..88c5f1d3
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/env.py
@@ -0,0 +1,173 @@
+from dataclasses import dataclass, field
+import tempfile
+import random
+
+from flatland.envs.rail_env_action import RailEnvActions
+from flatland.envs.persistence import RailEnvPersister
+
+from flatland.core.policy import Policy
+from flatland.envs.rail_env import RailEnv
+from flatland.envs.predictions import ShortestPathPredictorForRailEnv
+from flatland.envs.observations import TreeObsForRailEnv
+from flatland.envs import malfunction_generators as mal_gen
+
+from .scenario.hack4rail import Hack4RailEnvGenerator
+
+
+class EnvDoneException(Exception):
+ """Exception raised when the environment is done."""
+
+
+@dataclass
+class EnvOption:
+ policy: Policy
+ env: RailEnv
+ steps: list = field(default_factory=list)
+
+ def update_env(self, env: RailEnv):
+ """Update the environment for the policy."""
+ self.env = env
+ if hasattr(self.policy, "env"):
+ self.policy.env = env
+
+ def reset(self):
+ """Reset the environment and policy."""
+ self.env.reset()
+ self.steps = []
+
+ def step(self, explicit_actions=None) -> None:
+ """Exeecute the next step in the environment based on the policy."""
+ if explicit_actions is None:
+ explicit_actions = {}
+ if self.env.dones.get("__all__", False):
+ raise EnvDoneException("Environment done, call reset() to start a new episode")
+ actions = self.policy.act_many(self.env.obs_dict)
+ actions.update(
+ {
+ a: RailEnvActions.from_value(action)
+ for a, action in explicit_actions.items()
+ }
+ )
+ self.env.step(actions)
+ self.steps.append(self._step_to_dict())
+
+ def _step_to_dict(self):
+ return {
+ str(agent.handle): {
+ "position": (
+ None
+ if agent.position is None
+ else tuple(int(c) for c in agent.position)
+ ),
+ "direction": agent.direction,
+ "moving": agent.moving,
+ "speed_counter": agent.speed_counter,
+ "target": (
+ None
+ if agent.target is None
+ else tuple(int(c) for c in agent.target)
+ ),
+ "malfunction": agent.malfunction_handler.malfunction_down_counter,
+ }
+ for agent in self.env.agents
+ }
+
+ def simulate(self):
+ """Run all steps until the environment is done."""
+ if self.env.dones.get("__all__", False):
+ raise EnvDoneException("Environment done, call reset() to start a new episode")
+ malfunction_generator = self.env.malfunction_generator
+ self.env.malfunction_generator = mal_gen.NoMalfunctionGen()
+ while not self.env.dones.get("__all__", False):
+ self.step()
+ self.env.malfunction_generator = malfunction_generator
+
+ def switch_policy(self, new_policy: Policy):
+ """Switch the policy for the environment."""
+ tmp_file_name = tempfile.NamedTemporaryFile(suffix=".pkl").name
+ RailEnvPersister.save(self.env, tmp_file_name)
+ obs_builder = TreeObsForRailEnv(
+ max_depth=1, predictor=ShortestPathPredictorForRailEnv()
+ )
+ env_copy, _ = RailEnvPersister.load_new(
+ tmp_file_name,
+ obs_builder_object=obs_builder,
+ )
+ env_copy.obs_builder.reset()
+ copy = EnvOption(policy=new_policy, env=env_copy, steps=self.steps.copy())
+ copy.update_env(env_copy)
+ return copy
+
+
+class InteractiveEnv:
+ def __init__(
+ self, generator: Hack4RailEnvGenerator, baseline_policy, plan_policies
+ ):
+ self.generator: Hack4RailEnvGenerator = generator
+ self.baseline_env = EnvOption(
+ baseline_policy, generator.create_hack4rail_env(enable_malfunctions=False)
+ )
+ self.plan_envs = [
+ EnvOption(plan_policy, generator.create_hack4rail_env())
+ for plan_policy in plan_policies
+ ]
+ self.history_env = EnvOption(baseline_policy, generator.create_hack4rail_env())
+ self.reset()
+
+ def reset(self):
+ for env_option in [self.baseline_env, self.history_env] + self.plan_envs:
+ env_option.update_env(self.generator.create_hack4rail_env())
+ env_option.reset()
+
+ # Generate baseline env
+ self.baseline_env.reset()
+ self.baseline_env.simulate()
+ for plan_env in self.plan_envs:
+ plan_env.simulate()
+
+ def step(self, plan_index) -> int | None:
+ """Step the environment and return the observations, rewards, done flags, info, and actions."""
+ self.history_env = self.history_env.switch_policy(
+ self.plan_envs[plan_index].policy
+ )
+ # update history env with the current step
+ #TODO: Investigate why we need to step twice. Reloaded env seems to be in a wierd state.
+ self.history_env.step()
+ self.history_env.step()
+ # update the plan envs with the current step
+ for i, plan_env in enumerate(self.plan_envs):
+ updated = self.history_env.switch_policy(plan_env.policy)
+ # simulate the plan with the new state
+ updated.simulate()
+ self.plan_envs[i] = updated
+
+ # evaluate the plans to determine the best solution
+ # placholder returning a random value for the index of plan_env
+ best_plan_index = random.randint(0, len(self.plan_envs) - 1)
+
+ return best_plan_index
+
+
+# Import hack4rail environment generator providing a static environment
+from .scenario.hack4rail import Hack4RailEnvGenerator
+
+
+# Import the RandomPolicy from the policies module
+# from .policy.random_policy import RandomPolicy
+
+# Create a random agent policy
+# random_policy = RandomPolicy()
+
+# Import the DeadLockAvoidancePolicy from the policies module
+from .policy.deadlock_avoidance_policy import DeadLockAvoidancePolicy
+
+
+# Initialize the interactive environment with env and policy
+interactive_env = InteractiveEnv(
+ generator=Hack4RailEnvGenerator(),
+ baseline_policy=DeadLockAvoidancePolicy(),
+ plan_policies=[
+ DeadLockAvoidancePolicy(default_eps=0.2, enable_eps=True),
+ DeadLockAvoidancePolicy(default_eps=0.8, enable_eps=True),
+ ],
+)
diff --git a/flatland-hmi-hack4rail/backend/app/policy/__init__.py b/flatland-hmi-hack4rail/backend/app/policy/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/flatland-hmi-hack4rail/backend/app/policy/deadlock_avoidance_policy.py b/flatland-hmi-hack4rail/backend/app/policy/deadlock_avoidance_policy.py
new file mode 100644
index 00000000..258ea1bd
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/policy/deadlock_avoidance_policy.py
@@ -0,0 +1,253 @@
+from functools import lru_cache
+from typing import Union
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from flatland.core.policy import Policy
+from flatland.envs.fast_methods import fast_count_nonzero
+from flatland.envs.rail_env import RailEnv, RailEnvActions
+from flatland.envs.step_utils.states import TrainState
+from .shortest_distance_walker import ShortestDistanceWalker
+
+# activate LRU caching
+flatland_deadlock_avoidance_policy_lru_cache_functions = []
+
+
+def _enable_flatland_deadlock_avoidance_policy_lru_cache(*args, **kwargs):
+ def decorator(func):
+ func = lru_cache(*args, **kwargs)(func)
+ flatland_deadlock_avoidance_policy_lru_cache_functions.append(func)
+ return func
+
+ return decorator
+
+
+def _send_flatland_deadlock_avoidance_policy_data_change_signal_to_reset_lru_cache():
+ for func in flatland_deadlock_avoidance_policy_lru_cache_functions:
+ func.cache_clear()
+
+
+class DeadlockAvoidanceShortestDistanceWalker(ShortestDistanceWalker):
+ def __init__(self, env: RailEnv):
+ super().__init__(env)
+ self.shortest_distance_agent_map = None
+ self.full_shortest_distance_agent_map = None
+ self.agent_positions = None
+ self.opp_agent_map = {}
+ self.same_agent_map = {}
+
+ def reset(self, env: RailEnv):
+ super(DeadlockAvoidanceShortestDistanceWalker, self).reset(env)
+ self.shortest_distance_agent_map = None
+ self.full_shortest_distance_agent_map = None
+ self.agent_positions = None
+ self.opp_agent_map = {}
+ self.same_agent_map = {}
+ _send_flatland_deadlock_avoidance_policy_data_change_signal_to_reset_lru_cache()
+
+ def clear(self, agent_positions):
+ self.shortest_distance_agent_map = (
+ np.zeros(
+ (self.env.get_num_agents(), self.env.height, self.env.width), dtype=int
+ )
+ - 1
+ )
+
+ self.full_shortest_distance_agent_map = (
+ np.zeros(
+ (self.env.get_num_agents(), self.env.height, self.env.width), dtype=int
+ )
+ - 1
+ )
+
+ self.agent_positions = agent_positions
+
+ self.opp_agent_map = {}
+ self.same_agent_map = {}
+
+ def getData(self):
+ return self.shortest_distance_agent_map, self.full_shortest_distance_agent_map
+
+ def callback(
+ self, handle, agent, position, direction, action, possible_transitions
+ ) -> bool:
+ opp_a = self.agent_positions[position]
+ if opp_a != -1 and opp_a != handle:
+ if self.env.agents[opp_a].direction != direction:
+ d = self.opp_agent_map.get(handle, [])
+ if opp_a not in d:
+ d.append(opp_a)
+ self.opp_agent_map.update({handle: d})
+ else:
+ if len(self.opp_agent_map.get(handle, [])) == 0:
+ d = self.same_agent_map.get(handle, [])
+ if opp_a not in d:
+ d.append(opp_a)
+ self.same_agent_map.update({handle: d})
+
+ if len(self.opp_agent_map.get(handle, [])) == 0:
+ if self._is_no_switch_cell(position):
+ self.shortest_distance_agent_map[(handle, position[0], position[1])] = 1
+ self.full_shortest_distance_agent_map[(handle, position[0], position[1])] = 1
+ return True
+
+ @_enable_flatland_deadlock_avoidance_policy_lru_cache(maxsize=100000)
+ def _is_no_switch_cell(self, position) -> bool:
+ for new_dir in range(4):
+ possible_transitions = self.env.rail.get_transitions(*position, new_dir)
+ num_transitions = fast_count_nonzero(possible_transitions)
+ if num_transitions > 1:
+ return False
+ return True
+
+
+class DeadLockAvoidancePolicy(Policy):
+ def __init__(
+ self,
+ action_size: int = 5,
+ min_free_cell: int = 1,
+ enable_eps: bool = False,
+ show_debug_plot: bool = False,
+ env: RailEnv = None,
+ default_eps: float = 0.0,
+ ):
+ super(Policy, self).__init__()
+ self.env: RailEnv = None
+ self.loss = 0
+ self.default_eps = default_eps
+ self.action_size = action_size
+ self.agent_can_move = {}
+ self.show_debug_plot = show_debug_plot
+ self.enable_eps = enable_eps
+ self.shortest_distance_walker: Union[
+ DeadlockAvoidanceShortestDistanceWalker, None
+ ] = None
+ self.min_free_cell = min_free_cell
+ self.agent_positions = None
+ self.env = env
+
+ def act_many(self, observations, eps=None):
+ return {a: self.act(a, obs, eps) for a, obs in observations.items()}
+
+ def act(self, handle, state, eps=None):
+ if isinstance(state, RailEnv):
+ self.env = state
+ if handle == 0:
+ self.start_step()
+
+ # Epsilon-greedy action selection
+ if self.enable_eps:
+ if eps is None:
+ eps = self.default_eps
+ if np.random.random() < eps:
+ return np.random.choice(np.arange(self.action_size))
+
+ # agent = self.env.agents[state[0]]
+ check = self.agent_can_move.get(handle, None)
+ act = RailEnvActions.STOP_MOVING
+ if check is not None:
+ act = check[3]
+ # TODO port to client.py: File "msgpack/_packer.pyx", line 257, in msgpack._cmsgpack.Packer._pack_inner
+ # submission-1 | TypeError: can not serialize 'RailEnvActions' object
+ # if isinstance(act, RailEnvActions):
+ # act = act.value
+ return RailEnvActions.from_value(act)
+
+ def start_step(self):
+ self._build_agent_position_map()
+ self._shortest_distance_mapper()
+ self._extract_agent_can_move()
+
+ def _build_agent_position_map(self):
+ # build map with agent positions (only active agents)
+ self.agent_positions = (
+ np.zeros((self.env.height, self.env.width), dtype=int) - 1
+ )
+ for handle in range(self.env.get_num_agents()):
+ agent = self.env.agents[handle]
+ if agent.state in [
+ TrainState.MOVING,
+ TrainState.STOPPED,
+ TrainState.MALFUNCTION,
+ ]:
+ if agent.position is not None:
+ self.agent_positions[agent.position] = handle
+
+ def _shortest_distance_mapper(self):
+ if self.shortest_distance_walker is None:
+ self.shortest_distance_walker = DeadlockAvoidanceShortestDistanceWalker(
+ self.env
+ )
+ self.shortest_distance_walker.clear(self.agent_positions)
+ for handle in range(self.env.get_num_agents()):
+ agent = self.env.agents[handle]
+ if agent.state <= TrainState.MALFUNCTION:
+ self.shortest_distance_walker.walk_to_target(handle)
+
+ def _extract_agent_can_move(self):
+ self.agent_can_move = {}
+ shortest_distance_agent_map, full_shortest_distance_agent_map = (
+ self.shortest_distance_walker.getData()
+ )
+ for handle in range(self.env.get_num_agents()):
+ agent = self.env.agents[handle]
+ if agent.state < TrainState.DONE:
+ if self._check_agent_can_move(
+ handle,
+ shortest_distance_agent_map[handle],
+ self.shortest_distance_walker.same_agent_map.get(handle, []),
+ self.shortest_distance_walker.opp_agent_map.get(handle, []),
+ full_shortest_distance_agent_map,
+ ):
+ next_position, next_direction, action, _ = (
+ self.shortest_distance_walker.walk_one_step(handle)
+ )
+ self.agent_can_move.update(
+ {
+ handle: [
+ next_position[0],
+ next_position[1],
+ next_direction,
+ action,
+ ]
+ }
+ )
+
+ if self.show_debug_plot:
+ a = np.floor(np.sqrt(self.env.get_num_agents()))
+ b = np.ceil(self.env.get_num_agents() / a)
+ for handle in range(self.env.get_num_agents()):
+ plt.subplot(a, b, handle + 1)
+ plt.imshow(
+ full_shortest_distance_agent_map[handle]
+ + shortest_distance_agent_map[handle]
+ )
+ plt.show(block=False)
+ plt.pause(0.01)
+
+ def _check_agent_can_move(
+ self,
+ handle,
+ my_shortest_walking_path,
+ same_agents,
+ opp_agents,
+ full_shortest_distance_agent_map,
+ ):
+ agent_positions_map = (self.agent_positions > -1).astype(int)
+ len_opp_agents = len(opp_agents)
+ for opp_a in opp_agents:
+ opp = full_shortest_distance_agent_map[opp_a]
+ delta = ((my_shortest_walking_path - opp - agent_positions_map) > 0).astype(
+ int
+ )
+ sum_delta = np.sum(delta)
+ if sum_delta < (self.min_free_cell + len_opp_agents):
+ return False
+ return True
+
+ def save(self, filename):
+ pass
+
+ def load(self, filename):
+ pass
diff --git a/flatland-hmi-hack4rail/backend/app/policy/random_policy.py b/flatland-hmi-hack4rail/backend/app/policy/random_policy.py
new file mode 100644
index 00000000..b8d6d76f
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/policy/random_policy.py
@@ -0,0 +1,10 @@
+import random
+from flatland.envs.rail_env_action import RailEnvActions
+
+
+class RandomPolicy:
+ def act(self, _obs):
+ return RailEnvActions.from_value(random.randint(0, 4))
+
+ def act_many(self, obs):
+ return {a: self.act(o) for a, o in obs.items()}
diff --git a/flatland-hmi-hack4rail/backend/app/policy/shortest_distance_walker.py b/flatland-hmi-hack4rail/backend/app/policy/shortest_distance_walker.py
new file mode 100644
index 00000000..8b5dabff
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/policy/shortest_distance_walker.py
@@ -0,0 +1,131 @@
+from functools import lru_cache
+
+import numpy as np
+
+from flatland.core.grid.grid4_utils import get_new_position
+from flatland.envs.fast_methods import fast_count_nonzero, fast_argmax
+from flatland.envs.rail_env import RailEnv, RailEnvActions
+
+# activate LRU caching
+_flatland_shortest_distance_walker_lru_cache_functions = []
+
+
+def _enable_flatland_shortest_distance_walker_lru_cache(*args, **kwargs):
+ def decorator(func):
+ func = lru_cache(*args, **kwargs)(func)
+ _flatland_shortest_distance_walker_lru_cache_functions.append(func)
+ return func
+
+ return decorator
+
+
+def _send_flatland_shortest_distance_walker_data_change_signal_to_reset_lru_cache():
+ for func in _flatland_shortest_distance_walker_lru_cache_functions:
+ func.cache_clear()
+
+
+class ShortestDistanceWalker:
+ def __init__(self, env: RailEnv):
+ self.env = env
+ self.distance_map = None
+
+ def reset(self, env: RailEnv):
+ _send_flatland_shortest_distance_walker_data_change_signal_to_reset_lru_cache()
+ self.env = env
+ self.distance_map = None
+
+ @_enable_flatland_shortest_distance_walker_lru_cache(maxsize=100000)
+ def walk(self, handle, position, direction):
+ if self.distance_map is None:
+ self.distance_map = self.env.distance_map.get()
+
+ possible_transitions = self.env.rail.get_transitions(*position, direction)
+ num_transitions = fast_count_nonzero(possible_transitions)
+ if num_transitions == 1:
+ new_direction = fast_argmax(possible_transitions)
+ new_position = get_new_position(position, new_direction)
+ dist = self.distance_map[handle, new_position[0], new_position[1], new_direction]
+ return new_position, new_direction, dist, RailEnvActions.MOVE_FORWARD, possible_transitions
+ else:
+ min_distances = []
+ positions = []
+ directions = []
+ for new_direction in [(direction + i) % 4 for i in range(-1, 2)]:
+ if possible_transitions[new_direction]:
+ new_position = get_new_position(position, new_direction)
+ min_distances.append(
+ self.distance_map[handle, new_position[0], new_position[1], new_direction]
+ )
+ positions.append(new_position)
+ directions.append(new_direction)
+ else:
+ min_distances.append(np.inf)
+ positions.append(None)
+ directions.append(None)
+
+ a = self.get_action(min_distances)
+ return positions[a], directions[a], min_distances[a], a + 1, possible_transitions
+
+ def get_action(self, min_distances):
+ return np.argmin(min_distances)
+
+ def callback(self, handle, agent, position, direction, action, possible_transitions) -> bool:
+ return True
+
+ @_enable_flatland_shortest_distance_walker_lru_cache(maxsize=100000)
+ def get_agent_position_and_direction(self, agent_position, agent_direction, agent_initial_position):
+ if agent_position is not None:
+ position = agent_position
+ else:
+ position = agent_initial_position
+ direction = agent_direction
+ return position, direction
+
+ def walk_to_target(self, handle, position=None, direction=None, max_step=500):
+ agent = self.env.agents[handle]
+ position, direction = self._get_pos_dir_wtt(position, direction,
+ agent.position, agent.direction,
+ agent.initial_position)
+
+ agent = self.env.agents[handle]
+ step = 0
+ while (position != agent.target) and (step < max_step):
+ position, direction, dist, action, possible_transitions = self.walk(handle, position, direction)
+ if position is None:
+ break
+ if not self.callback(handle, agent, position, direction, action, possible_transitions):
+ break
+ step += 1
+
+ @_enable_flatland_shortest_distance_walker_lru_cache(maxsize=100000)
+ def _get_pos_dir_wtt(self, position, direction, agent_pos, agent_dir, agent_initial_position):
+
+ if position is None and direction is None:
+ position, direction = self.get_agent_position_and_direction(agent_pos, agent_dir, agent_initial_position)
+ elif position is None:
+ position, _ = self.get_agent_position_and_direction(agent_pos, agent_dir, agent_initial_position)
+ elif direction is None:
+ _, direction = self.get_agent_position_and_direction(agent_pos, agent_dir, agent_initial_position)
+
+ return position, direction
+
+ def callback_one_step(self, handle, agent, position, direction, action, possible_transitions):
+ pass
+
+ def walk_one_step(self, handle):
+ agent = self.env.agents[handle]
+ if agent.position is not None:
+ position = agent.position
+ else:
+ position = agent.initial_position
+ direction = agent.direction
+ possible_transitions = (0, 1, 0, 0)
+ new_position = agent.target
+ new_direction = agent.direction
+ action = RailEnvActions.STOP_MOVING
+ if position != agent.target:
+ new_position, new_direction, dist, action, possible_transitions = self.walk(handle, position, direction)
+ if new_position is None:
+ return position, direction, RailEnvActions.STOP_MOVING, possible_transitions
+ self.callback_one_step(handle, agent, new_position, new_direction, action, possible_transitions)
+ return new_position, new_direction, action, possible_transitions
diff --git a/flatland-hmi-hack4rail/backend/app/routes.py b/flatland-hmi-hack4rail/backend/app/routes.py
new file mode 100644
index 00000000..03f7acb8
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/routes.py
@@ -0,0 +1,46 @@
+from fastapi import APIRouter
+from app.env import interactive_env, EnvDoneException
+
+router = APIRouter()
+
+
+@router.get("/transitions")
+def get_transitions():
+ return interactive_env.baseline_env.env.rail.grid.tolist()
+
+
+@router.get("/baseline")
+def get_baseline():
+ return interactive_env.baseline_env.steps
+
+
+@router.get("/history")
+def get_history():
+ return interactive_env.history_env.steps
+
+
+@router.get("/plans")
+def get_plans():
+ return [plan.steps for plan in interactive_env.plan_envs]
+
+
+@router.post("/step")
+def step_env(plan_index: int):
+ return interactive_env.step(plan_index)
+
+
+@router.post("/reset")
+def reset_env():
+ interactive_env.reset()
+
+
+@router.post("/all_steps")
+def step_all():
+ plan_index = 0
+ try:
+ while True:
+ # Step through the environment until a plan is selected
+ plan_index = interactive_env.step(plan_index)
+ print(f"Selected plan index: {plan_index}")
+ except EnvDoneException:
+ return
diff --git a/flatland-hmi-hack4rail/backend/app/scenario/__init__.py b/flatland-hmi-hack4rail/backend/app/scenario/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/flatland-hmi-hack4rail/backend/app/scenario/hack4rail.py b/flatland-hmi-hack4rail/backend/app/scenario/hack4rail.py
new file mode 100644
index 00000000..59d9493d
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/scenario/hack4rail.py
@@ -0,0 +1,296 @@
+import numpy as np
+from flatland.envs.timetable_utils import Line, Timetable
+from flatland.envs.rail_grid_transition_map import RailGridTransitionMap
+from flatland.core.grid.rail_env_grid import RailEnvTransitions
+from flatland.envs.observations import TreeObsForRailEnv
+from flatland.envs.predictions import ShortestPathPredictorForRailEnv
+from flatland.envs.malfunction_generators import (
+ ParamMalfunctionGen,
+ MalfunctionParameters,
+)
+
+from .static import create_static_env
+from dataclasses import dataclass
+
+@dataclass
+class Hack4RailEnvGenerator:
+ """
+ Static environment for the Hack4Rail competition.
+ """
+
+ width = 34
+ height = 6
+
+ line=Line(
+ agent_positions=[
+ [(3, 4)],
+ [(2, 4), (3, 16)],
+ [(2, 29)],
+ [(3, 29), (3, 16)],
+ ],
+ agent_directions=[[1], [1, 1], [3], [3, 3]],
+ agent_targets=[(2, 29), (3, 29), (3, 4), (2, 4)],
+ agent_speeds=[1.0, 0.8, 1.0, 0.8],
+ )
+
+ timetable=Timetable(
+ earliest_departures=[
+ [4, None],
+ [0, 21, None],
+ [4, None],
+ [0, 24, None],
+ [0, None],
+ ],
+ latest_arrivals=[
+ [None, 29],
+ [None, 19, 43],
+ [None, 29],
+ [None, 22, 43],
+ [None, 0],
+ ],
+ max_episode_steps=120,
+ )
+
+ map = RailGridTransitionMap(
+ width=width, height=height, transitions=RailEnvTransitions()
+ )
+ map.grid = np.array(
+ [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ],
+ [
+ 0,
+ 0,
+ 4,
+ 1025,
+ 1025,
+ 1025,
+ 4608,
+ 0,
+ 0,
+ 0,
+ 16386,
+ 1025,
+ 1025,
+ 1025,
+ 4608,
+ 0,
+ 0,
+ 0,
+ 16386,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 17411,
+ 1025,
+ 1025,
+ 1025,
+ 5633,
+ 1025,
+ 1025,
+ 1025,
+ 256,
+ 0,
+ 0,
+ ],
+ [
+ 0,
+ 0,
+ 4,
+ 1025,
+ 1025,
+ 1025,
+ 1097,
+ 1025,
+ 1025,
+ 1025,
+ 3089,
+ 1025,
+ 1025,
+ 1025,
+ 1097,
+ 1025,
+ 1025,
+ 1025,
+ 3089,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 2064,
+ 0,
+ 0,
+ 0,
+ 72,
+ 1025,
+ 1025,
+ 1025,
+ 256,
+ 0,
+ 0,
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ],
+ ]
+ )
+
+ def create_hack4rail_env(self, enable_malfunctions=True):
+ return create_static_env(
+ width=self.width,
+ height=self.height,
+ map=self.map,
+ line=self.line,
+ timetable=self.timetable,
+ obs_builder=TreeObsForRailEnv(
+ max_depth=1, predictor=ShortestPathPredictorForRailEnv()
+ ),
+ malfunction_generator=ParamMalfunctionGen(
+ MalfunctionParameters(
+ min_duration=10,
+ max_duration=50,
+ malfunction_rate=1.0 / 50.0,
+ )
+ )if enable_malfunctions else None,
+ )
+
diff --git a/flatland-hmi-hack4rail/backend/app/scenario/random.py b/flatland-hmi-hack4rail/backend/app/scenario/random.py
new file mode 100644
index 00000000..0882ba5d
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/scenario/random.py
@@ -0,0 +1,34 @@
+from flatland.envs.observations import TreeObsForRailEnv
+from flatland.envs.predictions import ShortestPathPredictorForRailEnv
+from flatland.envs.rail_env import RailEnv
+from flatland.envs.rail_generators import sparse_rail_generator
+from flatland.envs.line_generators import sparse_line_generator
+from flatland.envs.observations import TreeObsForRailEnv
+from flatland.envs.predictions import ShortestPathPredictorForRailEnv
+
+
+# Create a Flatland environment
+def create_random_env(
+ width=32,
+ height=32,
+ obs_builder=None,
+ malfunction_generator=None,
+):
+ """
+ Create a random Flatland environment with specified width and height.
+ The environment will have sparse rail generation and a tree observation builder.
+ """
+ return RailEnv(
+ width=width,
+ height=height,
+ rail_generator=sparse_rail_generator(
+ max_num_cities=4,
+ grid_mode=False,
+ max_rails_between_cities=2,
+ max_rail_pairs_in_city=1,
+ ),
+ line_generator=sparse_line_generator(),
+ number_of_agents=5,
+ obs_builder_object=obs_builder,
+ malfunction_generator=malfunction_generator,
+ )
diff --git a/flatland-hmi-hack4rail/backend/app/scenario/static.py b/flatland-hmi-hack4rail/backend/app/scenario/static.py
new file mode 100644
index 00000000..96e26b71
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/app/scenario/static.py
@@ -0,0 +1,52 @@
+from flatland.envs.rail_env import RailEnv
+
+
+def rail_generator_from_grid_map(grid_map):
+ def rail_generator(*args, **kwargs):
+ return grid_map, {
+ "agents_hints": {"city_positions": {}},
+ "level_free_positions": [],
+ }
+
+ return rail_generator
+
+
+def line_generator_from_line(line):
+ def line_generator(*args, **kwargs):
+ return line
+
+ return line_generator
+
+
+def timetable_generator_from_timetable(timetable):
+ def timetable_generator(*args, **kwargs):
+ return timetable
+
+ return timetable_generator
+
+
+def create_static_env(
+ width=32,
+ height=32,
+ map=None,
+ line=None,
+ timetable=None,
+ obs_builder=None,
+ malfunction_generator=None,
+):
+
+ assert map is not None, "Grid must be provided"
+ assert line is not None, "Line must be provided"
+ assert timetable is not None, "Timetable must be provided"
+
+ env = RailEnv(
+ width=width,
+ height=height,
+ rail_generator=rail_generator_from_grid_map(map),
+ line_generator=line_generator_from_line(line),
+ timetable_generator=timetable_generator_from_timetable(timetable),
+ obs_builder_object=obs_builder,
+ malfunction_generator=malfunction_generator,
+ )
+
+ return env
diff --git a/flatland-hmi-hack4rail/backend/main.py b/flatland-hmi-hack4rail/backend/main.py
new file mode 100644
index 00000000..8726b6c4
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/main.py
@@ -0,0 +1,21 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from app.routes import router
+
+middleware_config = {
+ "allow_origins": [
+ "http://localhost:4200",
+ ],
+ "allow_credentials": True,
+ "allow_methods": ["*"],
+ "allow_headers": ["*"],
+}
+
+app = FastAPI()
+
+app.add_middleware(
+ CORSMiddleware,
+ **middleware_config
+)
+
+app.include_router(router)
diff --git a/flatland-hmi-hack4rail/backend/requirements.txt b/flatland-hmi-hack4rail/backend/requirements.txt
new file mode 100644
index 00000000..7afed202
--- /dev/null
+++ b/flatland-hmi-hack4rail/backend/requirements.txt
@@ -0,0 +1,4 @@
+fastapi==0.97.0
+numpy==1.26.4
+uvicorn==0.22.0
+flatland-rl==4.1.3
\ No newline at end of file
diff --git a/flatland-hmi-hack4rail/flatland-hmi.png b/flatland-hmi-hack4rail/flatland-hmi.png
new file mode 100644
index 00000000..70a567d3
Binary files /dev/null and b/flatland-hmi-hack4rail/flatland-hmi.png differ
diff --git a/flatland-hmi-hack4rail/frontend/.editorconfig b/flatland-hmi-hack4rail/frontend/.editorconfig
new file mode 100644
index 00000000..f166060d
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/.editorconfig
@@ -0,0 +1,17 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+ij_typescript_use_double_quotes = false
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/flatland-hmi-hack4rail/frontend/.gitignore b/flatland-hmi-hack4rail/frontend/.gitignore
new file mode 100644
index 00000000..cc7b1413
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/.gitignore
@@ -0,0 +1,42 @@
+# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
diff --git a/flatland-hmi-hack4rail/frontend/README.md b/flatland-hmi-hack4rail/frontend/README.md
new file mode 100644
index 00000000..30f758aa
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/README.md
@@ -0,0 +1,67 @@
+# Frontend
+
+This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.3.
+
+## Installing dependencies
+
+Install npm packages
+
+```bash
+npm install
+```
+
+## Development server
+
+To start a local development server, run:
+
+```bash
+npm run start
+```
+
+Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
+
+## Code scaffolding
+
+Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
+
+```bash
+ng generate component component-name
+```
+
+For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
+
+```bash
+ng generate --help
+```
+
+## Building
+
+To build the project run:
+
+```bash
+ng build
+```
+
+This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
+
+## Running unit tests
+
+To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
+
+```bash
+ng test
+```
+
+## Running end-to-end tests
+
+For end-to-end (e2e) testing, run:
+
+```bash
+ng e2e
+```
+
+Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
+
+## Additional Resources
+
+For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
diff --git a/flatland-hmi-hack4rail/frontend/angular.json b/flatland-hmi-hack4rail/frontend/angular.json
new file mode 100644
index 00000000..eaa44779
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/angular.json
@@ -0,0 +1,105 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "frontend": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "scss"
+ }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:application",
+ "options": {
+ "outputPath": "dist/frontend",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": [
+ "zone.js"
+ ],
+ "tsConfig": "tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "public"
+ }
+ ],
+ "styles": [
+ "src/styles.scss"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kB",
+ "maximumError": "1MB"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "4kB",
+ "maximumError": "8kB"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "frontend:build:production"
+ },
+ "development": {
+ "buildTarget": "frontend:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ],
+ "tsConfig": "tsconfig.spec.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "public"
+ }
+ ],
+ "styles": [
+ "src/styles.scss"
+ ],
+ "scripts": []
+ }
+ }
+ }
+ }
+ },
+ "cli": {
+ "analytics": false
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/package-lock.json b/flatland-hmi-hack4rail/frontend/package-lock.json
new file mode 100644
index 00000000..aee688e7
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/package-lock.json
@@ -0,0 +1,14873 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@angular/common": "^19.2.0",
+ "@angular/compiler": "^19.2.0",
+ "@angular/core": "^19.2.0",
+ "@angular/forms": "^19.2.0",
+ "@angular/platform-browser": "^19.2.0",
+ "@angular/platform-browser-dynamic": "^19.2.0",
+ "@angular/router": "^19.2.0",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.15.0"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^19.2.3",
+ "@angular/cli": "^19.2.3",
+ "@angular/compiler-cli": "^19.2.0",
+ "@types/jasmine": "~5.1.0",
+ "jasmine-core": "~5.6.0",
+ "karma": "~6.4.0",
+ "karma-chrome-launcher": "~3.2.0",
+ "karma-coverage": "~2.2.0",
+ "karma-jasmine": "~5.1.0",
+ "karma-jasmine-html-reporter": "~2.1.0",
+ "typescript": "~5.7.2"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/architect": {
+ "version": "0.1902.12",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.12.tgz",
+ "integrity": "sha512-LfUc7k84YL290hAxsG+FvjQpXugQXyw5aDzrQQB4iTYhBgaABu2aaNOU4eu3JH+F8NeXd2EBF/YMr2LDSkYlMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "19.2.12",
+ "rxjs": "7.8.1"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/architect/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@angular-devkit/build-angular": {
+ "version": "19.2.12",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.12.tgz",
+ "integrity": "sha512-gPx3Vi7QFzHkSV388en6VqSqasojitJKuKmgTMPOV5keLtpOylPv3rjnr8oO9rYbYmLsT/WTUsP7bYiZhrr19Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "2.3.0",
+ "@angular-devkit/architect": "0.1902.12",
+ "@angular-devkit/build-webpack": "0.1902.12",
+ "@angular-devkit/core": "19.2.12",
+ "@angular/build": "19.2.12",
+ "@babel/core": "7.26.10",
+ "@babel/generator": "7.26.10",
+ "@babel/helper-annotate-as-pure": "7.25.9",
+ "@babel/helper-split-export-declaration": "7.24.7",
+ "@babel/plugin-transform-async-generator-functions": "7.26.8",
+ "@babel/plugin-transform-async-to-generator": "7.25.9",
+ "@babel/plugin-transform-runtime": "7.26.10",
+ "@babel/preset-env": "7.26.9",
+ "@babel/runtime": "7.26.10",
+ "@discoveryjs/json-ext": "0.6.3",
+ "@ngtools/webpack": "19.2.12",
+ "@vitejs/plugin-basic-ssl": "1.2.0",
+ "ansi-colors": "4.1.3",
+ "autoprefixer": "10.4.20",
+ "babel-loader": "9.2.1",
+ "browserslist": "^4.21.5",
+ "copy-webpack-plugin": "12.0.2",
+ "css-loader": "7.1.2",
+ "esbuild-wasm": "0.25.4",
+ "fast-glob": "3.3.3",
+ "http-proxy-middleware": "3.0.5",
+ "istanbul-lib-instrument": "6.0.3",
+ "jsonc-parser": "3.3.1",
+ "karma-source-map-support": "1.4.0",
+ "less": "4.2.2",
+ "less-loader": "12.2.0",
+ "license-webpack-plugin": "4.0.2",
+ "loader-utils": "3.3.1",
+ "mini-css-extract-plugin": "2.9.2",
+ "open": "10.1.0",
+ "ora": "5.4.1",
+ "picomatch": "4.0.2",
+ "piscina": "4.8.0",
+ "postcss": "8.5.2",
+ "postcss-loader": "8.1.1",
+ "resolve-url-loader": "5.0.0",
+ "rxjs": "7.8.1",
+ "sass": "1.85.0",
+ "sass-loader": "16.0.5",
+ "semver": "7.7.1",
+ "source-map-loader": "5.0.0",
+ "source-map-support": "0.5.21",
+ "terser": "5.39.0",
+ "tree-kill": "1.2.2",
+ "tslib": "2.8.1",
+ "webpack": "5.98.0",
+ "webpack-dev-middleware": "7.4.2",
+ "webpack-dev-server": "5.2.0",
+ "webpack-merge": "6.0.1",
+ "webpack-subresource-integrity": "5.1.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "optionalDependencies": {
+ "esbuild": "0.25.4"
+ },
+ "peerDependencies": {
+ "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/localize": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/ssr": "^19.2.12",
+ "@web/test-runner": "^0.20.0",
+ "browser-sync": "^3.0.2",
+ "jest": "^29.5.0",
+ "jest-environment-jsdom": "^29.5.0",
+ "karma": "^6.3.0",
+ "ng-packagr": "^19.0.0 || ^19.2.0-next.0",
+ "protractor": "^7.0.0",
+ "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0",
+ "typescript": ">=5.5 <5.9"
+ },
+ "peerDependenciesMeta": {
+ "@angular/localize": {
+ "optional": true
+ },
+ "@angular/platform-server": {
+ "optional": true
+ },
+ "@angular/service-worker": {
+ "optional": true
+ },
+ "@angular/ssr": {
+ "optional": true
+ },
+ "@web/test-runner": {
+ "optional": true
+ },
+ "browser-sync": {
+ "optional": true
+ },
+ "jest": {
+ "optional": true
+ },
+ "jest-environment-jsdom": {
+ "optional": true
+ },
+ "karma": {
+ "optional": true
+ },
+ "ng-packagr": {
+ "optional": true
+ },
+ "protractor": {
+ "optional": true
+ },
+ "tailwindcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@angular-devkit/build-webpack": {
+ "version": "0.1902.12",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.12.tgz",
+ "integrity": "sha512-JNwvzaN2RVbG1IClFPXhNpysVwf55nWmVsNN5iQHRXkD3kpqnaOfhUBtlhBBjLf/i6cwKEne2TI8zciaEYr+iw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/architect": "0.1902.12",
+ "rxjs": "7.8.1"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "webpack": "^5.30.0",
+ "webpack-dev-server": "^5.0.2"
+ }
+ },
+ "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@angular-devkit/core": {
+ "version": "19.2.12",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.12.tgz",
+ "integrity": "sha512-v5pdfZHZ8MTZozfpkhKoPFBpXQW+2GFbTfdyis8FBtevJWCbIsCR3xhodgI4jwzkSEAraN4oVtWvSytdNyBC6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "8.17.1",
+ "ajv-formats": "3.0.1",
+ "jsonc-parser": "3.3.1",
+ "picomatch": "4.0.2",
+ "rxjs": "7.8.1",
+ "source-map": "0.7.4"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "chokidar": "^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "chokidar": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular-devkit/core/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@angular-devkit/schematics": {
+ "version": "19.2.12",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.12.tgz",
+ "integrity": "sha512-vK5NI/asi1snWFkw02DpmC8tLq6u5ZbUwwXxgALKuVwGl3g1VLzrHrkoSCrcsOO9Nu6GQOPbxax2lR/DICmytg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "19.2.12",
+ "jsonc-parser": "3.3.1",
+ "magic-string": "0.30.17",
+ "ora": "5.4.1",
+ "rxjs": "7.8.1"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/schematics/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@angular/build": {
+ "version": "19.2.12",
+ "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.12.tgz",
+ "integrity": "sha512-G28ux1T5QDlWporwupWbcodBN3rcyHfK2Dh5M3UC5hj0GstpfEHcpBHxawZzIxhqPKy//tdVLlzORUgvAwnqbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "2.3.0",
+ "@angular-devkit/architect": "0.1902.12",
+ "@babel/core": "7.26.10",
+ "@babel/helper-annotate-as-pure": "7.25.9",
+ "@babel/helper-split-export-declaration": "7.24.7",
+ "@babel/plugin-syntax-import-attributes": "7.26.0",
+ "@inquirer/confirm": "5.1.6",
+ "@vitejs/plugin-basic-ssl": "1.2.0",
+ "beasties": "0.3.2",
+ "browserslist": "^4.23.0",
+ "esbuild": "0.25.4",
+ "fast-glob": "3.3.3",
+ "https-proxy-agent": "7.0.6",
+ "istanbul-lib-instrument": "6.0.3",
+ "listr2": "8.2.5",
+ "magic-string": "0.30.17",
+ "mrmime": "2.0.1",
+ "parse5-html-rewriting-stream": "7.0.0",
+ "picomatch": "4.0.2",
+ "piscina": "4.8.0",
+ "rollup": "4.34.8",
+ "sass": "1.85.0",
+ "semver": "7.7.1",
+ "source-map-support": "0.5.21",
+ "vite": "6.2.7",
+ "watchpack": "2.4.2"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "optionalDependencies": {
+ "lmdb": "3.2.6"
+ },
+ "peerDependencies": {
+ "@angular/compiler": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/localize": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0",
+ "@angular/ssr": "^19.2.12",
+ "karma": "^6.4.0",
+ "less": "^4.2.0",
+ "ng-packagr": "^19.0.0 || ^19.2.0-next.0",
+ "postcss": "^8.4.0",
+ "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0",
+ "typescript": ">=5.5 <5.9"
+ },
+ "peerDependenciesMeta": {
+ "@angular/localize": {
+ "optional": true
+ },
+ "@angular/platform-server": {
+ "optional": true
+ },
+ "@angular/service-worker": {
+ "optional": true
+ },
+ "@angular/ssr": {
+ "optional": true
+ },
+ "karma": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "ng-packagr": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tailwindcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular/build/node_modules/vite": {
+ "version": "6.2.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.7.tgz",
+ "integrity": "sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "postcss": "^8.5.3",
+ "rollup": "^4.30.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular/build/node_modules/vite/node_modules/postcss": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "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.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/@angular/cli": {
+ "version": "19.2.12",
+ "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.12.tgz",
+ "integrity": "sha512-cZkHpM16uh3VouHG1XdWSk0ZWisQRxMVADk5IJlM9jMcPqnFyJwD/UXCS+XTaW3POpNDwsmbh2UB9Xabdgo7rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/architect": "0.1902.12",
+ "@angular-devkit/core": "19.2.12",
+ "@angular-devkit/schematics": "19.2.12",
+ "@inquirer/prompts": "7.3.2",
+ "@listr2/prompt-adapter-inquirer": "2.0.18",
+ "@schematics/angular": "19.2.12",
+ "@yarnpkg/lockfile": "1.1.0",
+ "ini": "5.0.0",
+ "jsonc-parser": "3.3.1",
+ "listr2": "8.2.5",
+ "npm-package-arg": "12.0.2",
+ "npm-pick-manifest": "10.0.0",
+ "pacote": "20.0.0",
+ "resolve": "1.22.10",
+ "semver": "7.7.1",
+ "symbol-observable": "4.0.0",
+ "yargs": "17.7.2"
+ },
+ "bin": {
+ "ng": "bin/ng.js"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular/common": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.11.tgz",
+ "integrity": "sha512-/ZnF2Nfp6S6TAu3VlvUAIp4NVd81WE1Q95wuwSSuoEx2aSyXzI+1myyKWSYe/jYCyGuppmocjTciEh8mAInmOw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "19.2.11",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@angular/compiler": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.11.tgz",
+ "integrity": "sha512-/ZGFAEO2TyqkaE4neR8lGL9I2QeO2sRVFqulQv7Bu8zKTPStjcsFCwNkp+TNX8Oq/1rLcY9XWAOsUk1//AZd8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ }
+ },
+ "node_modules/@angular/compiler-cli": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.11.tgz",
+ "integrity": "sha512-15aoOg+qj7Z3Uap1JKHMy51y12M09AOnseDBa0SYKidSx15XwZi8d01hv7sRaQJX/6Ie5cug9GiAbLKts6R33w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "7.26.9",
+ "@jridgewell/sourcemap-codec": "^1.4.14",
+ "chokidar": "^4.0.0",
+ "convert-source-map": "^1.5.1",
+ "reflect-metadata": "^0.2.0",
+ "semver": "^7.0.0",
+ "tslib": "^2.3.0",
+ "yargs": "^17.2.1"
+ },
+ "bin": {
+ "ng-xi18n": "bundles/src/bin/ng_xi18n.js",
+ "ngc": "bundles/src/bin/ngc.js",
+ "ngcc": "bundles/ngcc/index.js"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@angular/compiler": "19.2.11",
+ "typescript": ">=5.5 <5.9"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/@babel/core": {
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
+ "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.9",
+ "@babel/helper-compilation-targets": "^7.26.5",
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helpers": "^7.26.9",
+ "@babel/parser": "^7.26.9",
+ "@babel/template": "^7.26.9",
+ "@babel/traverse": "^7.26.9",
+ "@babel/types": "^7.26.9",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@angular/core": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.11.tgz",
+ "integrity": "sha512-kmtJQB7B5F2V1JIzy1oBPS6WrRyedSYkuge+XoX1mCSFJDef8HRNd7GopnQ0Zaz0vOTGvCCkWvvaH/+7s2lmAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "rxjs": "^6.5.3 || ^7.4.0",
+ "zone.js": "~0.15.0"
+ }
+ },
+ "node_modules/@angular/forms": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.11.tgz",
+ "integrity": "sha512-ZH9ccuT6rTirNSbiMRtGRkRrj69a2/+BVaa/kEpUHjh41wDQXxhOlOfPZd/sfj04QiAzIpsYmVJrmoV7/LxPSw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "19.2.11",
+ "@angular/core": "19.2.11",
+ "@angular/platform-browser": "19.2.11",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@angular/platform-browser": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.11.tgz",
+ "integrity": "sha512-wAPJtgzmxBEpW31sa2eg9QssCHBZ52Zc9nm6azTflDlOAyfm9bzqec7y3wqy5sgVue/qID2gzHqmpS3Nx3o0xg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@angular/animations": "19.2.11",
+ "@angular/common": "19.2.11",
+ "@angular/core": "19.2.11"
+ },
+ "peerDependenciesMeta": {
+ "@angular/animations": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular/platform-browser-dynamic": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.11.tgz",
+ "integrity": "sha512-1/0FmjSAvsK+A6gWLgEc60YMnWQchP9fP6y4sE1uQOThIgK+qLnLjZqZn7uOw8zMDBMtxB7SlepajnXftVXddw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "19.2.11",
+ "@angular/compiler": "19.2.11",
+ "@angular/core": "19.2.11",
+ "@angular/platform-browser": "19.2.11"
+ }
+ },
+ "node_modules/@angular/router": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.11.tgz",
+ "integrity": "sha512-nBwMwRgQ3s1c1CPItPnTJTf81NDOQHvK41r2MIJGHa3H9LONlcbY07q/9p49fqt/xn/dgoOmQTtJ22b/nbIJAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "19.2.11",
+ "@angular/core": "19.2.11",
+ "@angular/platform-browser": "19.2.11",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
+ "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
+ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.10",
+ "@babel/helper-compilation-targets": "^7.26.5",
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helpers": "^7.26.10",
+ "@babel/parser": "^7.26.10",
+ "@babel/template": "^7.26.9",
+ "@babel/traverse": "^7.26.10",
+ "@babel/types": "^7.26.10",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz",
+ "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.26.10",
+ "@babel/types": "^7.26.10",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz",
+ "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
+ "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz",
+ "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz",
+ "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "regexpu-core": "^6.2.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz",
+ "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz",
+ "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.6",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "debug": "^4.1.1",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.14.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
+ "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-wrap-function": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz",
+ "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
+ "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.24.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz",
+ "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
+ "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
+ "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz",
+ "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
+ "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
+ "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz",
+ "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz",
+ "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
+ "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
+ "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.26.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz",
+ "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.26.5",
+ "@babel/helper-remap-async-to-generator": "^7.25.9",
+ "@babel/traverse": "^7.26.8"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz",
+ "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-plugin-utils": "^7.25.9",
+ "@babel/helper-remap-async-to-generator": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
+ "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz",
+ "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz",
+ "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz",
+ "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz",
+ "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz",
+ "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
+ "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/template": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz",
+ "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz",
+ "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
+ "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
+ "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
+ "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz",
+ "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz",
+ "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz",
+ "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz",
+ "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz",
+ "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz",
+ "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
+ "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
+ "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
+ "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz",
+ "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
+ "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
+ "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz",
+ "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz",
+ "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz",
+ "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.27.1",
+ "@babel/plugin-transform-parameters": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
+ "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz",
+ "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz",
+ "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz",
+ "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz",
+ "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz",
+ "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
+ "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz",
+ "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regexp-modifiers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz",
+ "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
+ "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-runtime": {
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz",
+ "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-plugin-utils": "^7.26.5",
+ "babel-plugin-polyfill-corejs2": "^0.4.10",
+ "babel-plugin-polyfill-corejs3": "^0.11.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz",
+ "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz",
+ "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz",
+ "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
+ "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
+ "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
+ "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz",
+ "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz",
+ "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz",
+ "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz",
+ "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.26.8",
+ "@babel/helper-compilation-targets": "^7.26.5",
+ "@babel/helper-plugin-utils": "^7.26.5",
+ "@babel/helper-validator-option": "^7.25.9",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.26.0",
+ "@babel/plugin-syntax-import-attributes": "^7.26.0",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.25.9",
+ "@babel/plugin-transform-async-generator-functions": "^7.26.8",
+ "@babel/plugin-transform-async-to-generator": "^7.25.9",
+ "@babel/plugin-transform-block-scoped-functions": "^7.26.5",
+ "@babel/plugin-transform-block-scoping": "^7.25.9",
+ "@babel/plugin-transform-class-properties": "^7.25.9",
+ "@babel/plugin-transform-class-static-block": "^7.26.0",
+ "@babel/plugin-transform-classes": "^7.25.9",
+ "@babel/plugin-transform-computed-properties": "^7.25.9",
+ "@babel/plugin-transform-destructuring": "^7.25.9",
+ "@babel/plugin-transform-dotall-regex": "^7.25.9",
+ "@babel/plugin-transform-duplicate-keys": "^7.25.9",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9",
+ "@babel/plugin-transform-dynamic-import": "^7.25.9",
+ "@babel/plugin-transform-exponentiation-operator": "^7.26.3",
+ "@babel/plugin-transform-export-namespace-from": "^7.25.9",
+ "@babel/plugin-transform-for-of": "^7.26.9",
+ "@babel/plugin-transform-function-name": "^7.25.9",
+ "@babel/plugin-transform-json-strings": "^7.25.9",
+ "@babel/plugin-transform-literals": "^7.25.9",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.25.9",
+ "@babel/plugin-transform-member-expression-literals": "^7.25.9",
+ "@babel/plugin-transform-modules-amd": "^7.25.9",
+ "@babel/plugin-transform-modules-commonjs": "^7.26.3",
+ "@babel/plugin-transform-modules-systemjs": "^7.25.9",
+ "@babel/plugin-transform-modules-umd": "^7.25.9",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9",
+ "@babel/plugin-transform-new-target": "^7.25.9",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6",
+ "@babel/plugin-transform-numeric-separator": "^7.25.9",
+ "@babel/plugin-transform-object-rest-spread": "^7.25.9",
+ "@babel/plugin-transform-object-super": "^7.25.9",
+ "@babel/plugin-transform-optional-catch-binding": "^7.25.9",
+ "@babel/plugin-transform-optional-chaining": "^7.25.9",
+ "@babel/plugin-transform-parameters": "^7.25.9",
+ "@babel/plugin-transform-private-methods": "^7.25.9",
+ "@babel/plugin-transform-private-property-in-object": "^7.25.9",
+ "@babel/plugin-transform-property-literals": "^7.25.9",
+ "@babel/plugin-transform-regenerator": "^7.25.9",
+ "@babel/plugin-transform-regexp-modifiers": "^7.26.0",
+ "@babel/plugin-transform-reserved-words": "^7.25.9",
+ "@babel/plugin-transform-shorthand-properties": "^7.25.9",
+ "@babel/plugin-transform-spread": "^7.25.9",
+ "@babel/plugin-transform-sticky-regex": "^7.25.9",
+ "@babel/plugin-transform-template-literals": "^7.26.8",
+ "@babel/plugin-transform-typeof-symbol": "^7.26.7",
+ "@babel/plugin-transform-unicode-escapes": "^7.25.9",
+ "@babel/plugin-transform-unicode-property-regex": "^7.25.9",
+ "@babel/plugin-transform-unicode-regex": "^7.25.9",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.25.9",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.10",
+ "babel-plugin-polyfill-corejs3": "^0.11.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.1",
+ "core-js-compat": "^3.40.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-env/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
+ "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
+ "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.1",
+ "@babel/parser": "^7.27.1",
+ "@babel/template": "^7.27.1",
+ "@babel/types": "^7.27.1",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
+ "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.1",
+ "@babel/types": "^7.27.1",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
+ "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@colors/colors": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
+ "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.17.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
+ "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
+ "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
+ "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
+ "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
+ "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
+ "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
+ "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
+ "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
+ "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
+ "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
+ "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
+ "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
+ "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
+ "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
+ "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
+ "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
+ "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
+ "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
+ "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
+ "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/checkbox": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.6.tgz",
+ "integrity": "sha512-62u896rWCtKKE43soodq5e/QcRsA22I+7/4Ov7LESWnKRO6BVo2A1DFLDmXL9e28TB0CfHc3YtkbPm7iwajqkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/figures": "^1.0.11",
+ "@inquirer/type": "^3.0.6",
+ "ansi-escapes": "^4.3.2",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz",
+ "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.7",
+ "@inquirer/type": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "10.1.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.11.tgz",
+ "integrity": "sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/figures": "^1.0.11",
+ "@inquirer/type": "^3.0.6",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/editor": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.11.tgz",
+ "integrity": "sha512-YoZr0lBnnLFPpfPSNsQ8IZyKxU47zPyVi9NLjCWtna52//M/xuL0PGPAxHxxYhdOhnvY2oBafoM+BI5w/JK7jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/type": "^3.0.6",
+ "external-editor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/expand": {
+ "version": "4.0.13",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.13.tgz",
+ "integrity": "sha512-HgYNWuZLHX6q5y4hqKhwyytqAghmx35xikOGY3TcgNiElqXGPas24+UzNPOwGUZa5Dn32y25xJqVeUcGlTv+QQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/type": "^3.0.6",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz",
+ "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/input": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.10.tgz",
+ "integrity": "sha512-kV3BVne3wJ+j6reYQUZi/UN9NZGZLxgc/tfyjeK3mrx1QI7RXPxGp21IUTv+iVHcbP4ytZALF8vCHoxyNSC6qg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/type": "^3.0.6"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/number": {
+ "version": "3.0.13",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.13.tgz",
+ "integrity": "sha512-IrLezcg/GWKS8zpKDvnJ/YTflNJdG0qSFlUM/zNFsdi4UKW/CO+gaJpbMgQ20Q58vNKDJbEzC6IebdkprwL6ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/type": "^3.0.6"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/password": {
+ "version": "4.0.13",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.13.tgz",
+ "integrity": "sha512-NN0S/SmdhakqOTJhDwOpeBEEr8VdcYsjmZHDb0rblSh2FcbXQOr+2IApP7JG4WE3sxIdKytDn4ed3XYwtHxmJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/type": "^3.0.6",
+ "ansi-escapes": "^4.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/prompts": {
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz",
+ "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/checkbox": "^4.1.2",
+ "@inquirer/confirm": "^5.1.6",
+ "@inquirer/editor": "^4.2.7",
+ "@inquirer/expand": "^4.0.9",
+ "@inquirer/input": "^4.1.6",
+ "@inquirer/number": "^3.0.9",
+ "@inquirer/password": "^4.0.9",
+ "@inquirer/rawlist": "^4.0.9",
+ "@inquirer/search": "^3.0.9",
+ "@inquirer/select": "^4.0.9"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/rawlist": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.1.tgz",
+ "integrity": "sha512-VBUC0jPN2oaOq8+krwpo/mf3n/UryDUkKog3zi+oIi8/e5hykvdntgHUB9nhDM78RubiyR1ldIOfm5ue+2DeaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/type": "^3.0.6",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/search": {
+ "version": "3.0.13",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.13.tgz",
+ "integrity": "sha512-9g89d2c5Izok/Gw/U7KPC3f9kfe5rA1AJ24xxNZG0st+vWekSk7tB9oE+dJv5JXd0ZSijomvW0KPMoBd8qbN4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/figures": "^1.0.11",
+ "@inquirer/type": "^3.0.6",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/select": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.1.tgz",
+ "integrity": "sha512-gt1Kd5XZm+/ddemcT3m23IP8aD8rC9drRckWoP/1f7OL46Yy2FGi8DSmNjEjQKtPl6SV96Kmjbl6p713KXJ/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.11",
+ "@inquirer/figures": "^1.0.11",
+ "@inquirer/type": "^3.0.6",
+ "ansi-escapes": "^4.3.2",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz",
+ "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@jsonjoy.com/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pack": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz",
+ "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/base64": "^1.1.1",
+ "@jsonjoy.com/util": "^1.1.2",
+ "hyperdyperid": "^1.2.0",
+ "thingies": "^1.20.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/util": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz",
+ "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@leichtgewicht/ip-codec": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@listr2/prompt-adapter-inquirer": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz",
+ "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/type": "^1.5.5"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@inquirer/prompts": ">= 3 < 8"
+ }
+ },
+ "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz",
+ "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
+ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@lmdb/lmdb-darwin-arm64": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz",
+ "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@lmdb/lmdb-darwin-x64": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz",
+ "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@lmdb/lmdb-linux-arm": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz",
+ "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@lmdb/lmdb-linux-arm64": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz",
+ "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@lmdb/lmdb-linux-x64": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz",
+ "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@lmdb/lmdb-win32-x64": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz",
+ "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
+ "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
+ "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
+ "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
+ "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
+ "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
+ "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@napi-rs/nice": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz",
+ "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/nice-android-arm-eabi": "1.0.1",
+ "@napi-rs/nice-android-arm64": "1.0.1",
+ "@napi-rs/nice-darwin-arm64": "1.0.1",
+ "@napi-rs/nice-darwin-x64": "1.0.1",
+ "@napi-rs/nice-freebsd-x64": "1.0.1",
+ "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1",
+ "@napi-rs/nice-linux-arm64-gnu": "1.0.1",
+ "@napi-rs/nice-linux-arm64-musl": "1.0.1",
+ "@napi-rs/nice-linux-ppc64-gnu": "1.0.1",
+ "@napi-rs/nice-linux-riscv64-gnu": "1.0.1",
+ "@napi-rs/nice-linux-s390x-gnu": "1.0.1",
+ "@napi-rs/nice-linux-x64-gnu": "1.0.1",
+ "@napi-rs/nice-linux-x64-musl": "1.0.1",
+ "@napi-rs/nice-win32-arm64-msvc": "1.0.1",
+ "@napi-rs/nice-win32-ia32-msvc": "1.0.1",
+ "@napi-rs/nice-win32-x64-msvc": "1.0.1"
+ }
+ },
+ "node_modules/@napi-rs/nice-android-arm-eabi": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz",
+ "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-android-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz",
+ "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-freebsd-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz",
+ "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-arm-gnueabihf": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz",
+ "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-arm64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz",
+ "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-arm64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz",
+ "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-ppc64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz",
+ "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-riscv64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz",
+ "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-s390x-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz",
+ "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-x64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz",
+ "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-linux-x64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz",
+ "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-win32-arm64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz",
+ "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-win32-ia32-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz",
+ "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/nice-win32-x64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz",
+ "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@ngtools/webpack": {
+ "version": "19.2.12",
+ "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.12.tgz",
+ "integrity": "sha512-MTxkM+jZPQP55q0BWx/1w2kaN9mSFC14V9+p4sfNm/OXk7fibtxz5lXH/2sDGFWJi36s4gppKqfHBhp9OTdHCQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0",
+ "typescript": ">=5.5 <5.9",
+ "webpack": "^5.54.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@npmcli/agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz",
+ "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.1",
+ "lru-cache": "^10.0.1",
+ "socks-proxy-agent": "^8.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/agent/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz",
+ "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz",
+ "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^8.0.0",
+ "ini": "^5.0.0",
+ "lru-cache": "^10.0.1",
+ "npm-pick-manifest": "^10.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@npmcli/git/node_modules/which": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+ "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/installed-package-contents": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz",
+ "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-bundled": "^4.0.0",
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "bin": {
+ "installed-package-contents": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/node-gyp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz",
+ "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.1.tgz",
+ "integrity": "sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^8.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.5.3",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz",
+ "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn/node_modules/which": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+ "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/redact": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz",
+ "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/run-script": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz",
+ "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/node-gyp": "^4.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "node-gyp": "^11.0.0",
+ "proc-log": "^5.0.0",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/which": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+ "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+ "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher/node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/@parcel/watcher/node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
+ "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
+ "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
+ "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
+ "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
+ "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
+ "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
+ "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
+ "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
+ "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
+ "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
+ "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
+ "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
+ "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
+ "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
+ "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
+ "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
+ "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
+ "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
+ "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
+ "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@schematics/angular": {
+ "version": "19.2.12",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.12.tgz",
+ "integrity": "sha512-6S6tclFctLrjMvhpi8eVvswIpXqlybRpZLCTWyVeWIC6PHYLEyFmFoOhuhcSmOdtnwudvzOt6xWnWEVb3qXZbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "19.2.12",
+ "@angular-devkit/schematics": "19.2.12",
+ "jsonc-parser": "3.3.1"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@sigstore/bundle": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz",
+ "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/protobuf-specs": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.2.tgz",
+ "integrity": "sha512-F2ye+n1INNhqT0MW+LfUEvTUPc/nS70vICJcxorKl7/gV9CO39+EDCw+qHNKEqvsDWk++yGVKCbzK1qLPvmC8g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/sign": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz",
+ "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "make-fetch-happen": "^14.0.2",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/tuf": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz",
+ "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.1",
+ "tuf-js": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/verify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz",
+ "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
+ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tufjs/canonical-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
+ "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@tufjs/models": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz",
+ "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/canonical-json": "2.0.0",
+ "minimatch": "^9.0.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@tufjs/models/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@tufjs/models/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.5",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/bonjour": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz",
+ "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect-history-api-fallback": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz",
+ "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express-serve-static-core": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.18",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.18.tgz",
+ "integrity": "sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
+ "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/express/node_modules/@types/express-serve-static-core": {
+ "version": "4.19.6",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
+ "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.16",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
+ "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/jasmine": {
+ "version": "5.1.8",
+ "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.8.tgz",
+ "integrity": "sha512-u7/CnvRdh6AaaIzYjCgUuVbREFgulhX05Qtf6ZtW+aOcjCKKVvKgpkPYJBFTZSHtFBYimzU4zP0V2vrEsq9Wcg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.15.18",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
+ "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/node-forge": {
+ "version": "1.3.11",
+ "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
+ "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.18",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
+ "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
+ "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-index": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz",
+ "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.7",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+ "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/sockjs": {
+ "version": "0.3.36",
+ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
+ "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@vitejs/plugin-basic-ssl": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz",
+ "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/abbrev": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
+ "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/adjust-sourcemap-loader": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz",
+ "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "regex-parser": "^2.2.11"
+ },
+ "engines": {
+ "node": ">=8.9"
+ }
+ },
+ "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+ "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-html-community": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
+ "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
+ "dev": true,
+ "engines": [
+ "node >= 0.8.0"
+ ],
+ "license": "Apache-2.0",
+ "bin": {
+ "ansi-html": "bin/ansi-html"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.20",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+ "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.23.3",
+ "caniuse-lite": "^1.0.30001646",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/babel-loader": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
+ "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-cache-dir": "^4.0.0",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0",
+ "webpack": ">=5"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.13",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz",
+ "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.22.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.4",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
+ "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.3",
+ "core-js-compat": "^3.40.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz",
+ "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/beasties": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.2.tgz",
+ "integrity": "sha512-p4AF8uYzm9Fwu8m/hSVTCPXrRBPmB34hQpHsec2KOaR9CZmgoU8IOv4Cvwq4hgz2p4hLMNbsdNl5XeA6XbAQwA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "htmlparser2": "^10.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.49",
+ "postcss-media-query-parser": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bonjour-service": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
+ "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
+ "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001716",
+ "electron-to-chromium": "^1.5.149",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "19.0.1",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz",
+ "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/fs": "^4.0.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^10.2.2",
+ "lru-cache": "^10.0.1",
+ "minipass": "^7.0.3",
+ "minipass-collect": "^2.0.1",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^7.0.2",
+ "ssri": "^12.0.0",
+ "tar": "^7.4.3",
+ "unique-filename": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/cacache/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cacache/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/cacache/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cacache/node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001718",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
+ "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/clone-deep/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/common-path-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
+ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
+ "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compression/node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/connect": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
+ "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "finalhandler": "1.1.2",
+ "parseurl": "~1.3.3",
+ "utils-merge": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/connect-history-api-fallback": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+ "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/connect/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/connect/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/copy-anything": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
+ "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^3.14.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/copy-webpack-plugin": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz",
+ "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.1",
+ "globby": "^14.0.0",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.2.0",
+ "serialize-javascript": "^6.0.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ }
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.42.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz",
+ "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-loader": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
+ "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.1.0",
+ "postcss": "^8.4.33",
+ "postcss-modules-extract-imports": "^3.1.0",
+ "postcss-modules-local-by-default": "^4.0.5",
+ "postcss-modules-scope": "^3.2.0",
+ "postcss-modules-values": "^4.0.0",
+ "postcss-value-parser": "^4.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "webpack": "^5.27.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
+ "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/custom-event": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+ "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/date-format": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
+ "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
+ "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
+ "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/di": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+ "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dns-packet": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
+ "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dom-serialize": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+ "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "custom-event": "~1.0.0",
+ "ent": "~2.2.0",
+ "extend": "^3.0.0",
+ "void-elements": "^2.0.0"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.155",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
+ "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
+ "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/ent": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz",
+ "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "punycode": "^1.4.1",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/errno": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+ "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
+ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.4",
+ "@esbuild/android-arm": "0.25.4",
+ "@esbuild/android-arm64": "0.25.4",
+ "@esbuild/android-x64": "0.25.4",
+ "@esbuild/darwin-arm64": "0.25.4",
+ "@esbuild/darwin-x64": "0.25.4",
+ "@esbuild/freebsd-arm64": "0.25.4",
+ "@esbuild/freebsd-x64": "0.25.4",
+ "@esbuild/linux-arm": "0.25.4",
+ "@esbuild/linux-arm64": "0.25.4",
+ "@esbuild/linux-ia32": "0.25.4",
+ "@esbuild/linux-loong64": "0.25.4",
+ "@esbuild/linux-mips64el": "0.25.4",
+ "@esbuild/linux-ppc64": "0.25.4",
+ "@esbuild/linux-riscv64": "0.25.4",
+ "@esbuild/linux-s390x": "0.25.4",
+ "@esbuild/linux-x64": "0.25.4",
+ "@esbuild/netbsd-arm64": "0.25.4",
+ "@esbuild/netbsd-x64": "0.25.4",
+ "@esbuild/openbsd-arm64": "0.25.4",
+ "@esbuild/openbsd-x64": "0.25.4",
+ "@esbuild/sunos-x64": "0.25.4",
+ "@esbuild/win32-arm64": "0.25.4",
+ "@esbuild/win32-ia32": "0.25.4",
+ "@esbuild/win32-x64": "0.25.4"
+ }
+ },
+ "node_modules/esbuild-wasm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.4.tgz",
+ "integrity": "sha512-2HlCS6rNvKWaSKhWaG/YIyRsTsL3gUrMP2ToZMBIjw9LM7vVcIs+rz8kE2vExvTJgvM8OKPqNpcHawY/BQc/qQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
+ "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/express/node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-uri": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
+ "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/finalhandler/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/find-cache-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz",
+ "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "common-path-prefix": "^3.0.0",
+ "pkg-dir": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
+ "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^7.1.0",
+ "path-exists": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+ "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
+ "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^2.1.0",
+ "fast-glob": "^3.3.3",
+ "ignore": "^7.0.3",
+ "path-type": "^6.0.0",
+ "slash": "^5.1.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/handle-thing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz",
+ "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/hpack.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+ "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "obuf": "^1.0.0",
+ "readable-stream": "^2.0.1",
+ "wbuf": "^1.1.0"
+ }
+ },
+ "node_modules/hpack.js/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/hpack.js/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hpack.js/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
+ "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.1",
+ "entities": "^6.0.0"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
+ "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-deceiver": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+ "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-parser-js": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
+ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http-proxy-middleware": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz",
+ "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-proxy": "^1.17.15",
+ "debug": "^4.3.6",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.3",
+ "is-plain-object": "^5.0.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/hyperdyperid": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
+ "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.18"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz",
+ "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-walk": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz",
+ "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/ignore-walk/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/ignore-walk/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/image-size": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+ "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "image-size": "bin/image-size.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
+ "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
+ "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
+ "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-network-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
+ "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
+ "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isbinaryfile": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
+ "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.23.9",
+ "@babel/parser": "^7.23.9",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jasmine-core": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz",
+ "integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz",
+ "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonc-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
+ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+ "dev": true,
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/karma": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
+ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@colors/colors": "1.5.0",
+ "body-parser": "^1.19.0",
+ "braces": "^3.0.2",
+ "chokidar": "^3.5.1",
+ "connect": "^3.7.0",
+ "di": "^0.0.1",
+ "dom-serialize": "^2.2.1",
+ "glob": "^7.1.7",
+ "graceful-fs": "^4.2.6",
+ "http-proxy": "^1.18.1",
+ "isbinaryfile": "^4.0.8",
+ "lodash": "^4.17.21",
+ "log4js": "^6.4.1",
+ "mime": "^2.5.2",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.5",
+ "qjobs": "^1.2.0",
+ "range-parser": "^1.2.1",
+ "rimraf": "^3.0.2",
+ "socket.io": "^4.7.2",
+ "source-map": "^0.6.1",
+ "tmp": "^0.2.1",
+ "ua-parser-js": "^0.7.30",
+ "yargs": "^16.1.1"
+ },
+ "bin": {
+ "karma": "bin/karma"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/karma-chrome-launcher": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz",
+ "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which": "^1.2.1"
+ }
+ },
+ "node_modules/karma-coverage": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz",
+ "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.2.0",
+ "istanbul-lib-instrument": "^5.1.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.1",
+ "istanbul-reports": "^3.0.5",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma-coverage/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/karma-jasmine": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",
+ "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jasmine-core": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "karma": "^6.0.0"
+ }
+ },
+ "node_modules/karma-jasmine-html-reporter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz",
+ "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "jasmine-core": "^4.0.0 || ^5.0.0",
+ "karma": "^6.0.0",
+ "karma-jasmine": "^5.0.0"
+ }
+ },
+ "node_modules/karma-jasmine/node_modules/jasmine-core": {
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz",
+ "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/karma-source-map-support": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz",
+ "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map-support": "^0.5.5"
+ }
+ },
+ "node_modules/karma/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/karma/node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/karma/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/karma/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/karma/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/karma/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/karma/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/karma/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/tmp": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
+ "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/karma/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/karma/node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/karma/node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/launch-editor": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
+ "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.0.0",
+ "shell-quote": "^1.8.1"
+ }
+ },
+ "node_modules/less": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz",
+ "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "copy-anything": "^2.0.1",
+ "parse-node-version": "^1.0.1",
+ "tslib": "^2.3.0"
+ },
+ "bin": {
+ "lessc": "bin/lessc"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "optionalDependencies": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "make-dir": "^2.1.0",
+ "mime": "^1.4.1",
+ "needle": "^3.1.0",
+ "source-map": "~0.6.0"
+ }
+ },
+ "node_modules/less-loader": {
+ "version": "12.2.0",
+ "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz",
+ "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "less": "^3.5.0 || ^4.0.0",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/less/node_modules/make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/less/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/less/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/less/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/license-webpack-plugin": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz",
+ "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "webpack-sources": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ },
+ "webpack-sources": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2": {
+ "version": "8.2.5",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz",
+ "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^4.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/lmdb": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz",
+ "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "msgpackr": "^1.11.2",
+ "node-addon-api": "^6.1.0",
+ "node-gyp-build-optional-packages": "5.2.2",
+ "ordered-binary": "^1.5.3",
+ "weak-lru-cache": "^1.2.2"
+ },
+ "bin": {
+ "download-lmdb-prebuilds": "bin/download-prebuilds.js"
+ },
+ "optionalDependencies": {
+ "@lmdb/lmdb-darwin-arm64": "3.2.6",
+ "@lmdb/lmdb-darwin-x64": "3.2.6",
+ "@lmdb/lmdb-linux-arm": "3.2.6",
+ "@lmdb/lmdb-linux-arm64": "3.2.6",
+ "@lmdb/lmdb-linux-x64": "3.2.6",
+ "@lmdb/lmdb-win32-x64": "3.2.6"
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
+ "node_modules/loader-utils": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
+ "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
+ "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^6.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-escapes": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
+ "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
+ "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
+ "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log4js": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz",
+ "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "date-format": "^4.0.14",
+ "debug": "^4.3.4",
+ "flatted": "^3.2.7",
+ "rfdc": "^1.3.0",
+ "streamroller": "^3.1.5"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz",
+ "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/agent": "^3.0.0",
+ "cacache": "^19.0.1",
+ "http-cache-semantics": "^4.1.1",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^1.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "ssri": "^12.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memfs": {
+ "version": "4.17.2",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz",
+ "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/json-pack": "^1.0.3",
+ "@jsonjoy.com/util": "^1.3.0",
+ "tree-dump": "^1.0.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mini-css-extract-plugin": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz",
+ "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "schema-utils": "^4.0.0",
+ "tapable": "^2.2.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
+ "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz",
+ "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.0.3",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minizlib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
+ "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/msgpackr": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.3.tgz",
+ "integrity": "sha512-mNdO4s/W54QCghwGNSqO5ULVJ6QUimP/1hRlWVx5f7frTLaClg+4sBRjUTgP1OrBRgVtkH1tI9vi4Dqg/JX3Kg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "optionalDependencies": {
+ "msgpackr-extract": "^3.0.2"
+ }
+ },
+ "node_modules/msgpackr-extract": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
+ "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "node-gyp-build-optional-packages": "5.2.2"
+ },
+ "bin": {
+ "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
+ },
+ "optionalDependencies": {
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
+ }
+ },
+ "node_modules/multicast-dns": {
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+ "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dns-packet": "^5.2.2",
+ "thunky": "^1.0.2"
+ },
+ "bin": {
+ "multicast-dns": "cli.js"
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "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/needle": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
+ "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "needle": "bin/needle"
+ },
+ "engines": {
+ "node": ">= 4.4.x"
+ }
+ },
+ "node_modules/needle/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-addon-api": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/node-forge": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+ "dev": true,
+ "license": "(BSD-3-Clause OR GPL-2.0)",
+ "engines": {
+ "node": ">= 6.13.0"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz",
+ "integrity": "sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^14.0.3",
+ "nopt": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "tar": "^7.4.3",
+ "tinyglobby": "^0.2.12",
+ "which": "^5.0.0"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/node-gyp-build-optional-packages": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
+ "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.1"
+ },
+ "bin": {
+ "node-gyp-build-optional-packages": "bin.js",
+ "node-gyp-build-optional-packages-optional": "optional.js",
+ "node-gyp-build-optional-packages-test": "build-test.js"
+ }
+ },
+ "node_modules/node-gyp/node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/node-gyp/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/node-gyp/node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/node-gyp/node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/node-gyp/node_modules/which": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+ "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
+ "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^3.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-bundled": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz",
+ "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz",
+ "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
+ "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz",
+ "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-packlist": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz",
+ "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "ignore-walk": "^7.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz",
+ "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^7.1.0",
+ "npm-normalize-package-bin": "^4.0.0",
+ "npm-package-arg": "^12.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-registry-fetch": {
+ "version": "18.0.2",
+ "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz",
+ "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/redact": "^3.0.0",
+ "jsonparse": "^1.3.1",
+ "make-fetch-happen": "^14.0.0",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minizlib": "^3.0.1",
+ "npm-package-arg": "^12.0.0",
+ "proc-log": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
+ "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ora/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ordered-binary": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz",
+ "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
+ "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
+ "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz",
+ "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
+ "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.2",
+ "is-network-error": "^1.0.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry/node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/pacote": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz",
+ "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "@npmcli/run-script": "^9.0.0",
+ "cacache": "^19.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^12.0.0",
+ "npm-packlist": "^9.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "pacote": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-json/node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse-node-version": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+ "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-html-rewriting-stream": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz",
+ "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.3.0",
+ "parse5": "^7.0.0",
+ "parse5-sax-parser": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-sax-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz",
+ "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
+ "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
+ "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
+ "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/piscina": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz",
+ "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "@napi-rs/nice": "^1.0.1"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
+ "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
+ "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
+ "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.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-loader": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
+ "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cosmiconfig": "^9.0.0",
+ "jiti": "^1.20.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "postcss": "^7.0.0 || ^8.0.1",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-media-query-parser": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
+ "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+ "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
+ "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^7.0.0",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+ "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
+ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/proc-log": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
+ "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-addr/node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qjobs": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
+ "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.9"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
+ "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regex-parser": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz",
+ "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regexpu-core": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz",
+ "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.2.0",
+ "regjsgen": "^0.8.0",
+ "regjsparser": "^0.12.0",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regjsparser": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
+ "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.0.2"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/regjsparser/node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-url-loader": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz",
+ "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "adjust-sourcemap-loader": "^4.0.0",
+ "convert-source-map": "^1.7.0",
+ "loader-utils": "^2.0.0",
+ "postcss": "^8.2.14",
+ "source-map": "0.6.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/loader-utils": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+ "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.34.8",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
+ "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.34.8",
+ "@rollup/rollup-android-arm64": "4.34.8",
+ "@rollup/rollup-darwin-arm64": "4.34.8",
+ "@rollup/rollup-darwin-x64": "4.34.8",
+ "@rollup/rollup-freebsd-arm64": "4.34.8",
+ "@rollup/rollup-freebsd-x64": "4.34.8",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
+ "@rollup/rollup-linux-arm-musleabihf": "4.34.8",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.8",
+ "@rollup/rollup-linux-arm64-musl": "4.34.8",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
+ "@rollup/rollup-linux-riscv64-gnu": "4.34.8",
+ "@rollup/rollup-linux-s390x-gnu": "4.34.8",
+ "@rollup/rollup-linux-x64-gnu": "4.34.8",
+ "@rollup/rollup-linux-x64-musl": "4.34.8",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.8",
+ "@rollup/rollup-win32-ia32-msvc": "4.34.8",
+ "@rollup/rollup-win32-x64-msvc": "4.34.8",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
+ "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sass": {
+ "version": "1.85.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
+ "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/sass-loader": {
+ "version": "16.0.5",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
+ "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "neo-async": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
+ "sass": "^1.3.0",
+ "sass-embedded": "*",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "node-sass": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/schema-utils": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
+ "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/select-hose": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selfsigned": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz",
+ "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node-forge": "^1.3.0",
+ "node-forge": "^1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/send/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.6.2",
+ "mime-types": "~2.1.17",
+ "parseurl": "~1.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/serve-index/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-index/node_modules/setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-static/node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
+ "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sigstore": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz",
+ "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "@sigstore/sign": "^3.1.0",
+ "@sigstore/tuf": "^3.1.0",
+ "@sigstore/verify": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/slash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.3.4",
+ "ws": "~8.17.1"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sockjs": {
+ "version": "0.3.24",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+ "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "faye-websocket": "^0.11.3",
+ "uuid": "^8.3.2",
+ "websocket-driver": "^0.7.4"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
+ "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "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/source-map-loader": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz",
+ "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.72.1"
+ }
+ },
+ "node_modules/source-map-loader/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.21",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
+ "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/spdy": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "handle-thing": "^2.0.0",
+ "http-deceiver": "^1.2.7",
+ "select-hose": "^2.0.0",
+ "spdy-transport": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/spdy-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+ "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "detect-node": "^2.0.4",
+ "hpack.js": "^2.1.6",
+ "obuf": "^1.1.2",
+ "readable-stream": "^3.0.6",
+ "wbuf": "^1.7.3"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ssri": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
+ "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/streamroller": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
+ "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "date-format": "^4.0.14",
+ "debug": "^4.3.4",
+ "fs-extra": "^8.1.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-observable": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
+ "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/tar/node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/terser": {
+ "version": "5.39.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
+ "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.14",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
+ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "serialize-javascript": "^6.0.2",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/thingies": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz",
+ "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==",
+ "dev": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=10.18"
+ },
+ "peerDependencies": {
+ "tslib": "^2"
+ }
+ },
+ "node_modules/thunky": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
+ "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tree-dump": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz",
+ "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "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==",
+ "license": "0BSD"
+ },
+ "node_modules/tuf-js": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz",
+ "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/models": "3.0.1",
+ "debug": "^4.3.6",
+ "make-fetch-happen": "^14.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-assert": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz",
+ "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/typescript": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ua-parser-js": {
+ "version": "0.7.40",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz",
+ "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
+ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unique-filename": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz",
+ "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "unique-slug": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz",
+ "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz",
+ "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
+ "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
+ "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
+ "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
+ "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
+ "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
+ "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
+ "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
+ "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
+ "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
+ "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
+ "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
+ "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
+ "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
+ "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
+ "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
+ "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
+ "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
+ "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
+ "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/vite/node_modules/postcss": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "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",
+ "peer": true,
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/vite/node_modules/rollup": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
+ "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "1.0.7"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.40.2",
+ "@rollup/rollup-android-arm64": "4.40.2",
+ "@rollup/rollup-darwin-arm64": "4.40.2",
+ "@rollup/rollup-darwin-x64": "4.40.2",
+ "@rollup/rollup-freebsd-arm64": "4.40.2",
+ "@rollup/rollup-freebsd-x64": "4.40.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.40.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.40.2",
+ "@rollup/rollup-linux-arm64-musl": "4.40.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.40.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-musl": "4.40.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.40.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.40.2",
+ "@rollup/rollup-win32-x64-msvc": "4.40.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/void-elements": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
+ "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
+ "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/wbuf": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+ "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/weak-lru-cache": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz",
+ "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/webpack": {
+ "version": "5.98.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
+ "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.6",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.14.0",
+ "browserslist": "^4.24.0",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.17.1",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.0",
+ "tapable": "^2.1.1",
+ "terser-webpack-plugin": "^5.3.11",
+ "watchpack": "^2.4.1",
+ "webpack-sources": "^3.2.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-middleware": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz",
+ "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.10",
+ "memfs": "^4.6.0",
+ "mime-types": "^2.1.31",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-server": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz",
+ "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/bonjour": "^3.5.13",
+ "@types/connect-history-api-fallback": "^1.5.4",
+ "@types/express": "^4.17.21",
+ "@types/serve-index": "^1.9.4",
+ "@types/serve-static": "^1.15.5",
+ "@types/sockjs": "^0.3.36",
+ "@types/ws": "^8.5.10",
+ "ansi-html-community": "^0.0.8",
+ "bonjour-service": "^1.2.1",
+ "chokidar": "^3.6.0",
+ "colorette": "^2.0.10",
+ "compression": "^1.7.4",
+ "connect-history-api-fallback": "^2.0.0",
+ "express": "^4.21.2",
+ "graceful-fs": "^4.2.6",
+ "http-proxy-middleware": "^2.0.7",
+ "ipaddr.js": "^2.1.0",
+ "launch-editor": "^2.6.1",
+ "open": "^10.0.3",
+ "p-retry": "^6.2.0",
+ "schema-utils": "^4.2.0",
+ "selfsigned": "^2.4.1",
+ "serve-index": "^1.9.1",
+ "sockjs": "^0.3.24",
+ "spdy": "^4.0.2",
+ "webpack-dev-middleware": "^7.4.2",
+ "ws": "^8.18.0"
+ },
+ "bin": {
+ "webpack-dev-server": "bin/webpack-dev-server.js"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ },
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
+ "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/ws": {
+ "version": "8.18.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
+ "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
+ "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "flat": "^5.0.2",
+ "wildcard": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack-subresource-integrity": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz",
+ "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "typed-assert": "^1.0.8"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "html-webpack-plugin": ">= 5.0.0-beta.1 < 6",
+ "webpack": "^5.12.0"
+ },
+ "peerDependenciesMeta": {
+ "html-webpack-plugin": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack/node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
+ "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
+ "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zone.js": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz",
+ "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/package.json b/flatland-hmi-hack4rail/frontend/package.json
new file mode 100644
index 00000000..dc159579
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "ng test"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/common": "^19.2.0",
+ "@angular/compiler": "^19.2.0",
+ "@angular/core": "^19.2.0",
+ "@angular/forms": "^19.2.0",
+ "@angular/platform-browser": "^19.2.0",
+ "@angular/platform-browser-dynamic": "^19.2.0",
+ "@angular/router": "^19.2.0",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.15.0"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^19.2.3",
+ "@angular/cli": "^19.2.3",
+ "@angular/compiler-cli": "^19.2.0",
+ "@types/jasmine": "~5.1.0",
+ "jasmine-core": "~5.6.0",
+ "karma": "~6.4.0",
+ "karma-chrome-launcher": "~3.2.0",
+ "karma-coverage": "~2.2.0",
+ "karma-jasmine": "~5.1.0",
+ "karma-jasmine-html-reporter": "~2.1.0",
+ "typescript": "~5.7.2"
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/public/favicon.ico b/flatland-hmi-hack4rail/frontend/public/favicon.ico
new file mode 100644
index 00000000..57614f9c
Binary files /dev/null and b/flatland-hmi-hack4rail/frontend/public/favicon.ico differ
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Bahnhof.svg b/flatland-hmi-hack4rail/frontend/public/svg/Bahnhof.svg
new file mode 100644
index 00000000..b1bb3fd1
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Bahnhof.svg
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Deadend.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Deadend.svg
new file mode 100644
index 00000000..5bfa0f10
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Deadend.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Diamond_Crossing.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Diamond_Crossing.svg
new file mode 100644
index 00000000..885e0437
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Diamond_Crossing.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_oben_links.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_oben_links.svg
new file mode 100644
index 00000000..6c9557ad
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_oben_links.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_oben_rechts.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_oben_rechts.svg
new file mode 100644
index 00000000..e4da9682
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_oben_rechts.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_unten_links.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_unten_links.svg
new file mode 100644
index 00000000..412637b8
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_unten_links.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_unten_rechts.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_unten_rechts.svg
new file mode 100644
index 00000000..3728174a
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_Kurve_unten_rechts.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_horizontal.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_horizontal.svg
new file mode 100644
index 00000000..3465220f
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_horizontal.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Gleis_vertikal.svg b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_vertikal.svg
new file mode 100644
index 00000000..79225067
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Gleis_vertikal.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Bergwelt_B.svg b/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Bergwelt_B.svg
new file mode 100644
index 00000000..88c4a25f
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Bergwelt_B.svg
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Laubbaume_A.svg b/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Laubbaume_A.svg
new file mode 100644
index 00000000..ee2ef762
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Laubbaume_A.svg
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Nadelbaume_B.svg b/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Nadelbaume_B.svg
new file mode 100644
index 00000000..39e3da78
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Scenery-Nadelbaume_B.svg
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Scenery_Water.svg b/flatland-hmi-hack4rail/frontend/public/svg/Scenery_Water.svg
new file mode 100644
index 00000000..27ae56d0
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Scenery_Water.svg
@@ -0,0 +1,143 @@
+
+
+
+image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Double_Slip.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Double_Slip.svg
new file mode 100644
index 00000000..567a30b9
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Double_Slip.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Single_Slip.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Single_Slip.svg
new file mode 100644
index 00000000..c83a0699
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Single_Slip.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Symetrical.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Symetrical.svg
new file mode 100644
index 00000000..948e2eb3
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_Symetrical.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_oben_links.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_oben_links.svg
new file mode 100644
index 00000000..4177f31b
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_oben_links.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_oben_rechts.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_oben_rechts.svg
new file mode 100644
index 00000000..264d1d48
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_oben_rechts.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_unten_links.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_unten_links.svg
new file mode 100644
index 00000000..69cf1fab
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_unten_links.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_unten_rechts.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_unten_rechts.svg
new file mode 100644
index 00000000..eebdbc73
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_horizontal_unten_rechts.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_oben_links.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_oben_links.svg
new file mode 100644
index 00000000..687c0cf1
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_oben_links.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_oben_rechts.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_oben_rechts.svg
new file mode 100644
index 00000000..ebda89e6
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_oben_rechts.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_unten_links.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_unten_links.svg
new file mode 100644
index 00000000..fed61a5d
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_unten_links.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_unten_rechts.svg b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_unten_rechts.svg
new file mode 100644
index 00000000..5873c151
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Weiche_vertikal_unten_rechts.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/public/svg/Zug_Gleis.svg b/flatland-hmi-hack4rail/frontend/public/svg/Zug_Gleis.svg
new file mode 100644
index 00000000..a828b453
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/public/svg/Zug_Gleis.svg
@@ -0,0 +1 @@
+N_Zug_Gleis_#d50000
\ No newline at end of file
diff --git a/flatland-hmi-hack4rail/frontend/src/app/app.component.html b/flatland-hmi-hack4rail/frontend/src/app/app.component.html
new file mode 100644
index 00000000..0680b43f
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/app.component.html
@@ -0,0 +1 @@
+
diff --git a/flatland-hmi-hack4rail/frontend/src/app/app.component.scss b/flatland-hmi-hack4rail/frontend/src/app/app.component.scss
new file mode 100644
index 00000000..13175dd7
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/app.component.scss
@@ -0,0 +1,21 @@
+.container {
+ display: grid;
+ grid-template-columns: auto 700px auto;
+ grid-template-rows: auto auto auto;
+ width: 100%;
+ height: 100vh;
+ gap: 20px;
+}
+
+main {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ app-marey {
+ width: 100%;
+ height: 100vh;
+ display: block;
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/app.component.ts b/flatland-hmi-hack4rail/frontend/src/app/app.component.ts
new file mode 100644
index 00000000..d74fa746
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/app.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core'
+import { RouterOutlet } from '@angular/router'
+
+@Component({
+ selector: 'app-root',
+ imports: [RouterOutlet],
+ templateUrl: './app.component.html',
+ styleUrl: './app.component.scss',
+})
+export class AppComponent {}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/app.config.ts b/flatland-hmi-hack4rail/frontend/src/app/app.config.ts
new file mode 100644
index 00000000..eeb9dfe7
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/app.config.ts
@@ -0,0 +1,13 @@
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'
+import { provideRouter } from '@angular/router'
+
+import { routes } from './app.routes'
+import { provideHttpClient } from '@angular/common/http'
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
+ provideHttpClient(),
+ ],
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/app.routes.ts b/flatland-hmi-hack4rail/frontend/src/app/app.routes.ts
new file mode 100644
index 00000000..a1c8b3db
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/app.routes.ts
@@ -0,0 +1,9 @@
+import { Routes } from '@angular/router'
+import { MapComponent } from './map/map.component'
+import { MareyComponent } from './marey/marey.component'
+
+export const routes: Routes = [
+ { path: '', redirectTo: 'map', pathMatch: 'full' },
+ { path: 'map', component: MapComponent },
+ { path: 'marey', component: MareyComponent },
+]
diff --git a/flatland-hmi-hack4rail/frontend/src/app/controller.service.ts b/flatland-hmi-hack4rail/frontend/src/app/controller.service.ts
new file mode 100644
index 00000000..2594dbaf
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/controller.service.ts
@@ -0,0 +1,45 @@
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { firstValueFrom, Subject } from 'rxjs'
+
+const BACKEND_URL = 'http://localhost:5001'
+
+export interface State {
+ steps: number
+ done: {
+ __all__: boolean
+ [key: string]: boolean
+ }
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ControllerService {
+ private resetEvent = new Subject()
+
+ constructor(private http: HttpClient) {
+ // Removed auto-reset on load — simulation is controlled from SystemX
+ }
+
+ public stepEnv(policyIndex: number = 0) {
+ // Tell Flask brain to start running (simulation is continuous)
+ return firstValueFrom(
+ this.http.post(`${BACKEND_URL}/control`, { command: 'start' })
+ ).then(() => 0)
+ }
+
+ public resetEnv() {
+ // Reset via Flask brain's control endpoint
+ return firstValueFrom(
+ this.http.post(`${BACKEND_URL}/control`, { command: 'reset' })
+ ).then((state) => {
+ this.resetEvent.next()
+ return state as State
+ })
+ }
+
+ public observeReset() {
+ return this.resetEvent.asObservable()
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/data.service.ts b/flatland-hmi-hack4rail/frontend/src/app/data.service.ts
new file mode 100644
index 00000000..c85e40e3
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/data.service.ts
@@ -0,0 +1,38 @@
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { firstValueFrom } from 'rxjs'
+
+const BACKEND_URL = 'http://localhost:5001'
+
+export type Transitions = Array>
+
+export interface Agent {
+ position: [number, number] | null
+ direction: number
+ moving: boolean
+ target: [number, number]
+ malfunction: number
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class DataService {
+ constructor(private http: HttpClient) { }
+
+ public getTransitions() {
+ return firstValueFrom(this.http.get(`${BACKEND_URL}/transitions`))
+ }
+
+ public getAgents() {
+ return firstValueFrom(this.http.get>(`${BACKEND_URL}/agents`))
+ }
+
+ public getHistory() {
+ return firstValueFrom(this.http.get>>(`${BACKEND_URL}/history`))
+ }
+
+ public getPlans() {
+ return firstValueFrom(this.http.get>>>(`${BACKEND_URL}/plans`))
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/map/map.component.html b/flatland-hmi-hack4rail/frontend/src/app/map/map.component.html
new file mode 100644
index 00000000..18984c4b
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/map/map.component.html
@@ -0,0 +1,46 @@
+
+
+ @for (row of mapClasses; track $index) {
+
+ @for (col of row; track $index) {
+
+ @if (col.objects) {
+
+ }
+
+ }
+
+ }
+
+
+ @for (agent of agents; let i = $index; track $index) {
+ @if (agent.position) {
+
+
{{ agentNames[i] || 'Train_' + i }}
+ }
+ }
+
+
+
+ @for (st of stations; let si = $index; track si) {
+
+ }
+
+
diff --git a/flatland-hmi-hack4rail/frontend/src/app/map/map.component.scss b/flatland-hmi-hack4rail/frontend/src/app/map/map.component.scss
new file mode 100644
index 00000000..df90ad7e
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/map/map.component.scss
@@ -0,0 +1,264 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ width: 100%;
+}
+
+.controller {
+ display: flex;
+ gap: 8px;
+}
+
+.map {
+ position: relative;
+ display: inline-block;
+
+ .error {
+ background-color: salmon;
+ }
+
+ .environment {
+ z-index: 5;
+
+ .row {
+ display: flex;
+ flex-direction: row;
+ .cell {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ &.track {
+ &.rotation_90 {
+ transform: rotate(90deg);
+ }
+ &.rotation_180 {
+ transform: rotate(180deg);
+ }
+ &.rotation_270 {
+ transform: rotate(270deg);
+ }
+
+ &.transition_we {
+ background-image: url('/svg/Gleis_Deadend.svg');
+ }
+
+ &.transition_ww_ee_nn_ss {
+ background-image: url('/svg/Gleis_Diamond_Crossing.svg');
+ }
+
+ &.transition_ww_ee {
+ background-image: url('/svg/Gleis_horizontal.svg');
+ }
+
+ &.transition_en_sw {
+ background-image: url('/svg/Gleis_Kurve_oben_links.svg');
+ }
+
+ &.transition_wn_se {
+ background-image: url('/svg/Gleis_Kurve_oben_rechts.svg');
+ }
+
+ &.transition_es_nw {
+ background-image: url('/svg/Gleis_Kurve_unten_links.svg');
+ }
+
+ &.transition_ne_ws {
+ background-image: url('/svg/Gleis_Kurve_unten_rechts.svg');
+ }
+
+ &.transition_nn_ss {
+ background-image: url('/svg/Gleis_vertikal.svg');
+ }
+
+ &.transition_nn_ss_ee_ww_es_nw_se_wn {
+ background-image: url('/svg/Weiche_Double_Slip.svg');
+ }
+
+ &.transition_ee_ww_en_sw {
+ background-image: url('/svg/Weiche_horizontal_oben_links.svg');
+ }
+
+ &.transition_ee_ww_se_wn {
+ background-image: url('/svg/Weiche_horizontal_oben_rechts.svg');
+ }
+
+ &.transition_ee_ww_es_nw {
+ background-image: url('/svg/Weiche_horizontal_unten_links.svg');
+ }
+
+ &.transition_ee_ww_ne_ws {
+ background-image: url('/svg/Weiche_horizontal_unten_rechts.svg');
+ }
+
+ &.transition_nn_ss_ee_ww_nw_es {
+ background-image: url('/svg/Weiche_Single_Slip.svg');
+ }
+
+ &.transition_ne_nw_es_ws {
+ background-image: url('/svg/Weiche_Symetrical.svg');
+ }
+
+ &.transition_nn_ss_en_sw {
+ background-image: url('/svg/Weiche_vertikal_oben_links.svg');
+ }
+
+ &.transition_nn_ss_se_wn {
+ background-image: url('/svg/Weiche_vertikal_oben_rechts.svg');
+ }
+
+ &.transition_nn_ss_nw_es {
+ background-image: url('/svg/Weiche_vertikal_unten_links.svg');
+ }
+
+ &.transition_nn_ss_ne_ws {
+ background-image: url('/svg/Weiche_vertikal_unten_rechts.svg');
+ }
+ }
+
+ .target {
+ width: 100%;
+ height: 100%;
+ background-image: url('/svg/Bahnhof.svg');
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ &.rotation_90 {
+ transform: rotate(-90deg);
+ }
+ &.rotation_180 {
+ transform: rotate(-180deg);
+ }
+ &.rotation_270 {
+ transform: rotate(-270deg);
+ }
+ }
+ }
+ }
+ }
+
+ .agents {
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ position: absolute;
+ z-index: 10;
+
+ .agent {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ background-image: url('/svg/Zug_Gleis.svg');
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ &.direction_1 {
+ transform: rotate(90deg);
+ }
+ &.direction_2 {
+ transform: rotate(180deg);
+ }
+ &.direction_3 {
+ transform: rotate(270deg);
+ }
+
+ &.malfunction {
+ background-color: rgba(150, 150, 150, 0.5);
+ }
+ }
+
+ .agent-label {
+ position: absolute;
+ font-size: 9px;
+ font-weight: bold;
+ color: #fff;
+ background: rgba(0,0,0,0.65);
+ border-radius: 3px;
+ padding: 1px 3px;
+ white-space: nowrap;
+ pointer-events: none;
+ transform: translate(22px, 2px);
+ z-index: 11;
+ letter-spacing: 0.3px;
+ }
+ }
+}
+
+.plans {
+ display: flex;
+ gap: 20px;
+ justify-content: center;
+ align-items: center;
+ height: 120px;
+ width: 100%;
+ opacity: 0.5;
+ border-radius: 5px;
+ border: 2px solid #ccc;
+
+ &.interrupted {
+ opacity: 1;
+ border-color: #007bff;
+ .plan {
+ background-color: #007bff;
+ }
+ }
+
+ .plan {
+ border-radius: 3px;
+ background-color: lightgrey;
+ padding: 10px;
+ color: white;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 80px;
+ width: 200px;
+ cursor: pointer;
+ &:hover {
+ background-color: #0056b3;
+ }
+ &.selected {
+ background-color: #0056b3;
+ }
+ }
+}
+
+button {
+ background-color: #007bff;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 16px;
+
+ &:hover {
+ background-color: #0056b3;
+ }
+}
+
+.station-markers {
+ left: 0; right: 0; top: 0; bottom: 0;
+ position: absolute;
+ z-index: 8;
+ pointer-events: none;
+}
+
+.station-marker {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ background-image: url('/svg/Bahnhof.svg');
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/map/map.component.ts b/flatland-hmi-hack4rail/frontend/src/app/map/map.component.ts
new file mode 100644
index 00000000..911da0e1
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/map/map.component.ts
@@ -0,0 +1,138 @@
+import { Component, OnInit } from '@angular/core'
+import { firstValueFrom } from 'rxjs'
+import { StateService } from '../state.service'
+import { MapCell, RendererService } from '../renderer.service'
+import { Agent } from '../data.service'
+import { ControllerService } from '../controller.service'
+
+const BACKEND_URL = 'http://localhost:5001'
+
+@Component({
+ selector: 'app-map',
+ imports: [],
+ templateUrl: './map.component.html',
+ styleUrl: './map.component.scss',
+})
+export class MapComponent implements OnInit {
+ public mapClasses: Array> = []
+ public agents: Array = []
+ public plans: Array>> = []
+ public selectedPlan?: number
+ public interrupted: boolean = false
+ public hasMalfunction: boolean = false
+ public affectedIndices: Set = new Set()
+ public selectedIndex: number = -1
+ public agentNames: string[] = []
+ public stations: Array<{id: any, r: number, c: number, name: string, type?: string}> = []
+
+ private sessionRunning = false // track whether a scenario session is active
+ private pollTimer: any
+
+ constructor(
+ public stateService: StateService,
+ public rendererService: RendererService,
+ public controllerService: ControllerService,
+ ) {}
+
+ ngOnInit() {
+ this.stateService.getPlan().subscribe(p => { this.selectedPlan = p })
+ this.stateService.getNewMalfunction().subscribe(() => { this.interrupted = true })
+ this.stateService.getPlans().subscribe(plans => { this.plans = plans })
+
+ // Render map when transitions update
+ this.stateService.getTransitions().subscribe(transitions =>
+ firstValueFrom(this.stateService.getAgents()).then(agents => {
+ this.mapClasses = this.rendererService.renderMap(transitions, agents)
+ })
+ )
+
+ // Only update agent overlays when a session is running — prevents rogue trains in preview mode
+ this.stateService.getAgents().subscribe(agents => {
+ if (this.sessionRunning) {
+ this.agents = agents
+ this.hasMalfunction = agents.some(a => a.malfunction > 0)
+ } else {
+ this.agents = []
+ this.hasMalfunction = false
+ }
+ })
+
+ this.controllerService.observeReset().subscribe(() => {
+ this.interrupted = false
+ this.selectedPlan = undefined
+ })
+
+ // Status poll — drives sessionRunning flag and station/name loading
+ this.pollTimer = setInterval(() => this.pollStatus(), 500)
+ this.fetchStations()
+ setInterval(() => this.fetchStations(), 5000)
+
+ // Grid-change detection — re-render when scenario map changes
+ let lastGridKey = ''
+ setInterval(async () => {
+ try {
+ const res = await fetch(`${BACKEND_URL}/transitions`)
+ if (!res.ok) return
+ const grid: number[][] = await res.json()
+ if (!Array.isArray(grid) || grid.length === 0) return
+ const key = `${grid.length}x${grid[0]?.length ?? 0}_${(grid[0]?.[0] ?? 0)}_${(grid[Math.floor(grid.length/2)]?.[Math.floor((grid[0]?.length??0)/2)] ?? 0)}`
+ if (key === lastGridKey) return
+ lastGridKey = key
+
+ // Render new grid (agents already gated by sessionRunning — no rogue trains)
+ const newMap = this.rendererService.renderMap(grid as any, this.agents)
+ if (newMap && newMap.length > 0) this.mapClasses = newMap
+
+ // Fetch stations for the new map
+ this.stations = []
+ this.agentNames = []
+ setTimeout(() => this.fetchStations(), 300)
+ } catch {}
+ }, 2000)
+ }
+
+ async fetchStations() {
+ try {
+ const res = await fetch(`${BACKEND_URL}/stations`)
+ const data = await res.json()
+ if (Array.isArray(data)) this.stations = data as any
+ } catch {}
+ }
+
+ async pollStatus() {
+ try {
+ const res = await fetch(`${BACKEND_URL}/session/status`)
+ const data = await res.json()
+
+ this.sessionRunning = data.state === 'running' || data.state === 'paused_for_decision'
+
+ const affected: string[] = data.affected_trains || []
+ this.affectedIndices = new Set(affected.map((t: string) =>
+ parseInt(t.replace('Train_', ''), 10)
+ ))
+ const sel: string = data.selected_train || ''
+ this.selectedIndex = sel ? parseInt(sel.replace('Train_', ''), 10) : -1
+
+ if (this.sessionRunning && this.agentNames.length === 0) {
+ try {
+ const ar = await fetch(`${BACKEND_URL}/agents`)
+ const ad = await ar.json()
+ this.agentNames = ad.map((a: any, i: number) => a.name || `Train_${i}`)
+ } catch {}
+ }
+ if (!this.sessionRunning) {
+ this.agentNames = []
+ // Keep stations — they come from preview endpoint, not session
+ }
+ } catch {}
+ }
+
+ isAffectedAgent(i: number): boolean { return this.affectedIndices.has(i) }
+ isSelectedAgent(i: number): boolean { return this.selectedIndex === i }
+
+ selectPlan(planIndex: number | undefined) {
+ this.interrupted = false
+ this.stateService.setPlan(planIndex)
+ this.stateService.play()
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.html b/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.html
new file mode 100644
index 00000000..a4b56676
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.html
@@ -0,0 +1,125 @@
+
+
+
+ @if (eventBands.length > 0) {
+ @for (band of eventBands; track $index) {
+ ⚠ event
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ {{ maxTime * 0.2 | number: '1.0' }}
+ {{ maxTime * 0.4 | number: '1.0' }}
+ {{ maxTime * 0.6 | number: '1.0' }}
+ {{ maxTime * 0.8 | number: '1.0' }}
+ {{ maxTime }}
+
+
+
+
+ 0
+ {{ maxDistance * 0.2 | number: '1.0' }}
+ {{ maxDistance * 0.4 | number: '1.0' }}
+ {{ maxDistance * 0.6 | number: '1.0' }}
+ {{ maxDistance * 0.8 | number: '1.0' }}
+ {{ maxDistance | number: '1.0-1' }}
+
+
+
+
+ Time (steps)
+
+
+
+
+ Position (column)
+
+
+
+ @for (plan of plannedRuns; let i = $index; track $index) {
+ @for (train of plan; track $index) {
+
+ }
+ }
+
+
+ @for (train of trainRuns; let i = $index; track $index) {
+
+
+
+
+
+
+ @for (coord of train.coordinates; track $index) {
+
+ }
+
+ @if (train.coordinates.length > 0) {
+ Train {{ train.name }}
+ }
+
+ }
+
diff --git a/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.scss b/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.scss
new file mode 100644
index 00000000..a994993b
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.scss
@@ -0,0 +1,14 @@
+:host {
+ display: block;
+}
+
+:host {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+svg {
+ width: 100%;
+ height: 100%;
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.ts b/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.ts
new file mode 100644
index 00000000..8d34c6ce
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/marey/marey.component.ts
@@ -0,0 +1,241 @@
+import { DecimalPipe } from '@angular/common'
+import { Component, Input, OnInit } from '@angular/core'
+import { StateService } from '../state.service'
+import { Agent } from '../data.service'
+import { ControllerService } from '../controller.service'
+
+export interface TrainCoordinate {
+ x: number
+ y: number
+}
+
+export interface TrainRun {
+ name?: string
+ coordinates: TrainCoordinate[]
+}
+
+export interface EventBand {
+ start: number
+ end: number
+ train: string
+}
+
+const PLAN_CUTOFF = 20
+const BACKEND_URL = 'http://localhost:5001'
+
+// Trains with active events — polled from Flask brain
+let affectedTrains: Set = new Set()
+// Position mapping: "r,c" -> x_distance (linearized route position)
+let positionMapping: Record = {}
+// Event step ranges for the ZWL band (step ranges where events occurred)
+let eventBandRanges: EventBand[] = []
+
+@Component({
+ selector: 'app-marey',
+ imports: [DecimalPipe],
+ templateUrl: './marey.component.html',
+ styleUrl: './marey.component.scss',
+})
+export class MareyComponent implements OnInit {
+ @Input() svgWidth: number = 600
+ @Input() svgHeight: number = 400
+ @Input() marginLeft: number = 50
+ @Input() marginTop: number = 50
+ @Input() marginRight: number = 50
+ @Input() marginBottom: number = 50
+
+ get chartWidth(): number { return this.svgWidth - this.marginLeft - this.marginRight }
+ get chartHeight(): number { return this.svgHeight - this.marginTop - this.marginBottom }
+ get maxTime(): number {
+ if (this.trainRuns.length === 0) return 50
+ let max = 0
+ this.trainRuns.forEach((train) => {
+ train.coordinates.forEach((coord) => { max = Math.max(max, coord.y) })
+ })
+ return max + PLAN_CUTOFF
+ }
+
+ public maxDistance: number = 0
+ public trainRuns: Array = []
+ public agents: Array = []
+ public timestep: number = 0
+ public plannedRuns: Array> = []
+ public selectedPlan?: number
+ public eventBands: EventBand[] = []
+
+ // Track which step ranges each train was stopped
+ private trainStoppedAt: Map = new Map()
+
+ constructor(
+ public stateService: StateService,
+ public controllerService: ControllerService,
+ ) {}
+
+ ngOnInit() {
+ this.stateService.getPlan().subscribe((planIndex) => {
+ this.selectedPlan = planIndex
+ })
+
+ this.stateService.getTransitions().subscribe((transitions) => {
+ // Use mapping max distance if available, else fall back to grid width
+ const mappingValues = Object.values(positionMapping)
+ this.maxDistance = mappingValues.length > 0
+ ? Math.max(...mappingValues)
+ : transitions[0].length - 1
+ })
+
+ this.stateService.getHistory().subscribe((history) => {
+ this.timestep = history.length
+
+ // Build train runs
+ const agentHistories = history.reduce((agentHistory: Record, timestep) => {
+ for (const agent in timestep) {
+ agentHistory[agent] ??= []
+ agentHistory[agent].push(timestep[agent])
+ }
+ return agentHistory
+ }, {})
+
+ this.trainRuns = Object.entries(agentHistories).map(([name, coordinates]) => ({
+ name,
+ coordinates: coordinates
+ .map(({ position }, index) => {
+ if (!position) return { x: undefined as unknown as number, y: index }
+ const key = `${position[0]},${position[1]}`
+ const mappedX = positionMapping[key]
+ const x = mappedX !== undefined ? mappedX : position[1]
+ return { x, y: index }
+ })
+ .filter((coord): coord is { x: number; y: number } => coord.x !== undefined),
+ }))
+
+ // Detect stopped trains (position unchanged) to build event bands
+ this.updateEventBands(history)
+ })
+
+ this.stateService.getPlans().subscribe((plans) => {
+ this.plannedRuns = plans.map((plan) => {
+ const agentHistories = plan
+ .filter((_, index) => index >= this.timestep)
+ .reduce((agentHistory: Record, timestep) => {
+ for (const agent in timestep) {
+ agentHistory[agent] ??= []
+ agentHistory[agent].push(timestep[agent])
+ }
+ return agentHistory
+ }, {})
+ return Object.entries(agentHistories).map(([name, coordinates]) => ({
+ name,
+ coordinates: coordinates
+ .map(({ position }, index) => {
+ if (!position) return { x: undefined as unknown as number, y: this.timestep + index }
+ const key = `${position[0]},${position[1]}`
+ const mappedX = positionMapping[key]
+ const x = mappedX !== undefined ? mappedX : position[1]
+ return { x, y: this.timestep + index }
+ })
+ .filter((coord, index): coord is { x: number; y: number } =>
+ coord.x !== undefined && index < PLAN_CUTOFF
+ ),
+ }))
+ })
+ })
+
+ // Poll session status and mapping
+ const fetchMapping = async () => {
+ try {
+ const r = await fetch(`${BACKEND_URL}/mapping`)
+ const d = await r.json()
+ if (d && Object.keys(d).length > 0) positionMapping = d
+ else positionMapping = {}
+ } catch {}
+ }
+ fetchMapping()
+ setInterval(async () => {
+ try {
+ const res = await fetch(`${BACKEND_URL}/session/status`)
+ const data = await res.json()
+ affectedTrains = new Set((data.affected_trains || []).map((t: string) =>
+ t.replace('Train_', '')
+ ))
+ } catch {}
+ }, 2000)
+ setInterval(fetchMapping, 4000)
+
+ this.controllerService.observeReset().subscribe(() => {
+ this.trainRuns = []
+ this.plannedRuns = []
+ this.timestep = 0
+ this.eventBands = []
+ })
+ }
+
+ private updateEventBands(history: Array>) {
+ // Detect runs where a train's position didn't change (stopped)
+ // Build event bands for visual highlighting
+ const stoppedRanges: Map = new Map()
+
+ for (const [agentId, agents] of Object.entries(
+ history.reduce((acc: Record, step) => {
+ for (const id in step) {
+ acc[id] ??= []
+ acc[id].push(step[id])
+ }
+ return acc
+ }, {})
+ )) {
+ const ranges: { start: number, end: number }[] = []
+ let stopStart: number | null = null
+ let prevPos: string | null = null
+
+ agents.forEach((agent, idx) => {
+ const pos = agent.position ? JSON.stringify(agent.position) : null
+ const stopped = pos !== null && pos === prevPos
+ if (stopped && stopStart === null) stopStart = idx - 1
+ if (!stopped && stopStart !== null && idx - stopStart > 3) {
+ ranges.push({ start: stopStart, end: idx })
+ stopStart = null
+ }
+ prevPos = pos
+ })
+ if (stopStart !== null && agents.length - stopStart > 3) {
+ ranges.push({ start: stopStart, end: agents.length - 1 })
+ }
+ if (ranges.length > 0) stoppedRanges.set(agentId, ranges)
+ }
+
+ this.eventBands = []
+ stoppedRanges.forEach((ranges, train) => {
+ ranges.forEach(({ start, end }) => {
+ this.eventBands.push({ start, end, train })
+ })
+ })
+ }
+
+ isAffectedTrain(name: string | undefined): boolean {
+ if (!name) return false
+ return affectedTrains.has(name) || this.eventBands.some(b => b.train === name)
+ }
+
+ // Return coordinates outside event bands (normal segments)
+ getNormalCoords(train: TrainRun): TrainCoordinate[] {
+ return train.coordinates
+ }
+
+ // Return coordinates inside event bands (highlighted segments)
+ getEventCoords(train: TrainRun): TrainCoordinate[] {
+ return train.coordinates.filter(coord =>
+ this.eventBands.some(b => b.train === train.name && coord.y >= b.start && coord.y <= b.end)
+ )
+ }
+
+ getPolylinePoints(coordinates: TrainCoordinate[]): string {
+ return coordinates
+ .map((coord) => {
+ const x = this.marginLeft + (coord.x / this.maxDistance) * this.chartWidth
+ const y = this.marginTop + (coord.y / this.maxTime) * this.chartHeight
+ return `${x},${y}`
+ })
+ .join(' ')
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/renderer.service.ts b/flatland-hmi-hack4rail/frontend/src/app/renderer.service.ts
new file mode 100644
index 00000000..b8e712df
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/renderer.service.ts
@@ -0,0 +1,131 @@
+import { Injectable } from '@angular/core'
+import { Agent, Transitions } from './data.service'
+
+export interface MapCell {
+ ground: string
+ objects?: string
+}
+
+const BACKGROUND_CLASSES_WEIGHT = {
+ grass: 8,
+ water: 1,
+ trees: 20,
+ forest: 16,
+ mountain: 6,
+}
+
+const TRANSITION_CLASSES_MAP = Object.fromEntries(
+ [
+ 'WE',
+ 'WW EE NN SS',
+ 'WW EE',
+ 'EN SW',
+ 'WN SE',
+ 'ES NW',
+ 'NE WS',
+ 'NN SS',
+ 'NN SS EE WW ES NW SE WN',
+ 'EE WW EN SW',
+ 'EE WW SE WN',
+ 'EE WW ES NW',
+ 'EE WW NE WS',
+ 'NN SS EE WW NW ES',
+ 'NE NW ES WS',
+ 'NN SS EN SW',
+ 'NN SS SE WN',
+ 'NN SS NW ES',
+ 'NN SS NE WS',
+ ].flatMap((transition) => {
+ if (transition === '') {
+ return [[0, []]]
+ }
+ const binaryList = Array(16).fill('0')
+ for (const dir of transition.split(' ')) {
+ const iDirIn = 'NESW'.indexOf(dir[0])
+ const iDirOut = 'NESW'.indexOf(dir[1])
+ const iTrans = 4 * iDirIn + iDirOut
+ binaryList[iTrans] = '1'
+ }
+ const bitmap = parseInt(binaryList.join(''), 2)
+ return [0, 1, 2, 3].map((direction) => [
+ rotateTransition(bitmap, direction * 90),
+ [`rotation_${direction * 90}`, 'track', `transition_${transition.split(' ').join('_').toLocaleLowerCase()}`],
+ ])
+ }),
+)
+
+function rotateTransition(transition: number, rotation: number): number {
+ const rotationSteps = (rotation / 90) % 4
+ if (rotationSteps === 0) return transition
+
+ let value = transition
+ for (let i = 0; i < 4; i++) {
+ const mask = 0xf << (i * 4)
+ const rowBits = (value & mask) >> (i * 4)
+ const rotatedBits = ((rowBits << (4 - rotationSteps)) | (rowBits >> rotationSteps)) & 0xf
+ value = (value & ~mask) | (rotatedBits << (i * 4))
+ }
+
+ const lowerMask = (1 << (rotationSteps * 4)) - 1
+ const lowerBits = value & lowerMask
+ const upperBits = value >> (rotationSteps * 4)
+ value = (lowerBits << ((4 - rotationSteps) * 4)) | upperBits
+
+ return value
+}
+
+function getBackgroundClasses() {
+ const sum = Object.values(BACKGROUND_CLASSES_WEIGHT).reduce((acc, weight) => acc + weight, 0)
+ const random = Math.floor(Math.random() * sum)
+ let lastWeight = 0
+ for (const [key, weight] of Object.entries(BACKGROUND_CLASSES_WEIGHT)) {
+ lastWeight += weight
+ if (random < lastWeight) {
+ return ['bkgnd', `bkgnd_${key}`]
+ }
+ }
+ return ''
+}
+
+function getLocationKey(i: number, j: number) {
+ return `${i},${j}`
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class RendererService {
+ constructor() {}
+
+ public getMapClasses(transition: number): string {
+ return (TRANSITION_CLASSES_MAP[transition] || getBackgroundClasses()).join(' ')
+ }
+
+ public getTargetClasses(transition: number): string {
+ return TRANSITION_CLASSES_MAP[transition]?.[0] ?? 'error'
+ }
+
+ public getAgentClasses(agent: Agent | undefined): string {
+ return agent ? `direction_${agent.direction} ${agent.malfunction > 0 ? 'malfunction' : ''}` : ''
+ }
+
+ public renderMap(transitions: Transitions, agents: Array) {
+ const targetsMap = new Map()
+ for (const agent of agents) {
+ targetsMap.set(getLocationKey(agent.target[0], agent.target[1]), true)
+ }
+ const mapClasses: Array> = []
+ for (let i = 0; i < transitions.length; i++) {
+ const row = transitions[i]
+ const mapRow: Array = []
+ for (let j = 0; j < row.length; j++) {
+ const cell = row[j]
+ const ground = this.getMapClasses(cell)
+ const objects = targetsMap.has(getLocationKey(i, j)) ? this.getTargetClasses(cell) : undefined
+ mapRow.push({ ground, objects })
+ }
+ mapClasses.push(mapRow)
+ }
+ return mapClasses
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/app/state.service.ts b/flatland-hmi-hack4rail/frontend/src/app/state.service.ts
new file mode 100644
index 00000000..cbabb97f
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/app/state.service.ts
@@ -0,0 +1,120 @@
+import { Injectable } from '@angular/core'
+import { Agent, DataService, Transitions } from './data.service'
+import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs'
+import { ControllerService, State } from './controller.service'
+
+@Injectable({
+ providedIn: 'root',
+})
+export class StateService {
+ private transitions = new ReplaySubject(1)
+ private agents = new ReplaySubject>(1)
+ private state = new ReplaySubject(1)
+ private interval?: number
+ private plans = new Subject>>>()
+ private history = new Subject>>()
+ private currentPolicyIndex = 0
+ private selectedPlan = new BehaviorSubject(undefined)
+ private malfunctions: Record = {}
+ private newMalfunction = new Subject()
+
+ public get playing() {
+ return this.interval !== undefined
+ }
+
+ constructor(
+ private dataService: DataService,
+ private controllerService: ControllerService,
+ ) {
+ // Initial load
+ this.dataService.getTransitions().then((transitions) => {
+ this.transitions.next(transitions)
+ })
+ this.dataService.getHistory().then((history) => {
+ this.history.next(history)
+ })
+
+ setInterval(() => {
+ this.dataService.getHistory().then((history) => {
+ this.history.next(history)
+ if (history.length > 0) {
+ const agents = Object.values(history[history.length - 1])
+ this.agents.next(agents)
+ }
+ })
+ }, 1000)
+ }
+
+ public getNewMalfunction() {
+ return this.newMalfunction.asObservable()
+ }
+
+ public setCurrentPolicyIndex(index: number) {
+ this.currentPolicyIndex = index
+ }
+
+ public setPlan(planIndex: number | undefined) {
+ this.selectedPlan.next(planIndex)
+ }
+
+ public getPlan() {
+ return this.selectedPlan.asObservable()
+ }
+
+ public getPlans() {
+ return this.plans.asObservable()
+ }
+
+ public getTransitions() {
+ return this.transitions.asObservable()
+ }
+
+ public getAgents() {
+ return this.agents.asObservable()
+ }
+
+ public getState() {
+ return this.state.asObservable()
+ }
+
+ public next() {
+ return this.controllerService.stepEnv(this.currentPolicyIndex).then(() => {
+ return this.dataService.getHistory().then((history) => {
+ this.history.next(history)
+ return false
+ })
+ })
+ }
+
+ public reset() {
+ this.stop()
+ this.controllerService.resetEnv().then(() => {
+ this.dataService.getTransitions().then((transitions) => {
+ this.transitions.next(transitions)
+ this.agents.next([])
+ })
+ })
+ }
+
+ public play() {
+ this.interval = window.setTimeout(() => {
+ this.next().then(() => {
+ if (this.interval !== undefined) {
+ this.play()
+ }
+ })
+ }, 500)
+ }
+
+ public stop() {
+ if (this.interval) {
+ clearTimeout(this.interval)
+ this.interval = undefined
+ this.malfunctions = {}
+ }
+ }
+
+ public getHistory() {
+ return this.history.asObservable()
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/src/index.html b/flatland-hmi-hack4rail/frontend/src/index.html
new file mode 100644
index 00000000..71cdfcc9
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Frontend
+
+
+
+
+
+
+
+
diff --git a/flatland-hmi-hack4rail/frontend/src/main.ts b/flatland-hmi-hack4rail/frontend/src/main.ts
new file mode 100644
index 00000000..c3d8f9af
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/main.ts
@@ -0,0 +1,5 @@
+import { bootstrapApplication } from '@angular/platform-browser'
+import { appConfig } from './app/app.config'
+import { AppComponent } from './app/app.component'
+
+bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err))
diff --git a/flatland-hmi-hack4rail/frontend/src/styles.scss b/flatland-hmi-hack4rail/frontend/src/styles.scss
new file mode 100644
index 00000000..bd4d09b6
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/src/styles.scss
@@ -0,0 +1,35 @@
+/* Global styles */
+
+html,
+body {
+ padding: 0;
+ margin: 0;
+ height: 100%;
+ width: 100%;
+}
+
+* {
+ box-sizing: border-box;
+ font-family: 'Roboto', sans-serif;
+}
+
+/* Make the Angular app root and router outlet fill the viewport */
+app-root,
+router-outlet + * {
+ display: block;
+ height: 100%;
+ width: 100%;
+}
+
+/* Marey and map components fill their container */
+app-marey,
+app-map {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+app-marey svg {
+ width: 100%;
+ height: 100%;
+}
diff --git a/flatland-hmi-hack4rail/frontend/test.png b/flatland-hmi-hack4rail/frontend/test.png
new file mode 100644
index 00000000..18954103
Binary files /dev/null and b/flatland-hmi-hack4rail/frontend/test.png differ
diff --git a/flatland-hmi-hack4rail/frontend/tsconfig.app.json b/flatland-hmi-hack4rail/frontend/tsconfig.app.json
new file mode 100644
index 00000000..3775b37e
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/tsconfig.app.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/flatland-hmi-hack4rail/frontend/tsconfig.json b/flatland-hmi-hack4rail/frontend/tsconfig.json
new file mode 100644
index 00000000..5525117c
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/tsconfig.json
@@ -0,0 +1,27 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/flatland-hmi-hack4rail/frontend/tsconfig.spec.json b/flatland-hmi-hack4rail/frontend/tsconfig.spec.json
new file mode 100644
index 00000000..5fb748d9
--- /dev/null
+++ b/flatland-hmi-hack4rail/frontend/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/flatland-hmi-hack4rail/package-lock.json b/flatland-hmi-hack4rail/package-lock.json
new file mode 100644
index 00000000..8c043efb
--- /dev/null
+++ b/flatland-hmi-hack4rail/package-lock.json
@@ -0,0 +1,28 @@
+{
+ "name": "flatland-hmi",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "prettier": "^3.6.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz",
+ "integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ }
+ }
+}
diff --git a/flatland-hmi-hack4rail/package.json b/flatland-hmi-hack4rail/package.json
new file mode 100644
index 00000000..cb821ac0
--- /dev/null
+++ b/flatland-hmi-hack4rail/package.json
@@ -0,0 +1,5 @@
+{
+ "devDependencies": {
+ "prettier": "^3.6.0"
+ }
+}
diff --git a/flatland-hmi-hack4rail/ui-demo.mov b/flatland-hmi-hack4rail/ui-demo.mov
new file mode 100644
index 00000000..a727c50f
Binary files /dev/null and b/flatland-hmi-hack4rail/ui-demo.mov differ
diff --git a/flatland-hmi-hack4rail/zwl.Dockerfile b/flatland-hmi-hack4rail/zwl.Dockerfile
new file mode 100644
index 00000000..94f799c2
--- /dev/null
+++ b/flatland-hmi-hack4rail/zwl.Dockerfile
@@ -0,0 +1,35 @@
+# Stage 1: Build
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+# Build arg: URL of the Flask Railway brain reachable from the BROWSER
+# Local dev: http://localhost:5001
+# Deployment: http://:5001 or https://railway.yourdomain.com
+ARG RAILWAY_SIMU_URL=http://localhost:5001
+
+COPY frontend/package*.json ./
+RUN npm ci
+
+COPY frontend/ .
+
+# Replace hardcoded localhost:5001 with the configured URL
+RUN find /app/src -name "*.ts" \
+ -exec sed -i "s|http://localhost:5001|${RAILWAY_SIMU_URL}|g" {} \;
+
+RUN npm run build
+
+# Stage 2: Serve with nginx
+FROM nginx:alpine
+
+COPY --from=builder /app/dist/frontend /usr/share/nginx/html
+
+# Simple nginx config — no proxy needed since URL is baked in
+RUN echo 'server { \
+ listen 80; \
+ root /usr/share/nginx/html; \
+ index index.html; \
+ location / { try_files $uri $uri/ /index.html; } \
+}' > /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
diff --git a/frontend/.gitignore b/frontend/.gitignore
index d59ddeb1..a95962a6 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -12,7 +12,12 @@ node_modules
dist
dist-ssr
coverage
+# NB: .dockerignore is a symlink to this file, and the two dialects differ. git matches
+# `*.local` at any depth, Docker only at the context root - so env/.env.local was ignored by
+# git but still copied into the image, where Vite inlined every VITE_* it defined into the
+# public bundle. The `**/` form is what excludes it from the build context.
*.local
+**/*.local
/cypress/videos/
/cypress/screenshots/
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 2c2093fd..3994922f 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -5,11 +5,9 @@ COPY . /app
ARG VITE_POWERGRID_SIMU
ARG VITE_RAILWAY_SIMU
ARG VITE_ATM_SIMU
-ARG VITE_COGNITIVE_TOKEN
ENV VITE_POWERGRID_SIMU=$VITE_POWERGRID_SIMU
ENV VITE_RAILWAY_SIMU=$VITE_RAILWAY_SIMU
ENV VITE_ATM_SIMU=$VITE_ATM_SIMU
-ENV VITE_COGNITIVE_TOKEN=$VITE_COGNITIVE_TOKEN
RUN npm ci
RUN npm run build
diff --git a/frontend/README.md b/frontend/README.md
index 412a90bc..bb4eca5c 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -19,6 +19,12 @@ The platform make use of the project OperatorFabric for notification management
Type Support for your custom entity
+
+ Post-logout survey
+
+
Project Setup
@@ -119,6 +125,58 @@ export type ENTITY = {
It is also the right place to define your other custom types.
You can then import and add your types to `src/config.ts` (cf. [Adding your custom entity](#adding-your-custom-entity))
+## Post-logout survey
+
+When an operator logs out, the platform hands them the HMI questionnaire chain
+served from `public/surveys/` (files taken from the
+[hmisurveys](https://github.com/AI4REALNET/hmisurveys) repository, `html/`). `surveychainer.html`
+plays the questionnaires one after another in an iframe and, on *Finish*,
+downloads the aggregated answers as a JSON file - exactly as it does when opened
+standalone.
+
+The operator never types their identity: `Navbar.vue` reads the current trace
+session id and use case **before** `logout()` clears them, queues them with
+`utils/survey.ts`, and the `/survey` view opens
+
+```
+/surveys/surveychainer.html?participant=&condition=
+```
+
+so both fields are prefilled and hidden - only *Start* is left to press.
+
+The session report (`exportTraceSession`) still writes its JSON and HTML files
+on logout, but it no longer opens itself in a new tab there: that tab takes the
+focus, and an operator who did not notice the survey behind it never took it.
+The report is held back (`openSummary: false`) and offered once the
+questionnaire is over — *Delete the remaining alerts?* → survey → *Open the
+session report?* — so the full order is
+
+1. `Navbar.leave` exports the session, keeping the report in memory.
+2. The operator answers the questionnaire, or skips it.
+3. The survey page asks whether to open the report, then returns to the login
+ page.
+
+Logging out without a recorded session skips both steps and opens the report
+immediately, as it always did.
+
+### Changing the questionnaires
+
+The chain lives at the top of `public/surveys/surveychainer.html`:
+
+- `DEFAULT_CHAIN` - the chain every use case plays today.
+- `CHAINS` - per-use-case chains, keyed by the `condition` sent by the platform
+ (`PowerGrid`, `ATM`, `Railway`). Add an entry to give one use case its own
+ questionnaires; use cases without an entry keep `DEFAULT_CHAIN`.
+
+Adding a questionnaire means copying its file from the hmisurveys repository
+into `public/surveys/` (keeping the folder layout the URLs use) and listing it in
+the relevant chain. A questionnaire must post its answers to `window.parent`,
+not `window.top`: the chainer is itself embedded in the InteractiveAI page.
+
+The vendored questionnaires stay under their own **GPL-3.0** license, not the
+MPL-2.0 of the rest of the frontend; `public/surveys/NOTICE.md` records where
+each file comes from and every change made to it.
+
## Project Setup
```sh
diff --git a/frontend/default.conf b/frontend/default.conf
index c7a9f6d2..e76df39a 100644
--- a/frontend/default.conf
+++ b/frontend/default.conf
@@ -15,10 +15,16 @@ server {
try_files $uri $uri/ /index.html;
}
- # Proxy for the INESCTEC cognitive API (avoids browser CORS restrictions)
+ # Proxy for the INESCTEC cognitive API (avoids browser CORS restrictions).
+ # The bearer token is attached here, not by the browser: the placeholder on the
+ # Authorization line below is substituted from $COGNITIVE_TOKEN at container start
+ # (see start-webui.sh), so the token never reaches the public JS bundle and is
+ # rotated by restarting the pod. Deliberately not naming the placeholder in this
+ # comment: the substitution is a plain sed, so it would write the token here too.
location /cognitive-api/ {
proxy_set_header Host wesenss.inesctec.pt;
proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header Authorization "Bearer __COGNITIVE_TOKEN__";
proxy_ssl_server_name on;
proxy_ssl_verify off;
proxy_pass https://wesenss.inesctec.pt/api/v1/;
@@ -39,14 +45,17 @@ server {
# Proxy for the PowerGrid (grid2op) simulator — keeps the "apply" POST same-origin
# so the browser never does a cross-origin request (avoids CORS entirely).
# Frontend posts to /powergrid-simu/... with VITE_POWERGRID_SIMU=/powergrid-simu.
- # NB: this upstream is a LAN address reachable from cab-standalone; adjust per environment
- # (public deployments use https://interactivepowergrid.passerelle.irt-systemx.fr/).
+ # The proxy_pass placeholder below is substituted from $POWERGRID_SIMU_UPSTREAM at
+ # container start (see start-webui.sh). Per environment:
+ # local -> http://host.docker.internal:5122/ (the default)
+ # LAN -> http://192.168.208.61:5100/
+ # public -> https://interactivepowergrid.passerelle.irt-systemx.fr/
# No explicit Host header: nginx defaults it to $proxy_host, which is correct for an
# ip:port upstream and for a vhost upstream behind the passerelle alike.
location /powergrid-simu/ {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_ssl_server_name on;
proxy_ssl_verify off;
- proxy_pass http://192.168.208.61:5100/;
+ proxy_pass __POWERGRID_SIMU_UPSTREAM__;
}
}
diff --git a/frontend/env.d.ts b/frontend/env.d.ts
index 4c207149..a1b9d620 100644
--- a/frontend/env.d.ts
+++ b/frontend/env.d.ts
@@ -7,7 +7,6 @@ interface ImportMetaEnv {
readonly VITE_POWERGRID_SIMU: string
readonly VITE_RAILWAY_SIMU: string
readonly VITE_ATM_SIMU: string
- readonly VITE_COGNITIVE_TOKEN: string
}
interface ImportMeta {
diff --git a/frontend/env/.env b/frontend/env/.env
index f0556594..d76388e8 100644
--- a/frontend/env/.env
+++ b/frontend/env/.env
@@ -4,4 +4,3 @@ VITE_POWERGRID_SIMU=$VITE_POWERGRID_SIMU
VITE_RAILWAY_SIMU=$VITE_RAILWAY_SIMU
VITE_ATM_SIMU=$VITE_ATM_SIMU
VITE_API=""
-VITE_COGNITIVE_TOKEN=
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 679aa8fa..eed6d7ba 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cab-front",
- "version": "1.3.6",
+ "version": "1.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cab-front",
- "version": "1.3.6",
+ "version": "1.4.1",
"license": "MPL-2.0",
"dependencies": {
"@floating-ui/vue": "^1.0.6",
diff --git a/frontend/package.json b/frontend/package.json
index a3967949..8d1e9730 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "cab-front",
- "version": "1.3.6",
+ "version": "1.4.1",
"private": true,
"license": "MPL-2.0",
"type": "module",
diff --git a/frontend/public/surveys/LICENSE b/frontend/public/surveys/LICENSE
new file mode 100644
index 00000000..f288702d
--- /dev/null
+++ b/frontend/public/surveys/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/frontend/public/surveys/NOTICE.md b/frontend/public/surveys/NOTICE.md
new file mode 100644
index 00000000..bca7c0a4
--- /dev/null
+++ b/frontend/public/surveys/NOTICE.md
@@ -0,0 +1,36 @@
+# Vendored HMI surveys
+
+The HTML questionnaires in this folder come from the **hmisurveys** project by
+Clark Borst (Delft University of Technology, Control & Simulation), available at
+ and licensed under **GPL-3.0** (see
+`LICENSE` next to this file). They keep that license here — the MPL-2.0 of the
+rest of InteractiveAI does not apply to them.
+
+Only the files used by the chain are vendored:
+
+| File | Origin in hmisurveys |
+| --- | --- |
+| `surveychainer.html` | `html/surveychainer.html` |
+| `understanding/understanding.html` | `html/understanding/understanding.html` |
+| `experience/ueq_short.html` | `html/experience/ueq_short.html` |
+| `acceptance/vanderlaan.html` | `html/acceptance/vanderlaan.html` |
+| `workload/mch.html` | `html/workload/mch.html` |
+
+## Modifications
+
+Made so the chain can be embedded in InteractiveAI, and kept as small as
+possible so an upstream update stays easy to re-apply:
+
+1. **Every questionnaire** posts its answers to `window.parent` instead of
+ `window.top`. The chainer is itself inside an InteractiveAI iframe, so
+ `window.top` is the application shell and the chainer would never see the
+ answers.
+2. **`surveychainer.html`** reads `?participant=` and `?condition=` from its
+ URL. When both are present the Participant/Condition form is prefilled and
+ hidden — InteractiveAI passes the trace session id and the use case, so the
+ operator only presses *Start*. Without them the chainer behaves as before and
+ asks for both.
+3. **`surveychainer.html`** selects its questionnaires from `CHAINS[condition]`,
+ falling back to `DEFAULT_CHAIN`, so a use case can get its own chain.
+4. **`surveychainer.html`** lays its page out with flexbox so it fills the frame
+ it is given instead of assuming a full browser window (`height: 80vh`).
diff --git a/frontend/public/surveys/acceptance/vanderlaan.html b/frontend/public/surveys/acceptance/vanderlaan.html
new file mode 100644
index 00000000..659d0385
--- /dev/null
+++ b/frontend/public/surveys/acceptance/vanderlaan.html
@@ -0,0 +1,279 @@
+
+
+
+
+
+ Van der Laan Acceptance Scale (UEQ-style)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/public/surveys/experience/ueq_short.html b/frontend/public/surveys/experience/ueq_short.html
new file mode 100644
index 00000000..a62f1546
--- /dev/null
+++ b/frontend/public/surveys/experience/ueq_short.html
@@ -0,0 +1,327 @@
+
+
+
+
+
+ User Experience Questionnaire (UEQ)
+
+
+
+
+
+
+
+
+
Results
+
+
+
Subscale Means
+
+
+
+
+
+
+
+
diff --git a/frontend/public/surveys/surveychainer.html b/frontend/public/surveys/surveychainer.html
new file mode 100644
index 00000000..afc2acf5
--- /dev/null
+++ b/frontend/public/surveys/surveychainer.html
@@ -0,0 +1,241 @@
+
+
+
+
+
+ Survey Chain Orchestrator
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/public/surveys/understanding/understanding.html b/frontend/public/surveys/understanding/understanding.html
new file mode 100644
index 00000000..8f921fde
--- /dev/null
+++ b/frontend/public/surveys/understanding/understanding.html
@@ -0,0 +1,293 @@
+
+
+
+
+
+Comprehensive AI System Understanding Assessment (Fixed)
+
+
+
+
+ System Understanding Assessment
+
+ This instrument measures three aspects of user understanding:
+
+ Perceived Understanding — how well the user feels they understand the system.
+ Factual Accuracy — correct recall of objective system parameters.
+ Conceptual Comprehension — correct understanding of how or why the system behaves as it does.
+
+
+
+
+
+ Participant ID:
+
+
+ Condition ID:
+
+
+
+
+
+
+Statement / Question
+ 1 Strongly Disagree
+ 2 3
+ 4 Neutral
+ 5 6
+ 7 Strongly Agree
+
+
+
+
+
+
+Submit
+
+
+
+
+
+
+
diff --git a/frontend/public/surveys/workload/mch.html b/frontend/public/surveys/workload/mch.html
new file mode 100644
index 00000000..300ccea4
--- /dev/null
+++ b/frontend/public/surveys/workload/mch.html
@@ -0,0 +1,371 @@
+
+
+
+
+Cooper–Harper Handling Qualities / Workload Rating
+
+
+
+
+
+Modified Cooper–Harper Rating Scale
+
+ Follow the boxes from top to bottom. Answer each question Yes or No .
+ When the path is determined, choose the rating from the corresponding 3-point group.
+
+
+
+
+ Participant ID:
+
+
+ Condition ID:
+
+
+
+
+
+
+
+
↓
+
+
+
+
+
↓
+
+
+
+
+
+
+
+
+
Rating 10 — Major deficiencies, system redesign is mandatory
+
Impossible. Instructed task cannot be accomplished reliably.
+
Select Rating 10
+
+
+
+
+
Choose one: Major difficulty, system redesign recommended (7–9)
+
+
Confirm Selection
+
+
+
+
+
Choose one: Deficiencies warrant improvement (4–6)
+
+
Confirm Selection
+
+
+
+
+
Choose one: Satisfactory / desirable (1–3)
+
+
Confirm Selection
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/api/cards.ts b/frontend/src/api/cards.ts
index ebcea3de..bf769185 100644
--- a/frontend/src/api/cards.ts
+++ b/frontend/src/api/cards.ts
@@ -98,8 +98,15 @@ export function remove(id: Card['id']) {
return http.delete(`/cardspub/cards/${id}`)
}
-export function removeEvent(uid: Card['processInstanceId']) {
- return http.delete(`/cab_event/api/v1/event/${uid}`)
+/**
+ * Deletes the event *and* its card (the event-service drops `cabProcess.{uid}`
+ * from the card publication service on its way out).
+ *
+ * @param silent suppress the generic error modal - used by bulk deletions that
+ * report once instead of one popup per card
+ */
+export function removeEvent(uid: Card['processInstanceId'], silent = false) {
+ return http.delete(`/cab_event/api/v1/event/${uid}`, { _silent: silent })
}
export function acknowledge(card: Card) {
diff --git a/frontend/src/api/cognitive.ts b/frontend/src/api/cognitive.ts
index 4dbeb3cd..6ae41f11 100644
--- a/frontend/src/api/cognitive.ts
+++ b/frontend/src/api/cognitive.ts
@@ -3,19 +3,20 @@
* Fetches per-event, per-agent cognitive state factors and returns a
* structured snapshot that is attached to every session-trace entry.
*
- * Requests are routed through the nginx proxy at /cognitive-api/ to avoid
- * browser CORS restrictions when calling https://wesenss.inesctec.pt directly.
+ * Requests are routed through the nginx proxy at /cognitive-api/, which both avoids
+ * browser CORS restrictions when calling https://wesenss.inesctec.pt directly and
+ * attaches the bearer token server-side (see frontend/default.conf). The token is
+ * deliberately absent from this bundle: anything inlined here is readable by every
+ * visitor and can only be rotated by rebuilding the image.
*
* Hardcoded values (event_id = 885, first agent from the list) are used
* until the dynamic event → cognitive-event mapping is in place.
*/
-// Routed through nginx /cognitive-api/ → https://wesenss.inesctec.pt/api/v1/
+// Routed through nginx /cognitive-api/ → https://wesenss.inesctec.pt/api/v1/,
+// which injects the Authorization header from the COGNITIVE_TOKEN env var.
const COGNITIVE_BASE_URL = '/cognitive-api'
-// Injected at build time via VITE_COGNITIVE_TOKEN env var (never commit the token)
-const COGNITIVE_TOKEN = import.meta.env.VITE_COGNITIVE_TOKEN ?? ''
-
// Hardcoded until dynamic event_id resolution is implemented
const DEFAULT_EVENT_ID = 885
@@ -61,9 +62,8 @@ type LatestDataItem = {
}
async function cognitiveGet(path: string): Promise {
- const response = await fetch(`${COGNITIVE_BASE_URL}${path}`, {
- headers: { Authorization: `Bearer ${COGNITIVE_TOKEN}` }
- })
+ // No Authorization header: the nginx /cognitive-api/ proxy adds it.
+ const response = await fetch(`${COGNITIVE_BASE_URL}${path}`)
if (!response.ok) throw new Error(`Cognitive API ${response.status}: ${path}`)
return response.json() as Promise
}
diff --git a/frontend/src/api/services.ts b/frontend/src/api/services.ts
index cdfda14d..5892be71 100644
--- a/frontend/src/api/services.ts
+++ b/frontend/src/api/services.ts
@@ -51,13 +51,8 @@ export function sendTrace(payload: Trace) {
return http.post>('/cabhistoric/api/v1/traces', tracePayload)
}
-// TODO: TEMP HACK (eval-demo) — MUST BE REMOVED before next release
-// The real API call is disabled and replaced with a fake success response for demo purposes.
export function applyRecommendation(data: Action) {
- // [DISABLED] Simulator API is inactive — returning fake success for demo
- // To restore: uncomment the http.post and remove the Promise.resolve
- // return http.post<{ message: string }>('/api/v1/recommendations', data)
- return Promise.resolve({ data: { message: 'ok (simulated)' } }) // TEMP HACK: remove this line
+ return http.post<{ message: string }>('/api/v1/recommendations', data)
}
export function getProcedure(event_type: string) {
diff --git a/frontend/src/components/molecules/Navbar.vue b/frontend/src/components/molecules/Navbar.vue
index 9f50b0c4..fd01236e 100644
--- a/frontend/src/components/molecules/Navbar.vue
+++ b/frontend/src/components/molecules/Navbar.vue
@@ -70,12 +70,15 @@
diff --git a/frontend/src/entities/Railway/CAB/Context.vue b/frontend/src/entities/Railway/CAB/Context.vue
index 0a332317..d139b90e 100644
--- a/frontend/src/entities/Railway/CAB/Context.vue
+++ b/frontend/src/entities/Railway/CAB/Context.vue
@@ -1,70 +1,168 @@
-
+
+ style="display: flex; flex-direction: column; width: 100%; height: 100%;"
+ >
+
+
+
+ ▶ Start
+
+
+ ⏸ Pause
+
+
+ ↺ Zurücksetzen
+
+
+ Tempo
+
+ {{ speed }}×
+
+
Schritt {{ step }}
+
+
+
+
+
+
+
+
+
-
+
+
diff --git a/frontend/src/entities/Railway/CAB/FlatlandMap.vue b/frontend/src/entities/Railway/CAB/FlatlandMap.vue
new file mode 100644
index 00000000..c6adbd6c
--- /dev/null
+++ b/frontend/src/entities/Railway/CAB/FlatlandMap.vue
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+ Waiting for simulation...
+
+
+
+
+ Step {{ step }}
+
+
+
+
+ ⚠ Conflict detected
+
+
+
+
+
+
+ ▶ Start
+
+
+ ⏸ Pause
+
+
+ ↺ Zurücksetzen
+
+
+ Tempo
+
+ {{ speed }}x
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/entities/Railway/CAB/Notifications.vue b/frontend/src/entities/Railway/CAB/Notifications.vue
index 2ef25ec9..aec094a7 100644
--- a/frontend/src/entities/Railway/CAB/Notifications.vue
+++ b/frontend/src/entities/Railway/CAB/Notifications.vue
@@ -1,12 +1,9 @@
- {{
- $t(`card.event_type.${card.data.metadata.event_type}`) !==
- `card.event_type.${card.data.metadata.event_type}`
- ? $t(`card.event_type.${card.data.metadata.event_type}`)
- : card.data.metadata.event_type
- }}
+ {{ card.data.metadata.id_train }}
+ —
+ {{ card.data.metadata.event_type }}
@@ -29,7 +26,6 @@
+
+
diff --git a/frontend/src/entities/Railway/CAB/old/Assistant.vue b/frontend/src/entities/Railway/CAB/old/Assistant.vue
new file mode 100644
index 00000000..81b7bd95
--- /dev/null
+++ b/frontend/src/entities/Railway/CAB/old/Assistant.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+ {{ $t('cab.assistant.recommendations') }}
+
+
+
+
+
+
+ {{ appStore.card('Railway')!.data.metadata.id_train }}
+ —
+ {{ appStore.card('Railway')!.data.metadata.event_type }}
+
+ : {{ appStore.card('Railway')!.data.metadata.message }}
+
+
+
+
+ Get recommendations
+
+
+
+
+
+
+
+ R{{ index }}: {{ recommendation.title }}
+ {{ recommendation.description }}
+
+
+
+
+ {{ $t('recommendations.button.secondary') }}
+
+
+
+
+
+
+ KPI
+
+ R{{ index }}
+
+
+
+
+
+ {{ key }}
+
+ {{ recommendation.kpis?.[key] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/entities/Railway/api.ts b/frontend/src/entities/Railway/api.ts
index c1157ea1..fb691dca 100644
--- a/frontend/src/entities/Railway/api.ts
+++ b/frontend/src/entities/Railway/api.ts
@@ -1,12 +1,15 @@
-import http from '@/plugins/http'
import type { Action } from '@/types/entities'
-// TODO: TEMP HACK (eval-demo) — MUST BE REMOVED before next release
-// The real Railway simulator API call is disabled and replaced with a fake success response for demo purposes.
-// To restore: uncomment the http.post line and delete the Promise.resolve line.
+const BRAIN_URL = import.meta.env.VITE_RAILWAY_SIMU || 'http://localhost:5001'
+
export function applyRecommendation(data: Action<'Railway'>) {
- // [DISABLED] Simulator API is inactive — returning fake success for demo
- // To restore: uncomment the http.post and remove the Promise.resolve
- // return http.post<{ message: string }>(import.meta.env.VITE_RAILWAY_SIMU + '/transport_plan', data)
- return Promise.resolve({ data: { message: 'ok (simulated)' } }) // TEMP HACK: remove this line
+ // Send the operator's chosen resolution option directly to the Flask brain
+ return fetch(`${BRAIN_URL}/resolve`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ option_index: data.option_index }),
+ }).then(res => {
+ if (!res.ok) throw new Error('Failed to apply resolution')
+ return res.json()
+ })
}
diff --git a/frontend/src/entities/Railway/types.ts b/frontend/src/entities/Railway/types.ts
index 16c20600..b932d146 100644
--- a/frontend/src/entities/Railway/types.ts
+++ b/frontend/src/entities/Railway/types.ts
@@ -2,43 +2,41 @@ export type Railway = {
AppData: {
message: string
}
+
Context: {
- direction_agents: number[]
- list_of_target: {
- [key: `${number}`]: [number, number][]
- }
- position_agents: {
- [key: `${number}`]: [number, number]
- }
trains: {
- failure: boolean
id_train: string
- latitude: number
- longitude: number
- nb_passengers_connection: null
+ train_type: 'PASSENGER' | 'FREIGHT' | 'REGIONAL'
nb_passengers_onboard: number
+ position: [number, number] | null // Flatland grid [row, col]
+ direction: number
+ failure: boolean
speed: number
+ latitude?: number // optional, for real map overlay later
+ longitude?: number
}[]
+ position_agents: {
+ [key: `${number}`]: [number, number] | null
+ }
+ direction_agents: number[]
}
+
Metadata: {
+ // Platform-expected fields (keep for compatibility)
event_type: 'PASSENGER' | 'INFRASTRUCTURE' | 'IMPACT' | 'HARDWARE'
- travel_plan?: { name: string; startDate: number; endDate?: number }[]
id_train: string
agent_id: string
- agent_position?: [number, number]
+ delay: number
latitude?: number
longitude?: number
- delay: number
+ // Flatland-specific additions
+ train_b?: string
+ cell?: [number, number]
+ conflict_id?: string
+ message?: string
}
+
Action: {
- simulation_name: string
- targets_list: {
- agent_id: string
- targets: {
- passengers: number
- target_id: number
- target_type: 'STATION'
- }[]
- }[]
+ option_index: number
}
}
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 544186ec..7664840c 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -57,6 +57,7 @@
"modal.consent.cognitive": "Do you consent to the platform collecting and using your cognitive and stress data? ",
"modal.default": "Default modal message",
"modal.error.CONTEXT_FAILED": "Unable to retrieve context, try again?",
+ "modal.error.DELETE_ALERTS": "Some alerts could not be deleted ({n})",
"modal.error.DISCONNECTED": "Your session has expired",
"modal.error.DISCONNECT_USER_DUE_TO_NEW_CONNECTION": "A new connection has been opened, you have been disconnected",
"modal.error.ECONNABORTED": "Connection interrupted",
@@ -66,10 +67,17 @@
"modal.error.NO_CONTEXT": "Unable to retrieve recommendations without context",
"modal.error.SESSION_EXPIRED": "Your session has expired, please log in again",
"modal.error.default": "Request to {url} failed with code {code} and message {message}",
+ "modal.info.DELETE_ALERTS": "Delete the remaining alerts? They will not be shown at the next login.",
+ "modal.info.OPEN_SESSION_LOG": "Open the session report in a new tab? It is saved with the session files either way.",
+ "modal.info.SKIP_SURVEY": "Skip the questionnaire? Your answers will not be recorded and it cannot be reopened afterwards.",
"modal.info.SUBSCRIPTION_ACTIVE": "A user is logged in, log them out?",
"recommendations.description": "Description",
"recommendations.description.more": "{sign} details",
"recommendations.modal": "You are about to apply {recommendation}. Do you want to continue?",
"recommendations.title": "Recommendations",
+ "survey.intro": "Please answer this short questionnaire about the session you have just finished. Your participant and condition identifiers are already filled in - just press Start.",
+ "survey.skip": "Skip the questionnaire",
+ "survey.thanks": "Thank you, your answers have been saved.",
+ "survey.title": "Before you log out",
"to": "to"
}
diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json
index 9d5f8292..4ebcb780 100644
--- a/frontend/src/locales/fr.json
+++ b/frontend/src/locales/fr.json
@@ -57,6 +57,7 @@
"modal.consent.cognitive": "Consentez-vous à ce que la plateforme collecte et utilise vos données cognitives et de stress ? ",
"modal.default": "Message de modale par défaut",
"modal.error.CONTEXT_FAILED": "Impossible de récupérer le contexte, réessayer ?",
+ "modal.error.DELETE_ALERTS": "Certaines alertes n'ont pas pu être supprimées ({n})",
"modal.error.DISCONNECTED": "Votre session a expiré",
"modal.error.DISCONNECT_USER_DUE_TO_NEW_CONNECTION": "Une nouvelle connection a été ouverte, vous avez été déconnecté",
"modal.error.ECONNABORTED": "Connection interrompue",
@@ -66,10 +67,17 @@
"modal.error.NO_CONTEXT": "Impossible de récupérer les recommandations en l'absence de contexte",
"modal.error.SESSION_EXPIRED": "Votre session a expiré, veuillez vous reconnecter",
"modal.error.default": "La requête à {url} a échoué avec le code {code} et le message {message}",
+ "modal.info.DELETE_ALERTS": "Supprimer les alertes restantes ? Elles ne seront plus affichées à la prochaine connexion.",
+ "modal.info.OPEN_SESSION_LOG": "Ouvrir le rapport de session dans un nouvel onglet ? Il est enregistré avec les fichiers de session dans tous les cas.",
+ "modal.info.SKIP_SURVEY": "Passer le questionnaire ? Vos réponses ne seront pas enregistrées et il ne pourra plus être rouvert ensuite.",
"modal.info.SUBSCRIPTION_ACTIVE": "Un utilisateur est connecté, le déconnecter ?",
"recommendations.description": "Description",
"recommendations.description.more": "{sign} détails",
"recommendations.modal": "Vous êtes sur le point d'appliquer {recommendation}. Voulez-vous continuer ?",
"recommendations.title": "Recommandations",
+ "survey.intro": "Merci de répondre à ce court questionnaire sur la session que vous venez de terminer. Vos identifiants de participant et de condition sont déjà renseignés : il suffit d'appuyer sur Start.",
+ "survey.skip": "Passer le questionnaire",
+ "survey.thanks": "Merci, vos réponses ont été enregistrées.",
+ "survey.title": "Avant de vous déconnecter",
"to": "à"
}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 09f48dad..c7a7cafd 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -3,9 +3,11 @@ import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ENTITIES, type Entity } from '@/types/entities'
import { handleSessionExpired } from '@/utils/session'
+import { pendingSurvey } from '@/utils/survey'
import CAB from '@/views/CAB.vue'
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
+import Survey from '@/views/Survey.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -26,6 +28,16 @@ const router = createRouter({
auth: false
}
},
+ {
+ // Post-logout questionnaire chain. Reached only through `logout()`, which
+ // queues the request just before clearing the session.
+ path: '/survey',
+ name: 'survey',
+ component: Survey,
+ meta: {
+ auth: false
+ }
+ },
{
path: `/cab/:entity(${ENTITIES.join('|')})`,
name: 'cab',
@@ -52,6 +64,8 @@ router.beforeEach(async (to) => {
if (to.meta.auth && !authStore.user) return { name: 'login' }
if (!to.meta.auth && authStore.user) return { name: 'home' }
+ // Nothing to answer (survey taken, skipped, or the URL typed by hand)
+ if (to.name === 'survey' && !pendingSurvey()) return { name: 'login' }
if (to.name === 'home' && authStore.entities.length === 1)
return { name: 'cab', params: { entity: authStore.entities[0] } }
if (!to.name || (to.name === 'cab' && !authStore.entities.includes(to.params.entity as Entity)))
diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts
index 560a6aab..b473dc93 100644
--- a/frontend/src/stores/auth.ts
+++ b/frontend/src/stores/auth.ts
@@ -127,12 +127,18 @@ export const useAuthStore = defineStore(
/**
* @param format trace export format
* @param options `force: false` skips the export when the session recorded
- * no trace - used on session expiry so no empty file is downloaded
+ * no trace - used on session expiry so no empty file is downloaded.
+ * `openSummary: false` keeps the HTML report from taking over the tab,
+ * which would hide the post-logout survey; the file is written regardless
*/
- function logout(format: 'json' | 'csv' = 'json', options: { force?: boolean } = {}) {
+ function logout(
+ format: 'json' | 'csv' = 'json',
+ options: { force?: boolean; openSummary?: boolean } = {}
+ ) {
try {
exportTraceSession(format, {
force: options.force ?? true,
+ openSummary: options.openSummary,
userLogin: user.value?.userData.login
})
} catch (error) {
diff --git a/frontend/src/stores/cards.ts b/frontend/src/stores/cards.ts
index b857d31d..00096cb6 100644
--- a/frontend/src/stores/cards.ts
+++ b/frontend/src/stores/cards.ts
@@ -160,6 +160,23 @@ export const useCardsStore = defineStore('cards', () => {
cardsApi.remove(card.id)
}
+ /**
+ * Deletes every card currently held, so the next session starts on a clean
+ * board instead of replaying the alerts of the previous one (the card
+ * subscription re-sends everything published in the last 24h).
+ *
+ * Deletions are attempted in parallel and never reject: the user is on their
+ * way out, so a card that cannot be deleted is counted, not popped up.
+ *
+ * @returns how many cards could not be deleted
+ */
+ async function removeAll() {
+ const ids = [...new Set(_cards.value.map((card) => card.processInstanceId))].filter(Boolean)
+ const results = await Promise.allSettled(ids.map((id) => cardsApi.removeEvent(id, true)))
+ _cards.value = []
+ return results.filter((result) => result.status === 'rejected').length
+ }
+
/** Set the card's criticality to 'ND' (resolved) after the user confirms a recommendation. */
function resolveCriticality(card: Card) {
if (card.data.criticality !== 'ND') {
@@ -168,5 +185,14 @@ export const useCardsStore = defineStore('cards', () => {
}
}
- return { _cards, cards, subscribe, unsubscribe, acknowledge, remove, resolveCriticality }
+ return {
+ _cards,
+ cards,
+ subscribe,
+ unsubscribe,
+ acknowledge,
+ remove,
+ removeAll,
+ resolveCriticality
+ }
})
diff --git a/frontend/src/utils/survey.ts b/frontend/src/utils/survey.ts
new file mode 100644
index 00000000..5d9f0f28
--- /dev/null
+++ b/frontend/src/utils/survey.ts
@@ -0,0 +1,71 @@
+/**
+ * Post-logout HMI survey.
+ *
+ * When the operator logs out, InteractiveAI hands them the questionnaire chain
+ * vendored in `public/surveys/` (taken from the hmisurveys repository). The
+ * chain is opened with the identity the platform already knows, so the operator
+ * never types it: the trace session id becomes the survey's Participant ID, and
+ * the use case they were working on becomes its Condition ID.
+ *
+ * Backed by localStorage because `logout()` wipes the Pinia stores just before
+ * the survey route is entered, and because reloading /survey must not lose the
+ * pending request.
+ */
+
+import type { Entity } from '@/types/entities'
+
+const STORAGE_KEY = 'interactiveai.pending-survey.v1'
+
+/** Condition sent when the operator was not on a use case page. */
+export const UNKNOWN_USE_CASE = 'unknown'
+
+export type PendingSurvey = {
+ /** Trace session being closed -> Participant ID. */
+ sessionId: string
+ /** Use case the operator was on -> Condition ID. */
+ useCase: Entity | typeof UNKNOWN_USE_CASE
+ requestedAt: string
+}
+
+/** Queue the survey for the session that is about to end. */
+export function requestSurvey(survey: Omit): void {
+ try {
+ localStorage.setItem(
+ STORAGE_KEY,
+ JSON.stringify({ ...survey, requestedAt: new Date().toISOString() } satisfies PendingSurvey)
+ )
+ } catch (error) {
+ console.warn('Unable to queue the post-logout survey:', error)
+ }
+}
+
+/** The survey waiting to be taken, if any. */
+export function pendingSurvey(): PendingSurvey | undefined {
+ const raw = localStorage.getItem(STORAGE_KEY)
+ if (!raw) return undefined
+ try {
+ return JSON.parse(raw) as PendingSurvey
+ } catch {
+ localStorage.removeItem(STORAGE_KEY)
+ return undefined
+ }
+}
+
+/** Forget the request, once the survey has been taken or skipped. */
+export function clearPendingSurvey(): void {
+ localStorage.removeItem(STORAGE_KEY)
+}
+
+/**
+ * URL of the survey chain for a pending request. One chainer serves every use
+ * case: it picks its questionnaires from the `condition` it is given, so a
+ * per-use-case chain is declared in `public/surveys/surveychainer.html`
+ * (`CHAINS`) rather than here.
+ */
+export function surveyUrl(survey: PendingSurvey): string {
+ const params = new URLSearchParams({
+ participant: survey.sessionId,
+ condition: survey.useCase
+ })
+ return `${import.meta.env.BASE_URL}surveys/surveychainer.html?${params}`
+}
diff --git a/frontend/src/utils/traceSessionExport.ts b/frontend/src/utils/traceSessionExport.ts
index 5e2914e2..21e4cb32 100644
--- a/frontend/src/utils/traceSessionExport.ts
+++ b/frontend/src/utils/traceSessionExport.ts
@@ -4,7 +4,7 @@ import type { Trace } from '@/types/services'
import { hasCognitiveConsent } from '@/utils/consent'
type ExportFormat = 'json' | 'csv'
-type SessionStep = Trace['step'] | 'FEEDBACK'
+type SessionStep = Trace['step'] | 'FEEDBACK' | 'RECOMMENDATIONS'
type StoredTrace = {
date: string
@@ -23,6 +23,13 @@ type TraceSession = {
type ExportOptions = {
force?: boolean
userLogin?: string
+ /**
+ * Whether the HTML summary is opened in a new tab right away. The file is
+ * written either way. `false` holds the report back instead - the tab steals
+ * the focus, which would cover the post-logout survey, so it is offered once
+ * the questionnaire is over (see `openDeferredSummary`).
+ */
+ openSummary?: boolean
}
const STORAGE_KEY = 'interactiveai.trace-session.v1'
@@ -57,7 +64,47 @@ function loadSession(): TraceSession | undefined {
}
function saveSession(session: TraceSession) {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(session))
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(session))
+ } catch (error) {
+ // Quota exceeded: traces carry base64 snapshots and full observations, so a
+ // long session can fill the 5 MB store. Keep what is already recorded
+ // instead of letting the write reject and stop the recording altogether.
+ console.warn('Unable to persist the trace session (storage full?):', error)
+ }
+}
+
+/** Narrow an unknown trace payload to something spreadable. */
+function asRecord(value: unknown): Record {
+ return value !== null && typeof value === 'object' ? (value as Record) : {}
+}
+
+/**
+ * The grid state the operator was looking at when an event fired - the very
+ * payload the recommendation service is handed, kept verbatim (~20 kB of JSON
+ * per event). Only the PowerGrid context carries an `observation`; the other
+ * use cases record nothing.
+ *
+ * The services store is imported lazily: it reaches the auth store, which
+ * imports this module, and a static cycle would leave the import undefined at
+ * module-init time.
+ */
+async function currentObservation(useCase: Trace['use_case']): Promise {
+ try {
+ const { useServicesStore } = await import('@/stores/services')
+ const stored = asRecord(useServicesStore().context(useCase)?.data).observation
+ if (stored !== undefined) return stored
+
+ // The store only publishes a context once its id *changes*, so it is still
+ // empty right after login - fetch directly rather than lose the first
+ // events of a session.
+ const { getContext } = await import('@/api/services')
+ const { data } = await getContext()
+ return asRecord(data.find((item) => item.use_case === useCase)?.data).observation
+ } catch (error) {
+ console.warn('Unable to attach the context observation to the trace:', error)
+ return undefined
+ }
}
function eventKey(data: unknown): string | undefined {
@@ -82,6 +129,13 @@ type StructuredEvent = StoredTrace & {
interactions: StoredTrace[]
/** Time in ms between ASKFORHELP and AWARD. null when the user didn't choose a solution. */
decision_time_ms: number | null
+ /**
+ * Time in ms between the recommendations appearing on screen and the operator
+ * applying one - the decision time with the agent's own latency taken out.
+ * null when no solution was applied, or when the session predates the
+ * RECOMMENDATIONS trace.
+ */
+ human_decision_time_ms: number | null
}
type StructuredTrace = StoredTrace | StructuredEvent
@@ -91,6 +145,13 @@ type SessionKpis = {
total_session_time_ms: number
/** Average decision time across ALL events (sum of decision times / total events). null if no events. */
avg_decision_time_ms: number | null
+ /**
+ * Average human decision time, over the events where one could be measured
+ * (recommendations displayed *and* a solution applied) rather than over every
+ * event - an event the operator never acted on says nothing about how long
+ * they take to decide. null when no event qualifies.
+ */
+ avg_human_decision_time_ms: number | null
}
function isStructuredEvent(t: StructuredTrace): t is StructuredEvent {
@@ -108,6 +169,22 @@ function computeDecisionTime(interactions: StoredTrace[]): number | null {
return new Date(awardDate).getTime() - new Date(askDate).getTime()
}
+/**
+ * How long the operator themselves took: from the recommendations being shown
+ * to the apply. `decision_time_ms` starts one step earlier, at ASKFORHELP, so it
+ * also carries however long the recommendation service took to answer.
+ */
+function computeHumanDecisionTime(interactions: StoredTrace[]): number | null {
+ let shownDate: string | undefined
+ let awardDate: string | undefined
+ for (let i = 0; i < interactions.length; i++) {
+ if (interactions[i].step === 'RECOMMENDATIONS' && !shownDate) shownDate = interactions[i].date
+ if (interactions[i].step === 'AWARD' && !awardDate) awardDate = interactions[i].date
+ }
+ if (!shownDate || !awardDate) return null
+ return new Date(awardDate).getTime() - new Date(shownDate).getTime()
+}
+
/** Map legacy event_type values to human-readable labels for export. */
function normalizeEventType(eventType: string): string {
if (eventType === 'KPI') return 'Overload'
@@ -124,7 +201,12 @@ function buildStructuredTraces(flat: StoredTrace[]): StructuredTrace[] {
for (const trace of flat) {
if (trace.step === 'EVENT') {
- const structured: StructuredEvent = { ...trace, interactions: [], decision_time_ms: null }
+ const structured: StructuredEvent = {
+ ...trace,
+ interactions: [],
+ decision_time_ms: null,
+ human_decision_time_ms: null
+ }
const data = trace.data as Record | undefined
const cardId = data?.card_id as string | undefined
if (cardId) eventByCardId[cardId] = structured
@@ -161,6 +243,7 @@ function buildStructuredTraces(flat: StoredTrace[]): StructuredTrace[] {
for (const entry of result) {
if (isStructuredEvent(entry)) {
entry.decision_time_ms = computeDecisionTime(entry.interactions)
+ entry.human_decision_time_ms = computeHumanDecisionTime(entry.interactions)
}
}
@@ -232,6 +315,7 @@ function stepBadge(step: string): string {
EVENT: '#2563eb',
ASKFORHELP: '#d97706',
FEEDBACK: '#7c3aed',
+ RECOMMENDATIONS: '#0ea5e9',
AWARD: '#059669',
SOLUTION: '#0891b2'
}
@@ -268,6 +352,28 @@ function isLargeBlob(val: unknown): boolean {
return str.length > MAX_VALUE_LENGTH
}
+/**
+ * Build an ` ` source from a raw base64 payload.
+ *
+ * The PowerGrid simulator renders its observation snapshots as SVG since the
+ * zoom feature landed (`plt.savefig(..., format="svg")`), while older sessions
+ * still carry PNG. A `data:image/png` URI holding SVG bytes renders as a broken
+ * image, so the media type is sniffed from the payload instead of assumed.
+ */
+function imageDataUri(base64: string): string {
+ if (base64.startsWith('data:')) return base64
+ let head = ''
+ try {
+ // 64 chars is a whole number of base64 quanta, so a prefix decodes cleanly
+ head = atob(base64.slice(0, 64)).trimStart()
+ } catch {
+ // Undecodable on its own - fall back to PNG, the historical format
+ }
+ const isSvg =
+ head.startsWith('
@@ -279,7 +385,7 @@ function eventMetadataHtml(data: unknown): string {
const key = keys[i]
const val = meta[key]
if (key === 'event_context' && isLargeBlob(val) && typeof val === 'string') {
- const src = val.startsWith('data:') ? val : 'data:image/png;base64,' + val
+ const src = imageDataUri(val)
rows.push('' + escapeHtml(key) + ' ')
} else if (isLargeBlob(val)) {
rows.push('' + escapeHtml(key) + ' [large data omitted] ')
@@ -290,6 +396,23 @@ function eventMetadataHtml(data: unknown): string {
return ''
}
+/**
+ * The observation is ~65 arrays of floats: useful to have, unreadable inline.
+ * Render it folded so the event card stays scannable.
+ */
+function observationHtml(data: unknown): string {
+ const observation = asRecord(data).observation
+ if (!observation || typeof observation !== 'object') return ''
+ const fields = Object.keys(observation as Record).length
+ return (
+ 'Grid observation (' +
+ fields +
+ ' fields) ' +
+ escapeHtml(JSON.stringify(observation, null, 2)) +
+ ' '
+ )
+}
+
function cognitiveSnapshotHtml(data: unknown): string {
if (!data || typeof data !== 'object') return ''
const d = data as Record
@@ -388,13 +511,19 @@ function buildHtmlSummary(
html += 'h3{margin:0 0 6px 0;font-size:15px}'
html += '.tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:12px;background:#e5e7eb;color:#374151;margin-right:4px}'
html += '.no-solution{color:#dc2626;font-style:italic;font-size:13px}'
- html += '@media print{body{padding:12px}.card{box-shadow:none;break-inside:avoid}}'
+ html += '.copy-btn{margin-left:6px;padding:1px 8px;border:1px solid #d1d5db;border-radius:4px;background:#fff;color:#374151;font-size:11px;font-family:inherit;cursor:pointer;vertical-align:middle}'
+ html += '.copy-btn:hover{background:#f3f4f6}'
+ html += '.copy-btn.copied{border-color:#059669;color:#059669}'
+ html += '@media print{body{padding:12px}.card{box-shadow:none;break-inside:avoid}.copy-btn{display:none}}'
html += ''
// Header
html += ''
html += '
Session Summary '
- html += '
User: ' + escapeHtml(session.userLogin ?? 'unknown') + ' · Session: ' + escapeHtml(session.sessionId) + '
'
+ // The session id is what ties this report to the survey answers and the JSON
+ // export, so it is made copyable rather than retyped by hand.
+ html += '
User: ' + escapeHtml(session.userLogin ?? 'unknown') + ' · Session: ' + escapeHtml(session.sessionId) + ' '
+ html += 'Copy
'
html += '
' + formatTime(session.startedAt) + ' → ' + formatTime(endedAt) + '
'
html += '
'
@@ -405,7 +534,9 @@ function buildHtmlSummary(
const resolved = events.filter(function (e) { return e.decision_time_ms !== null })
html += '' + resolved.length + ' / ' + events.length + '
Assistance relevance
'
- html += '' + formatMs(kpis.avg_decision_time_ms) + '
Avg Decision Time (across all events)
'
+ html += '' + formatMs(kpis.avg_decision_time_ms) + '
Average Total Decision Time
'
+ const humanDecided = events.filter(function (e) { return e.human_decision_time_ms !== null })
+ html += '' + formatMs(kpis.avg_human_decision_time_ms) + '
Average Human Response Time
'
html += ''
// Per-event details
@@ -428,11 +559,15 @@ function buildHtmlSummary(
if (eventSummary) html += '' + escapeHtml(eventSummary) + '
'
html += '' + formatTime(evt.date) + '
'
html += eventMetadataHtml(evt.data)
+ html += observationHtml(evt.data)
html += cognitiveSnapshotHtml(evt.data)
// Decision time
if (evt.decision_time_ms !== null) {
- html += '⏱ Decision time: ' + formatMs(evt.decision_time_ms) + '
'
+ html += '⏱ Total Decision Time: ' + formatMs(evt.decision_time_ms) + '
'
+ if (evt.human_decision_time_ms !== null) {
+ html += '👤 Human Response Time: ' + formatMs(evt.human_decision_time_ms) + '
'
+ }
} else {
html += 'No solution selected
'
}
@@ -460,10 +595,69 @@ function buildHtmlSummary(
html += ''
}
+ // Inline so the report keeps working as a standalone file. `execCommand` is
+ // the fallback for the downloaded copy: opened from file://, some browsers
+ // refuse the async clipboard API.
+ html += '
+
diff --git a/frontend/start-webui.sh b/frontend/start-webui.sh
index c906e6b7..4db61630 100755
--- a/frontend/start-webui.sh
+++ b/frontend/start-webui.sh
@@ -17,7 +17,16 @@
The container will be run by running this file
'
-export resolver=$(grep nameserver /etc/resolv.conf | awk '{ print $2 }')
+# All nameservers on ONE line: nginx's resolver takes several addresses, and a multi-line
+# value would splice a newline into the sed expression below, which fails with
+# "sed: unmatched '/'" and writes an EMPTY default.conf. nginx then starts with no server
+# block at all and answers nothing - a silent outage. Hosts with two nameservers in
+# /etc/resolv.conf are common enough to hit this.
+export resolver=$(awk '/^[[:space:]]*nameserver/ { printf "%s ", $2 }' /etc/resolv.conf)
+if [ -z "$resolver" ]; then
+ echo "ERROR: no nameserver in /etc/resolv.conf - cannot build the nginx resolver line." >&2
+ exit 1
+fi
resolver_replace="resolver $resolver ipv6=off;"
resolver_replaced=".*resolver.*"
nginx_conf_path_default="/etc/nginx"
@@ -46,18 +55,113 @@ fi
echo "The resolver in the personal default.conf:"
grep -e "$resolver_replaced" $defaultconf_personal
+# The sed above is the one step that can fail while leaving a valid-but-useless config
+# behind (an empty file passes `nginx -t`), so check the result rather than the exit code.
+if [ ! -s $defaultconf_personal ]; then
+ echo "ERROR: $defaultconf_personal is empty - the resolver substitution failed." >&2
+ exit 1
+fi
+
cat $nginx_conf_path_default/nginx.conf > $nginx_conf_path_personal/nginx.conf
sed -i "s/$(echo $nginx_conf_path_default | sed 's/\//\\\//g')\/conf\.d/$(echo $nginx_conf_path_personal | sed 's/\//\\\//g')\/conf\.d/" $nginx_conf_path_personal/nginx.conf
echo "The conf.d path in the personal nginx.conf file:"
grep "conf.d" $nginx_conf_path_personal/nginx.conf
-# PowerGrid simulator upstream — environment-specific, injected here so one nginx
-# config serves every environment. Defaults to the local-dev host container.
-# No-op for configs that don't contain the placeholder (e.g. kubernetes).
-powergrid_simu_upstream="${POWERGRID_SIMU_UPSTREAM:-http://host.docker.internal:5122/}"
-echo "PowerGrid simulator upstream: $powergrid_simu_upstream"
-sed -i "s#__POWERGRID_SIMU_UPSTREAM__#${powergrid_simu_upstream}#g" $defaultconf_personal
+: '
+ Runtime configuration of the nginx conf.
+ Every environment-specific value lives in the conf as a __NAME__ placeholder and is
+ substituted here from the matching env var. To add one: give it a default below, append
+ its name to SUBST_VARS, and use __NAME__ in the conf. Nothing else needs to change.
+
+ Placeholders (rather than one conf per environment) keep a single conf serving local dev,
+ cab-standalone and k8s alike. Secrets belong here too, not in the frontend bundle: a
+ VITE_* value is inlined into the public JS at build time, so it is readable by anyone
+ loading the app and can only be rotated by rebuilding the image.
+
+ NB: this script only substitutes into conf.d/default.conf. In k8s that file comes from the
+ cab-assistant-platform-config ConfigMap mounted over /etc/nginx/conf.d, which overrides
+ the default.conf baked into the image - so the placeholders must be present there too.
+'
+
+# Where nginx forwards /powergrid-simu/. Defaults to a simulator container on the host.
+: "${POWERGRID_SIMU_UPSTREAM:=http://host.docker.internal:5122/}"
+# Bearer token for the INESCTEC cognitive API, injected into the /cognitive-api/ proxy.
+: "${COGNITIVE_TOKEN:=}"
+
+SUBST_VARS="POWERGRID_SIMU_UPSTREAM COGNITIVE_TOKEN"
+
+# Names that must carry a NON-EMPTY value, space- or comma-separated. An empty required
+# variable aborts startup instead of quietly substituting nothing.
+#
+# Opt-in rather than "everything is required" because an absent value is not always wrong.
+# Local dev runs the whole stack without a cognitive token and merely loses the cognitive
+# panel, so demanding one there would block work on unrelated features. A public deployment
+# is the opposite case: an empty token means nginx sends "Bearer " with nothing after it and
+# every /cognitive-api/ call 401s while the pod reports itself healthy. So
+# deploy-chart/values.ovh.yaml sets REQUIRED_VARS=COGNITIVE_TOKEN and the pod crashloops
+# instead - which in k8s leaves the previous pod serving, and puts the reason in the logs.
+: "${REQUIRED_VARS:=}"
+
+missing_required=""
+
+for name in $SUBST_VARS; do
+ eval "value=\$$name"
+ # Escape the sed replacement metacharacters, including the # delimiter, so tokens and
+ # URLs containing them cannot break out of the expression.
+ escaped=$(printf '%s' "$value" | sed -e 's/[\\]/\\&/g')
+ sed -i "s#__${name}__#${escaped}#g" $defaultconf_personal
+ # Secrets are not echoed; report only whether a value arrived.
+ case "$name" in
+ *TOKEN*|*SECRET*|*PASSWORD*)
+ if [ -n "$value" ]; then echo "$name: set (${#value} chars)"; else echo "$name: EMPTY"; fi ;;
+ *) echo "$name: $value" ;;
+ esac
+ # Collected rather than fatal on the spot, so the log names every missing variable in
+ # one go instead of one per restart.
+ if [ -z "$value" ] && printf '%s' "$REQUIRED_VARS" | tr ',' ' ' | grep -qw "$name"; then
+ missing_required="$missing_required $name"
+ fi
+done
+
+if [ -n "$missing_required" ]; then
+ echo "ERROR: required runtime variable(s) empty:$missing_required" >&2
+ echo "They are listed in REQUIRED_VARS=\"$REQUIRED_VARS\" and would have been substituted" >&2
+ echo "into the nginx conf as empty strings - producing a proxy that answers requests but" >&2
+ echo "sends no credentials, which surfaces only as a 401 from the upstream. Refusing to" >&2
+ echo "start instead." >&2
+ echo "In k8s: check the env var on the deployment, and that the secret and key it" >&2
+ echo "references both exist (deploy-chart/apply-nginx-conf.sh reports this)." >&2
+ exit 1
+fi
+
+# A placeholder that survives means its env var was never set. nginx would either refuse to
+# start on an invalid proxy_pass or, worse, serve a silently broken proxy - so fail here,
+# where the reason is obvious in the container logs.
+leftover=$(grep -o '__[A-Z_][A-Z_]*__' $defaultconf_personal | sort -u)
+if [ -n "$leftover" ]; then
+ echo "ERROR: unsubstituted placeholders in $defaultconf_personal:" >&2
+ echo "$leftover" >&2
+ echo "Set the matching env vars, or add them to SUBST_VARS in start-webui.sh." >&2
+ exit 1
+fi
+
+# Validate before handing over to the daemon, so a bad conf fails at startup with a message
+# rather than at the first request.
+if ! nginx -t -c $nginx_conf_path_personal/nginx.conf; then
+ echo "ERROR: nginx rejected the generated configuration (see above)." >&2
+ exit 1
+fi
+
+# `nginx -t` accepts a config with no server block, so it would not catch a conf.d file that
+# got mangled into something empty or serverless. Check we will actually serve something.
+if ! grep -q "listen" $defaultconf_personal; then
+ echo "ERROR: no listen directive in $defaultconf_personal - nginx would start and serve" >&2
+ echo "nothing. The file is $(wc -c < $defaultconf_personal) bytes; its locations are:" >&2
+ # Never cat the file: it now carries the substituted secrets.
+ grep -n "location" $defaultconf_personal >&2 || echo " (none)" >&2
+ exit 1
+fi
/usr/sbin/crond
diff --git a/usecases_examples/Railway/ConflictResolver.py b/usecases_examples/Railway/ConflictResolver.py
new file mode 100644
index 00000000..e8b6b024
--- /dev/null
+++ b/usecases_examples/Railway/ConflictResolver.py
@@ -0,0 +1,1234 @@
+"""
+Conflict Detection and Resolution for train dispatching.
+
+Components:
+- ConflictDetector: Projects train positions and finds conflicts
+- ResolutionGenerator: Computes alternative routes and wait times
+- ConflictResolver: Coordinates detection and resolution
+
+Resolution strategies:
+- REROUTE: Send lower-priority train via alternative route
+- WAIT: Hold lower-priority train until higher-priority clears
+
+The resolver compares costs and chooses the better option.
+"""
+
+from collections import deque
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import List, Dict, Tuple, Optional, Set
+from flatland.core.grid.grid4_utils import get_new_position
+from flatland.envs.rail_generators import RailEnvTransitions
+
+
+class ResolutionType(Enum):
+ """Type of resolution for a conflict."""
+ REROUTE = "reroute" # Send train via alternative route
+ WAIT = "wait" # Hold train until path is clear
+
+
+@dataclass
+class Conflict:
+ """Represents a detected conflict between two trains."""
+ train_a: int
+ train_b: int
+ cell: Tuple[int, int]
+ timestep: int
+
+
+@dataclass
+class Resolution:
+ """Represents a resolution for a conflict."""
+ resolution_type: ResolutionType
+ train_to_delay: int # Train that gets delayed (rerouted or waits)
+ delay_added: int # Additional timesteps compared to original
+
+ # For REROUTE resolutions
+ original_route: Optional[List[Tuple[int, int]]] = None
+ new_route: Optional[List[Tuple[int, int]]] = None
+
+ # For WAIT resolutions
+ wait_until: Optional[int] = None # Timestep when train can proceed
+ wait_at_cell: Optional[Tuple[int, int]] = None # Cell where train waits
+
+
+# ============== CONFLICT DETECTOR ==============
+
+class ConflictDetector:
+ """Detects conflicts by projecting train positions over time."""
+
+ def __init__(self, env, timetable):
+ self.env = env
+ self.timetable = timetable
+
+ def project_train_positions(self, train_id, current_step) -> Dict[int, Tuple[int, int]]:
+ """
+ Project where train will be at each future timestep.
+
+ """
+ schedule = self.timetable.get_schedule(train_id)
+ if schedule is None:
+ return {} # agent not in this scenario's timetable
+ agent = self.env.agents[train_id]
+ route = schedule.route
+ speed = getattr(schedule, 'speed', 1.0) # 1.0 = normal, 0.5 = 2 steps per cell
+
+ if not route:
+ return {}
+
+ # Check if already done
+ state_name = agent.state.name if hasattr(agent.state, 'name') else str(agent.state)
+ if 'DONE' in state_name:
+ return {}
+
+ # Determine starting point and time
+ if agent.position is None:
+ # Not on grid yet - will start from beginning of route at planned departure
+ current_idx = 0
+ # Train enters grid at planned_departure + 1 (action taken, then moves)
+ start_time = schedule.planned_departure + 1
+
+ # Account for hold instruction
+ if getattr(schedule, 'was_held', False) and getattr(schedule, 'hold_until', None):
+ # Train is held until hold_until, so effective start is later
+ start_time = max(start_time, schedule.hold_until + 1)
+ else:
+ # Already on grid - project from current position
+ try:
+ current_idx = route.index(agent.position)
+ except ValueError:
+ # Position not in route - might have been rerouted
+ return {}
+ start_time = current_step
+
+ # Project future positions accounting for speed
+ # With speed=0.5, train takes 2 steps per cell
+ # Formula: position_index = floor(timestep_offset * speed)
+ positions = {}
+ remaining_route = route[current_idx:]
+
+ # If held, add positions during hold period at first position
+ if getattr(schedule, 'was_held', False) and getattr(schedule, 'hold_until', None):
+ if agent.position is None:
+ hold_pos = route[0]
+ for t in range(schedule.planned_departure + 1, schedule.hold_until + 1):
+ positions[t] = hold_pos
+
+ if speed >= 1.0:
+ # Normal or fast speed - 1 step per cell (fast trains not fully supported yet)
+ for i, pos in enumerate(remaining_route):
+ positions[start_time + i] = pos
+ else:
+ # Slow speed - multiple steps per cell
+ steps_per_cell = int(1.0 / speed) # e.g., speed=0.5 → 2 steps per cell
+ timestep = start_time
+ for pos in remaining_route:
+ # Train stays at this position for steps_per_cell timesteps
+ for _ in range(steps_per_cell):
+ positions[timestep] = pos
+ timestep += 1
+
+ return positions
+
+ def detect_conflicts(self, current_step) -> Tuple[List[Conflict], Dict]:
+ """
+ Find all conflicts between trains.
+
+ Detects:
+ 1. Same cell at same time (standard collision)
+ 2. Head-on collision (trains swap positions - try to pass through each other)
+
+ Args:
+ current_step: Current simulation step
+
+ Returns:
+ Tuple of (List of Conflict objects, projections dict)
+ """
+ num_agents = self.env.get_num_agents()
+
+ # Project all trains
+ projections = {}
+ for i in range(num_agents):
+ projections[i] = self.project_train_positions(i, current_step)
+
+ # Find conflicts
+ conflicts = []
+ for i in range(num_agents):
+ for j in range(i + 1, num_agents):
+ for step, pos_i in projections[i].items():
+ if step in projections[j]:
+ pos_j = projections[j][step]
+
+ # Type 1: Same cell at same time
+ if pos_i == pos_j:
+ conflicts.append(Conflict(
+ train_a=i,
+ train_b=j,
+ cell=pos_i,
+ timestep=step,
+ ))
+
+ # Type 2: Head-on collision (position swap)
+ next_step = step + 1
+ if next_step in projections[i] and next_step in projections[j]:
+ next_pos_i = projections[i][next_step]
+ next_pos_j = projections[j][next_step]
+
+ # Check if they swap positions (try to pass through each other)
+ if pos_i == next_pos_j and pos_j == next_pos_i:
+ conflicts.append(Conflict(
+ train_a=i,
+ train_b=j,
+ cell=pos_i, # Report where train_i is (conflict zone)
+ timestep=step,
+ ))
+
+ return conflicts, projections
+
+
+# ============== RESOLUTION GENERATOR ==============
+
+class ResolutionGenerator:
+ """Generates resolution options for conflicts (rerouting)."""
+
+ def __init__(self, env):
+ self.env = env
+ self.rail_trans = RailEnvTransitions()
+
+ def compute_route_avoiding_cells(
+ self,
+ start: Tuple[int, int],
+ target: Tuple[int, int],
+ blocked_cells: Set[Tuple[int, int]],
+ start_direction: Optional[int] = None,
+ use_simple_connectivity: bool = False # Changed default to False
+ ) -> List[Tuple[int, int]]:
+ """
+ Find route avoiding blocked cells using Flatland transitions.
+
+ Args:
+ start: Starting position
+ target: Target position
+ blocked_cells: Cells to avoid
+ start_direction: Optional starting direction
+ use_simple_connectivity: If True, use simple neighbor checking (legacy)
+
+ Returns:
+ List of positions forming the route, or empty if no route found
+ """
+ grid = self.env.rail.grid
+ height, width = grid.shape
+
+ if start == target:
+ return [start]
+
+ # Direction offsets: N=0, E=1, S=2, W=3
+ dir_offsets = [(-1, 0), (0, 1), (1, 0), (0, -1)]
+
+ if use_simple_connectivity:
+ # Simple mode: just check if neighbor cells have track
+ # Only used for custom tracks that don't follow Flatland rules
+ queue = deque([(start, [start])])
+ visited = {start}
+
+ while queue:
+ pos, path = queue.popleft()
+
+ if pos == target:
+ return path
+
+ r, c = pos
+
+ for dr, dc in dir_offsets:
+ nr, nc = r + dr, c + dc
+ next_pos = (nr, nc)
+
+ if (0 <= nr < height and 0 <= nc < width and
+ grid[next_pos] != 0 and
+ next_pos not in visited and
+ next_pos not in blocked_cells):
+ visited.add(next_pos)
+ queue.append((next_pos, path + [next_pos]))
+
+ return [] # No path found
+
+ # Use Flatland transitions to find valid route
+ # First, find all valid routes to target
+ all_routes = self._find_all_routes(start, target, max_routes=10, start_direction=start_direction)
+
+ # Filter to routes that avoid blocked cells
+ valid_routes = []
+ for route in all_routes:
+ # Check if any cell in route (except start) is blocked
+ if not any(cell in blocked_cells for cell in route[1:]):
+ valid_routes.append(route)
+
+ if not valid_routes:
+ return []
+
+ # Return shortest valid route
+ return min(valid_routes, key=len)
+
+ def calculate_wait_cost(
+ self,
+ train_to_wait: int,
+ train_with_priority: int,
+ conflict: Conflict,
+ timetable,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> Tuple[int, int, Optional[Tuple[int, int]]]:
+ """
+ Calculate the cost (delay) of waiting for the priority train to pass.
+
+ Args:
+ train_to_wait: Train that would wait
+ train_with_priority: Train that has priority
+ conflict: The conflict being resolved
+ timetable: Current timetable
+ projections: Position projections for all trains
+
+ Returns:
+ Tuple of (wait_cost, wait_until_timestep, wait_at_cell)
+ Returns (infinity, -1, None) if waiting is not possible
+ """
+ schedule_wait = timetable.get_schedule(train_to_wait)
+ schedule_priority = timetable.get_schedule(train_with_priority)
+
+ # BOUNDS CHECK: Make sure schedules exist
+ if schedule_wait is None or schedule_priority is None:
+ return (float('inf'), -1, None)
+
+ route_wait = schedule_wait.route
+ route_priority = schedule_priority.route
+
+ if not route_wait or not route_priority:
+ return (float('inf'), -1, None)
+
+ # BOUNDS CHECK: Make sure projections exist for both trains
+ if train_to_wait not in projections or train_with_priority not in projections:
+ return (float('inf'), -1, None)
+
+ # Find the conflict zone (overlapping cells)
+ overlap = set(route_wait) & set(route_priority)
+ if not overlap:
+ return (float('inf'), -1, None)
+
+ # Find the first overlap cell in the waiting train's route
+ # The train should wait BEFORE entering this cell
+ first_overlap_idx = None
+ for i, cell in enumerate(route_wait):
+ if cell in overlap:
+ first_overlap_idx = i
+ break
+
+ if first_overlap_idx is None or first_overlap_idx == 0:
+ # Can't wait - already in conflict zone or would start there
+ return (float('inf'), -1, None)
+
+ # Wait at the cell just before the conflict zone
+ wait_at_cell = route_wait[first_overlap_idx - 1]
+
+ #
+ # Window size = 4 cells (the junction cell + 3 cells ahead).
+ # Buffer = 3 extra steps after clear so Train A doesn't tailgate.
+ BUFFER_STEPS = 3
+ WINDOW_SIZE = 4
+
+ window_cells = set(route_wait[first_overlap_idx:first_overlap_idx + WINDOW_SIZE])
+
+ proj_priority = projections[train_with_priority]
+
+ clear_time = -1
+ for timestep, pos in proj_priority.items():
+ if pos in window_cells:
+ clear_time = max(clear_time, timestep)
+
+ if clear_time == -1:
+ # Priority train never enters the window — no conflict, no wait needed
+ return (float('inf'), -1, None)
+
+ # Train can proceed after priority train clears the window + buffer
+ wait_until = clear_time + BUFFER_STEPS
+
+ # Calculate how long the waiting train would need to wait
+ # Find when waiting train would reach the conflict entry without waiting
+ proj_wait = projections[train_to_wait]
+ arrival_at_conflict = None
+ for timestep, pos in proj_wait.items():
+ if pos == wait_at_cell:
+ # This is when train reaches wait cell, next step enters conflict
+ arrival_at_conflict = timestep + 1
+ break
+
+ if arrival_at_conflict is None:
+ return (float('inf'), -1, None)
+
+ # Wait cost = time spent waiting
+ wait_cost = max(0, wait_until - arrival_at_conflict)
+
+ return (wait_cost, wait_until, wait_at_cell)
+
+ def calculate_reroute_option(
+ self,
+ train_to_reroute: int,
+ train_with_priority: int,
+ timetable,
+ ) -> Tuple[int, List[Tuple[int, int]], List[Tuple[int, int]]]:
+ """
+ Calculate the cost (delay) of rerouting.
+
+ Returns:
+ Tuple of (reroute_cost, original_route, new_route)
+ Returns (infinity, [], []) if rerouting is not possible
+ """
+ schedule_reroute = timetable.get_schedule(train_to_reroute)
+ schedule_priority = timetable.get_schedule(train_with_priority)
+
+ if schedule_reroute is None:
+ return (float('inf'), [], [])
+
+ original_route = list(schedule_reroute.route)
+ priority_route = list(schedule_priority.route) if schedule_priority else []
+
+ # BOUNDS CHECK: Make sure train exists in environment
+ if train_to_reroute >= len(self.env.agents):
+ return (float('inf'), original_route, [])
+
+ # Get departure times
+ departure_reroute = schedule_reroute.planned_departure
+ departure_priority = schedule_priority.planned_departure if schedule_priority else 0
+
+ # Get current state — use actual direction if spawned, initial if not
+ agent = self.env.agents[train_to_reroute]
+ if agent.position is None:
+ start = original_route[0] if original_route else agent.initial_position
+ start_direction = int(agent.initial_direction)
+ else:
+ start = tuple(agent.position)
+ start_direction = int(agent.direction)
+
+ target = tuple(agent.target) if hasattr(agent.target, '__iter__') else agent.target
+
+ # Find all possible routes from start to target, respecting direction
+ all_routes = self._find_all_routes(start, target, max_routes=5,
+ start_direction=start_direction)
+
+ if not all_routes:
+ return (float('inf'), original_route, [])
+
+ # Build priority train's temporal occupancy: timestep -> position
+ priority_occupancy = {}
+ for step_offset, cell in enumerate(priority_route):
+ timestep = departure_priority + step_offset
+ priority_occupancy[timestep] = cell
+
+ original_set = set(original_route)
+
+ best_route = None
+ best_cost = float('inf')
+
+ for route in all_routes:
+ # VALIDATION: Route must actually reach the target
+ if not route or len(route) < 2:
+ continue
+
+ if route[-1] != target:
+ continue # Route doesn't reach destination
+
+ if route[0] != start:
+ continue # Route doesn't start from correct position
+
+ # VALIDATION: Route should be reasonable length (not truncated)
+ if len(route) < len(original_route) * 0.5:
+ continue # Suspiciously short, probably truncated
+
+ route_set = set(route)
+
+ # Skip if it's the same as original
+ if route_set == original_set:
+ continue
+
+ # TEMPORAL COLLISION CHECK:
+ # Project where rerouted train will be at each timestep
+ # and check if it collides with priority train
+ has_temporal_collision = False
+
+ for step_offset, cell in enumerate(route):
+ timestep = departure_reroute + step_offset
+
+ # Check if priority train is at the same cell at this timestep
+ if timestep in priority_occupancy:
+ if priority_occupancy[timestep] == cell:
+ has_temporal_collision = True
+ break
+
+ # Also check adjacent timesteps for head-on collisions
+ # (trains swapping positions)
+ if step_offset > 0:
+ prev_cell = route[step_offset - 1]
+ prev_timestep = timestep - 1
+
+ # Check if trains are swapping positions (head-on)
+ if (prev_timestep in priority_occupancy and
+ timestep in priority_occupancy):
+ priority_prev = priority_occupancy.get(prev_timestep)
+ priority_curr = priority_occupancy.get(timestep)
+
+ if priority_prev == cell and priority_curr == prev_cell:
+ # Trains would swap positions = head-on collision
+ has_temporal_collision = True
+ break
+
+ if has_temporal_collision:
+ continue # This route still collides, skip it
+
+ # This route avoids temporal collision!
+ # Cost = extra travel time
+ cost = max(0, len(route) - len(original_route))
+ if cost < best_cost:
+ best_cost = cost
+ best_route = route
+
+ if best_route is None:
+ return (float('inf'), original_route, [])
+
+ return (best_cost, original_route, best_route)
+
+ def _find_all_routes(
+ self,
+ start: Tuple[int, int],
+ target: Tuple[int, int],
+ max_routes: int = 5,
+ start_direction: int = None
+ ) -> List[List[Tuple[int, int]]]:
+ """Find multiple routes from start to target respecting Flatland transitions.
+ """
+ from collections import deque
+
+ grid = self.env.rail.grid
+ height, width = grid.shape
+
+ if start == target:
+ return [[start]]
+
+ # Direction offsets: N=0, E=1, S=2, W=3
+ dir_offsets = [(-1, 0), (0, 1), (1, 0), (0, -1)]
+
+ # BFS with direction tracking: (position, travel_direction, path)
+ queue = deque()
+
+ # At start, try all possible travel directions
+ cell = grid[start]
+ for travel_dir in range(4):
+ valid_exits = self.rail_trans.get_transitions(cell, travel_dir)
+ if any(valid_exits):
+ for exit_dir in range(4):
+ if valid_exits[exit_dir]:
+ dr, dc = dir_offsets[exit_dir]
+ next_pos = (start[0] + dr, start[1] + dc)
+ if 0 <= next_pos[0] < height and 0 <= next_pos[1] < width:
+ if grid[next_pos] != 0:
+ queue.append((next_pos, exit_dir, [start, next_pos]))
+
+ routes = []
+ max_length = height + width + 20
+ iterations = 0
+ max_iterations = 5000 # Prevent infinite loops
+
+ # Track visited states PER route length to allow finding longer alternatives
+ # Key: (pos, dir), Value: shortest path length that visited this state
+ visited_at_length = {}
+
+ while queue and len(routes) < max_routes * 3 and iterations < max_iterations:
+ iterations += 1
+ pos, travel_dir, path = queue.popleft()
+
+ if pos == target:
+ # Check if this is a genuinely different route
+ path_tuple = tuple(path)
+ if path_tuple not in [tuple(r) for r in routes]:
+ routes.append(path)
+ continue
+
+ if len(path) > max_length:
+ continue
+
+ # Allow revisiting a state if we're on a different (longer) path
+ # This enables finding bypass routes that merge back
+ state = (pos, travel_dir)
+ if state in visited_at_length:
+ # Only skip if a shorter path already explored this state
+ # and we're not significantly longer (allow some slack for bypasses)
+ if len(path) > visited_at_length[state] + 4:
+ continue
+ visited_at_length[state] = min(
+ visited_at_length.get(state, float('inf')),
+ len(path)
+ )
+
+ r, c = pos
+ cell = grid[r, c]
+
+ if cell == 0:
+ continue
+
+ valid_exits = self.rail_trans.get_transitions(cell, travel_dir)
+
+ for exit_dir in range(4):
+ if not valid_exits[exit_dir]:
+ continue
+
+ dr, dc = dir_offsets[exit_dir]
+ nr, nc = r + dr, c + dc
+ next_pos = (nr, nc)
+
+ if not (0 <= nr < height and 0 <= nc < width):
+ continue
+
+ next_cell = grid[next_pos]
+ if next_cell == 0:
+ continue
+
+ if next_pos in path:
+ continue
+
+ new_path = path + [next_pos]
+
+ if next_pos == target:
+ routes.append(new_path)
+ else:
+ # Continue traveling in the exit direction
+ queue.append((next_pos, exit_dir, new_path))
+
+ # Sort by length and select diverse routes
+ routes.sort(key=len)
+
+ selected = []
+ seen = set()
+ for route in routes:
+ if len(selected) >= max_routes:
+ break
+ route_tuple = tuple(route)
+ if route_tuple in seen:
+ continue
+ seen.add(route_tuple)
+
+ route_set = set(route)
+ is_different = all(
+ len(route_set - set(existing)) >= 2
+ for existing in selected
+ )
+ if is_different or not selected:
+ selected.append(route)
+
+ return selected
+
+ def _hold_cell_penalty(
+ self,
+ hold_cell,
+ hold_until: int,
+ holding_train: int,
+ timetable,
+ projections: Dict,
+ ) -> int:
+ """
+ Return a penalty added to a WAIT option's cost if the hold cell is
+ in another train's projected path during the hold period.
+
+ A WAIT that blocks a third train is worse than one that doesn't.
+ Penalty = 20 per other train that passes through the hold cell
+ during [current_step, hold_until]. This biases the CostCalculator
+ toward choosing options that don't create secondary conflicts.
+ """
+ if hold_cell is None:
+ return 0
+ penalty = 0
+ for tid, proj in projections.items():
+ if tid == holding_train:
+ continue
+ for t, pos in proj.items():
+ if t <= hold_until and pos == hold_cell:
+ penalty += 20
+ break
+ return penalty
+
+ def generate_all_options(
+ self,
+ conflict: Conflict,
+ priorities: Dict[int, float],
+ timetable,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> List[Resolution]:
+ """
+ Generate ALL possible resolution options for a conflict.
+
+ Returns a list of options that can be ranked by CostCalculator.
+
+ Args:
+ conflict: The conflict to resolve
+ priorities: Train priorities (higher = more important)
+ timetable: Current timetable
+ projections: Position projections for all trains
+
+ Returns:
+ List of Resolution objects (may be empty if no resolution found)
+ """
+ options = []
+
+ # EARLY VALIDATION: Check both trains exist and have projections
+ if conflict.train_a not in projections or conflict.train_b not in projections:
+ # Can't resolve - missing projection data
+ return options
+
+ schedule_a = timetable.get_schedule(conflict.train_a)
+ schedule_b = timetable.get_schedule(conflict.train_b)
+ if schedule_a is None or schedule_b is None:
+ return options
+
+ # Determine which train has lower priority (will be delayed)
+ priority_a = priorities.get(conflict.train_a, 0)
+ priority_b = priorities.get(conflict.train_b, 0)
+
+ if priority_a >= priority_b:
+ train_to_delay = conflict.train_b
+ train_with_priority = conflict.train_a
+ else:
+ train_to_delay = conflict.train_a
+ train_with_priority = conflict.train_b
+
+ # Generate WAIT option
+ wait_cost, wait_until, wait_at_cell = self.calculate_wait_cost(
+ train_to_delay, train_with_priority, conflict, timetable, projections
+ )
+
+ if wait_cost < float('inf'):
+ # Penalise hold cells that are also in other trains' routes —
+ # holding there will create a secondary conflict.
+ hold_penalty = self._hold_cell_penalty(
+ wait_at_cell, wait_until, train_to_delay, timetable, projections)
+ options.append(Resolution(
+ resolution_type=ResolutionType.WAIT,
+ train_to_delay=train_to_delay,
+ delay_added=wait_cost + hold_penalty,
+ wait_until=wait_until,
+ wait_at_cell=wait_at_cell,
+ ))
+
+ # Generate REROUTE option
+ reroute_cost, original_route, new_route = self.calculate_reroute_option(
+ train_to_delay, train_with_priority, timetable
+ )
+
+ if reroute_cost < float('inf') and new_route:
+ options.append(Resolution(
+ resolution_type=ResolutionType.REROUTE,
+ train_to_delay=train_to_delay,
+ delay_added=reroute_cost,
+ original_route=original_route,
+ new_route=new_route,
+ ))
+
+ # Also try delaying the OTHER train (even if higher priority)
+ # This gives more options for the cost calculator to choose from
+ other_train = conflict.train_a if train_to_delay == conflict.train_b else conflict.train_b
+
+ # WAIT option for other train
+ wait_cost2, wait_until2, wait_at_cell2 = self.calculate_wait_cost(
+ other_train, train_to_delay, conflict, timetable, projections
+ )
+
+ if wait_cost2 < float('inf'):
+ options.append(Resolution(
+ resolution_type=ResolutionType.WAIT,
+ train_to_delay=other_train,
+ delay_added=wait_cost2,
+ wait_until=wait_until2,
+ wait_at_cell=wait_at_cell2,
+ ))
+
+ # REROUTE option for other train
+ reroute_cost2, original_route2, new_route2 = self.calculate_reroute_option(
+ other_train, train_to_delay, timetable
+ )
+
+ if reroute_cost2 < float('inf') and new_route2:
+ options.append(Resolution(
+ resolution_type=ResolutionType.REROUTE,
+ train_to_delay=other_train,
+ delay_added=reroute_cost2,
+ original_route=original_route2,
+ new_route=new_route2,
+ ))
+
+ return options
+
+ def generate_resolution(
+ self,
+ conflict: Conflict,
+ priorities: Dict[int, float],
+ timetable,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> Optional[Resolution]:
+ """
+ Generate the best resolution for a conflict.
+ +
+ """
+ # EARLY VALIDATION: Check both trains exist and have projections
+ if conflict.train_a not in projections or conflict.train_b not in projections:
+ return None
+
+ schedule_a = timetable.get_schedule(conflict.train_a)
+ schedule_b = timetable.get_schedule(conflict.train_b)
+ if schedule_a is None or schedule_b is None:
+ return None
+
+ # Determine which train has lower priority (will be delayed)
+ priority_a = priorities.get(conflict.train_a, 0)
+ priority_b = priorities.get(conflict.train_b, 0)
+
+ if priority_a >= priority_b:
+ train_to_delay = conflict.train_b
+ train_with_priority = conflict.train_a
+ else:
+ train_to_delay = conflict.train_a
+ train_with_priority = conflict.train_b
+
+ # Calculate WAIT option
+ wait_cost, wait_until, wait_at_cell = self.calculate_wait_cost(
+ train_to_delay, train_with_priority, conflict, timetable, projections
+ )
+
+ # Calculate REROUTE option
+ reroute_cost, original_route, new_route = self.calculate_reroute_option(
+ train_to_delay, train_with_priority, timetable
+ )
+
+ # Choose the better option (lower cost)
+ # Prefer WAIT if costs are equal (simpler, no route change)
+ if wait_cost <= reroute_cost and wait_cost < float('inf'):
+ return Resolution(
+ resolution_type=ResolutionType.WAIT,
+ train_to_delay=train_to_delay,
+ delay_added=wait_cost,
+ wait_until=wait_until,
+ wait_at_cell=wait_at_cell,
+ )
+ elif reroute_cost < float('inf'):
+ return Resolution(
+ resolution_type=ResolutionType.REROUTE,
+ train_to_delay=train_to_delay,
+ delay_added=reroute_cost,
+ original_route=original_route,
+ new_route=new_route,
+ )
+ else:
+ return None # No resolution possible
+
+
+# ============== CONFLICT RESOLVER ==============
+
+class ConflictResolver:
+ """
+ Main class that coordinates conflict detection and resolution.
+ """
+
+ def __init__(self, env, timetable, priorities: Dict[int, float],
+ cost_weights=None, use_cost_calculator: bool = False, verbose: bool = False):
+ """
+ Initialize the conflict resolver.
+
+ Args:
+ env: Flatland RailEnv
+ timetable: Timetable with train schedules
+ priorities: Dict mapping train_id -> priority (higher = more important)
+ cost_weights: Optional CostWeights for cost-based decisions (not yet implemented)
+ use_cost_calculator: Whether to use cost calculator (not yet implemented)
+ verbose: Whether to print verbose output
+ """
+ self.env = env
+ self.timetable = timetable
+ self.priorities = priorities
+ self.detector = ConflictDetector(env, timetable)
+ self.generator = ResolutionGenerator(env)
+ self.verbose = verbose
+
+ # Cost calculator integration (placeholder)
+ self.cost_weights = cost_weights
+ self.use_cost_calculator = use_cost_calculator
+ self.cost_breakdowns = {} # Placeholder for cost breakdowns
+
+ # Tracking
+ self.conflicts_detected: List[Conflict] = []
+ self.resolutions_applied: List[Resolution] = []
+ self.delayed_trains: Set[int] = set() # Trains that have been rerouted or held
+
+ # Track UNSOLVABLE conflicts
+ self.unresolvable_conflicts: List[Tuple[Conflict, str]] = [] # (conflict, reason)
+
+ def check_and_resolve(self, current_step, max_iterations: int = 50) -> List[str]:
+
+ from SafetyVerifier import SafetyVerifier
+
+ messages = []
+
+ # Track attempted resolutions to avoid infinite loops
+ # Key: (train_a, train_b, resolution_type, route_hash)
+ attempted_resolutions = set()
+
+ for iteration in range(max_iterations):
+
+ conflicts, projections = self.detector.detect_conflicts(current_step)
+ verifier = SafetyVerifier(self.timetable, max_steps=150)
+ _, violations = verifier.verify_safety(verbose=False, ignore_destination_conflicts=True)
+
+ # Convert SafetyVerifier violations to Conflict objects
+ safety_conflict_pairs = set()
+ for v in violations:
+ pair = (min(v.train_a, v.train_b), max(v.train_a, v.train_b))
+ safety_conflict_pairs.add((pair, v.timestep, v.position))
+
+ # Add any violations not already in conflicts list
+ existing_pairs = set()
+ for c in conflicts:
+ pair = (min(c.train_a, c.train_b), max(c.train_a, c.train_b))
+ existing_pairs.add(pair)
+
+ for (pair, timestep, position) in safety_conflict_pairs:
+ if pair not in existing_pairs:
+ # SafetyVerifier found a collision that ConflictDetector missed!
+ conflicts.append(Conflict(
+ train_a=pair[0],
+ train_b=pair[1],
+ cell=position,
+ timestep=timestep,
+ ))
+ existing_pairs.add(pair)
+
+ if not conflicts:
+ if iteration > 0:
+ messages.append(f"\n✅ All conflicts resolved after {iteration} iteration(s)")
+ break
+
+ # Find a conflict we haven't fully tried to resolve yet
+ resolved_this_iteration = False
+
+ # Deduplicate conflicts by pair for this iteration
+ seen_pairs = set()
+ unique_conflicts = []
+ for c in conflicts:
+ pair = (min(c.train_a, c.train_b), max(c.train_a, c.train_b))
+ if pair not in seen_pairs:
+ seen_pairs.add(pair)
+ unique_conflicts.append(c)
+
+ for conflict in unique_conflicts:
+ pair = (min(conflict.train_a, conflict.train_b),
+ max(conflict.train_a, conflict.train_b))
+
+ self.conflicts_detected.append(conflict)
+
+ # Generate warning message
+ messages.append("")
+ messages.append("=" * 60)
+ messages.append(f"⚠️ CONFLICT DETECTED (iteration {iteration + 1})")
+ messages.append("=" * 60)
+ messages.append(
+ f" Train {conflict.train_a} (priority: {self.priorities.get(conflict.train_a, 0):.2f}) "
+ f"and Train {conflict.train_b} (priority: {self.priorities.get(conflict.train_b, 0):.2f})"
+ )
+ messages.append(f" Will collide at cell {conflict.cell} at timestep {conflict.timestep}")
+
+ # Generate resolution
+ resolution = self.generator.generate_resolution(
+ conflict, self.priorities, self.timetable, projections
+ )
+
+ if resolution:
+ # Create a key to track this specific resolution attempt
+ route_hash = hash(tuple(resolution.new_route)) if resolution.new_route else 0
+ resolution_key = (pair[0], pair[1], resolution.resolution_type.value, route_hash)
+
+ if resolution_key in attempted_resolutions:
+ # Already tried this exact resolution, skip to avoid loop
+ messages.append(f" (Skipping - already tried this resolution)")
+ continue
+
+ attempted_resolutions.add(resolution_key)
+ self.resolutions_applied.append(resolution)
+ self.delayed_trains.add(resolution.train_to_delay)
+
+ # Apply resolution based on type
+ schedule = self.timetable.get_schedule(resolution.train_to_delay)
+
+ if resolution.resolution_type == ResolutionType.REROUTE:
+ # Update timetable with new route
+ schedule.route = resolution.new_route
+ schedule.planned_arrival += resolution.delay_added
+ schedule.was_rerouted = True
+ schedule.reroute_delay_added = getattr(schedule, 'reroute_delay_added', 0) + resolution.delay_added
+
+ messages.append("")
+ messages.append(f"🔀 RESOLUTION: REROUTE Train {resolution.train_to_delay}")
+ messages.append(f" (Lower priority train takes bypass)")
+ messages.append("")
+ messages.append(f" Original route ({len(resolution.original_route)} cells):")
+ messages.append(f" {' → '.join(str(p) for p in resolution.original_route)}")
+ messages.append("")
+ messages.append(f" New route ({len(resolution.new_route)} cells):")
+ messages.append(f" {' → '.join(str(p) for p in resolution.new_route)}")
+ messages.append("")
+ messages.append(f" Additional delay: +{resolution.delay_added} timesteps")
+
+ elif resolution.resolution_type == ResolutionType.WAIT:
+ # Update timetable with hold instruction
+ schedule.planned_arrival += resolution.delay_added
+ schedule.was_held = True
+ schedule.hold_at_cell = resolution.wait_at_cell
+ schedule.hold_until = resolution.wait_until
+ schedule.wait_delay_added = getattr(schedule, 'wait_delay_added', 0) + resolution.delay_added
+
+ messages.append("")
+ messages.append(f"⏸️ RESOLUTION: WAIT Train {resolution.train_to_delay}")
+ messages.append(f" (Lower priority train holds at cell {resolution.wait_at_cell})")
+ messages.append("")
+ messages.append(f" Wait until timestep: {resolution.wait_until}")
+ messages.append(f" Additional delay: +{resolution.delay_added} timesteps")
+
+ messages.append("=" * 60)
+ resolved_this_iteration = True
+
+ # Re-check if this specific pair still conflicts
+ from SafetyVerifier import SafetyVerifier
+ temp_verifier = SafetyVerifier(self.timetable, max_steps=100)
+ _, temp_violations = temp_verifier.verify_safety(verbose=False, ignore_destination_conflicts=True)
+
+ # Check if this pair still has collisions
+ still_colliding = False
+ for v in temp_violations:
+ v_pair = (min(v.train_a, v.train_b), max(v.train_a, v.train_b))
+ if v_pair == pair:
+ still_colliding = True
+ break
+
+ if still_colliding:
+ messages.append("")
+ messages.append(f"⚠️ WARNING: Resolution applied but collision STILL EXISTS!")
+ messages.append(f" The {resolution.resolution_type.value} did not fully resolve the conflict.")
+ messages.append(f" This pair will be tracked as PARTIALLY UNRESOLVED.")
+ self.unresolvable_conflicts.append((conflict,
+ f"Resolution ({resolution.resolution_type.value}) applied but ineffective - trains still collide"))
+
+ break # Re-detect conflicts after applying resolution
+ else:
+ # Diagnose WHY no resolution was found
+ reason = self._diagnose_unresolvable(conflict, projections)
+ self.unresolvable_conflicts.append((conflict, reason))
+
+ messages.append("")
+ messages.append(f"❌ UNSOLVABLE CONFLICT - Train {conflict.train_a} vs Train {conflict.train_b}")
+ messages.append(f" Cell: {conflict.cell}, Timestep: {conflict.timestep}")
+ messages.append(f" Reason: {reason}")
+ messages.append("")
+ messages.append(f" ⚠️ WARNING: These trains WILL COLLIDE without manual intervention!")
+ messages.append(f" Suggestions:")
+ messages.append(f" - Add bypass route between stations")
+ messages.append(f" - Stagger departure times further apart")
+ messages.append(f" - Reduce train density on this corridor")
+ messages.append("=" * 60)
+
+ if not resolved_this_iteration:
+ # No new resolutions possible
+ if conflicts:
+ messages.append(f"\n⚠️ {len(unique_conflicts)} conflict(s) remain unresolved")
+ if self.unresolvable_conflicts:
+ messages.append(f" Including {len(self.unresolvable_conflicts)} UNSOLVABLE conflict(s)")
+ break
+
+ return messages
+
+ def _diagnose_unresolvable(self, conflict: Conflict, projections: Dict) -> str:
+ """
+ Diagnose why a conflict cannot be resolved.
+
+ Returns a human-readable explanation.
+ """
+ train_a = conflict.train_a
+ train_b = conflict.train_b
+
+ schedule_a = self.timetable.get_schedule(train_a)
+ schedule_b = self.timetable.get_schedule(train_b)
+
+ reasons = []
+
+ # Check WAIT feasibility for both trains
+ wait_a, _, _ = self.generator.calculate_wait_cost(
+ train_a, train_b, conflict, self.timetable, projections
+ )
+ wait_b, _, _ = self.generator.calculate_wait_cost(
+ train_b, train_a, conflict, self.timetable, projections
+ )
+
+ # Check REROUTE feasibility for both trains
+ reroute_a, _, route_a = self.generator.calculate_reroute_option(
+ train_a, train_b, self.timetable
+ )
+ reroute_b, _, route_b = self.generator.calculate_reroute_option(
+ train_b, train_a, self.timetable
+ )
+
+ if wait_a == float('inf') and wait_b == float('inf'):
+ reasons.append("Neither train can WAIT (possibly head-on collision)")
+
+ if reroute_a == float('inf') and reroute_b == float('inf'):
+ reasons.append("No bypass routes available for either train")
+ elif reroute_a == float('inf'):
+ reasons.append(f"Train {train_a} has no bypass route")
+ elif reroute_b == float('inf'):
+ reasons.append(f"Train {train_b} has no bypass route")
+
+ # Check if trains are already rerouted (bypass already in use)
+ if getattr(schedule_a, 'was_rerouted', False):
+ reasons.append(f"Train {train_a} already rerouted (bypass in use)")
+ if getattr(schedule_b, 'was_rerouted', False):
+ reasons.append(f"Train {train_b} already rerouted (bypass in use)")
+
+ # Check if trains are already held
+ if getattr(schedule_a, 'was_held', False):
+ reasons.append(f"Train {train_a} already held")
+ if getattr(schedule_b, 'was_held', False):
+ reasons.append(f"Train {train_b} already held")
+
+ if not reasons:
+ reasons.append("Unknown - resolution generation failed")
+
+ return "; ".join(reasons)
+
+ def get_stats(self) -> Dict:
+ """Get statistics about conflicts and resolutions."""
+ waits = [r for r in self.resolutions_applied if r.resolution_type == ResolutionType.WAIT]
+ reroutes = [r for r in self.resolutions_applied if r.resolution_type == ResolutionType.REROUTE]
+
+ return {
+ 'conflicts_detected': len(self.conflicts_detected),
+ 'resolutions_applied': len(self.resolutions_applied),
+ 'waits': len(waits),
+ 'reroutes': len(reroutes),
+ 'trains_delayed': list(self.delayed_trains),
+ 'total_delay_added': sum(r.delay_added for r in self.resolutions_applied),
+ 'delay_from_waits': sum(r.delay_added for r in waits),
+ 'delay_from_reroutes': sum(r.delay_added for r in reroutes),
+ 'options_per_conflict': {}, # Placeholder for compatibility
+ 'unresolvable_conflicts': len(self.unresolvable_conflicts),
+ 'unresolvable_pairs': [(c.train_a, c.train_b, reason) for c, reason in self.unresolvable_conflicts],
+ }
+
+ def print_summary(self):
+ """Print summary of conflicts and resolutions."""
+ stats = self.get_stats()
+
+ print("\n" + "=" * 70)
+ print(" CONFLICT RESOLVER SUMMARY")
+ print("=" * 70)
+ print(f"\n Conflicts detected: {stats['conflicts_detected']}")
+ print(f" Resolutions applied: {stats['resolutions_applied']}")
+ print(f" - Waits: {stats['waits']}")
+ print(f" - Reroutes: {stats['reroutes']}")
+ print(f" Trains delayed: {stats['trains_delayed']}")
+ print(f" Total delay added: {stats['total_delay_added']} timesteps")
+ if stats['waits'] > 0:
+ print(f" - From waits: {stats['delay_from_waits']} timesteps")
+ if stats['reroutes'] > 0:
+ print(f" - From reroutes: {stats['delay_from_reroutes']} timesteps")
+
+ if self.conflicts_detected:
+ print("\n Conflict details:")
+ for c in self.conflicts_detected:
+ print(f" - Train {c.train_a} vs Train {c.train_b} at {c.cell}, timestep {c.timestep}")
+
+ if self.resolutions_applied:
+ print("\n Resolution details:")
+ for r in self.resolutions_applied:
+ if r.resolution_type == ResolutionType.WAIT:
+ print(f" - Train {r.train_to_delay}: WAIT at {r.wait_at_cell} until step {r.wait_until}, +{r.delay_added} delay")
+ else:
+ print(f" - Train {r.train_to_delay}: REROUTE via bypass, +{r.delay_added} delay")
+
+ # CRITICAL: Warn about unresolvable conflicts
+ if self.unresolvable_conflicts:
+ print("\n" + "!" * 70)
+ print(" ⚠️ WARNING: UNSOLVABLE CONFLICTS DETECTED")
+ print("!" * 70)
+ print(f"\n {len(self.unresolvable_conflicts)} conflict(s) have NO SOLUTION:")
+ print(" These trains WILL COLLIDE without manual intervention!\n")
+
+ for conflict, reason in self.unresolvable_conflicts:
+ print(f" ❌ Train {conflict.train_a} vs Train {conflict.train_b}")
+ print(f" Collision at: {conflict.cell}, timestep {conflict.timestep}")
+ print(f" Reason: {reason}")
+ print()
+
+ print(" RECOMMENDED ACTIONS:")
+ print(" 1. Add more bypass routes to the track layout")
+ print(" 2. Increase departure time gaps between conflicting trains")
+ print(" 3. Reduce the number of trains in this time window")
+ print(" 4. Change train priorities to allow different resolution order")
+ print("!" * 70)
+
+ print("=" * 70)
+
+ def get_unresolvable_report(self) -> str:
+ """
+ Get a detailed report of all unresolvable conflicts.
+
+ Returns:
+ Formatted string report suitable for logging or display
+ """
+ if not self.unresolvable_conflicts:
+ return "✅ All conflicts were successfully resolved."
+
+ lines = [
+ "",
+ "=" * 70,
+ " UNRESOLVABLE CONFLICTS REPORT",
+ "=" * 70,
+ "",
+ f" Total unresolvable: {len(self.unresolvable_conflicts)}",
+ "",
+ ]
+
+ for i, (conflict, reason) in enumerate(self.unresolvable_conflicts, 1):
+ lines.extend([
+ f" {i}. Train {conflict.train_a} vs Train {conflict.train_b}",
+ f" Location: {conflict.cell}",
+ f" Timestep: {conflict.timestep}",
+ f" Reason: {reason}",
+ "",
+ ])
+
+ lines.extend([
+ " INFRASTRUCTURE REQUIREMENTS:",
+ " To resolve these conflicts, consider:",
+ "",
+ ])
+
+
+ needs_bypass = any("bypass" in reason.lower() for _, reason in self.unresolvable_conflicts)
+ needs_timing = any("head-on" in reason.lower() or "wait" in reason.lower()
+ for _, reason in self.unresolvable_conflicts)
+ already_rerouted = any("already rerouted" in reason.lower()
+ for _, reason in self.unresolvable_conflicts)
+
+ if needs_bypass:
+ lines.append(" • Additional bypass tracks between key junctions")
+ if already_rerouted:
+ lines.append(" • Second-level bypass routes (bypass for the bypass)")
+ if needs_timing:
+ lines.append(" • Larger time gaps between train departures")
+ lines.append(" • Dedicated time slots for opposing directions")
+
+ lines.extend([
+ "",
+ "=" * 70,
+ ])
+
+ return "\n".join(lines)
\ No newline at end of file
diff --git a/usecases_examples/Railway/Corridor_environment.py b/usecases_examples/Railway/Corridor_environment.py
new file mode 100644
index 00000000..2abd8aef
--- /dev/null
+++ b/usecases_examples/Railway/Corridor_environment.py
@@ -0,0 +1,852 @@
+"""
+Corridor Environment: A larger, more realistic rail network.
+
+Layout:
+- Main east-west corridor (Geneva → Zurich)
+- Central hub with multiple junctions
+- Branch lines to secondary cities (Bern, Lyon, Basel, Milan)
+- Multiple bypass routes
+
+Grid: 35 wide × 20 tall
+Stations: 7 cities
+"""
+
+import numpy as np
+from flatland.envs.rail_env import RailEnv
+from flatland.envs.rail_generators import RailGenerator, RailEnvTransitions, RailGridTransitionMap
+from flatland.envs.line_generators import LineGenerator
+from collections import deque
+from typing import List, Tuple, Dict, Optional
+
+
+# ============== TRACK BUILDING HELPERS ==============
+
+def get_transition(directions: List[str]) -> int:
+ """
+ Create a transition value for a cell based on allowed movements.
+
+ Directions: 'N', 'E', 'S', 'W'
+ A train entering from direction X can exit to any other direction in the list.
+ """
+ rail_trans = RailEnvTransitions()
+
+ dir_map = {'N': 0, 'E': 1, 'S': 2, 'W': 3}
+ dir_indices = [dir_map[d] for d in directions]
+
+ transition = 0
+
+ # For each entry direction, set exit to all other directions
+ for entry in dir_indices:
+ for exit_dir in dir_indices:
+ if entry != exit_dir or len(dir_indices) == 1:
+ # Flatland transition encoding
+ transition |= (1 << (16 - 1 - (entry * 4 + exit_dir)))
+
+ return transition
+
+
+def create_straight_horizontal() -> int:
+ """Horizontal straight: ─ (E-W only, no N-S connections)"""
+ # Value 1025 is a proper horizontal straight:
+ # From E: → E (continue east)
+ # From W: → W (continue west)
+ # No north or south connections
+ return 1025
+
+
+def create_straight_vertical() -> int:
+ """Vertical straight: │ (N-S only, no E-W connections)"""
+ # Value 32800 is a proper vertical straight:
+ # From N: → N (continue north)
+ # From S: → S (continue south)
+ # No east or west connections
+ return 32800
+
+
+def create_junction_4way() -> int:
+ """4-way junction: ┼ (double-slip allows turning)"""
+ # Use double-slip crossing that allows turns at junction
+ # 56955 enables: From any direction, can exit to any perpendicular direction
+ # 33825 (diamond crossing) only allows straight-through - trains can't turn!
+ return 56955 # Double-slip crossover (allows turns)
+
+
+def create_junction_3way(missing: str) -> int:
+ """
+ 3-way junction (T-junction).
+ missing: which direction has no track ('N', 'E', 'S', or 'W')
+ """
+ # T-junctions encoded for different orientations
+ t_junctions = {
+ 'N': 17411, # ┬ (no north)
+ 'S': 38433, # ┴ (no south)
+ 'E': 32800, # ├ (no east)
+ 'W': 49186, # ┤ (no west)
+ }
+ return t_junctions.get(missing, 33825)
+
+
+def create_corner(dir1: str, dir2: str) -> int:
+ """
+ Corner connecting two directions.
+
+ The corner allows trains to turn between the two specified directions.
+
+ Correct values verified by checking Flatland transitions:
+ - SE (┌): 16386 - N→E, W→S - connects South vertical to East horizontal
+ - NE (└): 72 - S→E, W→N - connects North vertical to East horizontal
+ - SW (┐): 4608 - N→W, E→S - connects South vertical to West horizontal
+ - NW (┘): 2064 - E→N, S→W - connects North vertical to West horizontal
+ """
+ corners = {
+ # NE corner (└) - connects N vertical to E horizontal
+ ('N', 'E'): 72,
+ ('E', 'N'): 72,
+ # NW corner (┘) - connects N vertical to W horizontal
+ ('N', 'W'): 2064,
+ ('W', 'N'): 2064,
+ # SE corner (┌) - connects S vertical to E horizontal
+ ('S', 'E'): 16386,
+ ('E', 'S'): 16386,
+ # SW corner (┐) - connects S vertical to W horizontal
+ ('S', 'W'): 4608,
+ ('W', 'S'): 4608,
+ }
+ return corners.get((dir1, dir2), 0)
+
+
+def create_dead_end(direction: str) -> int:
+ """
+ Dead end (station/terminus) pointing in given direction.
+ """
+ dead_ends = {
+ 'N': 32800, # Entry from south
+ 'S': 72, # Entry from north
+ 'E': 2064, # Entry from west
+ 'W': 17411, # Entry from east
+ }
+ return dead_ends.get(direction, 0)
+
+
+# ============== CORRIDOR ENVIRONMENT ==============
+
+def create_corridor_env(n_agents: int = 2, agent_configs: List = None):
+ """
+ Create the corridor environment.
+
+ Layout (35×20):
+
+ 0 10 20 30
+ 0 ..........S.................S.... BERN(10,0) BASEL(27,0)
+ ..........|.................|....
+ ..........+---+.........+---+.... Northern connections
+ ..........|...|.........|...|....
+ 5 S---------+---+---------+---+----S GENEVA(0,5) ─── ZURICH(34,5)
+ ..........|...|.........|...|.... Main corridor + Hub
+ ..........+---+---------+---+....
+ ..........|...............|......
+ 10 ..........S...............S...... LYON(10,10) MILAN(24,10)
+ ....................................
+
+ Stations:
+ GENEVA: (5, 0) - West terminus
+ ZURICH: (5, 34) - East terminus
+ BERN: (0, 12) - North branch
+ BASEL: (0, 27) - Northeast
+ LYON: (10, 7) - Southwest branch
+ MILAN: (10, 27) - Southeast branch
+
+ Returns:
+ env: RailEnv instance
+ stations: Dict of station names to positions
+ junctions: List of junction positions
+ """
+
+ height = 12
+ width = 35
+
+ # Initialize empty grid
+ grid = np.zeros((height, width), dtype=np.uint16)
+
+ # Define stations (row, col)
+ stations = {
+ 'GENEVA': (5, 0),
+ 'ZURICH': (5, 34),
+ 'BERN': (0, 12),
+ 'BASEL': (0, 27),
+ 'LYON': (10, 7),
+ 'MILAN': (10, 27),
+ }
+
+ # Track pieces
+ H = create_straight_horizontal() # ─
+ V = create_straight_vertical() # │
+
+ # Corners
+ NE = create_corner('N', 'E') # └
+ NW = create_corner('N', 'W') # ┘
+ SE = create_corner('S', 'E') # ┌
+ SW = create_corner('S', 'W') # ┐
+
+ # T-junctions
+ T_N = create_junction_3way('N') # ┬
+ T_S = create_junction_3way('S') # ┴
+ T_E = create_junction_3way('E') # ├
+ T_W = create_junction_3way('W') # ┤
+
+ # 4-way junction
+ X = create_junction_4way() # ┼
+
+ # ===== BUILD THE TRACK =====
+
+ # Main corridor: Row 5, from col 0 to 34
+ for c in range(35):
+ grid[5, c] = H
+
+ # ===== WESTERN HUB (around col 7-12) =====
+
+ # Junction at (5, 7) - connects to Lyon
+ grid[5, 7] = T_N # Main line with branch south
+
+ # Track down to Lyon: col 7, rows 6-9
+ for r in range(6, 10):
+ grid[r, 7] = V
+ grid[10, 7] = V # Lyon station
+
+ # Junction at (5, 12) - connects to Bern
+ grid[5, 12] = T_S # Main line with branch north
+
+ # Track up to Bern: col 12, rows 1-4
+ for r in range(1, 5):
+ grid[r, 12] = V
+ grid[0, 12] = V # Bern station
+
+ # ===== WESTERN BYPASS (rows 3-7, cols 7-12) =====
+
+ # Northern bypass track
+ # Use 4-way junctions at corners to allow through-traffic
+ grid[3, 7] = X # Was SE corner, now junction for Lyon route
+ for c in range(8, 12):
+ grid[3, c] = H # Horizontal track
+ grid[3, 12] = X # Was SW corner, now junction for Bern route
+
+ # Connect bypass to main junctions
+ # Upgrade (5,7) to 4-way
+ grid[5, 7] = X
+ # Add vertical connector at (4,7)
+ grid[4, 7] = V
+ # Connect (3,7) properly - it's SE corner coming from junction
+
+ # Upgrade (5,12) to 4-way
+ grid[5, 12] = X
+ # Add vertical connector at (4,12)
+ grid[4, 12] = V
+
+ # Southern bypass track (rows 7-8)
+ # Use 4-way junctions at corners to allow through-traffic (Lyon route)
+ grid[7, 7] = X # Was NE corner, now junction for Lyon route
+ for c in range(8, 12):
+ grid[7, c] = H
+ grid[7, 12] = X # Was NW corner, now junction
+
+ # Connect southern bypass
+ grid[6, 7] = V
+ grid[6, 12] = V
+
+ # ===== EASTERN HUB (around col 22-27) =====
+
+ # Junction at (5, 22) - start of eastern hub
+ grid[5, 22] = T_N # Main line with branch south
+
+ # Junction at (5, 27) - connects to Basel and Milan
+ grid[5, 27] = X # 4-way junction
+
+ # Track up to Basel: col 27, rows 1-4
+ for r in range(1, 5):
+ grid[r, 27] = V
+ grid[0, 27] = V # Basel station
+
+ # Track down to Milan: col 27, rows 6-9
+ for r in range(6, 10):
+ grid[r, 27] = V
+ grid[10, 27] = V # Milan station
+
+ # ===== EASTERN BYPASS =====
+
+ # Northern bypass
+ grid[3, 22] = SE # ┌
+ for c in range(23, 27):
+ grid[3, c] = H
+ grid[3, 27] = SW # ┐ connects to Basel line
+
+ # Upgrade junction and add connectors
+ grid[5, 22] = X
+ grid[4, 22] = V
+ grid[4, 27] = V # Already have vertical from Basel
+
+ # Actually (3,27) needs to connect to the vertical going to Basel
+ # Make (3,27) a T-junction instead
+ grid[3, 27] = T_E # ├ - connects W, N, S
+
+ # Southern bypass
+ grid[7, 22] = NE # └
+ for c in range(23, 27):
+ grid[7, c] = H
+ grid[7, 27] = T_E # ├ connects to Milan line
+
+ grid[6, 22] = V
+ grid[6, 27] = V
+
+
+ # ===== MIDDLE CONNECTOR (optional bypass between hubs) =====
+
+ # Connect the two hub areas with an alternative route
+ # Upper middle: row 3, cols 12-22
+ for c in range(13, 22):
+ grid[3, c] = H
+
+ # Lower middle: row 7, cols 12-22
+ for c in range(13, 22):
+ grid[7, c] = H
+
+ # Update corners to 4-way junctions where bypasses meet vertical tracks
+ # These need full connectivity for routes like Lyon→Basel
+ grid[3, 12] = X # 4-way junction for Bern vertical + bypass
+ grid[3, 22] = X # 4-way junction for eastern bypass
+ grid[7, 12] = X # 4-way junction for southern vertical + bypass
+ grid[7, 22] = X # 4-way junction for eastern bypass
+
+ # Stations should be proper termini or through-stations
+
+ # Geneva (5, 0) - western terminus
+ grid[5, 0] = H # Simple endpoint
+
+ # Zurich (5, 34) - eastern terminus
+ grid[5, 34] = H # Simple endpoint
+
+ # Bern (0, 12) - northern terminus
+ grid[0, 12] = V # Simple endpoint
+
+ # Basel (0, 27) - northern terminus
+ grid[0, 27] = V # Simple endpoint
+
+ # Lyon (10, 7) - southern terminus
+ grid[10, 7] = V # Simple endpoint
+
+ # Milan (10, 27) - southern terminus
+ grid[10, 27] = V # Simple endpoint
+
+ # ===== JUNCTION LIST =====
+ junctions = [
+ (5, 7), # West hub - main junction
+ (5, 12), # West hub - Bern junction
+ (5, 22), # East hub - entry
+ (5, 27), # East hub - Basel/Milan junction
+ (3, 7), # North bypass west
+ (3, 12), # North bypass west-mid
+ (3, 22), # North bypass east-mid
+ (3, 27), # North bypass east
+ (7, 7), # South bypass west
+ (7, 12), # South bypass west-mid
+ (7, 22), # South bypass east-mid
+ (7, 27), # South bypass east
+ ]
+
+ # ===== CREATE ENVIRONMENT =====
+
+ # Create transition map
+ rail_trans = RailEnvTransitions()
+ grid_transition_map = RailGridTransitionMap(width=width, height=height, transitions=rail_trans)
+ grid_transition_map.grid = grid
+
+ def custom_rail_generator(width, height, num_agents, num_resets=0, np_random=None):
+ return grid_transition_map, None
+
+ # Default agent configurations
+ default_agent_configs = [
+ # (start_pos, start_dir, target_pos, target_dir)
+ (stations['GENEVA'], 1, stations['ZURICH'], 3), # Geneva → Zurich (E)
+ (stations['ZURICH'], 3, stations['GENEVA'], 1), # Zurich → Geneva (W)
+ (stations['BERN'], 2, stations['MILAN'], 0), # Bern → Milan (S)
+ (stations['MILAN'], 0, stations['BERN'], 2), # Milan → Bern (N)
+ (stations['LYON'], 0, stations['BASEL'], 2), # Lyon → Basel (N then E)
+ (stations['BASEL'], 2, stations['LYON'], 0), # Basel → Lyon (S then W)
+ (stations['GENEVA'], 1, stations['BERN'], 2), # Geneva → Bern
+ (stations['MILAN'], 0, stations['ZURICH'], 3), # Milan → Zurich
+ ]
+
+ configs_to_use = agent_configs if agent_configs else default_agent_configs[:n_agents]
+
+ def custom_line_generator(rail, num_agents, hints=None, num_resets=0, np_random=None):
+ from flatland.envs.timetable_utils import Line
+
+ agent_positions = []
+ agent_directions = []
+ agent_targets = []
+ agent_speeds = []
+
+ for i in range(min(num_agents, len(configs_to_use))):
+ start_pos, start_dir, target_pos, target_dir = configs_to_use[i]
+ agent_positions.append([start_pos, target_pos])
+ agent_directions.append([start_dir, target_dir])
+ agent_targets.append(target_pos)
+ agent_speeds.append(1.0)
+
+ # Handle different Flatland versions
+ fields = Line._fields if hasattr(Line, '_fields') else []
+
+ if len(fields) == 2:
+ try:
+ from flatland.envs.rail_trainrun_data_structures import Waypoint
+ except ImportError:
+ from flatland.envs.timetable_utils import Waypoint
+
+ waypoints = []
+ for i in range(len(agent_targets)):
+ wp = [
+ [Waypoint(position=agent_positions[i][0], direction=agent_directions[i][0])],
+ [Waypoint(position=agent_targets[i], direction=agent_directions[i][1])],
+ ]
+ waypoints.append(wp)
+ return Line(waypoints, agent_speeds)
+ else:
+ return Line(agent_positions, agent_directions, agent_targets, agent_speeds)
+
+ def custom_timetable_generator(agents, distance_map, agent_hint, max_episode_steps=None):
+ from flatland.envs.timetable_utils import Timetable
+
+ earliest_departures = []
+ latest_arrivals = []
+
+ for agent in agents:
+ earliest_departures.append([0, None])
+ latest_arrivals.append([None, 100])
+
+ return Timetable(earliest_departures, latest_arrivals, max_episode_steps or 100)
+
+ env = RailEnv(
+ width=width,
+ height=height,
+ rail_generator=custom_rail_generator,
+ line_generator=custom_line_generator,
+ timetable_generator=custom_timetable_generator,
+ number_of_agents=n_agents,
+ )
+
+ env._max_episode_steps = 100
+
+ return env, stations, junctions
+
+
+# ============== VISUALIZATION ==============
+
+def visualize_corridor(env, stations: Dict, junctions: List, step: int = 0):
+ """
+ Print a visual representation of the corridor environment.
+ """
+ grid = env.rail.grid
+ height, width = grid.shape
+
+ # Reverse lookup: position → station name
+ station_positions = {v: k[0] for k, v in stations.items()} # First letter of name
+ junction_set = set(junctions)
+
+ # Build display grid
+ display = [['.' for _ in range(width)] for _ in range(height)]
+
+ for r in range(height):
+ for c in range(width):
+ if grid[r, c] != 0:
+ pos = (r, c)
+ if pos in stations.values():
+ # Find station name
+ for name, spos in stations.items():
+ if spos == pos:
+ display[r][c] = name[0] # First letter
+ break
+ elif pos in junction_set:
+ display[r][c] = '+'
+ elif r == 5: # Main corridor
+ display[r][c] = '='
+ elif r == 3 or r == 7: # Bypass routes
+ display[r][c] = '-'
+ else:
+ display[r][c] = '|'
+
+ # Draw agents
+ for i, agent in enumerate(env.agents):
+ if agent.position is not None:
+ r, c = agent.position
+ display[r][c] = str(i)
+
+ # Print
+ print(f"\n Step {step}:")
+ print(" " + "".join(f"{c%10}" for c in range(width)))
+ print(" " + "-" * width)
+ for r in range(height):
+ print(f"{r:2} |" + "".join(display[r]))
+
+ # Print agent status
+ print()
+ for i, agent in enumerate(env.agents):
+ pos = agent.position if agent.position else "waiting"
+ state_name = agent.state.name if hasattr(agent.state, 'name') else str(agent.state)
+ target = agent.target
+ # Find target name
+ target_name = "?"
+ for name, spos in stations.items():
+ if spos == target:
+ target_name = name
+ break
+ print(f" Train {i}: {pos} → {target_name} ({target}) | state: {state_name}")
+
+
+def print_track_layout(stations: Dict, junctions: List):
+ """Print a schematic of the track layout."""
+ print("""
+ CORRIDOR ENVIRONMENT LAYOUT
+ ===========================
+
+ 0 10 20 30
+ 0 ..........B.................B.... BERN(0,12) BASEL(0,27)
+ ..........|.................|....
+ ..........+---+.........+---+.... Northern bypass
+ ..........|...|.........|...|....
+ 5 G=========+===+=========+===+====Z GENEVA(5,0) ─── ZURICH(5,34)
+ ..........|...|.........|...|.... Main corridor + Hubs
+ ..........+---+---------+---+.... Southern bypass
+ ..........|...............|......
+ 10 ..........L...............M...... LYON(10,7) MILAN(10,27)
+
+ Legend:
+ G/Z = Geneva/Zurich (main corridor termini)
+ B = Bern/Basel (northern branches)
+ L/M = Lyon/Milan (southern branches)
+ + = Junction
+ = = Main corridor
+ - = Bypass routes
+ | = Branch lines
+
+ Key Routes:
+ Geneva ↔ Zurich: Main corridor (direct) or via bypasses
+ Bern ↔ Milan: Through western and eastern hubs
+ Lyon ↔ Basel: Cross-network diagonal
+ """)
+
+ print("Stations:", stations)
+ print(f"Junctions: {len(junctions)} total")
+
+
+# ============== MULTI-ROUTE BFS ==============
+
+def compute_all_routes_bfs(env, start: Tuple[int, int], goal: Tuple[int, int],
+ max_routes: int = 5,
+ key_junctions: List[Tuple[int, int]] = None,
+ use_transitions: bool = False,
+ start_direction: Optional[int] = None) -> List[List[Tuple[int, int]]]:
+ """
+ Find multiple routes from start to goal using BFS.
+
+ Args:
+ env: Rail environment
+ start: Starting position (row, col)
+ goal: Goal position (row, col)
+ max_routes: Maximum number of routes to return
+ key_junctions: Optional list of junction positions for route diversity
+ use_transitions: If True, use strict Flatland transition checking (required
+ for loaded flatland maps). If False, use simple connectivity.
+ start_direction: If given, seed BFS only with this direction. This MUST
+ match agent.initial_direction for loaded maps so that
+ route[0]->route[1] is compatible with how the agent spawns.
+ Without this, the BFS may return routes starting in the
+ opposite direction to initial_direction, causing an immediate
+ OFF-ROUTE on the first step.
+ """
+ if start == goal:
+ return [[start]]
+
+ grid = env.rail.grid
+ height, width = grid.shape
+
+ # Direction offsets: N=0, E=1, S=2, W=3
+ dir_offsets = [(-1, 0), (0, 1), (1, 0), (0, -1)]
+
+ if use_transitions:
+ # Strict mode: use Flatland's transition system
+ from flatland.envs.rail_generators import RailEnvTransitions
+ rail_trans = RailEnvTransitions()
+ opposite_dir = [2, 3, 0, 1]
+
+ queue = deque()
+ # If start_direction given, only seed from that direction.
+ # This ensures route[0]->route[1] matches agent.initial_direction.
+ seed_dirs = [start_direction] if start_direction is not None else range(4)
+ for initial_dir in seed_dirs:
+ queue.append((start, initial_dir, [start]))
+
+ found_routes = []
+ visited = set()
+ max_path_length = height + width + 40
+
+ while queue and len(found_routes) < max_routes * 10:
+ pos, facing_dir, path = queue.popleft()
+
+ if len(path) > max_path_length:
+ continue
+
+ state = (pos, facing_dir)
+ if state in visited:
+ continue
+ visited.add(state)
+
+ r, c = pos
+ cell = grid[r, c]
+
+ if cell == 0:
+ continue
+
+ valid_exits = rail_trans.get_transitions(cell, facing_dir)
+
+ for exit_dir in range(4):
+ if not valid_exits[exit_dir]:
+ continue
+
+ dr, dc = dir_offsets[exit_dir]
+ nr, nc = r + dr, c + dc
+
+ if not (0 <= nr < height and 0 <= nc < width):
+ continue
+
+ next_cell = grid[nr, nc]
+ if next_cell == 0 or (nr, nc) in path:
+ continue
+
+ # Train continues traveling in the same direction (exit_dir)
+ travel_dir = exit_dir
+ next_valid = rail_trans.get_transitions(next_cell, travel_dir)
+ if not any(next_valid):
+ continue
+
+ new_path = path + [(nr, nc)]
+
+ if (nr, nc) == goal:
+ found_routes.append(new_path)
+ else:
+ queue.append(((nr, nc), travel_dir, new_path))
+ else:
+ # Simple mode: just check if cells are connected (non-zero neighbors)
+ queue = deque([(start, [start])])
+ found_routes = []
+ visited_at_length = {}
+ max_path_length = height + width + 40
+
+ while queue and len(found_routes) < max_routes * 10:
+ pos, path = queue.popleft()
+
+ if len(path) > max_path_length:
+ continue
+
+ # Allow revisiting with slightly longer paths for diversity
+ if pos in visited_at_length and len(path) > visited_at_length[pos] + 5:
+ continue
+ visited_at_length[pos] = min(visited_at_length.get(pos, 999), len(path))
+
+ r, c = pos
+ cell = grid[r, c]
+
+ if cell == 0:
+ continue
+
+ for dr, dc in dir_offsets:
+ nr, nc = r + dr, c + dc
+
+ if 0 <= nr < height and 0 <= nc < width:
+ next_cell = grid[nr, nc]
+
+ if next_cell != 0 and (nr, nc) not in path:
+ new_path = path + [(nr, nc)]
+
+ if (nr, nc) == goal:
+ found_routes.append(new_path)
+ else:
+ queue.append(((nr, nc), new_path))
+
+ # Sort by length
+ found_routes.sort(key=len)
+
+ # Remove duplicates and select diverse routes
+ unique_routes = []
+ seen_paths = set()
+
+ for route in found_routes:
+ route_tuple = tuple(route)
+ if route_tuple in seen_paths:
+ continue
+ seen_paths.add(route_tuple)
+
+ if len(unique_routes) >= max_routes:
+ break
+
+ route_rows = set(p[0] for p in route)
+
+ is_different = True
+ for existing in unique_routes:
+ existing_rows = set(p[0] for p in existing)
+
+ if route_rows == existing_rows:
+ route_middle = set(route[len(route)//3 : 2*len(route)//3])
+ existing_middle = set(existing[len(existing)//3 : 2*len(existing)//3])
+
+ if route_middle and existing_middle:
+ overlap = len(route_middle & existing_middle) / max(len(route_middle), len(existing_middle))
+ if overlap > 0.7:
+ is_different = False
+ break
+
+ if is_different:
+ unique_routes.append(route)
+
+ return unique_routes
+
+
+def compute_route_bfs(env, start: Tuple[int, int], goal: Tuple[int, int],
+ use_transitions: bool = False,
+ start_direction: Optional[int] = None) -> List[Tuple[int, int]]:
+ """
+ Find the shortest route from start to goal.
+
+ Args:
+ env: Rail environment
+ start: Starting position
+ goal: Goal position
+ use_transitions: If True, use strict Flatland transition rules (required for
+ loaded flatland maps).
+ start_direction: If given, constrain BFS to start in this direction only.
+ Pass agent.initial_direction for loaded maps.
+ """
+ routes = compute_all_routes_bfs(
+ env, start, goal, max_routes=1,
+ use_transitions=use_transitions,
+ start_direction=start_direction,
+ )
+ return routes[0] if routes else []
+
+
+# ============== TESTING ==============
+
+def test_environment():
+ """Test the corridor environment."""
+ print("=" * 60)
+ print("TESTING CORRIDOR ENVIRONMENT")
+ print("=" * 60)
+
+ # Create environment with 2 agents
+ env, stations, junctions = create_corridor_env(n_agents=2)
+ env.reset()
+
+ # Print layout
+ print_track_layout(stations, junctions)
+
+ # Visualize
+ visualize_corridor(env, stations, junctions, step=0)
+
+ # Test route finding
+ print("\n" + "=" * 60)
+ print("ROUTE FINDING TEST")
+ print("=" * 60)
+
+ test_pairs = [
+ ('GENEVA', 'ZURICH'),
+ ('BERN', 'MILAN'),
+ ('LYON', 'BASEL'),
+ ('GENEVA', 'BERN'),
+ ]
+
+ for start_name, goal_name in test_pairs:
+ start = stations[start_name]
+ goal = stations[goal_name]
+
+ print(f"\n{start_name} → {goal_name}:")
+ routes = compute_all_routes_bfs(env, start, goal, max_routes=3, key_junctions=junctions)
+
+ if routes:
+ for i, route in enumerate(routes):
+ # Analyze which rows (bypasses) the route uses
+ rows_used = sorted(set(p[0] for p in route))
+ row_desc = []
+ if 3 in rows_used:
+ row_desc.append("north bypass")
+ if 5 in rows_used:
+ row_desc.append("main corridor")
+ if 7 in rows_used:
+ row_desc.append("south bypass")
+
+ print(f" Route {i+1}: {len(route)} cells via {', '.join(row_desc)}")
+ else:
+ print(" No route found!")
+
+ return env, stations, junctions
+
+
+def visualize_route(env, stations: Dict, route: List[Tuple[int, int]], route_name: str = ""):
+ """Visualize a specific route on the grid."""
+ grid = env.rail.grid
+ height, width = grid.shape
+
+ route_set = set(route)
+
+ # Build display grid
+ display = [['.' for _ in range(width)] for _ in range(height)]
+
+ for r in range(height):
+ for c in range(width):
+ if grid[r, c] != 0:
+ pos = (r, c)
+ if pos in route_set:
+ display[r][c] = '*'
+ elif pos in stations.values():
+ for name, spos in stations.items():
+ if spos == pos:
+ display[r][c] = name[0]
+ break
+ elif r == 5:
+ display[r][c] = '='
+ elif r == 3 or r == 7:
+ display[r][c] = '-'
+ else:
+ display[r][c] = '|'
+
+ # Mark start and end
+ if route:
+ sr, sc = route[0]
+ er, ec = route[-1]
+ display[sr][sc] = 'S'
+ display[er][ec] = 'E'
+
+ print(f"\n Route: {route_name}")
+ print(" " + "".join(f"{c%10}" for c in range(width)))
+ for r in range(height):
+ print(f"{r:2} |" + "".join(display[r]))
+
+
+# ============== MAP LOADER (delegates to FlatlandMapLoader) ==============
+
+def load_corridor_env(pkl_path: str, name_map=None):
+ """
+ Load a pre-generated Flatland map from a .pkl file.
+ Delegates to FlatlandMapLoader.load_flatland_env — kept here for
+ backward-compatible imports.
+ """
+ from FlatlandMapLoader import load_flatland_env
+ return load_flatland_env(pkl_path, name_map=name_map)
+
+
+def build_timetable_from_loaded_env(env, stations, departure_offset=1,
+ stagger_departures=True):
+ """
+ Build a timetable from a loaded env's agents.
+ Delegates to FlatlandMapLoader.build_timetable_from_env.
+ """
+ from FlatlandMapLoader import build_timetable_from_env
+ return build_timetable_from_env(env, stations, departure_offset,
+ stagger_departures)
diff --git a/usecases_examples/Railway/CostCalculator.py b/usecases_examples/Railway/CostCalculator.py
new file mode 100644
index 00000000..12ae8157
--- /dev/null
+++ b/usecases_examples/Railway/CostCalculator.py
@@ -0,0 +1,801 @@
+"""
+Cost Calculator for train dispatching resolution evaluation.
+
+Provides:
+- CostWeights: Configurable weights for cost function components
+- SlackInfo: Information about schedule slack/buffer
+- CostCalculator: Main class for evaluating resolution costs
+
+Cost components:
+1. Direct delay cost (weighted by priority)
+2. Cascade delay cost (delays caused to other trains)
+3. Slack/robustness penalties and bonuses
+4. Route complexity (junction usage, extra length)
+
+Key insight: Slack measures how fragile a schedule is.
+Low slack = small disturbance causes cascading conflicts.
+"""
+
+from dataclasses import dataclass, field
+from typing import Dict, List, Tuple, Optional, Set, Any
+from enum import Enum
+import copy
+
+
+@dataclass
+class CostWeights:
+ """
+ Configurable weights for cost function.
+ Set any weight to 0 to disable that component.
+
+ Attributes:
+ direct_delay: Base cost per timestep of delay
+ priority_multiplier: If True, multiply delay by train priority
+ cascade_delay: Multiplier for delays caused to OTHER trains
+ slack_violation: Penalty when slack < min_safe_slack
+ min_safe_slack: Timesteps below which schedule is "fragile"
+ robustness_bonus: Reward per timestep of buffer above minimum
+ junction_usage: Cost per junction in rerouted path
+ route_length: Cost per extra cell vs original route
+ critical_delay: Above this = "major incident" applies multiplier
+ critical_multiplier: Applied to delays above critical threshold
+ """
+ # Primary: delay costs
+ direct_delay: float = 1.0
+ priority_multiplier: bool = True
+ cascade_delay: float = 1.5
+
+ # Slack/robustness
+ slack_violation: float = 3.0
+ min_safe_slack: int = 3
+ robustness_bonus: float = 0.2
+
+ # Network efficiency
+ junction_usage: float = 0.1
+ route_length: float = 0.05
+
+ # Thresholds
+ critical_delay: int = 15
+ critical_multiplier: float = 2.0
+
+ def __str__(self) -> str:
+ return (
+ f"CostWeights(direct={self.direct_delay}, cascade={self.cascade_delay}, "
+ f"slack_viol={self.slack_violation}, robustness={self.robustness_bonus})"
+ )
+
+
+@dataclass
+class SlackInfo:
+ """
+ Information about a train's schedule slack/buffer.
+
+ Slack = how much can this train be delayed before causing a conflict
+ with another train?
+
+ Low slack = fragile schedule, any disturbance causes cascade.
+ High slack = robust schedule, can absorb delays.
+ """
+ train_id: int
+ slack_timesteps: int # Buffer before next conflict
+ dependent_trains: List[int] # Trains that depend on us
+ blocking_sections: List[Tuple[int, int]] # Track sections we occupy
+ earliest_conflict_step: Optional[int] # When slack runs out
+
+ def is_fragile(self, threshold: int = 3) -> bool:
+ """Returns True if slack is below threshold."""
+ return self.slack_timesteps < threshold
+
+ def __str__(self) -> str:
+ status = "FRAGILE" if self.is_fragile() else "OK"
+ return (
+ f"Slack(train={self.train_id}, buffer={self.slack_timesteps} steps, "
+ f"dependents={self.dependent_trains}, status={status})"
+ )
+
+
+@dataclass
+class CostBreakdown:
+ """Detailed breakdown of cost calculation."""
+ direct_delay: float = 0.0
+ cascade_delay: float = 0.0
+ slack_violation: float = 0.0
+ robustness_bonus: float = 0.0
+ route_complexity: float = 0.0
+ total: float = 0.0
+
+ # Additional info
+ slack_info: Optional[SlackInfo] = None
+ cascade_count: int = 0
+ explanation: str = ""
+
+ def __str__(self) -> str:
+ lines = [
+ "Cost Breakdown:",
+ f" Direct delay: {self.direct_delay:>8.2f}",
+ f" Cascade delays: {self.cascade_delay:>8.2f}",
+ f" Slack violations: {self.slack_violation:>8.2f}",
+ f" Robustness bonus: -{self.robustness_bonus:>8.2f}",
+ f" Route complexity: {self.route_complexity:>8.2f}",
+ f" ───────────────────────────",
+ f" TOTAL: {self.total:>8.2f}",
+ ]
+ if self.slack_info:
+ lines.append(f"\n Slack: {self.slack_info.slack_timesteps} steps")
+ if self.cascade_count > 0:
+ lines.append(f" Cascades: {self.cascade_count}")
+ return "\n".join(lines)
+
+
+class CostCalculator:
+ """
+ Calculates resolution costs with configurable weights.
+
+ Main interface:
+ - evaluate_resolution(): Score a single resolution option
+ - evaluate_solution(): Score a complete solution (list of resolutions)
+ - compare_options(): Rank multiple resolution options
+ - calculate_slack(): Get slack info for a train
+
+ Usage:
+ calculator = CostCalculator(env, timetable)
+
+ # Evaluate options
+ options = generate_options(conflict)
+ ranked = calculator.compare_options(options, projections)
+ best_option = ranked[0]
+
+ # Get explanation
+ print(best_option.cost_breakdown)
+ """
+
+ def __init__(
+ self,
+ env,
+ timetable,
+ weights: Optional[CostWeights] = None,
+ verbose: bool = False
+ ):
+ """
+ Initialize the cost calculator.
+
+ Args:
+ env: Flatland RailEnv
+ timetable: Timetable with train schedules
+ weights: Optional custom weights (default weights used if None)
+ verbose: If True, print debug information
+ """
+ self.env = env
+ self.timetable = timetable
+ self.weights = weights or CostWeights()
+ self.verbose = verbose
+
+ # Cache for expensive calculations
+ self._slack_cache: Dict[int, SlackInfo] = {}
+ self._junction_cache: Set[Tuple[int, int]] = set()
+ self._build_junction_cache()
+
+ def _build_junction_cache(self):
+ """Identify all junction cells in the grid."""
+ from flatland.envs.rail_generators import RailEnvTransitions
+
+ grid = self.env.rail.grid
+ rail_trans = RailEnvTransitions()
+
+ for r in range(grid.shape[0]):
+ for c in range(grid.shape[1]):
+ cell = grid[r, c]
+ if cell == 0:
+ continue
+
+ # Count how many direction combinations have transitions
+ # A junction has more than 2 valid travel directions
+ valid_directions = 0
+ for travel_dir in range(4):
+ exits = rail_trans.get_transitions(cell, travel_dir)
+ if any(exits):
+ valid_directions += 1
+
+ if valid_directions >= 3:
+ self._junction_cache.add((r, c))
+
+ # ==================== MAIN INTERFACE ====================
+
+ def evaluate_resolution(
+ self,
+ resolution,
+ projections: Dict[int, Dict[int, Tuple[int, int]]],
+ cascade_conflicts: Optional[List] = None
+ ) -> Tuple[float, CostBreakdown]:
+ """
+ Evaluate total cost of a resolution.
+
+ Args:
+ resolution: Resolution object (REROUTE or WAIT)
+ projections: Position projections for all trains
+ cascade_conflicts: Optional pre-detected cascade conflicts
+
+ Returns:
+ Tuple of (total_cost, CostBreakdown)
+ """
+ w = self.weights
+ breakdown = CostBreakdown()
+
+ # 1. Direct delay cost
+ breakdown.direct_delay = self._calc_direct_delay_cost(resolution)
+
+ # 2. Cascade costs
+ if cascade_conflicts is None:
+ cascade_conflicts = self._detect_cascade_conflicts(resolution, projections)
+ breakdown.cascade_delay = self._calc_cascade_cost(
+ resolution, projections, cascade_conflicts
+ )
+ breakdown.cascade_count = len(cascade_conflicts) if cascade_conflicts else 0
+
+ # 3. Slack/robustness
+ slack_cost, slack_bonus, slack_info = self._calc_slack_cost(
+ resolution, projections
+ )
+ breakdown.slack_violation = slack_cost
+ breakdown.robustness_bonus = slack_bonus
+ breakdown.slack_info = slack_info
+
+ # 4. Route complexity
+ breakdown.route_complexity = self._calc_route_cost(resolution)
+
+ # Total
+ breakdown.total = (
+ breakdown.direct_delay +
+ breakdown.cascade_delay +
+ breakdown.slack_violation -
+ breakdown.robustness_bonus +
+ breakdown.route_complexity
+ )
+
+ # Build explanation
+ breakdown.explanation = self._build_explanation(resolution, breakdown)
+
+ return breakdown.total, breakdown
+
+ def evaluate_solution(
+ self,
+ decisions: List,
+ final_timetable=None
+ ) -> Tuple[float, Dict]:
+ """
+ Evaluate a complete solution (list of decisions from tree search).
+
+ Args:
+ decisions: List of Resolution objects
+ final_timetable: Optional final timetable state
+
+ Returns:
+ Tuple of (total_cost, breakdown_dict)
+ """
+ total_cost = 0.0
+ breakdown = {
+ 'direct_delay': 0.0,
+ 'cascade_delay': 0.0,
+ 'slack_violation': 0.0,
+ 'robustness_bonus': 0.0,
+ 'route_complexity': 0.0,
+ 'decisions': [],
+ 'total': 0.0,
+ }
+
+ for decision in decisions:
+ cost, dec_breakdown = self.evaluate_resolution(decision, {}, [])
+ total_cost += cost
+
+ breakdown['direct_delay'] += dec_breakdown.direct_delay
+ breakdown['cascade_delay'] += dec_breakdown.cascade_delay
+ breakdown['slack_violation'] += dec_breakdown.slack_violation
+ breakdown['robustness_bonus'] += dec_breakdown.robustness_bonus
+ breakdown['route_complexity'] += dec_breakdown.route_complexity
+ breakdown['decisions'].append(dec_breakdown)
+
+ # Final timetable evaluation
+ if final_timetable:
+ final_cost = self._calc_final_timetable_cost(final_timetable)
+ breakdown['final_delay'] = final_cost
+ total_cost += final_cost
+
+ breakdown['total'] = total_cost
+ return total_cost, breakdown
+
+ def compare_options(
+ self,
+ resolutions: List,
+ projections: Dict[int, Dict[int, Tuple[int, int]]],
+ cascade_conflicts_per_option: Optional[List[List]] = None
+ ) -> List[Tuple[Any, float, CostBreakdown]]:
+ """
+ Compare multiple resolutions and rank them by cost.
+
+ Args:
+ resolutions: List of Resolution objects to compare
+ projections: Position projections for all trains
+ cascade_conflicts_per_option: Optional list of cascade conflicts
+ per option (same order as resolutions)
+
+ Returns:
+ List of (resolution, cost, breakdown) sorted by cost (lowest first)
+ """
+ results = []
+
+ for i, res in enumerate(resolutions):
+ cascades = None
+ if cascade_conflicts_per_option and i < len(cascade_conflicts_per_option):
+ cascades = cascade_conflicts_per_option[i]
+
+ cost, breakdown = self.evaluate_resolution(res, projections, cascades)
+ results.append((res, cost, breakdown))
+
+ # Sort by cost (lowest first)
+ results.sort(key=lambda x: x[1])
+
+ return results
+
+ # ==================== COST COMPONENTS ====================
+
+ def _calc_direct_delay_cost(self, resolution) -> float:
+ """Cost from direct delay to the rerouted/waiting train."""
+ w = self.weights
+ delay = resolution.delay_added
+ train_id = resolution.train_to_delay
+
+ cost = delay * w.direct_delay
+
+ # Priority multiplier
+ if w.priority_multiplier:
+ priority = self.timetable.get_priority(train_id)
+ cost *= priority
+
+ # Critical delay threshold
+ if delay > w.critical_delay:
+ excess = delay - w.critical_delay
+ cost += excess * w.direct_delay * w.critical_multiplier
+
+ if self.verbose:
+ print(f" Direct delay cost: {delay} steps × priority = {cost:.2f}")
+
+ return cost
+
+ def _calc_cascade_cost(
+ self,
+ resolution,
+ projections: Dict[int, Dict[int, Tuple[int, int]]],
+ cascade_conflicts: List
+ ) -> float:
+ """Cost from delays caused to other trains."""
+ w = self.weights
+
+ if not cascade_conflicts:
+ return 0.0
+
+ cost = 0.0
+ for conflict in cascade_conflicts:
+ # Determine which train is the "other" one affected
+ if hasattr(conflict, 'train_a') and hasattr(conflict, 'train_b'):
+ other_train = (
+ conflict.train_b
+ if conflict.train_a == resolution.train_to_delay
+ else conflict.train_a
+ )
+ else:
+ # Simple conflict tuple format
+ other_train = conflict[1] if conflict[0] == resolution.train_to_delay else conflict[0]
+
+ # Estimate delay to the other train
+ estimated_delay = self._estimate_cascade_delay(conflict, resolution)
+ priority = self.timetable.get_priority(other_train)
+
+ conflict_cost = estimated_delay * priority * w.cascade_delay
+ cost += conflict_cost
+
+ if self.verbose:
+ print(f" Cascade to train {other_train}: {estimated_delay} steps × "
+ f"{priority:.2f} priority × {w.cascade_delay} = {conflict_cost:.2f}")
+
+ return cost
+
+ def _calc_slack_cost(
+ self,
+ resolution,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> Tuple[float, float, SlackInfo]:
+ """
+ Calculate slack-related costs and bonuses.
+
+ Returns:
+ Tuple of (violation_cost, robustness_bonus, slack_info)
+ """
+ w = self.weights
+ train_id = resolution.train_to_delay
+
+ # Calculate slack after resolution
+ slack_info = self._calculate_slack(train_id, resolution, projections)
+
+ violation_cost = 0.0
+ robustness_bonus = 0.0
+
+ if slack_info.slack_timesteps < w.min_safe_slack:
+ # Penalty for being in fragile zone
+ deficit = w.min_safe_slack - slack_info.slack_timesteps
+ violation_cost = deficit * w.slack_violation
+
+ # Extra penalty if zero slack (critical)
+ if slack_info.slack_timesteps == 0:
+ violation_cost *= 2.0
+
+ if self.verbose:
+ print(f" Slack violation: {slack_info.slack_timesteps} < {w.min_safe_slack} "
+ f"→ penalty {violation_cost:.2f}")
+ else:
+ # Bonus for having buffer
+ excess = slack_info.slack_timesteps - w.min_safe_slack
+ robustness_bonus = min(excess, 10) * w.robustness_bonus # Cap bonus at 10 steps
+
+ if self.verbose and robustness_bonus > 0:
+ print(f" Robustness bonus: {excess} steps buffer → +{robustness_bonus:.2f}")
+
+ return violation_cost, robustness_bonus, slack_info
+
+ def _calc_route_cost(self, resolution) -> float:
+ """Cost for route complexity (junctions, extra length)."""
+ w = self.weights
+
+ # Only applies to reroutes
+ if resolution.resolution_type.value != "reroute":
+ return 0.0
+
+ if not resolution.new_route:
+ return 0.0
+
+ cost = 0.0
+ route = resolution.new_route
+
+ # Junction count
+ junctions = sum(1 for cell in route if cell in self._junction_cache)
+ cost += junctions * w.junction_usage
+
+ # Extra length vs original
+ if resolution.original_route:
+ extra_cells = max(0, len(route) - len(resolution.original_route))
+ cost += extra_cells * w.route_length
+
+ if self.verbose and cost > 0:
+ print(f" Route complexity: {junctions} junctions, "
+ f"{len(route) - len(resolution.original_route or [])} extra cells → {cost:.2f}")
+
+ return cost
+
+ def _calc_final_timetable_cost(self, final_timetable) -> float:
+ """Evaluate final state: total delay vs original plan."""
+ w = self.weights
+ total_cost = 0.0
+
+ for train_id in range(self.env.get_num_agents()):
+ schedule = final_timetable.get_schedule(train_id)
+
+ if schedule.arrival_delay is not None and schedule.arrival_delay > 0:
+ delay = schedule.arrival_delay
+ priority = final_timetable.get_priority(train_id)
+
+ cost = delay * priority * w.direct_delay
+
+ # Critical threshold
+ if delay > w.critical_delay:
+ excess = delay - w.critical_delay
+ cost += excess * w.critical_multiplier
+
+ total_cost += cost
+
+ return total_cost
+
+ # ==================== SLACK CALCULATION ====================
+
+ def _calculate_slack(
+ self,
+ train_id: int,
+ resolution,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> SlackInfo:
+ """
+ Calculate how much slack/buffer a train has after resolution.
+
+ Slack = minimum time before this train's new schedule conflicts
+ with any other train on a shared track section.
+ """
+ schedule = self.timetable.get_schedule(train_id)
+
+ # Get the route (new route if rerouted, original otherwise)
+ if (resolution.resolution_type.value == "reroute" and
+ resolution.new_route):
+ route = resolution.new_route
+ else:
+ route = schedule.route
+
+ if not route:
+ return SlackInfo(
+ train_id=train_id,
+ slack_timesteps=999,
+ dependent_trains=[],
+ blocking_sections=[],
+ earliest_conflict_step=None
+ )
+
+ # Find all other trains that could conflict
+ dependent_trains = []
+ blocking_sections = []
+ min_slack = float('inf')
+ earliest_conflict = None
+
+ for other_id in range(self.env.get_num_agents()):
+ if other_id == train_id:
+ continue
+
+ if other_id not in projections:
+ continue
+
+ other_projection = projections[other_id]
+
+ # Check each cell in our route
+ for cell in route:
+ # Estimate when we'll be at this cell
+ our_time = self._estimate_time_at_cell(train_id, cell, resolution, route)
+
+ if our_time is None:
+ continue
+
+ # Find when they'll be at this cell
+ for their_time, their_pos in other_projection.items():
+ if their_pos == cell:
+ # Calculate buffer
+ time_diff = abs(their_time - our_time)
+
+ if time_diff < min_slack:
+ min_slack = time_diff
+ earliest_conflict = their_time
+
+ if other_id not in dependent_trains:
+ dependent_trains.append(other_id)
+ if cell not in blocking_sections:
+ blocking_sections.append(cell)
+
+ return SlackInfo(
+ train_id=train_id,
+ slack_timesteps=int(min_slack) if min_slack != float('inf') else 999,
+ dependent_trains=dependent_trains,
+ blocking_sections=blocking_sections,
+ earliest_conflict_step=earliest_conflict
+ )
+
+ def calculate_slack_for_train(
+ self,
+ train_id: int,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> SlackInfo:
+ """
+ Calculate slack for a train without any resolution applied.
+
+ Useful for evaluating initial schedule fragility.
+ """
+ # Create a dummy "no change" resolution
+ from dataclasses import dataclass
+
+ @dataclass
+ class DummyResolution:
+ resolution_type: Any = None
+ train_to_delay: int = 0
+ delay_added: int = 0
+ new_route: List = None
+
+ class DummyType:
+ value = "none"
+
+ dummy = DummyResolution()
+ dummy.resolution_type = DummyType()
+ dummy.train_to_delay = train_id
+ dummy.new_route = None
+
+ return self._calculate_slack(train_id, dummy, projections)
+
+ def _estimate_time_at_cell(
+ self,
+ train_id: int,
+ cell: Tuple[int, int],
+ resolution,
+ route: List[Tuple[int, int]]
+ ) -> Optional[int]:
+ """Estimate when train will reach a specific cell."""
+ schedule = self.timetable.get_schedule(train_id)
+
+ if cell not in route:
+ return None
+
+ cell_idx = route.index(cell)
+ departure = schedule.planned_departure
+
+ # Account for resolution delays
+ if resolution.resolution_type.value == "wait" and resolution.wait_until:
+ departure = max(departure, resolution.wait_until)
+ elif resolution.delay_added:
+ departure += resolution.delay_added
+
+ # Account for speed
+ speed = schedule.speed
+ if speed < 1.0:
+ steps_per_cell = int(1.0 / speed)
+ return departure + (cell_idx * steps_per_cell)
+ else:
+ return departure + cell_idx
+
+ # ==================== HELPER METHODS ====================
+
+ def _detect_cascade_conflicts(
+ self,
+ resolution,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> List:
+ """
+ Detect new conflicts caused by this resolution.
+
+ This simulates the resolution and checks for new conflicts
+ that weren't present before.
+ """
+ # This would integrate with ConflictDetector
+ # For now, return empty (caller should provide pre-detected cascades)
+ return []
+
+ def _estimate_cascade_delay(self, conflict, resolution) -> int:
+ """
+ Estimate delay to other train from a cascade conflict.
+
+ Simple heuristic: minimum wait time to clear the conflict.
+ Could be made more sophisticated with deeper simulation.
+ """
+ # Default estimate: 3 timesteps (one wait cycle)
+ # This is conservative; actual delay may be less or more
+ return 3
+
+ def _build_explanation(self, resolution, breakdown: CostBreakdown) -> str:
+ """Build human-readable explanation of cost calculation."""
+ lines = []
+
+ res_type = resolution.resolution_type.value.upper()
+ train = resolution.train_to_delay
+ delay = resolution.delay_added
+
+ lines.append(f"{res_type} Train {train} (adds {delay} delay)")
+
+ if breakdown.direct_delay > 0:
+ lines.append(f" Direct delay: {breakdown.direct_delay:.2f}")
+
+ if breakdown.cascade_count > 0:
+ lines.append(f" Causes {breakdown.cascade_count} cascade(s): {breakdown.cascade_delay:.2f}")
+
+ if breakdown.slack_info:
+ slack = breakdown.slack_info.slack_timesteps
+ if slack < self.weights.min_safe_slack:
+ lines.append(f" LOW SLACK WARNING: only {slack} steps buffer")
+ else:
+ lines.append(f" Slack: {slack} steps buffer")
+
+ if breakdown.robustness_bonus > 0:
+ lines.append(f" Robustness bonus: -{breakdown.robustness_bonus:.2f}")
+
+ if breakdown.route_complexity > 0:
+ lines.append(f" Route complexity: +{breakdown.route_complexity:.2f}")
+
+ lines.append(f" TOTAL: {breakdown.total:.2f}")
+
+ return "\n".join(lines)
+
+ # ==================== UTILITY ====================
+
+ def get_regret(
+ self,
+ chosen_option,
+ all_options: List,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ) -> Tuple[float, float, float]:
+ """
+ Calculate regret: how much worse is chosen vs optimal?
+
+ Returns:
+ Tuple of (regret, chosen_cost, optimal_cost)
+
+ regret = 0 means chosen IS optimal
+ regret > 0 means we could have done better
+ """
+ # Evaluate all options
+ ranked = self.compare_options(all_options, projections)
+
+ # Find optimal (first in ranked list)
+ optimal_cost = ranked[0][1] if ranked else 0.0
+
+ # Find chosen option's cost
+ chosen_cost, _ = self.evaluate_resolution(chosen_option, projections)
+
+ regret = chosen_cost - optimal_cost
+
+ return regret, chosen_cost, optimal_cost
+
+ def print_comparison(
+ self,
+ resolutions: List,
+ projections: Dict[int, Dict[int, Tuple[int, int]]]
+ ):
+ """Print formatted comparison of resolution options."""
+ ranked = self.compare_options(resolutions, projections)
+
+ print("\n" + "=" * 60)
+ print(" RESOLUTION OPTIONS COMPARISON")
+ print("=" * 60)
+
+ for i, (res, cost, breakdown) in enumerate(ranked):
+ marker = "★ BEST" if i == 0 else ""
+ res_type = res.resolution_type.value.upper()
+
+ print(f"\n{i+1}. {res_type} Train {res.train_to_delay} {marker}")
+ print(f" Delay added: {res.delay_added} steps")
+ print(f" Total cost: {cost:.2f}")
+ print(breakdown)
+
+ print("=" * 60)
+
+
+# ==================== PRESET WEIGHT CONFIGURATIONS ====================
+
+def get_priority_focused_weights() -> CostWeights:
+ """Weights that heavily prioritize high-priority trains."""
+ return CostWeights(
+ direct_delay=1.0,
+ priority_multiplier=True,
+ cascade_delay=2.0, # Cascades are very bad
+ slack_violation=4.0, # Strong penalty for fragility
+ min_safe_slack=5, # Higher slack requirement
+ robustness_bonus=0.3,
+ junction_usage=0.05,
+ route_length=0.02,
+ critical_delay=10, # Lower threshold
+ critical_multiplier=3.0,
+ )
+
+
+def get_balanced_weights() -> CostWeights:
+ """Balanced weights for general use."""
+ return CostWeights() # Default values
+
+
+def get_throughput_focused_weights() -> CostWeights:
+ """Weights that prioritize total throughput over individual trains."""
+ return CostWeights(
+ direct_delay=0.5, # Less penalty for individual delays
+ priority_multiplier=False, # Don't weight by priority
+ cascade_delay=2.0, # Still avoid cascades
+ slack_violation=2.0, # Moderate slack penalty
+ min_safe_slack=2, # Lower slack requirement
+ robustness_bonus=0.1,
+ junction_usage=0.2, # Prefer simpler routes
+ route_length=0.1,
+ critical_delay=20, # Higher threshold
+ critical_multiplier=1.5,
+ )
+
+
+def get_robustness_focused_weights() -> CostWeights:
+ """Weights that prioritize schedule robustness/slack."""
+ return CostWeights(
+ direct_delay=1.0,
+ priority_multiplier=True,
+ cascade_delay=1.5,
+ slack_violation=5.0, # Heavy penalty for low slack
+ min_safe_slack=5, # High slack requirement
+ robustness_bonus=0.5, # Good bonus for extra buffer
+ junction_usage=0.1,
+ route_length=0.05,
+ critical_delay=15,
+ critical_multiplier=2.0,
+ )
\ No newline at end of file
diff --git a/usecases_examples/Railway/ExperimentLogger.py b/usecases_examples/Railway/ExperimentLogger.py
new file mode 100644
index 00000000..9993774d
--- /dev/null
+++ b/usecases_examples/Railway/ExperimentLogger.py
@@ -0,0 +1,48 @@
+"""
+ExperimentLogger.py — Saves structured experiment logs as human-readable JSON.
+
+One JSON file per experiment run, saved to experiment_logs/.
+Format is designed to be readable by a psychologist without technical knowledge.
+"""
+
+import json
+import os
+from datetime import datetime, timezone
+
+LOG_DIR = "experiment_logs"
+
+
+def save_experiment_log(data: dict) -> str:
+ """
+ Save experiment log as formatted JSON.
+ Returns the filename of the saved log.
+ """
+ os.makedirs(LOG_DIR, exist_ok=True)
+ pid = data.get("participant_id", "unbekannt").upper()
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H-%M-%S")
+ filename = f"exp_{pid}_{timestamp}.json"
+ filepath = os.path.join(LOG_DIR, filename)
+
+ with open(filepath, "w", encoding="utf-8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+
+ print(f"[ExperimentLogger] Saved: {filepath}")
+ return filename
+
+
+def list_logs() -> list:
+ """Return list of all saved experiment logs."""
+ if not os.path.exists(LOG_DIR):
+ return []
+ files = [f for f in os.listdir(LOG_DIR) if f.endswith(".json")]
+ files.sort(reverse=True)
+ return files
+
+
+def read_log(filename: str) -> dict:
+ """Return contents of a specific log file."""
+ filepath = os.path.join(LOG_DIR, filename)
+ if not os.path.exists(filepath):
+ return {}
+ with open(filepath, "r", encoding="utf-8") as f:
+ return json.load(f)
diff --git a/usecases_examples/Railway/FlatlandMapLoader.py b/usecases_examples/Railway/FlatlandMapLoader.py
new file mode 100644
index 00000000..6d60a422
--- /dev/null
+++ b/usecases_examples/Railway/FlatlandMapLoader.py
@@ -0,0 +1,435 @@
+"""
+FlatlandMapLoader - Load pre-generated Flatland environments from .pkl files.
+
+Provides the same interface as create_corridor_env() so all existing code
+(ConflictResolver, SafetyVerifier, CostCalculator, etc.) works unchanged.
+
+Usage:
+ from FlatlandMapLoader import load_flatland_env, build_timetable_from_env
+
+ # Load map - same return signature as create_corridor_env()
+ env, stations, junctions = load_flatland_env("maps/4city_map.pkl")
+
+ # Build a timetable from the loaded agents
+ timetable, train_infos, priorities = build_timetable_from_env(env, stations)
+
+Station naming:
+ Cities are auto-detected from agent start/target positions.
+ Named as CITY_0, CITY_1, ... sorted by (row, col).
+
+"""
+
+import json
+from typing import Dict, List, Tuple, Optional
+from flatland.envs.persistence import RailEnvPersister
+from flatland.envs.rail_generators import RailEnvTransitions
+
+
+# ============== CITY / JUNCTION DETECTION ==============
+
+def detect_junctions(env) -> List[Tuple[int, int]]:
+ """
+ Detect all junction cells in the grid.
+
+ A junction is any cell where a train has 3 or more valid travel directions
+ (i.e. can branch, not just go straight or turn).
+ """
+ rail_trans = RailEnvTransitions()
+ grid = env.rail.grid
+ junctions = []
+
+ for r in range(grid.shape[0]):
+ for c in range(grid.shape[1]):
+ cell = grid[r, c]
+ if cell == 0:
+ continue
+
+ # Count directions from which this cell has valid exits
+ valid_entry_dirs = 0
+ for entry_dir in range(4):
+ exits = rail_trans.get_transitions(cell, entry_dir)
+ if any(exits):
+ valid_entry_dirs += 1
+
+ if valid_entry_dirs >= 3:
+ junctions.append((r, c))
+
+ return junctions
+
+
+def _cluster_positions(positions: set, cluster_radius: int = 3) -> List[Tuple[int, int]]:
+
+ sorted_pos = sorted(positions)
+ representatives = []
+
+ for pos in sorted_pos:
+ close_to_existing = any(
+ abs(pos[0] - rep[0]) + abs(pos[1] - rep[1]) <= cluster_radius
+ for rep in representatives
+ )
+ if not close_to_existing:
+ representatives.append(pos)
+
+ return representatives
+
+
+def detect_stations(
+ env,
+ name_map: Optional[Dict[Tuple[int, int], str]] = None,
+ cluster_radius: int = 3,
+) -> Dict[str, Tuple[int, int]]:
+
+ positions = set()
+ for agent in env.agents:
+ if agent.initial_position is not None:
+ positions.add(tuple(agent.initial_position))
+ if agent.target is not None:
+ positions.add(tuple(agent.target))
+
+ # Cluster so one city = one station
+ representatives = _cluster_positions(positions, cluster_radius)
+
+ stations = {}
+ auto_idx = 0
+ for rep in sorted(representatives):
+ if name_map and rep in name_map:
+ stations[name_map[rep]] = rep
+ else:
+ stations[f"CITY_{auto_idx}"] = rep
+ auto_idx += 1
+
+ return stations
+
+
+# ============== MAIN LOADER ==============
+
+def load_flatland_env(
+ pkl_path: str,
+ name_map: Optional[Dict[Tuple[int, int], str]] = None
+) -> Tuple:
+ """
+ Load a Flatland environment from a .pkl file.
+
+ Drop-in replacement for create_corridor_env() — returns the same
+ (env, stations, junctions) tuple so all downstream code works unchanged.
+
+ Args:
+ pkl_path: Path to .pkl file saved with RailEnvPersister.save()
+ name_map: Optional dict mapping (row, col) -> station name.
+ Example: {(5, 0): 'GENEVA', (5, 34): 'ZURICH'}
+ Positions not in the map get auto-names like CITY_0.
+
+ Returns:
+ env: RailEnv instance (already reset)
+ stations: Dict mapping name -> (row, col)
+ junctions: List of (row, col) junction positions
+ """
+ env, _ = RailEnvPersister.load_new(pkl_path)
+ env.reset()
+
+ stations = detect_stations(env, name_map)
+ junctions = detect_junctions(env)
+
+ print(f"Loaded: {pkl_path}")
+ print(f" Grid: {env.width}x{env.height}")
+ print(f" Agents: {env.get_num_agents()}")
+ print(f" Stations detected: {len(stations)}")
+ print(f" Junctions detected: {len(junctions)}")
+ for name, pos in sorted(stations.items()):
+ print(f" {name}: {pos}")
+
+ return env, stations, junctions
+
+
+# ============== TIMETABLE BUILDER ==============
+
+def build_timetable_from_env(
+ env,
+ stations: Dict[str, Tuple[int, int]],
+ departure_offset: int = 1,
+ stagger_departures: bool = True,
+) -> Tuple:
+ """
+ Build a Timetable directly from env.agents.
+
+ Reads each agent's initial_position, initial_direction, and target
+ from the loaded env — these are set correctly by flatland's generator
+ and work with any map.
+
+ Args:
+ env: Loaded RailEnv
+ stations: Stations dict from load_flatland_env()
+ departure_offset: First train departs at this timestep (default 1)
+ stagger_departures: If True, each train departs 1 step later than
+ the previous (avoids spawn collisions)
+
+ Returns:
+ timetable: Timetable object ready for ConflictResolver / SafetyVerifier
+ train_infos: Dict mapping train_id -> TrainInfo
+ priorities: Dict mapping train_id -> float priority
+ """
+ from Timetable import Timetable, TrainSchedule
+ from TrainInfo import TrainInfo, TrainType, calculate_priority
+ from Corridor_environment import compute_route_bfs
+
+ # Reverse lookup: (row, col) -> station name
+ pos_to_name = {v: k for k, v in stations.items()}
+
+ schedules = {}
+ train_infos = {}
+ priorities = {}
+
+ for i, agent in enumerate(env.agents):
+ start = tuple(agent.initial_position)
+ target = tuple(agent.target)
+
+ start_dir = int(agent.initial_direction)
+ route = compute_route_bfs(
+ env, start, target,
+ use_transitions=True,
+ start_direction=start_dir,
+ )
+ if not route:
+ # Fallback: try all directions (e.g. station with multiple entries)
+ route = compute_route_bfs(env, start, target, use_transitions=True)
+
+ if not route:
+ print(f" WARNING: No route for agent {i} ({start} -> {target}), skipping.")
+ continue
+
+ # Use agent's own earliest_departure if set, otherwise stagger
+ agent_dep = getattr(agent, 'earliest_departure', None)
+ if agent_dep and agent_dep > 0:
+ departure = agent_dep
+ else:
+ departure = departure_offset + (i if stagger_departures else 0)
+
+ schedules[i] = TrainSchedule(
+ train_id=i,
+ planned_departure=departure,
+ planned_arrival=departure + len(route),
+ route=route,
+ )
+
+ start_name = pos_to_name.get(start, str(start))
+ target_name = pos_to_name.get(target, str(target))
+
+ train_infos[i] = TrainInfo(
+ train_id=i,
+ name=f"Train-{i} ({start_name}->{target_name})",
+ train_type=TrainType.PASSENGER_LOCAL,
+ passenger_count=100,
+ connection_frequency=15,
+ )
+ priorities[i] = calculate_priority(train_infos[i])
+
+ timetable = Timetable(schedules=schedules, priorities=priorities)
+
+ print(f"\nTimetable built: {len(schedules)} trains scheduled")
+ for tid, s in sorted(schedules.items()):
+ print(f" {train_infos[tid].name}: dep={s.planned_departure}, "
+ f"route_len={len(s.route)}, arr={s.planned_arrival}")
+
+ return timetable, train_infos, priorities
+
+
+# ============== VISUALIZER ==============
+
+def visualize_loaded_env(env, stations: Dict[str, Tuple[int, int]],
+ junctions: List[Tuple[int, int]], step: int = 0):
+ """
+ ASCII visualization for any loaded flatland map.
+ Works the same as visualize_corridor() in Corridor_environment.py.
+ """
+ grid = env.rail.grid
+ height, width = grid.shape
+
+ station_pos_set = set(stations.values())
+ junction_set = set(junctions)
+
+ # Reverse lookup for station names
+ pos_to_name = {v: k for k, v in stations.items()}
+
+ display = [['.' for _ in range(width)] for _ in range(height)]
+
+ for r in range(height):
+ for c in range(width):
+ if grid[r, c] != 0:
+ pos = (r, c)
+ if pos in station_pos_set:
+ display[r][c] = pos_to_name[pos][0] # First letter of name
+ elif pos in junction_set:
+ display[r][c] = '+'
+ else:
+ display[r][c] = '-'
+
+ # Draw agents
+ for i, agent in enumerate(env.agents):
+ if agent.position is not None:
+ r, c = agent.position
+ display[r][c] = str(i % 10)
+
+ print(f"\n Step {step} — {env.width}x{env.height} grid:")
+ print(" " + "".join(f"{c % 10}" for c in range(width)))
+ print(" " + "-" * width)
+ for r in range(height):
+ print(f"{r:2} |" + "".join(display[r]))
+
+ print()
+ for i, agent in enumerate(env.agents):
+ pos = agent.position if agent.position else "waiting"
+ target = agent.target
+ target_name = pos_to_name.get(tuple(target) if target else None, str(target))
+ state_name = agent.state.name if hasattr(agent.state, 'name') else str(agent.state)
+ print(f" Train {i}: {pos} -> {target_name} | {state_name}")
+
+
+# ============== FLATLAND RENDERER ==============
+
+def render_flatland_env(env, show: bool = True):
+ """
+ Render using Flatland's built-in graphical renderer (PIL/SVG).
+
+ Requires: pip install flatland-rl (PIL renderer is included)
+
+ Args:
+ env: RailEnv instance (after reset)
+ show: If True, opens a display window
+ """
+ try:
+ from flatland.utils.rendertools import RenderTool
+ renderer = RenderTool(env, gl="PILSVG")
+ renderer.render_env(
+ show=show,
+ show_observations=False,
+ show_inactive_agents=True,
+ )
+ if show:
+ input("Press Enter to close renderer...")
+ return renderer
+ except Exception as e:
+ print(f"Renderer unavailable ({e}), use visualize_loaded_env() for ASCII view.")
+ return None
+
+
+
+
+# ============== JSON LOADER ==============
+
+def load_flatland_env_from_json(
+ json_path: str,
+ agent_defs: list,
+ max_episode_steps: int = 200,
+ template_pkl: str = "maps/4city_map.pkl",
+) -> Tuple:
+ """
+ Load a Flatland env from a drawn_environment_export.json file.
+ Drop-in replacement for load_flatland_env() — same (env, stations, junctions) return.
+
+ Args:
+ json_path: Path to .json map file
+ agent_defs: List of dicts: {start, target, dir, dep, arr}
+ max_episode_steps: Episode cap (default 200)
+ template_pkl: Existing pkl for structural metadata template
+
+ Returns:
+ env, stations, junctions
+ """
+ import copy
+ import pickle
+ import tempfile
+ import os
+ import numpy as np
+ from flatland.envs.agent_utils import Agent
+ from flatland.envs.rail_trainrun_data_structures import Waypoint
+ from flatland.envs.persistence import RailEnvPersister
+
+ with open(json_path, "r", encoding="utf-8") as f:
+ raw = json.load(f)
+
+ rows = raw["gridDimensions"]["rows"]
+ cols = raw["gridDimensions"]["cols"]
+ grid_list = raw["grid"]
+ print(f"Loaded JSON: {json_path} ({rows}x{cols})")
+
+ # Build env using proper Flatland 4.x API
+ from flatland.envs.rail_grid_transition_map import RailGridTransitionMap
+ from flatland.envs.rail_generators import rail_from_grid_transition_map
+ from flatland.envs.line_generators import Line, BaseLineGen
+ from flatland.envs.rail_trainrun_data_structures import Waypoint as FLWaypoint
+ from flatland.envs.rail_env import RailEnv
+
+ grid_np = np.array(grid_list, dtype=np.uint16)
+ rail_map = RailGridTransitionMap(
+ width=cols, height=rows,
+ transitions=RailEnvTransitions(), grid=grid_np
+ )
+
+ _agent_defs = agent_defs # capture for closure
+
+ class _FixedLineGen(BaseLineGen):
+ def generate(self, rail, num_agents, hints, num_resets, np_random):
+ wps = [
+ [[FLWaypoint(d["start"], d["dir"])], [FLWaypoint(d["target"], None)]]
+ for d in _agent_defs
+ ]
+ return Line(agent_waypoints=wps, agent_speeds=[1.0] * len(_agent_defs))
+
+ env = RailEnv(
+ width=cols, height=rows,
+ rail_generator=rail_from_grid_transition_map(rail_map),
+ line_generator=_FixedLineGen(),
+ number_of_agents=len(agent_defs),
+ )
+ env.reset()
+ env._max_episode_steps = max_episode_steps
+
+ # Set departure/arrival times on agents
+ for i, d in enumerate(agent_defs):
+ env.agents[i].earliest_departure = d["dep"]
+ env.agents[i].latest_arrival = d["arr"]
+
+ # Stations from JSON definitions
+ stations: Dict[str, Tuple[int, int]] = {}
+ for s in raw.get("stations", []):
+ stations[f"CITY_{s['id'] - 1}"] = (s["r"], s["c"])
+ if not stations:
+ stations = detect_stations(env)
+
+ junctions = detect_junctions(env)
+ print(f" Agents: {len(agent_defs)}, Stations: {len(stations)}, Junctions: {len(junctions)}")
+ for name, pos in sorted(stations.items()):
+ print(f" {name}: {pos}")
+
+ return env, stations, junctions
+
+
+# ============== QUICK TEST ==============
+
+if __name__ == "__main__":
+ import os
+
+ # Try to load the map generated by Make_map.py
+ MAP_PATH = "maps/4city_map.pkl"
+
+ if not os.path.exists(MAP_PATH):
+ print(f"Map not found at {MAP_PATH}")
+ print("Run Make_map.py first to generate a map.")
+ else:
+ # Load
+ env, stations, junctions = load_flatland_env(MAP_PATH)
+
+ # ASCII visualization
+ visualize_loaded_env(env, stations, junctions)
+
+ # Build timetable
+ timetable, train_infos, priorities = build_timetable_from_env(env, stations)
+
+ # Run SafetyVerifier on the initial timetable
+ from SafetyVerifier import SafetyVerifier
+ verifier = SafetyVerifier(timetable, max_steps=150)
+ is_safe, violations = verifier.verify_safety(verbose=True)
+
+ print(f"\nInitial timetable safe: {is_safe}")
+ if not is_safe:
+ print("Conflicts to resolve — run safe_resolver.py next.")
\ No newline at end of file
diff --git a/usecases_examples/Railway/Railway.Dockerfile b/usecases_examples/Railway/Railway.Dockerfile
new file mode 100644
index 00000000..ba517304
--- /dev/null
+++ b/usecases_examples/Railway/Railway.Dockerfile
@@ -0,0 +1,25 @@
+FROM python:3.10-slim
+
+WORKDIR /app
+
+# System deps for flatland/numpy/PIL
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ gcc g++ libffi-dev && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Python dependencies
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy all Railway source files
+COPY . .
+
+# Persistent volumes: logs, maps, sessions
+VOLUME ["/app/experiment_logs", "/app/maps"]
+
+EXPOSE 5001
+
+ENV FLASK_ENV=production
+ENV PYTHONUNBUFFERED=1
+
+CMD ["python", "app.py"]
diff --git a/usecases_examples/Railway/SafetyVerifier.py b/usecases_examples/Railway/SafetyVerifier.py
new file mode 100644
index 00000000..0eba0225
--- /dev/null
+++ b/usecases_examples/Railway/SafetyVerifier.py
@@ -0,0 +1,259 @@
+"""
+SafetyVerifier - Independent collision verification for timetables.
+
+"""
+
+from typing import Dict, List, Tuple, Set, Optional
+from dataclasses import dataclass
+from collections import defaultdict
+
+
+@dataclass
+class SafetyViolation:
+ """A detected safety violation."""
+ timestep: int
+ train_a: int
+ train_b: int
+ position: Tuple[int, int]
+ violation_type: str # 'same_cell' or 'head_on_swap'
+
+ def __str__(self):
+ return f"Step {self.timestep}: Train {self.train_a} and Train {self.train_b} at {self.position} ({self.violation_type})"
+
+
+class SafetyVerifier:
+ """
+ Independent safety verification for timetables.
+
+ Projects all train positions and checks for collisions,
+ completely independent of ConflictDetector logic.
+ """
+
+ def __init__(self, timetable, max_steps: int = 100):
+ self.timetable = timetable
+ self.max_steps = max_steps
+
+ def project_all_positions(self) -> Dict[int, Dict[int, Tuple[Optional[Tuple[int, int]], bool]]]:
+ """
+ Project all train positions at each timestep.
+
+ Returns:
+ Dict mapping train_id -> (Dict mapping timestep -> (position, has_arrived))
+ """
+ projections = {}
+
+ for train_id, schedule in self.timetable.schedules.items():
+ projections[train_id] = self._project_train(train_id, schedule)
+
+ return projections
+
+ def _project_train(self, train_id: int, schedule) -> Dict[int, Tuple[Optional[Tuple[int, int]], bool]]:
+ """Project one train's positions over time."""
+ positions = {}
+ route = schedule.route
+
+ if not route:
+ return positions
+
+ # Determine effective departure (accounting for holds)
+ effective_departure = schedule.planned_departure
+ if getattr(schedule, 'was_held', False) and getattr(schedule, 'hold_until', None):
+ effective_departure = max(effective_departure, schedule.hold_until)
+
+ # Get speed (default 1.0)
+ speed = getattr(schedule, 'speed', 1.0)
+ steps_per_cell = int(1.0 / speed) if speed < 1.0 else 1
+
+ destination = route[-1]
+
+ for step in range(self.max_steps):
+ if step < schedule.planned_departure:
+ # Not departed yet
+ positions[step] = (None, False)
+ elif step < effective_departure:
+ # Holding at start
+ positions[step] = (route[0], False)
+ else:
+ # Moving along route (accounting for speed)
+ steps_moving = step - effective_departure
+ route_idx = steps_moving // steps_per_cell
+
+ if route_idx >= len(route):
+ # At destination (arrived)
+ positions[step] = (destination, True)
+ else:
+ positions[step] = (route[route_idx], route_idx == len(route) - 1)
+
+ return positions
+
+ def verify_safety(self, verbose: bool = False,
+ ignore_destination_conflicts: bool = True) -> Tuple[bool, List[SafetyViolation]]:
+
+ projections = self.project_all_positions()
+ violations = []
+
+ train_ids = list(projections.keys())
+
+ for step in range(self.max_steps):
+ # Build position -> trains map for this step
+ pos_to_trains = defaultdict(list)
+
+ for train_id in train_ids:
+ proj = projections[train_id].get(step)
+ if proj is not None:
+ pos, arrived = proj
+ if pos is not None:
+ pos_to_trains[pos].append((train_id, arrived))
+
+ # Check for same-cell collisions
+ for pos, train_arrivals in pos_to_trains.items():
+ if len(train_arrivals) > 1:
+ # Multiple trains at same cell
+ for i in range(len(train_arrivals)):
+ for j in range(i + 1, len(train_arrivals)):
+ train_a, arrived_a = train_arrivals[i]
+ train_b, arrived_b = train_arrivals[j]
+
+ # Skip if both have arrived at destination (station capacity ok)
+ if ignore_destination_conflicts and arrived_a and arrived_b:
+ continue
+
+ violations.append(SafetyViolation(
+ timestep=step,
+ train_a=train_a,
+ train_b=train_b,
+ position=pos,
+ violation_type='same_cell'
+ ))
+
+ # Check for head-on swaps (trains passing through each other)
+ if step > 0:
+ for i, train_a in enumerate(train_ids):
+ for train_b in train_ids[i+1:]:
+ proj_a_now = projections[train_a].get(step)
+ proj_a_prev = projections[train_a].get(step - 1)
+ proj_b_now = projections[train_b].get(step)
+ proj_b_prev = projections[train_b].get(step - 1)
+
+ if not all([proj_a_now, proj_a_prev, proj_b_now, proj_b_prev]):
+ continue
+
+ pos_a_now, arrived_a = proj_a_now
+ pos_a_prev, _ = proj_a_prev
+ pos_b_now, arrived_b = proj_b_now
+ pos_b_prev, _ = proj_b_prev
+
+ # Skip if both have arrived
+ if ignore_destination_conflicts and arrived_a and arrived_b:
+ continue
+
+ if (pos_a_now and pos_b_now and pos_a_prev and pos_b_prev and
+ pos_a_now == pos_b_prev and pos_b_now == pos_a_prev):
+ violations.append(SafetyViolation(
+ timestep=step,
+ train_a=train_a,
+ train_b=train_b,
+ position=pos_a_now,
+ violation_type='head_on_swap'
+ ))
+
+ # Deduplicate
+ seen = set()
+ unique_violations = []
+ for v in violations:
+ key = (v.timestep, min(v.train_a, v.train_b), max(v.train_a, v.train_b), v.violation_type)
+ if key not in seen:
+ seen.add(key)
+ unique_violations.append(v)
+
+ is_safe = len(unique_violations) == 0
+
+ if verbose:
+ if is_safe:
+ print("✅ SAFETY VERIFIED: No collisions detected")
+ else:
+ print(f"❌ SAFETY VIOLATION: {len(unique_violations)} collision(s) detected")
+ for v in unique_violations[:10]:
+ print(f" {v}")
+ if len(unique_violations) > 10:
+ print(f" ... and {len(unique_violations) - 10} more")
+
+ return is_safe, unique_violations
+
+ def get_conflicting_pairs(self, ignore_destination_conflicts: bool = True) -> Set[Tuple[int, int]]:
+
+ _, violations = self.verify_safety(verbose=False,
+ ignore_destination_conflicts=ignore_destination_conflicts)
+
+ pairs = set()
+ for v in violations:
+ pair = (min(v.train_a, v.train_b), max(v.train_a, v.train_b))
+ pairs.add(pair)
+
+ return pairs
+
+
+def verify_timetable_safety(timetable,
+ train_names: Dict[int, str] = None,
+ max_steps: int = 100,
+ verbose: bool = True) -> Tuple[bool, List[SafetyViolation]]:
+
+ verifier = SafetyVerifier(timetable, max_steps)
+ is_safe, violations = verifier.verify_safety(verbose=False)
+
+ if verbose:
+ print("\n" + "=" * 60)
+ print(" SAFETY VERIFICATION")
+ print("=" * 60)
+
+ if is_safe:
+ print("\n ✅ SAFE: No collisions detected")
+ else:
+ print(f"\n ❌ UNSAFE: {len(violations)} collision(s)")
+
+ # Group by pair
+ by_pair = defaultdict(list)
+ for v in violations:
+ pair = (min(v.train_a, v.train_b), max(v.train_a, v.train_b))
+ by_pair[pair].append(v)
+
+ print(f"\n Conflicting pairs: {len(by_pair)}")
+ for pair, pair_violations in sorted(by_pair.items()):
+ name_a = train_names.get(pair[0], f"Train {pair[0]}") if train_names else f"Train {pair[0]}"
+ name_b = train_names.get(pair[1], f"Train {pair[1]}") if train_names else f"Train {pair[1]}"
+
+ # Get step range
+ steps = sorted(set(v.timestep for v in pair_violations))
+ step_range = f"steps {steps[0]}-{steps[-1]}" if len(steps) > 1 else f"step {steps[0]}"
+
+ print(f"\n {name_a} vs {name_b} ({len(pair_violations)} violations, {step_range}):")
+
+ # Show first few
+ for v in pair_violations[:3]:
+ print(f" Step {v.timestep}: both at {v.position}")
+ if len(pair_violations) > 3:
+ print(f" ... and {len(pair_violations) - 3} more")
+
+ print("=" * 60)
+
+ return is_safe, violations
+
+
+def run_safety_check(timetable, train_names: Dict[int, str] = None, max_steps: int = 100) -> bool:
+ """
+ Simple helper to run a safety check and return True/False.
+
+ Prints a one-line result.
+ """
+ verifier = SafetyVerifier(timetable, max_steps)
+ is_safe, violations = verifier.verify_safety(verbose=False, ignore_destination_conflicts=True)
+
+ if is_safe:
+ print(" ✅ SafetyVerifier: No collisions")
+ else:
+ pairs = set()
+ for v in violations:
+ pairs.add((min(v.train_a, v.train_b), max(v.train_a, v.train_b)))
+ print(f" ❌ SafetyVerifier: {len(violations)} collision(s) in {len(pairs)} pair(s)")
+
+ return is_safe
\ No newline at end of file
diff --git a/usecases_examples/Railway/ScenarioManager.py b/usecases_examples/Railway/ScenarioManager.py
new file mode 100644
index 00000000..08231b62
--- /dev/null
+++ b/usecases_examples/Railway/ScenarioManager.py
@@ -0,0 +1,511 @@
+"""
+ScenarioManager.py — Single source of truth for scenario state.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+from copy import deepcopy
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Tuple
+
+from scenarios import make_scenarios
+from safe_resolver import resolve_all_conflicts_safe, ResolutionResult
+from Timetable import Timetable
+
+# Path where learned resolutions are persisted between sessions
+_LEARNED_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ "learned_resolutions.json")
+
+
+@dataclass
+class ScenarioInfo:
+ index: int
+ name: str
+ description: str
+ n_trains: int
+ enable_random_delays: bool = False
+
+
+class ScenarioManager:
+ def __init__(self, env, stations: Dict, junctions: List):
+ self.env = env
+ self.stations = stations
+ self.junctions = junctions
+ self._scenarios: List[Dict] = make_scenarios(env, stations, train_infos=None)
+
+ self.active_index: Optional[int] = None
+ self.timetable: Optional[Timetable] = None
+ self.train_infos: Dict = {}
+ self.priorities: Dict = {}
+ self.last_result: Optional[ResolutionResult] = None
+
+ self.active_enable_delays: bool = False
+ self.active_delay_probability: float = 0.15
+
+ # ── Public API ─────────────────────────────────────────────────────────
+
+ def list_scenarios(self) -> List[ScenarioInfo]:
+ return [
+ ScenarioInfo(
+ index=i,
+ name=s['name'],
+ description=s.get('description', ''),
+ n_trains=len(s['timetable'].schedules),
+ enable_random_delays=s.get('enable_random_delays', False),
+ )
+ for i, s in enumerate(self._scenarios)
+ ]
+
+ def load_scenario(self, idx: int) -> Tuple[Timetable, Dict, Dict, ResolutionResult]:
+ """Full load: deep-copy, env reset, resolve. Used for fresh scenario start."""
+ scenario, timetable, train_infos, priorities = self._load_scenario_base(idx)
+
+ result = resolve_all_conflicts_safe(
+ self.env, timetable, priorities, train_infos,
+ max_iterations=200, verbose=False,
+ stagger_spawn=scenario.get('stagger_spawn', False),
+ rejected_resolutions=scenario.get('rejected_resolutions', None),
+ )
+
+ # Apply learned resolutions AFTER the resolver so they always take
+ # priority. Pre-applying before the resolver fails because
+ # ConflictDetector.project_train_positions ignores hold_until, so
+ # the detector still sees the conflict and overwrites the learned choice.
+ self._override_with_learned(idx, timetable)
+
+ self.last_result = result
+ return timetable, train_infos, priorities, result
+
+ def load_scenario_manual(self, idx: int) -> Tuple[Timetable, Dict, Dict]:
+ """
+ Load scenario WITHOUT running the conflict resolver.
+ Used in manual mode so the user resolves conflicts interactively.
+ """
+ self._load_scenario_base(idx)
+ return self.timetable, self.train_infos, self.priorities
+
+ def reload_active(self) -> Tuple[Timetable, Dict, Dict, ResolutionResult]:
+ """Re-run the active scenario from scratch (restores cancelled trains)."""
+ if self.active_index is None:
+ raise RuntimeError("No scenario loaded yet.")
+ return self.load_scenario(self.active_index)
+
+ def cancel_train_live(self, train_id: int) -> ResolutionResult:
+ """Remove a not-yet-spawned train and re-resolve without env reset."""
+ if self.timetable is None:
+ raise RuntimeError("No scenario loaded.")
+ if train_id not in self.timetable.schedules:
+ raise KeyError(f"Train {train_id} not in active timetable.")
+ if self.env.agents[train_id].position is not None:
+ raise ValueError(f"Train {train_id} is already on the grid — cannot cancel.")
+
+ self.timetable.schedules.pop(train_id, None)
+ self.timetable.priorities.pop(train_id, None)
+ self.train_infos.pop(train_id, None)
+ self.priorities.pop(train_id, None)
+
+ result = resolve_all_conflicts_safe(
+ self.env, self.timetable, self.priorities, self.train_infos,
+ max_iterations=200, verbose=False, stagger_spawn=False,
+ )
+ self.last_result = result
+ return result
+
+ def get_next_conflict_and_options(self):
+ """
+ Detect the first unresolved conflict and generate its resolution options
+ in a single detector pass (avoids running the detector twice).
+
+ Returns:
+ (conflict, options) — conflict is None if timetable is clean.
+ options is a list of Resolution objects sorted by delay_added (best first).
+ """
+ from ConflictResolver import ConflictDetector, ResolutionGenerator
+ if self.timetable is None:
+ return None, []
+
+ detector = ConflictDetector(self.env, self.timetable)
+ conflicts, projections = detector.detect_conflicts(0)
+
+ active = set(self.timetable.schedules.keys())
+ conflict = next(
+ (c for c in conflicts if c.train_a in active and c.train_b in active),
+ None,
+ )
+ if conflict is None:
+ return None, []
+
+ generator = ResolutionGenerator(self.env)
+ options = generator.generate_all_options(
+ conflict, self.priorities, self.timetable, projections)
+ return conflict, sorted(options, key=lambda o: o.delay_added)
+
+ def apply_resolution_option(self, option) -> None:
+ """Apply a user-chosen resolution option to the active timetable."""
+ from ConflictResolver import ResolutionType
+ schedule = self.timetable.get_schedule(option.train_to_delay)
+ if schedule is None:
+ return
+ if option.resolution_type == ResolutionType.REROUTE:
+ schedule.route = option.new_route
+ schedule.planned_arrival += option.delay_added
+ schedule.was_rerouted = True
+ schedule.reroute_delay_added += option.delay_added
+ elif option.resolution_type == ResolutionType.WAIT:
+ schedule.planned_arrival += option.delay_added
+ schedule.was_held = True
+ schedule.hold_until = option.wait_until
+ schedule.hold_at_cell = option.wait_at_cell
+ # WAIT delay is tracked separately from reroute delay so the
+ # table can show them independently — do NOT add to reroute_delay_added
+
+ # ── Learned resolution API
+ def save_learned_resolution(self, scenario_idx: int, option) -> bool:
+ """
+ Persist a manually chosen resolution for a scenario.
+ Returns True on success, False on failure (prints reason).
+ """
+ try:
+ data = self._load_learned_file()
+ key = str(scenario_idx)
+ if key not in data:
+ data[key] = []
+
+ entry = {
+ 'train_to_delay': int(option.train_to_delay),
+ 'resolution_type': str(option.resolution_type.value),
+ 'delay_added': int(option.delay_added),
+ 'wait_at_cell': [int(x) for x in option.wait_at_cell]
+ if option.wait_at_cell else None,
+ 'wait_until': int(option.wait_until)
+ if option.wait_until is not None else None,
+ }
+ # Replace any existing entry for this train (don't accumulate duplicates)
+ data[key] = [e for e in data[key]
+ if e['train_to_delay'] != option.train_to_delay]
+ data[key].append(entry)
+
+ with open(_LEARNED_PATH, 'w') as f:
+ import json as _json
+ _json.dump(data, f, indent=2)
+ print(f"[LEARNED] Saved to {_LEARNED_PATH}")
+ return True
+ except Exception as e:
+ print(f"[LEARNED] Save failed: {e}")
+ return False
+
+ def get_learned_resolutions(self, scenario_idx: int) -> List[dict]:
+ """Return all stored resolutions for a scenario (empty list if none)."""
+ data = self._load_learned_file()
+ return data.get(str(scenario_idx), [])
+
+ def has_learned_resolutions(self, scenario_idx: int) -> bool:
+ return len(self.get_learned_resolutions(scenario_idx)) > 0
+
+ def clear_learned_resolutions(self, scenario_idx: int) -> None:
+ """Delete all stored resolutions for a scenario."""
+ data = self._load_learned_file()
+ data.pop(str(scenario_idx), None)
+ self._save_learned_file(data)
+
+ def _override_with_learned(self, scenario_idx: int, timetable: Timetable) -> None:
+ """
+ Override resolver choices with stored learned resolutions.
+
+ Called AFTER resolve_all_conflicts_safe so the learned resolution
+ always takes priority. Resets the target train's schedule to its
+ base departure timing before applying, so there is no double-counting
+ of delays.
+ """
+ from ConflictResolver import ResolutionType
+ entries = self.get_learned_resolutions(scenario_idx)
+ if not entries:
+ return
+
+ # Get base (pre-resolve) schedules to reset from
+ base_scenario = self._scenarios[scenario_idx]
+ base_timetable = deepcopy(base_scenario['timetable'])
+
+ for entry in entries:
+ tid = entry['train_to_delay']
+ sched = timetable.schedules.get(tid)
+ base = base_timetable.schedules.get(tid)
+ if sched is None or base is None:
+ continue
+
+ rtype = entry['resolution_type']
+ if rtype == ResolutionType.WAIT.value:
+ # Reset to base arrival, then apply learned delay cleanly
+ sched.planned_arrival = base.planned_arrival + entry['delay_added']
+ sched.original_planned_arrival = base.planned_arrival + entry['delay_added']
+ sched.reroute_delay_added = entry['delay_added']
+ sched.was_held = True
+ sched.was_rerouted = False
+ sched.hold_until = entry.get('wait_until')
+ cell = entry.get('wait_at_cell')
+ sched.hold_at_cell = tuple(cell) if cell else None
+ sched.route = list(base.route) # restore original route
+
+ def _apply_learned_resolutions(self, scenario_idx: int, timetable: Timetable) -> None:
+ """
+ Apply stored resolutions to the timetable.
+ Used in manual mode to pre-select the learned option in the UI.
+ NOT used during auto load (use _override_with_learned instead).
+ """
+ from ConflictResolver import ResolutionType
+ entries = self.get_learned_resolutions(scenario_idx)
+ for entry in entries:
+ tid = entry['train_to_delay']
+ rtype = entry['resolution_type']
+ sched = timetable.schedules.get(tid)
+ if sched is None:
+ continue
+ if rtype == ResolutionType.WAIT.value:
+ sched.planned_arrival += entry['delay_added']
+ sched.was_held = True
+ sched.hold_until = entry.get('wait_until')
+ cell = entry.get('wait_at_cell')
+ sched.hold_at_cell = tuple(cell) if cell else None
+ sched.reroute_delay_added += entry['delay_added']
+
+ def _load_learned_file(self) -> dict:
+ try:
+ with open(_LEARNED_PATH, 'r') as f:
+ return json.load(f)
+ except Exception:
+ return {}
+
+ def _save_learned_file(self, data: dict) -> None:
+ try:
+ with open(_LEARNED_PATH, 'w') as f:
+ json.dump(data, f, indent=2)
+ except Exception:
+ pass
+
+ def turnback_train(self, train_id: int, new_target: tuple,
+ current_step: int = 0) -> ResolutionResult:
+ """
+ Reroute a running train to a new target city.
+
+ new_target is the station cluster position from the dialog.
+ We try nearby terminal cells to find one with a valid BFS route
+ from the train's current position.
+ """
+ from Corridor_environment import compute_route_bfs
+
+ if self.timetable is None:
+ raise RuntimeError("No scenario loaded.")
+ if train_id not in self.timetable.schedules:
+ raise KeyError(f"Train {train_id} not in timetable.")
+
+ agent = self.env.agents[train_id]
+ if agent.position is None:
+ raise ValueError(f"Train {train_id} is not on the grid yet.")
+
+ curr_pos = tuple(agent.position)
+ curr_dir = int(agent.direction)
+
+ # Collect all actual terminal cells used by agents (initial_positions + targets)
+ terminal_cells = set()
+ for a in self.env.agents:
+ if a.initial_position is not None:
+ terminal_cells.add(tuple(a.initial_position))
+ if a.target is not None:
+ terminal_cells.add(tuple(a.target))
+
+ # Sort candidates by distance to the requested station position
+ candidates = sorted(terminal_cells,
+ key=lambda c: abs(c[0]-new_target[0]) + abs(c[1]-new_target[1]))
+
+ # Find the closest candidate that has a valid BFS route
+ new_route = None
+ actual_target = None
+ for candidate in candidates:
+ if candidate == curr_pos:
+ continue
+ route = compute_route_bfs(
+ self.env, curr_pos, candidate,
+ use_transitions=True, start_direction=curr_dir,
+ ) or compute_route_bfs(
+ self.env, curr_pos, candidate, use_transitions=True)
+ if route:
+ new_route = route
+ actual_target = candidate
+ break
+
+ if not new_route:
+ raise LookupError(
+ f"No valid route from {curr_pos} to any terminal near {new_target}.")
+
+ # Update flatland agent target
+ agent.target = actual_target
+
+ # Rebuild every on-grid train's schedule from its current position
+ for tid, sched in self.timetable.schedules.items():
+ a = self.env.agents[tid]
+
+ sched.was_held = False
+ sched.hold_at_cell = None
+ sched.hold_until = None
+ sched.reroute_delay_added = 0
+ sched.was_rerouted = (tid == train_id)
+ sched.actual_arrival = None
+
+ if a.position is not None:
+ if tid == train_id:
+ route = new_route
+ else:
+ pos = tuple(a.position)
+ d = int(a.direction)
+ route = compute_route_bfs(
+ self.env, pos, tuple(a.target),
+ use_transitions=True, start_direction=d,
+ ) or compute_route_bfs(
+ self.env, pos, tuple(a.target),
+ use_transitions=True,
+ ) or sched.route
+
+ sched.route = route
+ sched.planned_departure = current_step
+ sched.planned_arrival = current_step + len(route)
+ sched.original_planned_arrival = sched.planned_arrival
+
+ self._sync_env_departures(self.timetable)
+
+ result = resolve_all_conflicts_safe(
+ self.env, self.timetable, self.priorities, self.train_infos,
+ max_iterations=200, verbose=False, stagger_spawn=False,
+ )
+
+ self._fix_rerouted_routes_after_turnback(current_step)
+ self._fix_blocking_holds(train_id, current_step)
+
+ self._fix_blocking_holds(train_id, current_step)
+
+ self.last_result = result
+ return result
+
+ def _fix_rerouted_routes_after_turnback(self, current_step: int):
+
+ from Corridor_environment import compute_route_bfs
+
+ for tid, sched in self.timetable.schedules.items():
+ agent = self.env.agents[tid]
+ if agent.position is None:
+ continue # unspawned — skip
+
+ curr_pos = tuple(agent.position)
+
+ # Check if route[0] matches current position
+ if sched.route and tuple(sched.route[0]) == curr_pos:
+ continue # already correct
+
+ # Route doesn't start at current position — recompute
+ d = int(agent.direction)
+ route = compute_route_bfs(
+ self.env, curr_pos, tuple(agent.target),
+ use_transitions=True, start_direction=d,
+ ) or compute_route_bfs(
+ self.env, curr_pos, tuple(agent.target),
+ use_transitions=True,
+ )
+ if route:
+ sched.route = route
+ sched.planned_departure = current_step
+ sched.planned_arrival = current_step + len(route)
+ sched.original_planned_arrival = sched.planned_arrival
+
+ def _fix_blocking_holds(self, turned_train_id: int, current_step: int):
+ sched = self.timetable.schedules.get(turned_train_id)
+ if sched is None or not sched.was_held or sched.hold_at_cell is None:
+ return
+
+ hold_cell = sched.hold_at_cell
+ hold_until = sched.hold_until or (current_step + sched.reroute_delay_added)
+
+ # Check if any other active train's route passes through the hold cell
+ # during the hold window
+ from Corridor_environment import compute_route_bfs
+ blocking = False
+ max_clear_step = hold_until
+
+ for tid, other in self.timetable.schedules.items():
+ if tid == turned_train_id:
+ continue
+ agent = self.env.agents[tid]
+ if agent.position is None:
+ continue # unspawned — can't conflict now
+ # Check if hold_cell appears in remaining route
+ try:
+ route_cells = [tuple(c) for c in other.route]
+ if tuple(hold_cell) in route_cells:
+ blocking = True
+ # Estimate when the other train passes the hold cell:
+ # it's at route_cells[0] now, moves 1 cell/step
+ idx = route_cells.index(tuple(hold_cell))
+ clear_at = current_step + idx + 5 # +5 buffer
+ max_clear_step = max(max_clear_step, clear_at)
+ except Exception:
+ continue
+
+ if blocking:
+ # Replace hold-at-cell with a simple pre-departure delay:
+ # train waits at its current position until all other trains clear
+ delay = max_clear_step - current_step
+ sched.was_held = True
+ sched.hold_at_cell = tuple(self.env.agents[turned_train_id].position)
+ sched.hold_until = current_step + delay
+ sched.planned_departure = current_step + delay
+ sched.planned_arrival = sched.planned_departure + len(sched.route)
+ sched.original_planned_arrival = sched.planned_arrival
+ sched.reroute_delay_added = delay
+
+ def active_info(self) -> Optional[ScenarioInfo]:
+ if self.active_index is None:
+ return None
+ return self.list_scenarios()[self.active_index]
+
+ # ── Internal helpers ───────────────────────────────────────────────────
+
+ def _load_scenario_base(self, idx: int):
+ """
+ Shared setup for load_scenario and load_scenario_manual.
+ Deep-copies scenario data, resets env, syncs departures, and sets
+ all active_* attributes. Returns (scenario_dict, timetable, train_infos, priorities).
+ """
+ if idx < 0 or idx >= len(self._scenarios):
+ raise IndexError(
+ f"Scenario index {idx} out of range (0–{len(self._scenarios)-1})")
+
+ scenario = self._scenarios[idx]
+ timetable = deepcopy(scenario['timetable'])
+ train_infos = deepcopy(scenario['train_infos'])
+ priorities = deepcopy(scenario['priorities'])
+
+ self.env.reset()
+ self._sync_env_departures(timetable)
+
+ self.active_index = idx
+ self.timetable = timetable
+ self.train_infos = train_infos
+ self.priorities = priorities
+ self.last_result = None
+ self.active_enable_delays = scenario.get('enable_random_delays', False)
+ self.active_delay_probability = scenario.get('delay_probability', 0.15)
+
+ return scenario, timetable, train_infos, priorities
+
+ def _sync_env_departures(self, timetable: Timetable):
+ for agent_id in range(len(self.env.agents)):
+ agent = self.env.agents[agent_id]
+ schedule = timetable.schedules.get(agent_id)
+ dep = schedule.planned_departure if schedule else 9999
+ if hasattr(agent, 'earliest_departure'):
+ agent.earliest_departure = dep
+ if hasattr(self.env, 'timetable') and self.env.timetable is not None:
+ try:
+ self.env.timetable.earliest_departures[agent_id][0] = dep
+ except Exception:
+ pass
\ No newline at end of file
diff --git a/usecases_examples/Railway/ScenarioPlayer.py b/usecases_examples/Railway/ScenarioPlayer.py
new file mode 100644
index 00000000..e507aa6d
--- /dev/null
+++ b/usecases_examples/Railway/ScenarioPlayer.py
@@ -0,0 +1,359 @@
+"""
+ScenarioPlayer.py — Runs scripted Flatland scenarios.
+
+A scenario defines:
+- A Flatland map + timetable
+- Events (things that happen at specific timesteps)
+- Decision points (where simulation pauses for operator input)
+- Pre-scripted action sequences for each decision option
+
+The player steps through the simulation, triggers events and decision
+points at the right timesteps, and applies pre-scripted actions after
+a decision is made.
+"""
+
+import threading
+from Corridor_environment import load_corridor_env
+from FlatlandMapLoader import load_flatland_env_from_json
+from TimetableDispatcher import TimetableDispatcher
+from ScenarioManager import ScenarioManager
+
+
+class ScenarioState:
+ RUNNING = "running"
+ PAUSED = "paused_for_decision"
+ EVENT = "event_triggered"
+ COMPLETE = "complete"
+
+
+class ScenarioPlayer:
+ """
+ Plays back a scripted scenario step by step.
+
+ Usage:
+ player = ScenarioPlayer(scenario_dict)
+ player.start() # begin stepping in background thread
+ player.apply_decision(option_index) # at a decision point
+ player.get_frame() # current agent positions
+ """
+
+ def __init__(self, scenario: dict, on_event=None):
+ import copy
+ self.scenario = copy.deepcopy(scenario) # deep copy so _triggered flag resets each session
+ self.on_event = on_event # callback(event_dict) when event triggers
+ self.state = ScenarioState.RUNNING
+ self.step = 0
+ self.lock = threading.Lock()
+ self.running = False
+ self.speed = 1.0
+
+ # Active decision — set when simulation pauses
+ self.active_decision = None # the decision_point dict
+ self.decision_index = 0 # which decision_point we're at
+
+ # Post-decision scripted actions
+ # Format: {train_id: [action, action, ...]} for remaining steps
+ self._scripted_actions = None
+ self._scripted_step = 0
+
+ # Holds — {train_id: steps_remaining}
+ self._holds = {}
+
+ # Load environment
+ map_path = scenario["map"]
+ if map_path.endswith(".json"):
+ env, stations, junctions = load_flatland_env_from_json(
+ json_path=map_path,
+ agent_defs=scenario.get("agent_defs", []),
+ )
+ else:
+ env, stations, junctions = load_corridor_env(map_path)
+ self.env = env
+ self.stations = stations
+
+ # Set up timetable
+ if map_path.endswith(".json"):
+ # JSON scenario: build timetable directly from agent_defs
+ from FlatlandMapLoader import build_timetable_from_env
+ timetable, train_infos, _ = build_timetable_from_env(env, stations)
+ else:
+ # PKL scenario: use ScenarioManager with pre-built timetable
+ manager = ScenarioManager(env, stations, junctions)
+ timetable, train_infos, _ = manager.load_scenario_manual(
+ scenario.get("scenario_index", 0)
+ )
+ self.dispatcher = TimetableDispatcher(
+ env, timetable,
+ train_infos=train_infos,
+ enable_random_delays=False,
+ )
+
+ # Cached renderer — created once, reused per request
+ from flatland.utils.rendertools import RenderTool
+ self.renderer = RenderTool(env, gl="PILSVG", screen_width=600, screen_height=600)
+
+ # History of agent states per step — for ZWL diagram
+ self._history: list = []
+
+ # ── Public API ─────────────────────────────────────────────────────────────
+
+ def start(self):
+ """Start the simulation loop in a background thread."""
+ self.running = True
+ t = threading.Thread(target=self._loop, daemon=True)
+ t.start()
+
+ def pause(self):
+ with self.lock:
+ self.running = False
+
+ def resume(self):
+ with self.lock:
+ self.running = True
+
+ def apply_decision(self, option_index: int):
+ """
+ Apply the operator's chosen option at the current decision point.
+ Resumes the simulation with the pre-scripted outcome.
+ """
+ with self.lock:
+ if self.active_decision is None:
+ return False
+ if option_index < 0 or option_index >= len(self.active_decision["options"]):
+ return False
+
+ option = self.active_decision["options"][option_index]
+ outcome = option.get("outcome", {})
+
+ # Apply hold if defined
+ hold_train = outcome.get("hold_train")
+ hold_steps = outcome.get("hold_steps", 0)
+ if hold_train and hold_steps > 0:
+ self._holds[hold_train] = hold_steps
+ # Support holding multiple trains with same duration
+ for t in outcome.get("hold_trains", []):
+ if hold_steps > 0:
+ self._holds[t] = hold_steps
+ # Support per-train hold durations via holds dict
+ for t, steps in outcome.get("holds", {}).items():
+ if steps > 0:
+ self._holds[t] = steps
+
+ # Apply pre-scripted actions if defined
+ scripted = outcome.get("scripted_actions")
+ if scripted:
+ self._scripted_actions = scripted
+ self._scripted_step = 0
+
+ self.active_decision = None
+ self.state = ScenarioState.RUNNING
+ self.running = True
+
+ return True
+
+ def get_history_steps(self) -> list:
+ """Return full history of agent states — for ZWL Marey diagram."""
+ with self.lock:
+ return list(self._history)
+
+ def get_frame(self) -> dict:
+ """Current agent states — same format as _step_to_dict in flatland-hmi."""
+ with self.lock:
+ result = {}
+ for agent in self.env.agents:
+ result[str(agent.handle)] = {
+ "position": (
+ [int(c) for c in agent.position]
+ if agent.position is not None else None
+ ),
+ "direction": int(agent.direction) if agent.direction is not None else 0,
+ "moving": bool(agent.moving) if hasattr(agent, "moving") else False,
+ "target": (
+ [int(c) for c in agent.target]
+ if agent.target is not None else None
+ ),
+ "malfunction": 0,
+ }
+ return result
+
+ def get_affected_trains(self) -> set:
+ """Return set of train IDs currently affected by an event (for red highlight)."""
+ with self.lock:
+ return set(self._holds.keys())
+
+ def get_status(self) -> dict:
+ with self.lock:
+ result = {
+ "state": self.state,
+ "step": self.step,
+ "active_decision": None,
+ "scenario_name": self.scenario.get("name", ""),
+ "affected_trains": list(self._holds.keys()),
+ }
+ if self.active_decision is not None:
+ result["active_decision"] = {
+ "timestep": self.active_decision.get("timestep", 0),
+ "description": self.active_decision.get("description", ""),
+ "options": [
+ {
+ "index": i,
+ "label": opt["label"],
+ "kpis": opt["kpis"],
+ }
+ for i, opt in enumerate(self.active_decision["options"])
+ ],
+ }
+ return result
+
+ def get_transitions(self) -> list:
+ """Rail grid for ZWL diagram."""
+ return self.env.rail.grid.tolist()
+
+ # ── Internal loop ──────────────────────────────────────────────────────────
+
+ def _loop(self):
+ import time
+ while True:
+ try:
+ with self.lock:
+ running = self.running
+ speed = self.speed
+ cur_state = self.state
+
+ if cur_state == ScenarioState.COMPLETE:
+ print("[ScenarioPlayer] Scenario complete, loop exiting")
+ break
+
+ if not running or cur_state == ScenarioState.PAUSED:
+ time.sleep(0.1)
+ continue
+
+ self._advance()
+ time.sleep(1.0 / max(speed, 0.1))
+
+ except Exception as e:
+ import traceback
+ print("[ScenarioPlayer] Loop error:", e)
+ traceback.print_exc()
+ time.sleep(0.5) # Brief pause before retrying
+
+ def _advance(self):
+ """Advance one simulation step."""
+ with self.lock:
+ step = self.step
+
+ # Check for events at this timestep
+ for event in self.scenario.get("events", []):
+ if event["timestep"] == step and not event.get("_triggered", False):
+ event["_triggered"] = True
+ self._on_event(event)
+
+ # Check for decision points at this timestep
+ decision_points = self.scenario.get("decision_points", [])
+ if self.decision_index < len(decision_points):
+ dp = decision_points[self.decision_index]
+ if dp["timestep"] == step:
+ with self.lock:
+ self.active_decision = dp
+ self.decision_index += 1
+ self.state = ScenarioState.PAUSED
+ self.running = False
+ return
+
+ # Build actions for this step
+ actions = self._build_actions(step)
+
+ # Step the environment
+ try:
+ self.env.step(actions)
+ except Exception as e:
+ print("[ScenarioPlayer] step error:", e)
+ return
+
+ with self.lock:
+ self.step = step + 1
+
+ # Record step for ZWL history
+ frame = {}
+ for agent in self.env.agents:
+ frame[str(agent.handle)] = {
+ "position": (
+ [int(c) for c in agent.position]
+ if agent.position is not None else None
+ ),
+ "direction": int(agent.direction) if agent.direction is not None else 0,
+ "moving": bool(agent.moving) if hasattr(agent, "moving") else False,
+ "target": (
+ [int(c) for c in agent.target]
+ if agent.target is not None else None
+ ),
+ "malfunction": 0,
+ }
+ with self.lock:
+ self._history.append(frame)
+
+ # Check if simulation is complete (all agents done)
+ if self.env.dones.get("__all__", False):
+ with self.lock:
+ self.state = ScenarioState.COMPLETE
+ self.running = False
+
+ def _build_actions(self, step: int) -> dict:
+ """Build action dict — scripted actions override dispatcher."""
+ # Start with dispatcher actions
+ try:
+ actions = self.dispatcher.get_actions(step)
+ except Exception:
+ actions = {}
+
+ # Apply holds — held trains get DO_NOTHING action
+ DO_NOTHING = 4 # RailEnvActions.DO_NOTHING
+ with self.lock:
+ holds = dict(self._holds)
+
+ for agent in self.env.agents:
+ handle = agent.handle
+ train_id = "Train_" + str(handle)
+ if train_id in holds and holds[train_id] > 0:
+ actions[handle] = DO_NOTHING
+ with self.lock:
+ self._holds[train_id] -= 1
+ if self._holds[train_id] <= 0:
+ del self._holds[train_id]
+
+ # Apply scripted actions if active
+ if self._scripted_actions is not None:
+ scripted_step = self._scripted_step
+ for train_id, action_seq in self._scripted_actions.items():
+ handle = int(train_id.replace("Train_", ""))
+ if scripted_step < len(action_seq):
+ actions[handle] = action_seq[scripted_step]
+
+ self._scripted_step += 1
+ # Clear scripted actions when exhausted
+ max_len = max(
+ (len(seq) for seq in self._scripted_actions.values()),
+ default=0
+ )
+ if self._scripted_step >= max_len:
+ self._scripted_actions = None
+ self._scripted_step = 0
+
+ return actions
+
+ def _on_event(self, event: dict):
+ """Called when an event timestep is reached."""
+ event_type = event.get("type")
+ if event_type == "train_delay":
+ train_id = event.get("train")
+ hold_steps = event.get("duration", 0)
+ if train_id and hold_steps > 0:
+ with self.lock:
+ self._holds[train_id] = hold_steps
+
+ # Fire callback so app.py can push the notification card
+ if self.on_event is not None:
+ try:
+ self.on_event(event)
+ except Exception as e:
+ print("[ScenarioPlayer] on_event callback error:", e)
diff --git a/usecases_examples/Railway/SessionManager.py b/usecases_examples/Railway/SessionManager.py
new file mode 100644
index 00000000..22af1872
--- /dev/null
+++ b/usecases_examples/Railway/SessionManager.py
@@ -0,0 +1,252 @@
+"""
+SessionManager.py — Manages user sessions, scenario ordering, and decision logging.
+
+Each session:
+- Gets a random ordering of the 4 scenarios
+- Tracks which scenario is current
+- Logs all decisions to SQLite
+
+Logging schema:
+ decisions(id, session_id, scenario_id, decision_index,
+ option_index, option_label, timestamp)
+"""
+
+import sqlite3
+import random
+import uuid
+from datetime import datetime, timezone
+
+
+DB_PATH = "sessions.db"
+
+
+def _init_db():
+ """Create all tables if they don't exist."""
+ conn = sqlite3.connect(DB_PATH)
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS decisions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ session_id TEXT NOT NULL,
+ scenario_id TEXT NOT NULL,
+ decision_index INTEGER NOT NULL,
+ option_index INTEGER NOT NULL,
+ option_label TEXT NOT NULL,
+ timestamp TEXT NOT NULL
+ )
+ """)
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS sessions (
+ session_id TEXT PRIMARY KEY,
+ scenario_order TEXT NOT NULL,
+ current_index INTEGER NOT NULL DEFAULT 0,
+ acronym TEXT NOT NULL DEFAULT '',
+ mode TEXT NOT NULL DEFAULT 'recommendation',
+ started_at TEXT NOT NULL
+ )
+ """)
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS reflections (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ session_id TEXT NOT NULL,
+ acronym TEXT NOT NULL DEFAULT '',
+ question_index INTEGER NOT NULL,
+ question_text TEXT NOT NULL,
+ answer TEXT NOT NULL,
+ timestamp TEXT NOT NULL
+ )
+ """)
+ # Migrate existing sessions table if needed
+ try:
+ conn.execute("ALTER TABLE sessions ADD COLUMN acronym TEXT NOT NULL DEFAULT ''")
+ except Exception:
+ pass
+ try:
+ conn.execute("ALTER TABLE sessions ADD COLUMN mode TEXT NOT NULL DEFAULT 'recommendation'")
+ except Exception:
+ pass
+ conn.commit()
+ conn.close()
+
+
+_init_db()
+
+# In-memory session state — maps session_id to dict
+_sessions: dict = {}
+
+
+class SessionManager:
+
+ @staticmethod
+ def create_session(scenario_ids: list, acronym: str = "", mode: str = "recommendation") -> str:
+ """
+ Create a new session with a random scenario order.
+ Returns the session_id.
+ """
+ session_id = str(uuid.uuid4())
+ shuffled = list(scenario_ids)
+ random.shuffle(shuffled)
+ started_at = datetime.now(timezone.utc).isoformat()
+
+ # Store in memory
+ _sessions[session_id] = {
+ "scenario_order": shuffled,
+ "current_index": 0,
+ "acronym": acronym,
+ "mode": mode,
+ "started_at": started_at,
+ }
+
+ # Persist to SQLite
+ conn = sqlite3.connect(DB_PATH)
+ conn.execute(
+ "INSERT INTO sessions (session_id, scenario_order, current_index, acronym, mode, started_at) "
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ (session_id, ",".join(shuffled), 0, acronym, mode, started_at)
+ )
+ conn.commit()
+ conn.close()
+
+ return session_id
+
+ @staticmethod
+ def get_session(session_id: str) -> dict | None:
+ """Return session dict or None if not found."""
+ return _sessions.get(session_id)
+
+ @staticmethod
+ def current_scenario_id(session_id: str) -> str | None:
+ """Return the ID of the current scenario for this session."""
+ session = _sessions.get(session_id)
+ if session is None:
+ return None
+ idx = session["current_index"]
+ order = session["scenario_order"]
+ if idx >= len(order):
+ return None
+ return order[idx]
+
+ @staticmethod
+ def advance_scenario(session_id: str) -> bool:
+ """
+ Move to the next scenario.
+ Returns True if there is a next scenario, False if all done.
+ """
+ session = _sessions.get(session_id)
+ if session is None:
+ return False
+
+ session["current_index"] += 1
+
+ # Persist
+ conn = sqlite3.connect(DB_PATH)
+ conn.execute(
+ "UPDATE sessions SET current_index = ? WHERE session_id = ?",
+ (session["current_index"], session_id)
+ )
+ conn.commit()
+ conn.close()
+
+ return session["current_index"] < len(session["scenario_order"])
+
+ @staticmethod
+ def log_decision(
+ session_id: str,
+ scenario_id: str,
+ decision_index: int,
+ option_index: int,
+ option_label: str,
+ ):
+ """Log a decision to SQLite."""
+ timestamp = datetime.now(timezone.utc).isoformat()
+ conn = sqlite3.connect(DB_PATH)
+ conn.execute(
+ "INSERT INTO decisions "
+ "(session_id, scenario_id, decision_index, option_index, option_label, timestamp) "
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ (session_id, scenario_id, decision_index, option_index, option_label, timestamp)
+ )
+ conn.commit()
+ conn.close()
+
+ @staticmethod
+ def log_reflection(
+ session_id: str,
+ acronym: str,
+ answers: list, # list of {question_index, question_text, answer}
+ ):
+ """Log reflection module answers to SQLite."""
+ timestamp = datetime.now(timezone.utc).isoformat()
+ conn = sqlite3.connect(DB_PATH)
+ for a in answers:
+ conn.execute(
+ "INSERT INTO reflections "
+ "(session_id, acronym, question_index, question_text, answer, timestamp) "
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ (session_id, acronym,
+ a.get("question_index", 0),
+ a.get("question_text", ""),
+ a.get("answer", ""),
+ timestamp)
+ )
+ conn.commit()
+ conn.close()
+
+ @staticmethod
+ def get_reflections(session_id: str) -> list:
+ """Return reflection answers for a session."""
+ conn = sqlite3.connect(DB_PATH)
+ rows = conn.execute(
+ "SELECT question_index, question_text, answer, timestamp "
+ "FROM reflections WHERE session_id = ? ORDER BY id",
+ (session_id,)
+ ).fetchall()
+ conn.close()
+ return [{"question_index": r[0], "question_text": r[1],
+ "answer": r[2], "timestamp": r[3]} for r in rows]
+
+ @staticmethod
+ def get_decisions(session_id: str) -> list:
+ """Return all decisions for a session (for end screen)."""
+ conn = sqlite3.connect(DB_PATH)
+ rows = conn.execute(
+ "SELECT scenario_id, decision_index, option_index, option_label, timestamp "
+ "FROM decisions WHERE session_id = ? ORDER BY id",
+ (session_id,)
+ ).fetchall()
+ conn.close()
+ return [
+ {
+ "scenario_id": row[0],
+ "decision_index": row[1],
+ "option_index": row[2],
+ "option_label": row[3],
+ "timestamp": row[4],
+ }
+ for row in rows
+ ]
+
+ @staticmethod
+ def is_complete(session_id: str) -> bool:
+ """Return True if all scenarios have been played."""
+ session = _sessions.get(session_id)
+ if session is None:
+ return True
+ return session["current_index"] >= len(session["scenario_order"])
+
+ @staticmethod
+ def sessions_summary() -> list:
+ """Return all sessions (for admin/research export)."""
+ conn = sqlite3.connect(DB_PATH)
+ rows = conn.execute(
+ "SELECT session_id, scenario_order, current_index, started_at FROM sessions"
+ ).fetchall()
+ conn.close()
+ return [
+ {
+ "session_id": row[0],
+ "scenario_order": row[1].split(","),
+ "current_index": row[2],
+ "started_at": row[3],
+ }
+ for row in rows
+ ]
diff --git a/usecases_examples/Railway/Timetable.py b/usecases_examples/Railway/Timetable.py
new file mode 100644
index 00000000..3e9df0dc
--- /dev/null
+++ b/usecases_examples/Railway/Timetable.py
@@ -0,0 +1,178 @@
+"""
+Timetable data structures for train scheduling.
+
+"""
+
+from dataclasses import dataclass, field
+from typing import List, Dict, Tuple, Optional
+
+
+@dataclass
+class TrainSchedule:
+ """Schedule for a single train."""
+ train_id: int
+ planned_departure: int # timestep
+ planned_arrival: int # timestep (may be updated if rerouted)
+ route: List[Tuple[int, int]] # list of (row, col) positions
+
+ # Train properties
+ speed: float = 1.0 # 1.0 = normal, 0.5 = takes 2 steps per cell
+
+ # Original plan - NEVER modified after creation
+ original_planned_arrival: Optional[int] = None
+ original_route_length: Optional[int] = None
+
+ # Runtime tracking
+ actual_departure: Optional[int] = None
+ actual_arrival: Optional[int] = None
+
+ # Rerouting / hold tracking
+ was_rerouted: bool = False
+ was_held: bool = False
+ hold_at_cell: Optional[Tuple[int, int]] = None
+ hold_until: Optional[int] = None
+ reroute_delay_added: int = 0 # total delay added by resolver (WAIT + REROUTE)
+
+ # Injected delay tracking (spontaneous/random delays)
+ injected_delay_steps: int = 0 # total steps held due to injection
+ _inject_hold_until: int = 0 # internal: step when current injection ends
+
+ def __post_init__(self):
+ """Store original values on creation."""
+ if self.original_planned_arrival is None:
+ self.original_planned_arrival = self.planned_arrival
+ if self.original_route_length is None:
+ self.original_route_length = len(self.route) if self.route else 0
+
+ @property
+ def departure_delay(self) -> Optional[int]:
+ """Delay at departure (vs planned)."""
+ if self.actual_departure is None:
+ return None
+ return max(0, self.actual_departure - self.planned_departure)
+
+ @property
+ def arrival_delay(self) -> Optional[int]:
+ """
+ Delay at arrival measured against ORIGINAL plan.
+ """
+ if self.actual_arrival is None:
+ return None
+ return max(0, self.actual_arrival - self.original_planned_arrival)
+
+ @property
+ def is_on_time(self) -> bool:
+ """On time = arrived by original planned arrival."""
+ if self.actual_arrival is None:
+ return False
+ return self.actual_arrival <= self.original_planned_arrival
+
+
+@dataclass
+class Timetable:
+ """
+ Complete timetable for all trains.
+
+ Tracks priorities for weighted delay calculation.
+ """
+ schedules: Dict[int, TrainSchedule]
+ priorities: Dict[int, float] = field(default_factory=dict)
+
+ def get_schedule(self, train_id: int):
+ return self.schedules.get(train_id, None) # None if not in timetable
+
+ def set_priorities(self, priorities: Dict[int, float]):
+ """Set train priorities for weighted delay calculation."""
+ self.priorities = priorities
+
+ def get_priority(self, train_id: int) -> float:
+ """Get priority for a train (default 1.0)."""
+ return self.priorities.get(train_id, 1.0)
+
+ def total_delay(self) -> int:
+ """Sum of all arrival delays (unweighted)."""
+ total = 0
+ for schedule in self.schedules.values():
+ if schedule.arrival_delay is not None:
+ total += schedule.arrival_delay
+ return total
+
+ def weighted_delay(self) -> float:
+ """
+ Priority-weighted total delay.
+
+ Formula: sum(delay_i * priority_i) for all trains
+
+ This is the key metric for evaluating dispatching decisions.
+ A high-priority train's delay counts more than a low-priority one.
+ """
+ total = 0.0
+ for train_id, schedule in self.schedules.items():
+ if schedule.arrival_delay is not None:
+ priority = self.get_priority(train_id)
+ total += schedule.arrival_delay * priority
+ return total
+
+ def print_summary(self):
+ """Print timetable summary and results."""
+ print("\n" + "="*70)
+ print(" TIMETABLE RESULTS")
+ print("="*70)
+
+ # Header
+ header = (f"{'Train':<6} {'Pri':<5} {'Orig Arr':<9} {'Act Arr':<9} "
+ f"{'Delay':<7} {'Wtd Del':<8} {'Rerouted':<9} {'Status':<10}")
+ print(f"\n{header}")
+ print("-"*70)
+
+ # Per-train results
+ total_weighted = 0.0
+ for train_id, s in sorted(self.schedules.items()):
+ priority = self.get_priority(train_id)
+ orig_arr = s.original_planned_arrival
+ act_arr = s.actual_arrival if s.actual_arrival is not None else "-"
+ delay = s.arrival_delay if s.arrival_delay is not None else 0
+ weighted = delay * priority if s.arrival_delay is not None else 0
+ total_weighted += weighted
+ rerouted = "Yes" if s.was_rerouted else "No"
+
+ if s.actual_arrival is None:
+ status = "INCOMPLETE"
+ elif s.is_on_time:
+ status = "✓ ON TIME"
+ else:
+ status = "✗ DELAYED"
+
+ print(f"{train_id:<6} {priority:<5.1f} {orig_arr:<9} {str(act_arr):<9} "
+ f"{delay:<7} {weighted:<8.1f} {rerouted:<9} {status:<10}")
+
+ print("-"*70)
+
+ # Summary statistics
+ print(f"\n Total Delay (unweighted): {self.total_delay()} timesteps")
+ print(f" Weighted Delay (Σ delay×priority): {self.weighted_delay():.1f}")
+
+ on_time = sum(1 for s in self.schedules.values() if s.is_on_time)
+ total = len(self.schedules)
+ print(f" On-Time Performance: {on_time}/{total} ({100*on_time/total:.0f}%)")
+
+ # Rerouting summary
+ rerouted_trains = [tid for tid, s in self.schedules.items() if s.was_rerouted]
+ held_trains = [tid for tid, s in self.schedules.items() if s.was_held]
+
+ rerouted_or_held = [tid for tid, s in self.schedules.items()
+ if s.was_rerouted or s.was_held]
+ injected = [tid for tid, s in self.schedules.items()
+ if s.injected_delay_steps > 0]
+ if rerouted_or_held or injected:
+ print()
+ if rerouted_or_held:
+ total_reroute = sum(s.reroute_delay_added for s in self.schedules.values())
+ print(f" Trains Rerouted/Held: {rerouted_or_held}")
+ print(f" Delay from Re-planning: {total_reroute} timesteps")
+ if injected:
+ total_injected = sum(s.injected_delay_steps for s in self.schedules.values())
+ print(f" Trains with Injected Delays: {injected}")
+ print(f" Total Injected Delay: {total_injected} timesteps")
+
+ print("="*70)
\ No newline at end of file
diff --git a/usecases_examples/Railway/TimetableDispatcher.py b/usecases_examples/Railway/TimetableDispatcher.py
new file mode 100644
index 00000000..5314a82b
--- /dev/null
+++ b/usecases_examples/Railway/TimetableDispatcher.py
@@ -0,0 +1,589 @@
+"""
+TimetableDispatcher - Real-time dispatcher bridging offline planning and Flatland execution.
+"""
+
+from typing import Dict, List, Optional, Tuple, Set
+from dataclasses import dataclass
+from collections import defaultdict
+from flatland.core.grid.grid4_utils import get_new_position
+from flatland.envs.rail_generators import RailEnvTransitions
+
+
+DELTA_TO_DIR: Dict[Tuple[int, int], int] = {
+ (-1, 0): 0,
+ ( 0, 1): 1,
+ ( 1, 0): 2,
+ ( 0, -1): 3,
+}
+
+def dir_to_action(current_dir: int, target_dir: int) -> int:
+ diff = (target_dir - current_dir) % 4
+ if diff == 0: return 2
+ elif diff == 1: return 3
+ elif diff == 3: return 1
+ else: return 2
+
+
+def route_to_directions(route: List[Tuple[int, int]]) -> List[int]:
+ dirs = []
+ for i in range(len(route) - 1):
+ dr = route[i+1][0] - route[i][0]
+ dc = route[i+1][1] - route[i][1]
+ d = DELTA_TO_DIR.get((dr, dc))
+ if d is None:
+ raise ValueError(f"Non-adjacent cells: {route[i]} -> {route[i+1]}")
+ dirs.append(d)
+ return dirs
+
+
+@dataclass
+class DispatchEvent:
+ step: int
+ event_type: str
+ agent_id: int
+ position: Optional[Tuple[int, int]] = None
+ detail: str = ""
+
+ def __str__(self):
+ pos = f" at {self.position}" if self.position else ""
+ return (f"Step {self.step:3d} | Train {self.agent_id} | "
+ f"{self.event_type:20s}{pos} | {self.detail}")
+
+
+@dataclass
+class AgentExecState:
+ agent_id: int
+ route_idx: int = 0
+ steps_blocked: int = 0
+ departed: bool = False
+ arrived: bool = False
+ off_route: bool = False
+ DEADLOCK_THRESHOLD: int = 25
+
+
+class TimetableDispatcher:
+ """
+ Executes a resolved Timetable in Flatland with real-time blocking awareness.
+ """
+
+ def __init__(self, env, timetable, ignore_holds: bool = False,
+ train_infos: Dict = None,
+ enable_random_delays: bool = False,
+ delay_probability: float = 0.15,
+ delay_min_steps: int = 5,
+ delay_max_steps: int = 30,
+ random_seed: int = None):
+ """
+ Args:
+ ignore_holds: Skip planned hold/wait instructions.
+ train_infos: Dict of train_id -> TrainInfo. When provided,
+ actual_arrival updates current_delay for live
+ priority recalculation.
+ enable_random_delays: If True, randomly inject delays during simulation.
+ delay_probability: Probability that any given train will experience
+ one delay event during its journey (default 0.15 = 15%).
+ Roll happens once when a train first moves, not every step.
+ delay_min_steps: Minimum injected delay duration in steps.
+ delay_max_steps: Maximum injected delay duration in steps.
+ random_seed: Seed for reproducible random delays.
+ """
+ self.env = env
+ self.timetable = timetable
+ self.ignore_holds = ignore_holds
+ self._train_infos = train_infos or {}
+
+ # Random delay injection config
+ self.enable_random_delays = enable_random_delays
+ self.delay_probability = delay_probability
+ self.delay_min_steps = delay_min_steps
+ self.delay_max_steps = delay_max_steps
+
+ import random
+ self._rng = random.Random(random_seed)
+
+ # Set when an injection fires this step — caller checks this to trigger re-plan
+ self._replan_needed: bool = False
+ self._injection_log: List[dict] = [] # full history of injections
+ # Tracks trains for which the "will this train get a delay?" decision
+ # has already been made. Maps train_id -> (will_be_delayed, at_step)
+ self._delay_decided: Dict[int, tuple] = {}
+
+ self.rail_trans = RailEnvTransitions()
+ self._directions: Dict[int, List[int]] = {}
+ self._exec: Dict[int, AgentExecState] = {}
+ self._step_events: List[DispatchEvent] = []
+ self._init_routes()
+
+ def _init_routes(self, preserve_active: bool = False):
+ """
+ Pre-compute direction sequences from position routes.
+
+ Args:
+ preserve_active: If True, preserve route_idx and exec state for
+ agents already on the grid (mid-journey re-plan).
+ Only recompute directions; don't reset progress.
+ New/unstarted agents are initialised normally.
+ """
+ for agent_id, schedule in self.timetable.schedules.items():
+ route = schedule.route
+ if route and len(route) >= 2:
+ try:
+ self._directions[agent_id] = route_to_directions(route)
+ except ValueError as e:
+ print(f" [Dispatcher] Warning agent {agent_id}: {e}")
+ self._directions[agent_id] = []
+ else:
+ self._directions[agent_id] = []
+
+ agent = self.env.agents[agent_id]
+ already_active = (agent.position is not None
+ or (agent_id in self._exec
+ and self._exec[agent_id].departed))
+
+ if preserve_active and already_active and agent_id in self._exec:
+ # Keep existing exec state — only sync route_idx to current position
+ state = self._exec[agent_id]
+ if agent.position is not None and route:
+ current_pos = tuple(agent.position)
+ # Find where the agent currently is in the new route
+ best_idx = 0
+ best_dist = float('inf')
+ for i, cell in enumerate(route):
+ dist = abs(cell[0]-current_pos[0]) + abs(cell[1]-current_pos[1])
+ if dist < best_dist:
+ best_dist = dist
+ best_idx = i
+ state.route_idx = best_idx
+ state.off_route = False # clear off-route flag after re-plan
+ state.steps_blocked = 0
+ else:
+ # Fresh init for agents not yet started
+ self._exec[agent_id] = AgentExecState(agent_id=agent_id)
+
+ def reset(self):
+ for state in self._exec.values():
+ state.route_idx = 0
+ state.steps_blocked = 0
+ state.departed = False
+ state.arrived = False
+ state.off_route = False
+ self._step_events = []
+ self._replan_needed = False
+ self._injection_log = []
+ self._delay_decided = {}
+
+ def get_pending_replan(self) -> bool:
+ """
+ Returns True if a delay was injected this step that requires
+ the caller to re-run the conflict resolver.
+
+ The caller is responsible for re-planning — the dispatcher does
+ not do this itself to keep concerns separated.
+
+ Call once per step after get_actions(). Resets automatically
+ on the next get_actions() call.
+ """
+ return self._replan_needed
+
+ def get_injection_log(self) -> List[dict]:
+ """
+ Return full history of injected delays.
+
+ Each entry: {step, train_id, delay_steps, position}
+ """
+ return list(self._injection_log)
+
+ def get_step_events(self) -> List[DispatchEvent]:
+ return list(self._step_events)
+
+ def get_actions(self, step: int) -> Dict[int, int]:
+ self._step_events = []
+ self._replan_needed = False # reset each step
+
+ # Inject random delays before computing actions
+ if self.enable_random_delays:
+ self._maybe_inject_delays(step)
+
+ # Pass 1: confirm moves from last step
+ for agent_id in range(self.env.get_num_agents()):
+ self._confirm_move(agent_id)
+
+ # Pass 2a: compute intentions
+ intentions: Dict[int, Tuple[Tuple[int, int], int]] = {}
+ for agent_id in range(self.env.get_num_agents()):
+ result = self._compute_intention(agent_id, step)
+ if result is not None:
+ intentions[agent_id] = result
+
+ # Pass 2b: resolve simultaneous movement conflicts by priority
+ cell_to_movers: Dict[Tuple[int, int], List[int]] = defaultdict(list)
+ for agent_id, (next_cell, action) in intentions.items():
+ if action in (1, 2, 3):
+ cell_to_movers[next_cell].append(agent_id)
+
+ priority_losers: Set[int] = set()
+ for cell, movers in cell_to_movers.items():
+ if len(movers) > 1:
+ movers.sort(key=lambda a: self.timetable.get_priority(a), reverse=True)
+ for loser in movers[1:]:
+ priority_losers.add(loser)
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='priority_yield',
+ agent_id=loser, position=cell,
+ detail=(f"yield to Train {movers[0]} "
+ f"(pri {self.timetable.get_priority(movers[0]):.1f}"
+ f" >= {self.timetable.get_priority(loser):.1f})")
+ ))
+
+ # Pass 2c: build final actions
+ actions: Dict[int, int] = {}
+ for agent_id in range(self.env.get_num_agents()):
+ if agent_id not in self._exec:
+ actions[agent_id] = 0 # not in this scenario's timetable
+ continue
+ state = self._exec[agent_id]
+ agent = self.env.agents[agent_id]
+
+ if agent_id in priority_losers:
+ actions[agent_id] = 4
+ state.steps_blocked += 1
+ elif agent_id in intentions:
+ _, action = intentions[agent_id]
+ actions[agent_id] = action
+ if action in (1, 2, 3):
+ state.steps_blocked = 0
+ else:
+ state.steps_blocked += 1
+ else:
+ # No intention = hold/wait this step.
+ # action=0 ("do nothing") is only correct for agents not yet
+ # on the grid. For active agents already on the grid, we must
+ # send action=4 (STOP_MOVING) otherwise flatland may continue
+ # moving the agent in its current direction.
+ if agent.position is None:
+ actions[agent_id] = 0 # not spawned yet — correct
+ else:
+ actions[agent_id] = 4 # active on grid — must explicitly stop
+
+ if state.steps_blocked >= state.DEADLOCK_THRESHOLD and not state.arrived:
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='deadlock_warning',
+ agent_id=agent_id,
+ position=(tuple(agent.position) if agent.position else None),
+ detail=f"blocked {state.steps_blocked} steps"
+ ))
+
+ return actions
+
+ def _maybe_inject_delays(self, step: int):
+ """
+ Randomly inject ONE delay event per train during its journey.
+
+ Probability model: when a train first starts moving, roll once to
+ decide whether it will experience a delay (delay_probability chance).
+ If yes, pick a random step during its remaining journey to apply it.
+ This gives each train a ~15% chance of ONE delay event total,
+ not a 15% chance every step.
+
+ Rules:
+ - Decision made once at first movement (not every step)
+ - At most one injection per train per journey
+ - Already-under-delay trains are skipped
+ - Planned hold_until is shifted forward if injection overlaps it
+ """
+ for agent_id, schedule in self.timetable.schedules.items():
+ agent = self.env.agents[agent_id]
+ state = self._exec.get(agent_id)
+
+ if state is None or state.arrived or state.off_route:
+ continue
+ if agent.position is None:
+ continue # not yet on grid
+ if schedule._inject_hold_until > step:
+ continue # already under injected delay
+ if schedule.injected_delay_steps > 0:
+ continue # already had a delay this journey
+
+ # Make the once-per-journey decision when train first appears on grid
+ if agent_id not in self._delay_decided:
+ will_delay = self._rng.random() < self.delay_probability
+ if will_delay:
+ # Pick a random step in the remaining route to trigger delay
+ route_remaining = len(schedule.route) - state.route_idx
+ trigger_offset = self._rng.randint(
+ max(1, route_remaining // 4),
+ max(2, route_remaining * 3 // 4)
+ )
+ trigger_step = step + trigger_offset
+ else:
+ trigger_step = None
+ self._delay_decided[agent_id] = (will_delay, trigger_step)
+
+ will_delay, trigger_step = self._delay_decided[agent_id]
+ if not will_delay or trigger_step is None:
+ continue
+ if step < trigger_step:
+ continue # not yet time to inject
+
+ delay = self._rng.randint(self.delay_min_steps, self.delay_max_steps)
+ schedule._inject_hold_until = step + delay
+ schedule.injected_delay_steps += delay
+
+ # If train has a planned hold, shift it forward too
+ if (getattr(schedule, 'was_held', False)
+ and schedule.hold_until is not None
+ and schedule.hold_until > step):
+ schedule.hold_until += delay
+
+ self._replan_needed = True
+
+ entry = {
+ 'step': step,
+ 'train_id': agent_id,
+ 'delay_steps': delay,
+ 'position': tuple(agent.position),
+ 'hold_until': step + delay,
+ }
+ self._injection_log.append(entry)
+
+ name = (self._train_infos[agent_id].name
+ if agent_id in self._train_infos else f"Train {agent_id}")
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='delay_injected',
+ agent_id=agent_id,
+ position=tuple(agent.position),
+ detail=f"{name} held {delay} steps (until step {step + delay})"
+ ))
+
+ def _confirm_move(self, agent_id: int):
+ """
+ CONFIRMATION-BASED route_idx advance.
+ Only advance when agent is provably at route[idx+1].
+ Never scan the whole route.
+ """
+ if agent_id not in self._exec:
+ return # agent not in this scenario's timetable
+ schedule = self.timetable.schedules.get(agent_id)
+ agent = self.env.agents[agent_id]
+ state = self._exec[agent_id]
+ if not schedule or agent.position is None:
+ return
+ route = schedule.route
+ if not route:
+ return
+ current_pos = tuple(agent.position)
+ idx = state.route_idx
+
+ if idx < len(route) and tuple(route[idx]) == current_pos:
+ return # still at expected position, no advance
+
+ if idx + 1 < len(route) and tuple(route[idx + 1]) == current_pos:
+ state.route_idx += 1
+ state.off_route = False
+ return # confirmed move to next cell
+
+ # Agent is at neither expected position — went off-route
+ if not state.off_route:
+ state.off_route = True
+ self._step_events.append(DispatchEvent(
+ step=0, event_type='off_route',
+ agent_id=agent_id, position=current_pos,
+ detail=(f"expected {tuple(route[min(idx, len(route)-1)])} "
+ f"or {tuple(route[min(idx+1, len(route)-1)])}")
+ ))
+
+ def _compute_intention(self, agent_id: int, step: int) -> Optional[Tuple[Tuple[int, int], int]]:
+ if agent_id not in self._exec:
+ return None # agent not in this scenario's timetable
+ schedule = self.timetable.schedules.get(agent_id)
+ if schedule is None:
+ return None
+ agent = self.env.agents[agent_id]
+ state = self._exec[agent_id]
+ state_name = agent.state.name if hasattr(agent.state, 'name') else str(agent.state)
+
+ if 'DONE' in state_name:
+ if not state.arrived:
+ state.arrived = True
+ schedule.actual_arrival = step
+ # Update current_delay in TrainInfo for live priority recalc
+ info = self._train_infos.get(agent_id)
+ if info and schedule.arrival_delay is not None:
+ info.current_delay = schedule.arrival_delay
+ delay_str = f"+{schedule.arrival_delay}" if schedule.arrival_delay else "on time"
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='arrived',
+ agent_id=agent_id,
+ detail=f"actual={step} planned={schedule.original_planned_arrival} delay={delay_str}"))
+ return None
+
+ if step < schedule.planned_departure:
+ return None
+
+ # Injected delay hold: freeze train regardless of other logic
+ if schedule._inject_hold_until > step:
+ return (tuple(agent.position), 4) if agent.position else None
+
+ # Hold logic:
+ # - hold_at_cell set: move normally until reaching that cell, then
+ # stop there until hold_until. Enables strategic waiting at junctions.
+ # - hold_at_cell None: pre-departure delay, stop all movement.
+ if (not self.ignore_holds
+ and getattr(schedule, 'was_held', False)
+ and getattr(schedule, 'hold_until', None)):
+ hold_cell = getattr(schedule, 'hold_at_cell', None)
+ if hold_cell is None:
+ if step < schedule.hold_until:
+ return None
+ elif (agent.position is not None
+ and tuple(agent.position) == tuple(hold_cell)
+ and step < schedule.hold_until):
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='holding_at_cell',
+ agent_id=agent_id,
+ position=tuple(agent.position),
+ detail=f"waiting until step {schedule.hold_until}"
+ ))
+ return None
+ # else: keep moving toward hold_at_cell
+
+ # Not yet on grid
+ if agent.position is None:
+ route = schedule.route
+ if not route:
+ return None
+ entry_cell = tuple(route[0])
+ if self._is_occupied(entry_cell, exclude=agent_id):
+ state.steps_blocked += 1
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='spawn_blocked',
+ agent_id=agent_id, position=entry_cell,
+ detail="entry cell occupied"))
+ return (entry_cell, 4)
+ if not state.departed:
+ state.departed = True
+ schedule.actual_departure = step
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='departed',
+ agent_id=agent_id, position=entry_cell,
+ detail=f"planned={schedule.planned_departure} actual={step}"))
+ return (entry_cell, 2)
+
+ # On grid
+ directions = self._directions.get(agent_id, [])
+ if not directions:
+ return None
+ idx = state.route_idx
+ if idx >= len(directions):
+ state.arrived = True
+ return None
+ if state.off_route:
+ state.steps_blocked += 1
+ return None
+
+ next_dir = directions[idx]
+ next_cell = tuple(get_new_position(agent.position, next_dir))
+
+ # Validate transition
+ cell_val = self.env.rail.grid[agent.position[0], agent.position[1]]
+ valid_exits = self.rail_trans.get_transitions(cell_val, agent.direction)
+ if not valid_exits[next_dir]:
+ best = self._pick_best_exit(directions, idx, valid_exits)
+ if best is None:
+ return None
+ next_dir = best
+ next_cell = tuple(get_new_position(agent.position, next_dir))
+
+ # Physical blocking
+ if self._is_occupied(next_cell, exclude=agent_id):
+ state.steps_blocked += 1
+ self._step_events.append(DispatchEvent(
+ step=step, event_type='blocked',
+ agent_id=agent_id, position=next_cell,
+ detail="next cell occupied"))
+ return (next_cell, 4)
+
+ return (next_cell, dir_to_action(agent.direction, next_dir))
+
+ def _is_occupied(self, cell: Tuple[int, int], exclude: int) -> bool:
+ for i, a in enumerate(self.env.agents):
+ if i != exclude and a.position is not None:
+ if tuple(a.position) == cell:
+ return True
+ return False
+
+ def _pick_best_exit(self, directions: List[int], idx: int,
+ valid_exits: Tuple) -> Optional[int]:
+ valid_dirs = [d for d in range(4) if valid_exits[d]]
+ if not valid_dirs:
+ return None
+ if len(valid_dirs) == 1:
+ return valid_dirs[0]
+ for intended in directions[idx:]:
+ if intended in valid_dirs:
+ return intended
+ return valid_dirs[0]
+
+ def print_timetable_plan(self, train_infos: Dict = None):
+ """Print goals, plan, and direction compatibility for all trains."""
+ DIR_NAMES = {0: "N", 1: "E", 2: "S", 3: "W"}
+ print("\n Timetable plan (train goals + timing + direction check):")
+ for agent_id, schedule in sorted(self.timetable.schedules.items()):
+ agent = self.env.agents[agent_id]
+ target = tuple(agent.target) if agent.target is not None else "?"
+ start = tuple(schedule.route[0]) if schedule.route else "?"
+ hold_str = (f", hold->step {schedule.hold_until}"
+ if getattr(schedule, 'was_held', False) else "")
+ reroute_str = " [REROUTED]" if getattr(schedule, 'was_rerouted', False) else ""
+ name = (train_infos[agent_id].name
+ if train_infos and agent_id in train_infos
+ else f"Train {agent_id}")
+
+ # Direction compatibility check
+ init_dir = int(agent.initial_direction)
+ route_first_dir = None
+ dir_ok = "?"
+ if schedule.route and len(schedule.route) >= 2:
+ try:
+ dirs = route_to_directions(schedule.route)
+ route_first_dir = dirs[0]
+ diff = (route_first_dir - init_dir) % 4
+ if diff == 0:
+ dir_ok = "OK"
+ elif diff == 2:
+ dir_ok = "MISMATCH-180" # opposite direction = bug
+ else:
+ dir_ok = f"turn({diff*90}deg)"
+ except ValueError:
+ dir_ok = "non-adjacent"
+
+ init_str = DIR_NAMES.get(init_dir, str(init_dir))
+ first_str = DIR_NAMES.get(route_first_dir, "?") if route_first_dir is not None else "?"
+
+ print(f" {name}: {start} -> {target} | "
+ f"dep={schedule.planned_departure}{hold_str} | "
+ f"route_len={len(schedule.route)}{reroute_str} | "
+ f"init_dir={init_str} route_dir={first_str} [{dir_ok}]")
+
+ def print_final_report(self, train_infos: Dict = None):
+ print("\n" + "=" * 70)
+ print(" DISPATCHER EXECUTION REPORT")
+ print("=" * 70)
+ for agent_id, state in sorted(self._exec.items()):
+ agent = self.env.agents[agent_id]
+ directions = self._directions.get(agent_id, [])
+ pos = tuple(agent.position) if agent.position else "off-grid"
+ progress = f"{state.route_idx}/{len(directions)}"
+ if state.arrived:
+ status = "DONE"
+ elif state.off_route:
+ status = "OFF-ROUTE"
+ elif state.steps_blocked > 10:
+ status = f"BLOCKED ({state.steps_blocked} steps)"
+ else:
+ status = "moving"
+ name = (train_infos[agent_id].name
+ if train_infos and agent_id in train_infos
+ else f"Train {agent_id}")
+ print(f" {name}: pos={pos} | progress={progress} | {status}")
+ print("=" * 70)
\ No newline at end of file
diff --git a/usecases_examples/Railway/TrainInfo.py b/usecases_examples/Railway/TrainInfo.py
new file mode 100644
index 00000000..4e37a97f
--- /dev/null
+++ b/usecases_examples/Railway/TrainInfo.py
@@ -0,0 +1,202 @@
+"""
+Train information and priority calculation.
+
+Provides:
+- TrainType: Enum for train categories
+- TrainInfo: Train properties that affect priority
+- calculate_priority(): Computes dynamic priority based on train properties
+
+Priority factors:
+- train_type: passenger > freight > maintenance
+- passenger_count: More passengers = higher priority
+- connection_frequency: Rare connections = higher priority (worse to miss)
+- current_delay: Already-delayed trains get slight boost to prevent cascade
+"""
+
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Dict, Optional
+
+
+class TrainType(Enum):
+ """Train categories with base priority weights."""
+ PASSENGER_EXPRESS = "passenger_express"
+ PASSENGER_LOCAL = "passenger_local"
+ FREIGHT = "freight"
+ MAINTENANCE = "maintenance"
+
+
+# Base priority weights by train type
+TRAIN_TYPE_WEIGHTS: Dict[TrainType, float] = {
+ TrainType.PASSENGER_EXPRESS: 1.5,
+ TrainType.PASSENGER_LOCAL: 1.0,
+ TrainType.FREIGHT: 0.3,
+ TrainType.MAINTENANCE: 0.1,
+}
+
+
+@dataclass
+class TrainInfo:
+ """
+ Information about a train that affects its priority.
+
+ Attributes:
+ train_id: Unique identifier
+ train_type: Category (passenger, freight, etc.)
+ passenger_count: Number of passengers (0 for freight)
+ connection_frequency: Minutes between services on this route
+ - 10 min = frequent service, missing one is not too bad
+ - 60 min = rare service, missing one is very bad
+ speed: Cells per timestep (1.0 = normal, 0.5 = slow freight)
+ current_delay: Current delay in timesteps (for cascade prevention)
+ """
+ train_id: int
+ train_type: TrainType = TrainType.PASSENGER_LOCAL
+ passenger_count: int = 0
+ connection_frequency: int = 10 # minutes between services
+ speed: float = 1.0 # cells per timestep
+ current_delay: int = 0 # current delay in timesteps
+
+ # Optional descriptive name
+ name: Optional[str] = None
+
+ def __post_init__(self):
+ if self.name is None:
+ self.name = f"Train {self.train_id}"
+
+
+def calculate_priority(train_info: TrainInfo) -> float:
+ """
+ Calculate dynamic priority for a train.
+
+ Formula:
+ priority = base_type × passenger_factor × connection_factor × delay_factor
+
+ Where:
+ - base_type: Weight from TRAIN_TYPE_WEIGHTS (1.5 for express, 0.3 for freight)
+ - passenger_factor: 1.0 + (passengers / 500), range [1.0, 2.0+]
+ - connection_factor: frequency / 10, higher = worse to miss
+ - delay_factor: 1.0 + (delay / 60) × 0.5, slight boost for delayed trains
+
+ Args:
+ train_info: TrainInfo object with train properties
+
+ Returns:
+ Priority score (higher = more important to not delay)
+
+ Examples:
+ Express with 400 passengers, 30-min frequency, no delay:
+ 1.5 × 1.8 × 3.0 × 1.0 = 8.1
+
+ Local with 50 passengers, 10-min frequency, 5-step delay:
+ 1.0 × 1.1 × 1.0 × 1.04 = 1.14
+
+ Freight, no passengers, 120-min frequency, no delay:
+ 0.3 × 1.0 × 12.0 × 1.0 = 3.6
+ """
+ # Base weight from train type
+ base = TRAIN_TYPE_WEIGHTS.get(train_info.train_type, 1.0)
+
+ # Passenger factor: more passengers = higher priority
+ # Range: 1.0 (empty) to 2.0+ (500+ passengers)
+ passenger_factor = 1.0 + (train_info.passenger_count / 500)
+
+ # Connection frequency factor: rare connections = higher priority
+ # 10-min service: factor = 1.0
+ # 60-min service: factor = 6.0 (6x worse to miss)
+ connection_factor = train_info.connection_frequency / 10
+
+ # Delay factor: slight boost for already-delayed trains to prevent cascade
+ # No delay: factor = 1.0
+ # 60-step delay: factor = 1.5
+ delay_factor = 1.0 + (train_info.current_delay / 60) * 0.5
+
+ return base * passenger_factor * connection_factor * delay_factor
+
+
+def create_default_train_infos(n_trains: int) -> Dict[int, TrainInfo]:
+ """
+ Create default TrainInfo objects for testing.
+
+ Train 0: Express passenger (high priority)
+ Train 1: Local passenger (low priority)
+ Additional trains: Freight
+ """
+ infos = {}
+
+ if n_trains >= 1:
+ infos[0] = TrainInfo(
+ train_id=0,
+ name="Express A",
+ train_type=TrainType.PASSENGER_EXPRESS,
+ passenger_count=300,
+ connection_frequency=30, # Every 30 min
+ speed=1.0,
+ )
+
+ if n_trains >= 2:
+ infos[1] = TrainInfo(
+ train_id=1,
+ name="Local B",
+ train_type=TrainType.PASSENGER_LOCAL,
+ passenger_count=50,
+ connection_frequency=10, # Every 10 min
+ speed=1.0,
+ )
+
+ for i in range(2, n_trains):
+ infos[i] = TrainInfo(
+ train_id=i,
+ name=f"Freight {i}",
+ train_type=TrainType.FREIGHT,
+ passenger_count=0,
+ connection_frequency=120, # Every 2 hours
+ speed=0.5, # Slower
+ )
+
+ return infos
+
+
+# ============== PRIORITY COMPARISON HELPERS ==============
+
+def compare_priorities(info_a: TrainInfo, info_b: TrainInfo) -> int:
+ """
+ Compare two trains by priority.
+
+ Returns:
+ 1 if A has higher priority
+ -1 if B has higher priority
+ 0 if equal
+ """
+ priority_a = calculate_priority(info_a)
+ priority_b = calculate_priority(info_b)
+
+ if priority_a > priority_b:
+ return 1
+ elif priority_b > priority_a:
+ return -1
+ else:
+ return 0
+
+
+def get_priority_explanation(train_info: TrainInfo) -> str:
+ """
+ Get human-readable explanation of priority calculation.
+
+ Useful for debugging and future reasoning system.
+ """
+ base = TRAIN_TYPE_WEIGHTS.get(train_info.train_type, 1.0)
+ passenger_factor = 1.0 + (train_info.passenger_count / 500)
+ connection_factor = train_info.connection_frequency / 10
+ delay_factor = 1.0 + (train_info.current_delay / 60) * 0.5
+ total = base * passenger_factor * connection_factor * delay_factor
+
+ lines = [
+ f"Priority calculation for {train_info.name}:",
+ f" Base ({train_info.train_type.value}): {base:.2f}",
+ f" × Passenger factor ({train_info.passenger_count} pax): {passenger_factor:.2f}",
+ f" × Connection factor ({train_info.connection_frequency} min): {connection_factor:.2f}",
+ f" × Delay factor ({train_info.current_delay} steps late): {delay_factor:.2f}",
+ f" = Total priority: {total:.2f}",
+ ]
+ return "\n".join(lines)
diff --git a/usecases_examples/Railway/app.py b/usecases_examples/Railway/app.py
new file mode 100644
index 00000000..5be6247e
--- /dev/null
+++ b/usecases_examples/Railway/app.py
@@ -0,0 +1,1527 @@
+"""
+app.py - Flask brain for the Railway use case.
+
+Endpoints:
+ GET /health - is the brain alive?
+ GET /state - current train positions + directions
+ POST /control - start / pause / resume / reset / speed
+ GET /conflicts - current conflict + resolution options
+ POST /resolve - apply a chosen resolution option
+ GET /render - PNG image of current Flatland state
+
+MAP FILE: Set MAP_PATH below.
+"""
+
+import io
+import threading
+import time
+from datetime import datetime, timezone
+
+import requests
+from flask import Flask, jsonify, request, send_file
+from flask_cors import CORS
+from PIL import Image
+
+from Corridor_environment import load_corridor_env
+from ScenarioManager import ScenarioManager
+from TimetableDispatcher import TimetableDispatcher
+from ScenarioPlayer import ScenarioPlayer, ScenarioState
+from SessionManager import SessionManager, _sessions
+from experiment_scenarios import ALL_SCENARIOS
+from ExperimentLogger import save_experiment_log, list_logs, read_log
+
+MAP_PATH = "maps/4city_map.pkl"
+
+CONTEXT_SERVICE_URL = "http://localhost:3200/cab_context/api/v1/contexts"
+EVENT_SERVICE_URL = "http://localhost:3200/cab_event/api/v1/events"
+INTERACTIVEAI_TOKEN = ""
+SNAPSHOT_INTERVAL_S = 3.0
+
+
+def _get_auth_token():
+ """Get a fresh token from Keycloak for pushing to InteractiveAI services."""
+ try:
+ response = requests.post(
+ "http://localhost:3200/auth/token",
+ data={
+ "username": "admin",
+ "password": "test",
+ "grant_type": "password",
+ "client_id": "opfab-client",
+ },
+ timeout=5,
+ )
+ data = response.json()
+ return data.get("access_token", "")
+ except Exception as e:
+ print("[auth] Failed to get token:", e)
+ return ""
+
+app = Flask(__name__)
+CORS(app)
+
+state_lock = threading.Lock()
+
+# ── Static presentation event (shown from startup for demo purposes) ──────────
+PRESENTATION_EVENT = {
+ # Platform-compatible fields
+ "event_type": "INFRASTRUCTURE",
+ "id_train": "Train_0",
+ "agent_id": "0",
+ "delay": 0,
+ # Flatland-specific fields
+ "train_b": "Train_1",
+ "cell": [15, 12],
+ "conflict_id": "demo_event_1",
+ "message": "Heavy snowfall on route City_1 to City_0",
+}
+
+PRESENTATION_OPTIONS = [
+ {
+ "index": 0,
+ "train_to_delay": 0,
+ "resolution_type": "reroute",
+ "delay_added": 8,
+ "description": "Reroute Train 0 via City_2 (+ 8 min delay)",
+ },
+ {
+ "index": 1,
+ "train_to_delay": 1,
+ "resolution_type": "reroute",
+ "delay_added": 6,
+ "description": "Reroute Train 1 via City_0 bypass (+ 6 min delay)",
+ },
+ {
+ "index": 2,
+ "train_to_delay": 0,
+ "resolution_type": "wait",
+ "delay_added": 12,
+ "description": "Hold Train 0 at City_1 station until track is cleared (+ 12 min delay)",
+ },
+]
+
+sim = {
+ "env": None,
+ "stations": None,
+ "junctions": None,
+ "manager": None,
+ "dispatcher": None,
+ "renderer": None,
+ "step": 0,
+ "running": False,
+ "speed": 1.0,
+ "active_conflict": None,
+ "options": [],
+ "conflict_pushed": False,
+ "history": [], # list of agent states per step, for ZWL diagram
+}
+
+# ── Scenario state ─────────────────────────────────────────────────────────────
+# Active scenario player (None when in free-run mode)
+scenario_player: ScenarioPlayer | None = None
+session_manager = SessionManager()
+# Current session mode: "recommendation" or "colearning"
+session_mode: str = "recommendation"
+# Train currently selected by user in CoLearning mode (for map highlight)
+selected_train: str = ""
+pushed_card_ids: list = []
+pushed_process_instance_ids: list = []
+preview_scenario_id: str = ""
+# Last decision made — stored for experiment log export
+last_decision: dict = {}
+
+
+def _init_simulation():
+ from flatland.utils.rendertools import RenderTool
+ env, stations, junctions = load_corridor_env(MAP_PATH)
+ manager = ScenarioManager(env, stations, junctions)
+ timetable, train_infos, priorities = manager.load_scenario_manual(0)
+ dispatcher = TimetableDispatcher(
+ env, timetable,
+ train_infos=train_infos,
+ enable_random_delays=False,
+ )
+ renderer = RenderTool(env, gl="PILSVG", screen_width=600, screen_height=600)
+ with state_lock:
+ sim["env"] = env
+ sim["stations"] = stations
+ sim["junctions"] = junctions
+ sim["manager"] = manager
+ sim["dispatcher"] = dispatcher
+ sim["renderer"] = renderer
+ sim["step"] = 0
+ sim["running"] = False
+ sim["active_conflict"] = None
+ sim["options"] = []
+ sim["conflict_pushed"] = False
+ sim["history"] = []
+
+
+def _sim_loop():
+ while True:
+ with state_lock:
+ running = sim["running"]
+ speed = sim["speed"]
+
+ if not running:
+ time.sleep(0.1)
+ continue
+
+ try:
+ _advance_one_step()
+ except Exception as e:
+ import traceback
+ print("[sim_loop] Step failed:")
+ traceback.print_exc()
+ with state_lock:
+ sim["running"] = False
+
+ time.sleep(1.0 / max(speed, 0.1))
+
+
+def _advance_one_step():
+ with state_lock:
+ env = sim["env"]
+ dispatcher = sim["dispatcher"]
+ step = sim["step"]
+
+ actions = dispatcher.get_actions(step)
+
+ # Hold Train 3 from step 15 to 29 (inclusive) in free-run mode
+ DO_NOTHING = 4
+ if 15 <= step <= 29:
+ for agent in env.agents:
+ if agent.handle == 3:
+ actions[agent.handle] = DO_NOTHING
+
+ # Train 2 is out of service in free-run mode (would create unwanted conflicts)
+ for agent in env.agents:
+ if agent.handle == 2:
+ actions[agent.handle] = DO_NOTHING
+
+ env.step(actions)
+
+ new_step = step + 1
+ with state_lock:
+ sim["step"] = new_step
+
+ # Record agent states for ZWL history
+ with state_lock:
+ env_ref = sim["env"]
+ if env_ref is not None:
+ step_record = {}
+ for agent in env_ref.agents:
+ step_record[str(agent.handle)] = {
+ "position": (
+ None if agent.position is None
+ else [int(c) for c in agent.position]
+ ),
+ "direction": int(agent.direction) if agent.direction is not None else 0,
+ "moving": bool(agent.moving) if hasattr(agent, "moving") else False,
+ "target": [int(c) for c in agent.target] if agent.target is not None else None,
+ "malfunction": 0,
+ }
+ sim["history"].append(step_record)
+
+ # Check for conflicts every 5 steps to avoid slowing the loop
+ if new_step % 5 == 0:
+ _check_for_conflict()
+
+
+def _check_for_conflict():
+ with state_lock:
+ manager = sim["manager"]
+ previous_conflict = sim["active_conflict"]
+
+ try:
+ conflict, options = manager.get_next_conflict_and_options()
+ except AttributeError:
+ return # ScenarioManager doesn't support conflict detection
+ except Exception as e:
+ print("[conflict] Detection failed:", e)
+ return
+
+ with state_lock:
+ sim["active_conflict"] = conflict
+ sim["options"] = options
+
+ if conflict is not None:
+ new_conflict = (
+ previous_conflict is None
+ or previous_conflict.train_a != conflict.train_a
+ or previous_conflict.train_b != conflict.train_b
+ or previous_conflict.cell != conflict.cell
+ )
+ if new_conflict and not sim["conflict_pushed"]:
+ sim["conflict_pushed"] = True
+ threading.Thread(
+ target=_push_event,
+ args=(conflict,),
+ daemon=True,
+ ).start()
+ else:
+ sim["conflict_pushed"] = False
+
+def _snapshot_loop():
+ while True:
+ time.sleep(SNAPSHOT_INTERVAL_S)
+ try:
+ _push_snapshot()
+ except Exception as e:
+ print("[snapshot_loop] Failed:", e)
+
+
+def _push_snapshot():
+ with state_lock:
+ env = sim["env"]
+ if env is None:
+ return
+ position_agents = {}
+ direction_agents = []
+ trains = []
+ for i, agent in enumerate(env.agents):
+ pos = [int(x) for x in agent.position] if agent.position is not None else None
+ direction = int(agent.direction) if agent.direction is not None else 0
+ direction_agents.append(direction)
+ position_agents[str(i)] = pos
+ trains.append({
+ "id_train": "Train_" + str(i),
+ "train_type": "PASSENGER",
+ "nb_passengers_onboard": 0,
+ "position": pos,
+ "direction": direction,
+ "failure": False,
+ "speed": 1,
+ })
+
+ payload = {
+ "use_case": "Railway",
+ "date": datetime.now(timezone.utc).isoformat(),
+ "data": {
+ "trains": trains,
+ "position_agents": position_agents,
+ "direction_agents": direction_agents,
+ }
+ }
+ token = _get_auth_token()
+ headers = {"Content-Type": "application/json"}
+ if token:
+ headers["Authorization"] = "Bearer " + token
+ try:
+ requests.post(CONTEXT_SERVICE_URL, json=payload, headers=headers, timeout=5)
+ except Exception as e:
+ print("[push_snapshot] Failed:", e)
+
+
+def _push_event(conflict):
+ conflict_id = (
+ str(int(conflict.train_a)) + "_" +
+ str(int(conflict.train_b)) + "_" +
+ str(int(conflict.cell[0])) + "_" +
+ str(int(conflict.cell[1]))
+ )
+ payload = {
+ "use_case": "Railway",
+ "title": "Conflict detected on network",
+ "description": (
+ "Conflict between Train " + str(int(conflict.train_a)) +
+ " and Train " + str(int(conflict.train_b)) +
+ " at cell " + str([int(x) for x in conflict.cell])
+ ),
+ "criticality": "HIGH",
+ "start_date": datetime.now(timezone.utc).isoformat(),
+ "data": {
+ # Platform-compatible fields
+ "event_type": "INFRASTRUCTURE",
+ "id_train": "Train_" + str(int(conflict.train_a)),
+ "agent_id": str(int(conflict.train_a)),
+ "delay": 0,
+ # Flatland-specific fields
+ "train_b": "Train_" + str(int(conflict.train_b)),
+ "cell": [int(x) for x in conflict.cell],
+ "conflict_id": conflict_id,
+ "message": (
+ "Conflict between Train " +
+ str(int(conflict.train_a)) + " and Train " +
+ str(int(conflict.train_b))
+ ),
+ }
+ }
+ token = _get_auth_token()
+ headers = {"Content-Type": "application/json"}
+ if token:
+ headers["Authorization"] = "Bearer " + token
+ try:
+ requests.post(EVENT_SERVICE_URL, json=payload, headers=headers, timeout=5)
+ except Exception as e:
+ print("[push_event] Failed:", e)
+
+
+@app.route("/health")
+def health():
+ return jsonify({"status": "ok"})
+
+
+@app.route("/state")
+def get_state():
+ global scenario_player
+ if scenario_player is not None:
+ env = scenario_player.env
+ step = scenario_player.step
+ else:
+ with state_lock:
+ env = sim["env"]
+ step = sim["step"]
+ if env is None:
+ return jsonify({"error": "simulation not initialised"}), 503
+
+ trains = []
+ for i, agent in enumerate(env.agents):
+ try:
+ state_str = agent.state.name if hasattr(agent.state, "name") else str(int(agent.state))
+ except Exception:
+ state_str = "UNKNOWN"
+ pos = [int(x) for x in agent.position] if agent.position is not None else None
+ trains.append({
+ "id": i,
+ "position": pos,
+ "direction": int(agent.direction) if agent.direction is not None else 0,
+ "state": state_str,
+ })
+ return jsonify({"step": step, "trains": trains})
+
+
+@app.route("/control", methods=["POST"])
+def control():
+ data = request.get_json(force=True)
+ command = data.get("command")
+
+ if command in ("start", "resume"):
+ with state_lock:
+ sim["running"] = True
+ return jsonify({"status": "running"})
+ elif command == "pause":
+ with state_lock:
+ sim["running"] = False
+ return jsonify({"status": "paused"})
+ elif command == "reset":
+ with state_lock:
+ sim["running"] = False
+ _init_simulation()
+ return jsonify({"status": "reset"})
+ elif command == "speed":
+ value = float(data.get("value", 1.0))
+ with state_lock:
+ sim["speed"] = max(0.1, value)
+ # Also update scenario player speed if active
+ if scenario_player is not None:
+ with scenario_player.lock:
+ scenario_player.speed = max(0.1, value)
+ return jsonify({"status": "ok", "speed": sim["speed"]})
+
+ return jsonify({"error": "unknown command"}), 400
+
+
+@app.route("/conflicts")
+def get_conflicts():
+ # ── Presentation mode: always return the static demo event ────────────
+ # The real conflict detection still runs in the background (see
+ # _check_for_conflict) and will override this if a real conflict is
+ # detected. For the demo, comment out the two lines below to re-enable
+ # real conflict detection.
+ return jsonify({
+ "conflict": PRESENTATION_EVENT,
+ "options": PRESENTATION_OPTIONS,
+ })
+
+ # ── Real conflict detection (kept for future use) ─────────────────────
+ with state_lock:
+ conflict = sim["active_conflict"]
+ options = sim["options"]
+
+ if conflict is None:
+ return jsonify({"conflict": None, "options": []})
+
+ options_data = []
+ for i, opt in enumerate(options):
+ options_data.append({
+ "index": i,
+ "train_to_delay": int(opt.train_to_delay),
+ "resolution_type": str(opt.resolution_type.value),
+ "delay_added": int(opt.delay_added),
+ "description": (
+ "Delay Train " + str(int(opt.train_to_delay)) +
+ " by " + str(int(opt.delay_added)) + " steps" +
+ " (" + str(opt.resolution_type.value) + ")"
+ ),
+ })
+
+ return jsonify({
+ "conflict": {
+ "train_a": int(conflict.train_a),
+ "train_b": int(conflict.train_b),
+ "cell": [int(x) for x in conflict.cell],
+ "timestep": int(conflict.timestep),
+ },
+ "options": options_data,
+ })
+
+
+@app.route("/resolve", methods=["POST"])
+def apply_resolution():
+ data = request.get_json(force=True)
+ option_index = int(data.get("option_index", 0))
+
+ with state_lock:
+ conflict = sim["active_conflict"]
+ options = sim["options"]
+ manager = sim["manager"]
+
+ if conflict is None:
+ return jsonify({"error": "no active conflict"}), 400
+ if option_index < 0 or option_index >= len(options):
+ return jsonify({"error": "invalid option index"}), 400
+
+ chosen = options[option_index]
+ try:
+ manager.apply_resolution_option(chosen)
+ with state_lock:
+ sim["dispatcher"].timetable = manager.timetable
+ sim["dispatcher"]._init_routes(preserve_active=True)
+ sim["active_conflict"] = None
+ sim["options"] = []
+ sim["conflict_pushed"] = False
+ return jsonify({"status": "applied", "option_index": option_index})
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return jsonify({"error": str(e)}), 500
+
+
+@app.route("/render")
+def render_map():
+ global scenario_player
+
+ # Use scenario player env if active, otherwise main sim
+ if scenario_player is not None:
+ env = scenario_player.env
+ renderer = None # create fresh renderer for scenario env
+ else:
+ with state_lock:
+ env = sim["env"]
+ renderer = sim["renderer"]
+ if env is None or renderer is None:
+ return jsonify({"error": "simulation not initialised"}), 503
+
+ try:
+ if renderer is None:
+ # Use scenario player's cached renderer
+ renderer = scenario_player.renderer
+
+ image = renderer.render_env(
+ show=False,
+ show_observations=False,
+ show_inactive_agents=True,
+ show_rowcols=False,
+ return_image=True,
+ )
+ if image is None:
+ return jsonify({"error": "render returned None"}), 500
+
+ pil_image = Image.fromarray(image.astype("uint8"))
+
+ # Draw train IDs and event markers on the image
+ from PIL import ImageDraw
+ draw = ImageDraw.Draw(pil_image)
+ img_w, img_h = pil_image.size
+ grid_h, grid_w = env.rail.grid.shape
+ cell_w = img_w / grid_w
+ cell_h = img_h / grid_h
+
+ # Get affected trains (those with active event holds)
+ affected = set()
+ if scenario_player is not None:
+ affected = scenario_player.get_affected_trains()
+
+ for agent in env.agents:
+ if agent.position is not None:
+ row, col = agent.position
+ x = int(col * cell_w)
+ y = int(row * cell_h)
+ # Red rectangle for affected trains
+ if "Train_" + str(agent.handle) in affected:
+ pad = 4
+ draw.rectangle(
+ [x - pad, y - pad, x + int(cell_w) + pad, y + int(cell_h) + pad],
+ outline=(255, 0, 0), width=3
+ )
+ # Train ID label
+ # Yellow rectangle for user-selected train (CoLearning mode)
+ if selected_train and selected_train == "Train_" + str(agent.handle):
+ pad2 = 6
+ draw.rectangle(
+ [x - pad2, y - pad2, x + int(cell_w) + pad2, y + int(cell_h) + pad2],
+ outline=(255, 200, 0), width=3
+ )
+ label_color = (255, 80, 80) if "Train_" + str(agent.handle) in affected else (255, 255, 0)
+ draw.text((x + 2, y + 2), "T" + str(agent.handle), fill=label_color)
+
+ buf = io.BytesIO()
+ pil_image.save(buf, format="PNG")
+ buf.seek(0)
+ return send_file(buf, mimetype="image/png")
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return jsonify({"error": str(e)}), 500
+
+
+def _push_presentation_event():
+ """Push the static demo event card to event-service at startup."""
+ import time
+ time.sleep(5) # Wait for services to be ready
+ payload = {
+ "use_case": "Railway",
+ "title": "Heavy snowfall on route City_1 to City_0",
+ "description": "Severe weather conditions affecting train services on this corridor.",
+ "criticality": "HIGH",
+ "start_date": datetime.now(timezone.utc).isoformat(),
+ "data": {
+ "event_type": "INFRASTRUCTURE",
+ "id_train": "Train_0",
+ "agent_id": "0",
+ "delay": 0,
+ "train_b": "Train_1",
+ "cell": [15, 12],
+ "conflict_id": "demo_event_1",
+ "message": "Heavy snowfall on route City_1 to City_0",
+ }
+ }
+ token = _get_auth_token()
+ headers = {"Content-Type": "application/json"}
+ if token:
+ headers["Authorization"] = "Bearer " + token
+ try:
+ response = requests.post(EVENT_SERVICE_URL, json=payload, headers=headers, timeout=5)
+ print("[startup] Presentation event pushed, status:", response.status_code)
+ if response.status_code not in (200, 201):
+ print("[startup] Response:", response.text)
+ except Exception as e:
+ print("[startup] Failed to push presentation event:", e)
+
+
+@app.route("/recommendations", methods=["GET", "POST"])
+def get_recommendations():
+ """
+ External agent API endpoint called by recommendation-service.
+ Returns resolution options in InteractiveAI's expected format.
+ Called when operator clicks "Get recommendation" on an event card.
+ """
+ return jsonify([
+ {
+ "title": "Reroute Train 0 via City_2",
+ "description": "Reroute Train 0 via City_2 to avoid the affected corridor. Estimated additional delay: 8 minutes.",
+ "use_case": "Railway",
+ "agent_type": "AI",
+ "actions": [{"option_index": 0}],
+ "kpis": {
+ "delay": "8 min",
+ "nb_impacted_trains": "1",
+ "best": "True",
+ }
+ },
+ {
+ "title": "Reroute Train 1 via City_0 bypass",
+ "description": "Reroute Train 1 via the City_0 bypass line. Estimated additional delay: 6 minutes.",
+ "use_case": "Railway",
+ "agent_type": "AI",
+ "actions": [{"option_index": 1}],
+ "kpis": {
+ "delay": "6 min",
+ "nb_impacted_trains": "1",
+ "best": "False",
+ }
+ },
+ {
+ "title": "Hold Train 0 at City_1 station",
+ "description": "Hold Train 0 at City_1 station until track is cleared. Estimated wait: 12 minutes.",
+ "use_case": "Railway",
+ "agent_type": "AI",
+ "actions": [{"option_index": 2}],
+ "kpis": {
+ "delay": "12 min",
+ "nb_impacted_trains": "1",
+ "best": "False",
+ }
+ },
+ ])
+
+
+
+@app.route("/scenario/select", methods=["POST"])
+def select_scenario():
+ """Store selected scenario for map preview — called when user picks from dropdown."""
+ global preview_scenario_id
+ data = request.get_json(silent=True) or {}
+ preview_scenario_id = data.get("scenario_id", "")
+ return jsonify({"status": "ok", "scenario_id": preview_scenario_id})
+
+
+@app.route("/transitions")
+def get_transitions():
+ """Rail grid transition table for ZWL frontend."""
+ import json as _json
+ # Use scenario env when active (may have different grid size)
+ if scenario_player is not None:
+ grid = scenario_player.env.rail.grid.tolist()
+ return jsonify(grid)
+ # Use preview scenario map when one is selected but no session running
+ if preview_scenario_id:
+ sc = ALL_SCENARIOS.get(preview_scenario_id)
+ if sc:
+ map_path = sc.get("map", "")
+ if map_path.endswith(".json"):
+ try:
+ with open(map_path, "r", encoding="utf-8") as f:
+ raw = _json.load(f)
+ return jsonify(raw["grid"])
+ except Exception:
+ pass
+ # Fall back to free-run simulation grid
+ with state_lock:
+ env = sim["env"]
+ if env is None:
+ return jsonify({"error": "simulation not initialised"}), 503
+ grid = env.rail.grid.tolist()
+ return jsonify(grid)
+
+
+def _extract_target(agent):
+ """Extract (row, col) target regardless of Flatland version."""
+ for attr in ("target", "targets"):
+ t = getattr(agent, attr, None)
+ if t is None:
+ continue
+ # Simple (row, col) tuple/list
+ if isinstance(t, (list, tuple)) and len(t) == 2 and isinstance(t[0], int):
+ return [int(t[0]), int(t[1])]
+ # Set of ((row,col), direction) — Flatland 4.x pkl format
+ if isinstance(t, (set, frozenset)):
+ for item in t:
+ if isinstance(item, (list, tuple)) and len(item) == 2:
+ pos = item[0]
+ if isinstance(pos, (list, tuple)) and len(pos) == 2:
+ return [int(pos[0]), int(pos[1])]
+ return None
+
+
+def _compute_marey_mapping(map_path, start_rc, end_rc):
+ """BFS through grid from start to end, returns {r,c: distance} mapping."""
+ import json as _json
+ from collections import deque
+ try:
+ with open(map_path, "r") as f:
+ raw = _json.load(f)
+ grid = raw["grid"]
+ except Exception:
+ return {}
+ rows, cols = len(grid), len(grid[0])
+ dist = {start_rc: 0}
+ queue = deque([start_rc])
+ directions = [(-1,0),(0,1),(1,0),(0,-1)]
+ while queue:
+ r, c = queue.popleft()
+ for dr, dc in directions:
+ nr, nc = r+dr, c+dc
+ if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != 0 and (nr, nc) not in dist:
+ dist[(nr, nc)] = dist[(r,c)] + 1
+ queue.append((nr, nc))
+ return {f"{r},{c}": d for (r,c), d in dist.items()}
+
+
+@app.route("/mapping")
+def get_mapping():
+ """Return linearized position mapping for Marey diagram."""
+ sc = None
+ if scenario_player is not None:
+ sc = scenario_player.scenario
+ elif preview_scenario_id:
+ sc = ALL_SCENARIOS.get(preview_scenario_id)
+ if sc is None:
+ return jsonify({})
+ link = sc.get("marey_link")
+ if not link:
+ return jsonify({})
+ map_path = sc.get("map", "")
+ if not map_path.endswith(".json"):
+ return jsonify({})
+ start = tuple(link["start"])
+ end = tuple(link["end"])
+ mapping = _compute_marey_mapping(map_path, start, end)
+ return jsonify(mapping)
+
+
+@app.route("/stations")
+def get_stations():
+ """Return station positions for the current or preview scenario map."""
+ # Determine which scenario to use
+ sc = None
+ if scenario_player is not None:
+ sc = scenario_player.scenario
+ elif preview_scenario_id:
+ sc = ALL_SCENARIOS.get(preview_scenario_id)
+
+ if sc is None:
+ return jsonify([])
+
+ stations = []
+ map_path = sc.get("map", "")
+ if map_path.endswith(".json"):
+ try:
+ with open(map_path, "r", encoding="utf-8") as f:
+ raw = json.load(f)
+ for s in raw.get("stations", []):
+ stations.append({
+ "id": s["id"],
+ "r": s["r"],
+ "c": s["c"],
+ "name": f"Station {s['id']}",
+ })
+ except Exception:
+ pass
+
+ # Agent start positions as station markers
+ for i, adef in enumerate(sc.get("agent_defs", [])):
+ r, c = adef["start"]
+ stations.append({
+ "id": f"start_{i}",
+ "r": r,
+ "c": c,
+ "name": adef.get("name", f"Train_{i}"),
+ "type": "start",
+ })
+ return jsonify(stations)
+
+
+@app.route("/agents")
+def get_agents():
+ """Current agent states for ZWL frontend."""
+ active_env = scenario_player.env if scenario_player is not None else None
+ if active_env is None:
+ with state_lock:
+ active_env = sim["env"]
+ if active_env is None:
+ return jsonify({"error": "simulation not initialised"}), 503
+
+ # Build train name map from scenario agent_defs
+ train_names = {}
+ if scenario_player is not None:
+ for i, adef in enumerate(scenario_player.scenario.get("agent_defs", [])):
+ train_names[i] = adef.get("name", f"Train_{i}")
+
+ agents = []
+ for agent in active_env.agents:
+ agents.append({
+ "position": [int(c) for c in agent.position] if agent.position is not None else None,
+ "direction": int(agent.direction) if agent.direction is not None else 0,
+ "moving": bool(agent.moving) if hasattr(agent, "moving") else False,
+ "target": _extract_target(agent),
+ "malfunction": 0,
+ "name": train_names.get(agent.handle, f"Train_{agent.handle}"),
+ })
+ return jsonify(agents)
+
+
+@app.route("/history")
+def get_history():
+ """Full simulation history for ZWL Marey diagram."""
+ global scenario_player
+ if scenario_player is not None:
+ return jsonify(scenario_player.get_history_steps())
+ with state_lock:
+ history = list(sim["history"])
+ return jsonify(history)
+
+
+@app.route("/plans")
+def get_plans():
+ """Plans endpoint — returns current history as single plan."""
+ with state_lock:
+ history = list(sim["history"])
+ return jsonify([history])
+
+
+def _push_resolved_event_card(scenario: dict):
+ """Push an ND (resolved) notification after a decision is applied."""
+ import time as _time
+ start_ms = int(_time.time() * 1000)
+ payload = {
+ "publisher": "publisher_test",
+ "processVersion": "1",
+ "process": "cabProcess",
+ "processInstanceId": "scenario_resolved_" + scenario.get("id", "unknown"),
+ "state": "messageState",
+ "groupRecipients": ["Dispatcher", "Planner", "ReadOnly"],
+ "entityRecipients": ["Railway"],
+ "severity": "INFORMATIONAL",
+ "startDate": start_ms,
+ "summary": {
+ "key": "cabProcess.summary",
+ "parameters": {"summary": "Störung behoben — Lösung angewendet."},
+ },
+ "title": {
+ "key": "cabProcess.title",
+ "parameters": {"title": "Gelöst: " + scenario.get("name", "Ereignis")},
+ },
+ "data": {
+ "metadata": {"event_type": "INFRASTRUCTURE", "id_train": "Train_3"},
+ "criticality": "ND",
+ }
+ }
+ try:
+ r = requests.post("http://localhost:2102/cards", json=payload, timeout=5)
+ print(f"[scenario] Resolved card pushed → {r.status_code}")
+ pid = payload.get("processInstanceId", "")
+ if pid and pid not in pushed_process_instance_ids:
+ pushed_process_instance_ids.append(pid)
+ except Exception as e:
+ print(f"[scenario] Failed to push resolved card: {e}")
+
+
+def _delete_all_pushed_cards():
+ """Clear all pushed notification cards. Fail-safe — never crashes session_start."""
+ global pushed_card_ids, pushed_process_instance_ids
+ try:
+ ts_ms = int(__import__("time").time() * 1000)
+ # Only send ND for cards actually pushed this session — avoids 404s on first run
+ all_pids = set(pid for pid in pushed_process_instance_ids if pid)
+ for pid in all_pids:
+ payload = {
+ "publisher": "publisher_test", "processVersion": "1",
+ "process": "cabProcess", "processInstanceId": pid,
+ "state": "messageState",
+ "groupRecipients": ["Dispatcher", "Planner", "ReadOnly"],
+ "entityRecipients": ["Railway"], "severity": "INFORMATIONAL",
+ "startDate": ts_ms - 10000, "endDate": ts_ms - 1,
+ "expirationDate": ts_ms - 1,
+ "summary": {"key": "cabProcess.summary", "parameters": {"summary": "Gelöscht"}},
+ "title": {"key": "cabProcess.title", "parameters": {"title": "Gelöscht"}},
+ "data": {"criticality": "ND"},
+ }
+ try:
+ requests.post("http://localhost:2102/cards", json=payload, timeout=2)
+ except Exception:
+ pass
+ pushed_card_ids = []
+ pushed_process_instance_ids = []
+ except Exception as e:
+ print(f"[notify] _delete_all_pushed_cards failed (non-fatal): {e}")
+ pushed_card_ids = []
+ pushed_process_instance_ids = []
+
+def _push_scenario_event_card(event: dict):
+ """
+ Push a scenario event card directly to cards-publication (port 2102).
+ This bypasses auth — same approach as sendCard.sh which works reliably.
+ Skip if event has push_card=False.
+ """
+ if not event.get("push_card", True):
+ return # event suppresses card notification
+ import time as _time
+ start_date_ms = int(_time.time() * 1000)
+ payload = {
+ "publisher": "publisher_test",
+ "processVersion": "1",
+ "process": "cabProcess",
+ "processInstanceId": "scenario_event_" + str(event.get("timestep", 0)),
+ "state": "messageState",
+ "groupRecipients": ["Dispatcher", "Planner", "ReadOnly"],
+ "entityRecipients": ["Railway"],
+ "severity": "ALARM",
+ "startDate": start_date_ms,
+ "summary": {
+ "key": "cabProcess.summary",
+ "parameters": {"summary": event.get("card_description", "")},
+ },
+ "title": {
+ "key": "cabProcess.title",
+ "parameters": {"title": event.get("card_title", "Event on network")},
+ },
+ "data": {
+ "metadata": {
+ "event_type": "INFRASTRUCTURE",
+ "id_train": event.get("train", "Train_0"),
+ "conflict_id": "scenario_event_" + str(event.get("timestep", 0)),
+ },
+ "criticality": "HIGH",
+ }
+ }
+ try:
+ r = requests.post("http://localhost:2102/cards", json=payload, timeout=5)
+ print("[scenario] Event card pushed:", event.get("card_title"), "→", r.status_code)
+ # Track processInstanceId for clearing via ND on next session start
+ pid = payload.get("processInstanceId", "")
+ if pid and pid not in pushed_process_instance_ids:
+ pushed_process_instance_ids.append(pid)
+ except Exception as e:
+ print("[scenario] Failed to push event card:", e)
+
+
+# ── Scenario / Session endpoints ───────────────────────────────────────────────
+
+@app.route("/session/start", methods=["POST"])
+def session_start():
+ """
+ Start a new session. Returns session_id and first scenario info.
+ POST body: {} (optional: {"scenario_ids": ["test", ...]})
+ """
+ global scenario_player
+ data = request.get_json(force=True) or {}
+ scenario_ids = data.get("scenario_ids", list(ALL_SCENARIOS.keys()))
+
+ global session_mode
+ session_mode = data.get("mode", "recommendation")
+ acronym = data.get("acronym", "")
+ session_id = SessionManager.create_session(scenario_ids, acronym=acronym, mode=session_mode)
+ scenario_id = SessionManager.current_scenario_id(session_id)
+ scenario = ALL_SCENARIOS.get(scenario_id)
+
+ if scenario is None:
+ return jsonify({"error": "No scenarios available"}), 400
+
+ # Clean up old scenario cards from MongoDB so they don't replay on login
+ try:
+ old_ids = ["cabProcess.scenario_event_0", "cabProcess.scenario_event_1",
+ "cabProcess.scenario_event_15", "cabProcess.scenario_event_30"]
+ for cid in old_ids:
+ requests.delete(f"http://localhost:2102/cards/{cid}", timeout=2)
+ except Exception:
+ pass # cleanup is best-effort
+
+ # Event callback — pushes scenario events as notification cards
+ def push_scenario_event(event: dict):
+ threading.Thread(
+ target=_push_scenario_event_card,
+ args=(event,),
+ daemon=True
+ ).start()
+
+ # Start scenario player
+ # Clear previous scenario notifications
+ threading.Thread(target=_delete_all_pushed_cards, daemon=True).start()
+
+ scenario_player = ScenarioPlayer(scenario, on_event=push_scenario_event)
+ scenario_player.start()
+
+ return jsonify({
+ "session_id": session_id,
+ "scenario_id": scenario_id,
+ "scenario_name": scenario["name"],
+ "total_scenarios": len(scenario_ids),
+ "current_index": 1,
+ })
+
+
+@app.route("/session/status")
+def session_status():
+ """Current scenario player status — polled by frontend."""
+ global scenario_player, session_mode
+ if scenario_player is None:
+ return jsonify({"state": "idle", "mode": session_mode})
+ status = scenario_player.get_status()
+ status["mode"] = session_mode
+ # Add active session_id so frontend can use it for decisions
+ for sid, sess in _sessions.items():
+ status["session_id"] = sid
+ break
+ # Add conflict trains for CoLearning mode
+ if session_mode == "colearning" and status.get("active_decision"):
+ # Read outcomes directly from scenario definition (not from status options which strip outcomes)
+ scenario_dps = scenario_player.scenario.get("decision_points", [])
+ dp_index = scenario_player.decision_index - 1
+ dp = scenario_dps[dp_index] if dp_index < len(scenario_dps) else {}
+
+ # Use scenario-level colearning_config override if defined
+ cl_config = scenario_player.scenario.get("colearning_config")
+ if cl_config:
+ trains = set(cl_config.get("trains", []))
+ train_actions = {t: list(cl_config.get("actions", ["warten"])) for t in trains}
+ else:
+ trains = set()
+ train_actions = {}
+
+ # Collect ALL trains mentioned anywhere in the options
+ all_dp_trains: set = set()
+ for opt in dp.get("options", []):
+ outcome = opt.get("outcome", {})
+ if outcome.get("hold_train"):
+ all_dp_trains.add(outcome["hold_train"])
+ all_dp_trains.update(outcome.get("hold_trains", []))
+ all_dp_trains.update(outcome.get("holds", {}).keys())
+ all_dp_trains.update(outcome.get("scripted_actions", {}).keys())
+
+ if not cl_config:
+ # Pass 1: collect all "warten" trains
+ for opt in dp.get("options", []):
+ outcome = opt.get("outcome", {})
+ ht = outcome.get("hold_train")
+ hts = list(outcome.get("hold_trains", [])) + list(outcome.get("holds", {}).keys())
+ if ht:
+ trains.add(ht)
+ train_actions.setdefault(ht, [])
+ if "warten" not in train_actions[ht]:
+ train_actions[ht].append("warten")
+ for t in hts:
+ trains.add(t)
+ train_actions.setdefault(t, [])
+ if "warten" not in train_actions[t]:
+ train_actions[t].append("warten")
+ # Pass 2: add "umleiten" only for trains not already covered by warten
+ for opt in dp.get("options", []):
+ outcome = opt.get("outcome", {})
+ for t in outcome.get("scripted_actions", {}):
+ if t not in trains:
+ trains.add(t)
+ train_actions.setdefault(t, [])
+ if "umleiten" not in train_actions[t]:
+ train_actions[t].append("umleiten")
+ # Pass 3: add "vorfahrt" to trains that appear as priority in any option
+ for opt in dp.get("options", []):
+ outcome = opt.get("outcome", {})
+ held = set(outcome.get("hold_trains", []))
+ held |= set(outcome.get("holds", {}).keys())
+ if outcome.get("hold_train"):
+ held.add(outcome["hold_train"])
+ if held:
+ for t in all_dp_trains:
+ if t not in held:
+ trains.add(t)
+ train_actions.setdefault(t, [])
+ if "vorfahrt" not in train_actions[t]:
+ train_actions[t].append("vorfahrt")
+ status["conflict_trains"] = sorted(trains)
+ status["train_actions"] = train_actions
+
+ # Build kpis_by_train: for each selectable train → its matched option's KPIs
+ kpis_by_train = {}
+ for opt in dp.get("options", []):
+ outcome = opt.get("outcome", {})
+ kpis = opt.get("kpis", {})
+ held = set(outcome.get("hold_trains", []))
+ held |= set(outcome.get("holds", {}).keys())
+ if outcome.get("hold_train"):
+ held.add(outcome["hold_train"])
+ for t in trains:
+ if t not in held:
+ # This train gets priority in this option
+ kpis_by_train[t] = kpis
+ status["kpis_by_train"] = kpis_by_train
+ # Include currently selected train for ZWL map highlight
+ status["selected_train"] = selected_train
+ # Human-readable train names from scenario agent_defs
+ train_names = {}
+ for i, adef in enumerate(scenario_player.scenario.get("agent_defs", [])):
+ train_names[f"Train_{i}"] = adef.get("name", f"Train_{i}")
+ status["train_names"] = train_names
+ return jsonify(status)
+
+
+@app.route("/session/decision", methods=["POST"])
+def session_decision():
+ """
+ Apply a decision at the current decision point.
+ Body: {
+ "session_id": "...",
+ "option_index": 0
+ }
+ """
+ global scenario_player
+ data = request.get_json(force=True)
+ session_id = data.get("session_id", "")
+ option_index = int(data.get("option_index", 0))
+
+ print(f"[decision] Received decision: option_index={option_index}, session_id={session_id!r}")
+
+ if scenario_player is None:
+ print("[decision] ERROR: No active scenario player")
+ return jsonify({"error": "No active scenario"}), 400
+
+ status = scenario_player.get_status()
+ print(f"[decision] Current state: {status['state']}, step: {status['step']}")
+
+ if status["state"] != ScenarioState.PAUSED:
+ print(f"[decision] ERROR: Not paused — state is {status['state']}")
+ return jsonify({"error": "Not at a decision point"}), 400
+
+ decision = status["active_decision"]
+ if decision is None or option_index >= len(decision["options"]):
+ return jsonify({"error": "Invalid option index"}), 400
+
+ option_label = decision["options"][option_index]["label"]
+ kpis = decision["options"][option_index]["kpis"]
+
+ # Get scenario_id from the player directly (session_id may be empty if started from Timeline)
+ scenario_id = scenario_player.scenario.get("id", "unknown")
+ decision_index = scenario_player.decision_index - 1
+
+ SessionManager.log_decision(
+ session_id or "anonymous", scenario_id, decision_index, option_index, option_label
+ )
+
+ # Apply decision and resume
+ success = scenario_player.apply_decision(option_index)
+ print(f"[decision] apply_decision returned: {success}, new state: {scenario_player.state}")
+
+ # Store for experiment log
+ global last_decision
+ last_decision = {
+ "type": "recommendation",
+ "option_index": option_index,
+ "option_label": option_label,
+ "kpis": kpis,
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ }
+
+ # Push ND (resolved) notification to InteractiveAI
+ threading.Thread(
+ target=_push_resolved_event_card,
+ args=(scenario_player.scenario,),
+ daemon=True
+ ).start()
+
+ return jsonify({
+ "status": "applied",
+ "option_index": option_index,
+ "option_label": option_label,
+ "kpis": kpis,
+ })
+
+
+@app.route("/session/next", methods=["POST"])
+def session_next():
+ """
+ Advance to the next scenario after current one completes.
+ Body: {"session_id": "..."}
+ """
+ global scenario_player
+ data = request.get_json(force=True)
+ session_id = data.get("session_id", "")
+
+ has_next = SessionManager.advance_scenario(session_id)
+
+ if not has_next:
+ scenario_player = None
+ decisions = SessionManager.get_decisions(session_id)
+ return jsonify({
+ "status": "session_complete",
+ "decisions": decisions,
+ })
+
+ scenario_id = SessionManager.current_scenario_id(session_id)
+ scenario = ALL_SCENARIOS.get(scenario_id)
+
+ if scenario is None:
+ return jsonify({"error": "Scenario not found"}), 400
+
+ def push_scenario_event_next(event: dict):
+ threading.Thread(
+ target=_push_scenario_event_card,
+ args=(event,),
+ daemon=True
+ ).start()
+
+ scenario_player = ScenarioPlayer(scenario, on_event=push_scenario_event_next)
+ scenario_player.start()
+
+ return jsonify({
+ "status": "next_scenario",
+ "scenario_id": scenario_id,
+ "scenario_name": scenario["name"],
+ })
+
+
+@app.route("/session/selected_train", methods=["POST"])
+def set_selected_train():
+ """Store which train the user has selected in CoLearning mode (for map highlight)."""
+ global selected_train
+ data = request.get_json(force=True)
+ selected_train = data.get("train", "")
+ return jsonify({"status": "ok", "selected_train": selected_train})
+
+
+@app.route("/experiment/log", methods=["POST"])
+def experiment_log():
+ """
+ Save a complete experiment run as a human-readable JSON file.
+ Called by the test module after scenario + reflection are complete.
+ Body: { participant_id, mode, scenario_id, decision, reflection_answers }
+ """
+ global last_decision, session_mode, scenario_player
+ data = request.get_json(force=True)
+
+ scenario_id = data.get("scenario_id", "")
+ scenario_name = data.get("scenario_name", "")
+ if scenario_player is not None:
+ scenario_id = scenario_player.scenario.get("id", scenario_id)
+ scenario_name = scenario_player.scenario.get("name", scenario_name)
+
+ log = {
+ "experiment_id": f"exp_{data.get('participant_id','?')}_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}",
+ "participant_id": data.get("participant_id", ""),
+ "type": "test_run",
+ "started_at": data.get("started_at", datetime.now(timezone.utc).isoformat()),
+ "completed_at": datetime.now(timezone.utc).isoformat(),
+ "szenario": {
+ "id": scenario_id,
+ "name": scenario_name,
+ "modus": session_mode,
+ },
+ "entscheidung": last_decision or data.get("decision", {}),
+ "reflexion": data.get("reflection_answers", []),
+ }
+
+ filename = save_experiment_log(log)
+ return jsonify({"status": "gespeichert", "datei": filename, "log": log})
+
+
+@app.route("/experiment/logs")
+def list_experiment_logs():
+ """List all saved experiment log files."""
+ files = list_logs()
+ return jsonify({"count": len(files), "files": files})
+
+
+@app.route("/experiment/logs/")
+def get_experiment_log(filename: str):
+ """Return content of a specific experiment log file."""
+ import os
+ log_path = os.path.join("experiment_logs", filename)
+ if not os.path.exists(log_path):
+ return jsonify({"error": "Datei nicht gefunden"}), 404
+ data = read_log(filename)
+ return jsonify(data)
+
+
+@app.route("/reflection", methods=["POST"])
+def save_reflection():
+ """
+ Save reflection module answers.
+ Body: { "session_id": "...", "acronym": "...", "answers": [{question_index, question_text, answer}] }
+ """
+ data = request.get_json(force=True)
+ session_id = data.get("session_id", "")
+ acronym = data.get("acronym", "")
+ answers = data.get("answers", [])
+ SessionManager.log_reflection(session_id, acronym, answers)
+ print(f"[reflection] Logged {len(answers)} answers for {acronym or 'anonymous'}")
+ return jsonify({"status": "saved", "count": len(answers)})
+
+
+@app.route("/reflection/")
+def get_reflection(session_id: str):
+ """Return reflection answers for a session."""
+ return jsonify(SessionManager.get_reflections(session_id))
+
+
+@app.route("/session/colearning_action", methods=["POST"])
+def colearning_action():
+ """
+ Apply a user-defined CoLearning action.
+ Body: { "session_id": "...", "train": "Train_0", "action": "warten" | "umleiten" }
+ Matches the user's choice to the closest predefined option in the scenario.
+ """
+ global scenario_player, session_mode
+ data = request.get_json(force=True)
+ session_id = data.get("session_id", "")
+ train_id = data.get("train", "") # e.g. "Train_0"
+ action = data.get("action", "") # "warten" or "umleiten"
+
+ if scenario_player is None:
+ return jsonify({"error": "Kein aktives Szenario"}), 400
+
+ status = scenario_player.get_status()
+ if status["state"] != ScenarioState.PAUSED:
+ return jsonify({"error": "Kein Entscheidungspunkt aktiv"}), 400
+
+ # Get decision point directly from scenario definition (outcomes not in status)
+ scenario_dps = scenario_player.scenario.get("decision_points", [])
+ dp_index = scenario_player.decision_index - 1
+ if dp_index < 0 or dp_index >= len(scenario_dps):
+ return jsonify({"feasible": False, "message": "Kein aktiver Entscheidungspunkt."}), 200
+
+ dp = scenario_dps[dp_index]
+
+ # Block actions marked as invalid in colearning_config
+ cl_cfg = scenario_player.scenario.get("colearning_config", {})
+ if action in cl_cfg.get("invalid_actions", []):
+ return jsonify({
+ "feasible": False,
+ "message": "Diese Aktion ist für dieses Szenario nicht möglich.",
+ }), 200
+
+ matching_index = None
+ matching_option = None
+
+ for i, opt in enumerate(dp.get("options", [])):
+ outcome = opt.get("outcome", {})
+ if action == "warten" and outcome.get("hold_train") == train_id:
+ matching_index = i
+ matching_option = opt
+ break
+ elif action == "warten" and train_id in outcome.get("hold_trains", []):
+ matching_index = i
+ matching_option = opt
+ break
+ elif action == "vorfahrt" and train_id not in outcome.get("hold_trains", []) and train_id not in outcome.get("holds", {}) and train_id != outcome.get("hold_train"):
+ matching_index = i
+ matching_option = opt
+ break
+ elif action == "umleiten" and "scripted_actions" in outcome and train_id in outcome["scripted_actions"]:
+ matching_index = i
+ matching_option = opt
+ break
+
+ if matching_option is None:
+ return jsonify({
+ "feasible": False,
+ "message": f"Keine Lösung für '{action}' mit {train_id} verfügbar."
+ }), 200
+
+ # Log and apply
+ scenario_id = scenario_player.scenario.get("id", "unknown")
+ decision_index = scenario_player.decision_index - 1
+ SessionManager.log_decision(
+ session_id or "anonymous",
+ scenario_id,
+ decision_index,
+ matching_index,
+ f"[Ko-Lernen] {action}: {train_id}"
+ )
+ scenario_player.apply_decision(matching_index)
+
+ # Store for experiment log
+ global last_decision
+ last_decision = {
+ "type": "colearning",
+ "train": train_id,
+ "action": action,
+ "option_index": matching_index,
+ "option_label": matching_option["label"],
+ "kpis": matching_option["kpis"],
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ }
+
+ # Push ND (resolved) notification
+ threading.Thread(
+ target=_push_resolved_event_card,
+ args=(scenario_player.scenario,),
+ daemon=True
+ ).start()
+
+ return jsonify({
+ "feasible": True,
+ "applied": True,
+ "option_index": matching_index,
+ "option_label": matching_option["label"],
+ "kpis": matching_option["kpis"],
+ })
+
+
+@app.route("/session/decisions")
+def session_decisions():
+ """Return all logged decisions for a session (for end screen)."""
+ session_id = request.args.get("session_id", "")
+ decisions = SessionManager.get_decisions(session_id)
+ return jsonify(decisions)
+
+
+@app.route("/session/stop", methods=["POST"])
+def session_stop():
+ """Stop the active scenario player and return to free-run mode."""
+ global scenario_player
+ _delete_all_pushed_cards()
+ if scenario_player is not None:
+ scenario_player.running = False
+ scenario_player = None
+ return jsonify({"status": "stopped"})
+
+
+@app.route("/session/render")
+def session_render():
+ """
+ Render the current scenario frame as PNG.
+ Falls back to main sim render if no scenario active.
+ """
+ global scenario_player
+ if scenario_player is None:
+ return render_map() # use existing render endpoint
+
+ try:
+ from flatland.utils.rendertools import RenderTool
+ from PIL import Image
+ renderer = RenderTool(
+ scenario_player.env, gl="PILSVG",
+ screen_width=600, screen_height=600
+ )
+ image = renderer.render_env(
+ show=False,
+ show_observations=False,
+ show_inactive_agents=True,
+ show_rowcols=False,
+ return_image=True,
+ )
+ if image is None:
+ return jsonify({"error": "render failed"}), 500
+
+ pil_image = Image.fromarray(image.astype("uint8"))
+
+ # Draw train IDs
+ from PIL import ImageDraw
+ draw = ImageDraw.Draw(pil_image)
+ img_w, img_h = pil_image.size
+ grid_h, grid_w = scenario_player.env.rail.grid.shape
+ for agent in scenario_player.env.agents:
+ if agent.position is not None:
+ row, col = agent.position
+ x = int(col * img_w / grid_w)
+ y = int(row * img_h / grid_h)
+ draw.text((x + 2, y + 2), "T" + str(agent.handle), fill=(255, 255, 0))
+
+ buf = io.BytesIO()
+ pil_image.save(buf, format="PNG")
+ buf.seek(0)
+ return send_file(buf, mimetype="image/png")
+
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return jsonify({"error": str(e)}), 500
+
+
+@app.route("/session/history")
+def session_history():
+ """History of agent positions for ZWL diagram during scenario."""
+ global scenario_player
+ if scenario_player is None:
+ return get_history() # fall back to main sim history
+ # Collect history from scenario player's env steps
+ with sim["env"] and True:
+ pass
+ return jsonify([]) # ZWL will use main /history for now
+
+
+@app.route("/scenarios")
+def list_scenarios():
+ """List all available scenarios."""
+ return jsonify([
+ {"id": sid, "name": s["name"]}
+ for sid, s in ALL_SCENARIOS.items()
+ ])
+
+if __name__ == "__main__":
+ _init_simulation()
+ threading.Thread(target=_sim_loop, daemon=True).start()
+ threading.Thread(target=_snapshot_loop, daemon=True).start()
+ # _push_presentation_event disabled — scenario events replace this
+ # threading.Thread(target=_push_presentation_event, daemon=True).start()
+ app.run(host="0.0.0.0", port=5001, debug=False, threaded=True)
diff --git a/usecases_examples/Railway/experiment_logs/exp_ASFSF_2026-08-27_12-30-31.json b/usecases_examples/Railway/experiment_logs/exp_ASFSF_2026-08-27_12-30-31.json
new file mode 100644
index 00000000..9d474557
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_ASFSF_2026-08-27_12-30-31.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_ASFSF_20260827_123031",
+ "participant_id": "ASFSF",
+ "type": "test_run",
+ "started_at": "2026-08-27T12:26:37.571Z",
+ "completed_at": "2026-08-27T12:30:31.252696+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T12:29:17.978219+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "terz"
+ },
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": "erzzr"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_ASFSF_2026-08-27_12-33-14.json b/usecases_examples/Railway/experiment_logs/exp_ASFSF_2026-08-27_12-33-14.json
new file mode 100644
index 00000000..015b064a
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_ASFSF_2026-08-27_12-33-14.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_ASFSF_20260827_123314",
+ "participant_id": "ASFSF",
+ "type": "test_run",
+ "started_at": "2026-08-27T12:30:39.580Z",
+ "completed_at": "2026-08-27T12:33:14.930087+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 1,
+ "option_label": "G 501 Vorfahrt — P 205 und P 312 warten",
+ "kpis": {
+ "local_delay": 29,
+ "global_delay": 20,
+ "energy": 72,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T12:31:47.658769+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "ewtzet"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "wetwet"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_ASF_2026-09-01_09-14-24.json b/usecases_examples/Railway/experiment_logs/exp_ASF_2026-09-01_09-14-24.json
new file mode 100644
index 00000000..96dc1f74
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_ASF_2026-09-01_09-14-24.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_ASF_20260901_091424",
+ "participant_id": "ASF",
+ "type": "test_run",
+ "started_at": "2026-09-01T08:38:57.850Z",
+ "completed_at": "2026-09-01T09:14:24.877549+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-09-01T09:13:14.062044+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": "dsf"
+ },
+ {
+ "frage": "Gab es ein Bauchgefühl, das ich ignoriert habe?",
+ "antwort": "sdf"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_DDFGDG_2026-08-27_07-24-57.json b/usecases_examples/Railway/experiment_logs/exp_DDFGDG_2026-08-27_07-24-57.json
new file mode 100644
index 00000000..9b3e1426
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_DDFGDG_2026-08-27_07-24-57.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_DDFGDG_20260827_072457",
+ "participant_id": "DDFGDG",
+ "type": "test_run",
+ "started_at": "2026-08-27T07:22:31.338Z",
+ "completed_at": "2026-08-27T07:24:57.609920+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_2",
+ "action": "warten",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T07:23:25.531304+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": ""
+ },
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_FG_2026-08-27_08-48-40.json b/usecases_examples/Railway/experiment_logs/exp_FG_2026-08-27_08-48-40.json
new file mode 100644
index 00000000..719ded7e
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_FG_2026-08-27_08-48-40.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_FG_20260827_084840",
+ "participant_id": "FG",
+ "type": "test_run",
+ "started_at": "2026-08-27T08:46:38.596Z",
+ "completed_at": "2026-08-27T08:48:40.376977+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 1,
+ "option_label": "S 420 und S 425 warten — IC 301 passiert zuerst",
+ "kpis": {
+ "local_delay": 32,
+ "global_delay": 18,
+ "energy": 69,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T08:47:32.798474+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "hzfdh"
+ },
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "fhfhf"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_FG_2026-08-27_08-51-08.json b/usecases_examples/Railway/experiment_logs/exp_FG_2026-08-27_08-51-08.json
new file mode 100644
index 00000000..516fe0e1
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_FG_2026-08-27_08-51-08.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_FG_20260827_085108",
+ "participant_id": "FG",
+ "type": "test_run",
+ "started_at": "2026-08-27T08:48:49.517Z",
+ "completed_at": "2026-08-27T08:51:08.144274+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 2,
+ "option_label": "P 312 Vorfahrt — P 205 und G 501 warten",
+ "kpis": {
+ "local_delay": 28,
+ "global_delay": 23,
+ "energy": 70,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T08:49:50.994149+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "g"
+ },
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "g"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-11-54.json b/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-11-54.json
new file mode 100644
index 00000000..8622803e
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-11-54.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_GDFG_20260827_121154",
+ "participant_id": "GDFG",
+ "type": "test_run",
+ "started_at": "2026-08-27T12:09:56.370Z",
+ "completed_at": "2026-08-27T12:11:54.091835+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T12:10:48.193486+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "tert"
+ },
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "rezerz"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-14-08.json b/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-14-08.json
new file mode 100644
index 00000000..40961756
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-14-08.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_GDFG_20260827_121408",
+ "participant_id": "GDFG",
+ "type": "test_run",
+ "started_at": "2026-08-27T12:12:03.108Z",
+ "completed_at": "2026-08-27T12:14:08.791716+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T12:13:02.528715+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": "kjmr"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "gerg"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-17-51.json b/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-17-51.json
new file mode 100644
index 00000000..13a4ebf1
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_GDFG_2026-08-27_12-17-51.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_GDFG_20260827_121751",
+ "participant_id": "GDFG",
+ "type": "test_run",
+ "started_at": "2026-08-27T12:14:18.398Z",
+ "completed_at": "2026-08-27T12:17:51.929532+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_1",
+ "action": "vorfahrt",
+ "option_index": 1,
+ "option_label": "G 501 Vorfahrt — P 205 und P 312 warten",
+ "kpis": {
+ "local_delay": 29,
+ "global_delay": 20,
+ "energy": 72,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T12:15:45.196093+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": ""
+ },
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_06-37-19.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_06-37-19.json
new file mode 100644
index 00000000..a63deb31
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_06-37-19.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_JANICK_20260827_063719",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T06:34:23.128Z",
+ "completed_at": "2026-08-27T06:37:19.593373+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T06:35:48.284826+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Gab es ein Bauchgefühl, das ich ignoriert habe?",
+ "antwort": "x"
+ },
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "x"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_06-42-51.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_06-42-51.json
new file mode 100644
index 00000000..687f5d98
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_06-42-51.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_064251",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T06:38:57.675Z",
+ "completed_at": "2026-08-27T06:42:51.908385+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_0",
+ "action": "warten",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T06:40:16.714959+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "r"
+ },
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "r"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_07-35-38.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_07-35-38.json
new file mode 100644
index 00000000..a051213c
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_07-35-38.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_073538",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T07:33:16.023Z",
+ "completed_at": "2026-08-27T07:35:38.992713+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_0",
+ "action": "warten",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T07:34:22.643112+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "hello"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "hi"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_07-38-05.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_07-38-05.json
new file mode 100644
index 00000000..b73f36db
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_07-38-05.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_073805",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T07:35:45.950Z",
+ "completed_at": "2026-08-27T07:38:05.809815+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_2",
+ "action": "vorfahrt",
+ "option_index": 2,
+ "option_label": "P 312 Vorfahrt — P 205 und G 501 warten",
+ "kpis": {
+ "local_delay": 28,
+ "global_delay": 23,
+ "energy": 70,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T07:36:47.349714+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "sf"
+ },
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "ffff"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_08-55-13.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_08-55-13.json
new file mode 100644
index 00000000..a482e073
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_08-55-13.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_085513",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T08:51:56.983Z",
+ "completed_at": "2026-08-27T08:55:13.143546+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_1",
+ "action": "vorfahrt",
+ "option_index": 1,
+ "option_label": "G 501 Vorfahrt — P 205 und P 312 warten",
+ "kpis": {
+ "local_delay": 29,
+ "global_delay": 20,
+ "energy": 72,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T08:53:39.728963+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "kljsfsdgf"
+ },
+ {
+ "frage": "Gab es ein Bauchgefühl, das ich ignoriert habe?",
+ "antwort": "dgsdg"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-44-46.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-44-46.json
new file mode 100644
index 00000000..c0136670
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-44-46.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_094446",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T09:06:38.765Z",
+ "completed_at": "2026-08-27T09:44:46.324394+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_0",
+ "action": "vorfahrt",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T09:07:38.341249+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "hhf"
+ },
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "hfhfh"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-47-11.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-47-11.json
new file mode 100644
index 00000000..ce5791be
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-47-11.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_JANICK_20260827_094711",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T09:44:55.767Z",
+ "completed_at": "2026-08-27T09:47:11.313665+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 2,
+ "option_label": "P 312 Vorfahrt — P 205 und G 501 warten",
+ "kpis": {
+ "local_delay": 28,
+ "global_delay": 23,
+ "energy": 70,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T09:45:54.896299+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "fhfdh"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "fhfh"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-50-13.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-50-13.json
new file mode 100644
index 00000000..b82ae341
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_09-50-13.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_095013",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T09:47:17.643Z",
+ "completed_at": "2026-08-27T09:50:13.788569+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_2",
+ "action": "warten",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T09:48:14.008713+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": "etet"
+ },
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "etet"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-08-30.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-08-30.json
new file mode 100644
index 00000000..5305c38d
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-08-30.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_JANICK_20260827_130830",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T13:06:13.067Z",
+ "completed_at": "2026-08-27T13:08:30.741000+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 1,
+ "option_label": "S 420 und S 425 warten — IC 301 passiert zuerst",
+ "kpis": {
+ "local_delay": 32,
+ "global_delay": 18,
+ "energy": 69,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T13:07:21.585564+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "gfg"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "gsddrfg"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-11-18.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-11-18.json
new file mode 100644
index 00000000..6ee9274d
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-11-18.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_131118",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T13:08:56.517Z",
+ "completed_at": "2026-08-27T13:11:18.839166+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_0",
+ "action": "warten",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T13:10:03.152874+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "zrz"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "rzrz"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-13-42.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-13-42.json
new file mode 100644
index 00000000..3ac97c7b
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-13-42.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_JANICK_20260827_131342",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T13:11:40.201Z",
+ "completed_at": "2026-08-27T13:13:42.314003+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T13:12:38.185610+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": "twe4rt"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "rtert"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-16-19.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-16-19.json
new file mode 100644
index 00000000..44a83017
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-08-27_13-16-19.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_JANICK_20260827_131619",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-08-27T13:13:49.337Z",
+ "completed_at": "2026-08-27T13:16:19.956274+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_2",
+ "action": "vorfahrt",
+ "option_index": 2,
+ "option_label": "P 312 Vorfahrt — P 205 und G 501 warten",
+ "kpis": {
+ "local_delay": 28,
+ "global_delay": 23,
+ "energy": 70,
+ "anschluss": 3
+ },
+ "timestamp": "2026-08-27T13:15:03.144276+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": "tert"
+ },
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "etrert"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-09-01_07-00-09.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-09-01_07-00-09.json
new file mode 100644
index 00000000..f2da384a
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-09-01_07-00-09.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_JANICK_20260901_070009",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-09-01T06:57:40.555Z",
+ "completed_at": "2026-09-01T07:00:09.168963+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 2,
+ "option_label": "P 312 Vorfahrt — P 205 und G 501 warten",
+ "kpis": {
+ "local_delay": 28,
+ "global_delay": 23,
+ "energy": 70,
+ "anschluss": 3
+ },
+ "timestamp": "2026-09-01T06:58:42.172807+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "nsd"
+ },
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "csgfg"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-09-04_07-13-18.json b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-09-04_07-13-18.json
new file mode 100644
index 00000000..ebc19cc0
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_JANICK_2026-09-04_07-13-18.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_JANICK_20260904_071318",
+ "participant_id": "JANICK",
+ "type": "test_run",
+ "started_at": "2026-09-04T07:09:38.248Z",
+ "completed_at": "2026-09-04T07:13:18.287477+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 1,
+ "option_label": "S 420 und S 425 warten — IC 301 passiert zuerst",
+ "kpis": {
+ "local_delay": 32,
+ "global_delay": 18,
+ "energy": 69,
+ "anschluss": 3
+ },
+ "timestamp": "2026-09-04T07:12:01.932849+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "jhdsf"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "fefdf"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_KK_2026-08-23_08-58-43.json b/usecases_examples/Railway/experiment_logs/exp_KK_2026-08-23_08-58-43.json
new file mode 100644
index 00000000..e717cdc4
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_KK_2026-08-23_08-58-43.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_KK_20260823_085843",
+ "participant_id": "KK",
+ "type": "test_run",
+ "started_at": "2026-08-23T08:57:18.797Z",
+ "completed_at": "2026-08-23T08:58:43.387063+00:00",
+ "szenario": {
+ "id": "test",
+ "name": "Technical Failure — Train 3",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_3",
+ "action": "warten",
+ "option_index": 1,
+ "option_label": "Hold Train 3 for 15 more steps — Train 0 passes first",
+ "kpis": {
+ "local_delay": 40,
+ "global_delay": 75,
+ "energy": 90,
+ "anschluss": 60
+ },
+ "timestamp": "2026-08-23T08:57:46.593446+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "hhkj"
+ },
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": "bjmb"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_SS_2026-08-27_07-18-35.json b/usecases_examples/Railway/experiment_logs/exp_SS_2026-08-27_07-18-35.json
new file mode 100644
index 00000000..80c61c61
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_SS_2026-08-27_07-18-35.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_SS_20260827_071835",
+ "participant_id": "SS",
+ "type": "test_run",
+ "started_at": "2026-08-27T07:16:01.771Z",
+ "completed_at": "2026-08-27T07:18:35.937743+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_0",
+ "action": "warten",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T07:17:00.206381+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": ""
+ },
+ {
+ "frage": "Gab es ein Bauchgefühl, das ich ignoriert habe?",
+ "antwort": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_SS_2026-08-27_07-18-48.json b/usecases_examples/Railway/experiment_logs/exp_SS_2026-08-27_07-18-48.json
new file mode 100644
index 00000000..9ed337d4
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_SS_2026-08-27_07-18-48.json
@@ -0,0 +1,36 @@
+{
+ "experiment_id": "exp_SS_20260827_071848",
+ "participant_id": "SS",
+ "type": "test_run",
+ "started_at": "2026-08-27T07:16:01.771Z",
+ "completed_at": "2026-08-27T07:18:48.381618+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "colearning"
+ },
+ "entscheidung": {
+ "type": "colearning",
+ "train": "Train_0",
+ "action": "warten",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T07:17:00.206381+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": ""
+ },
+ {
+ "frage": "Was ist die wichtigste Erkenntnis für das nächste Mal?",
+ "antwort": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_U216993_2026-08-27_14-04-02.json b/usecases_examples/Railway/experiment_logs/exp_U216993_2026-08-27_14-04-02.json
new file mode 100644
index 00000000..027a9ffe
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_U216993_2026-08-27_14-04-02.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_U216993_20260827_140402",
+ "participant_id": "U216993",
+ "type": "test_run",
+ "started_at": "2026-08-27T13:55:38.902Z",
+ "completed_at": "2026-08-27T14:04:02.792140+00:00",
+ "szenario": {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18,
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T13:59:59.152635+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Welche Faktoren habe ich für meine Entscheidung berücksichtigt?",
+ "antwort": "abc"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "xyz"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_logs/exp_URU_2026-08-27_12-39-15.json b/usecases_examples/Railway/experiment_logs/exp_URU_2026-08-27_12-39-15.json
new file mode 100644
index 00000000..2d68010b
--- /dev/null
+++ b/usecases_examples/Railway/experiment_logs/exp_URU_2026-08-27_12-39-15.json
@@ -0,0 +1,34 @@
+{
+ "experiment_id": "exp_URU_20260827_123915",
+ "participant_id": "URU",
+ "type": "test_run",
+ "started_at": "2026-08-27T12:36:26.073Z",
+ "completed_at": "2026-08-27T12:39:15.960370+00:00",
+ "szenario": {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "modus": "recommendation"
+ },
+ "entscheidung": {
+ "type": "recommendation",
+ "option_index": 0,
+ "option_label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20,
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2
+ },
+ "timestamp": "2026-08-27T12:38:15.190757+00:00"
+ },
+ "reflexion": [
+ {
+ "frage": "Auf welche Logik habe ich mich gestützt?",
+ "antwort": "etzewrt"
+ },
+ {
+ "frage": "Welche Informationen fehlten, die mir geholfen hätten?",
+ "antwort": "wtwet"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/experiment_scenarios/__init__.py b/usecases_examples/Railway/experiment_scenarios/__init__.py
new file mode 100644
index 00000000..47eda067
--- /dev/null
+++ b/usecases_examples/Railway/experiment_scenarios/__init__.py
@@ -0,0 +1,15 @@
+"""
+experiment_scenarios/__init__.py — All scripted scenarios.
+"""
+
+from experiment_scenarios.test_scenario import TEST_SCENARIO
+from experiment_scenarios.scenario1 import SCENARIO_1
+from experiment_scenarios.scenario2 import SCENARIO_2
+from experiment_scenarios.scenario3 import SCENARIO_3
+
+ALL_SCENARIOS = {
+ "test": TEST_SCENARIO,
+ "scenario1": SCENARIO_1,
+ "scenario2": SCENARIO_2,
+ "scenario3": SCENARIO_3,
+}
diff --git a/usecases_examples/Railway/experiment_scenarios/scenario1.py b/usecases_examples/Railway/experiment_scenarios/scenario1.py
new file mode 100644
index 00000000..116b517c
--- /dev/null
+++ b/usecases_examples/Railway/experiment_scenarios/scenario1.py
@@ -0,0 +1,116 @@
+"""
+scenario1.py — Szenario 1: Kreuzungskonflikt Einspurabschnitt
+
+Ablauf:
+ Schritte 1-5: Normaler Betrieb. IC 301 Richtung Süden, S 420 + S 425 Richtung Norden.
+ Schritt 5: Türstörung IC 301 → Zug bleibt stehen (12 Schritte).
+ Schritt 20: Warnung: Kreuzungskonflikt im Einspurabschnitt.
+ Schritt 22: Entscheidungspunkt: Wer kreuzt zuerst?
+
+Karte: maps/drawn_environment_export.json (15×20, Zweibahnhof-Korridor)
+Züge:
+ IC 301 (Agent 0): Stadtbahnhof (oben) → Südbahnhof (unten)
+ S 420 (Agent 1): Südbahnhof → Stadtbahnhof, Abfahrt +7 Schritte später
+ S 425 (Agent 2): Südbahnhof → Stadtbahnhof, Abfahrt +10 Schritte später
+"""
+
+SCENARIO_1 = {
+ "id": "scenario1",
+ "name": "Szenario 1 — Kreuzungskonflikt",
+ "map": "maps/drawn_environment_export.json",
+
+ # direction=3: Einstieg von Westen (Flatland 4.x Konvention für Ostfahrt)
+ "agent_defs": [
+ dict(start=(0, 5), target=(14, 8), dir=3, dep=1, arr=90, name="IC 301"),
+ dict(start=(14, 5), target=(0, 8), dir=3, dep=10, arr=90, name="S 420"),
+ dict(start=(13, 5), target=(0, 8), dir=3, dep=13, arr=95, name="S 425"),
+ ],
+
+ # Co-learning: only IC 301 (Train_0) and S 420 (Train_1) selectable
+ # S 425 follows automatically — dispatcher doesn't need to select it
+ "colearning_config": {
+ "trains": ["Train_0", "Train_1"],
+ "actions": ["warten", "umleiten"],
+ "invalid_actions": ["umleiten"],
+ },
+
+ # Marey link: bottom station (S trains start) → top station (S trains target)
+ "marey_link": {"start": [14, 5], "end": [0, 8]},
+
+ "events": [
+ {
+ "timestep": 15,
+ "type": "train_delay",
+ "train": "Train_0",
+ "duration": 8,
+ "card_title": "Türstörung — IC 301",
+ "card_description": (
+ "An IC 301 wurde eine Türstörung gemeldet. "
+ "Der Zug muss an der aktuellen Position anhalten. "
+ "Geschätzte Verzögerung: 8 Zeitschritte."
+ ),
+ },
+ {
+ "timestep": 23,
+ "type": "info",
+ "train": "Train_0",
+ "duration": 0,
+ "push_card": True,
+ "card_title": "Kreuzungskonflikt — Dispositionsentscheid erforderlich",
+ "card_description": (
+ "Infolge der Türstörung hat IC 301 Verspätung und trifft nun gleichzeitig "
+ "mit S 420 und S 425 am Einspurabschnitt ein. "
+ "Eine planmässige Kreuzung ist nicht mehr möglich. "
+ "Es ist zu entscheiden, welcher Zug den Abschnitt zuerst passiert."
+ ),
+ },
+ ],
+ "decision_points": [
+ {
+ "timestep": 23,
+ "description": (
+ "IC 301 und S 420 / S 425 nähern sich gleichzeitig dem Einspurabschnitt. "
+ "Durch die Türstörung ist die ursprünglich geplante Kreuzung nicht mehr möglich. "
+ "Bitte entscheiden, welcher Zug den Abschnitt zuerst passiert."
+ ),
+ "options": [
+ {
+ "label": "IC 301 wartet — S 420 und S 425 passieren zuerst",
+ "kpis": {
+ "local_delay": 18, # IC301 ~18 min verspätet
+ "global_delay": 22,
+ "energy": 74,
+ "anschluss": 2,
+ },
+ "outcome": {
+ "hold_train": "Train_0",
+ "hold_steps": 15, # released at step 38 (23+15)
+ # S trains take the bypass (right turn at junction)
+ "scripted_actions": {
+ "Train_1": [2]*9 + [3] + [2]*50, # right at step 32
+ "Train_2": [2]*10 + [3, 3, 3, 3] + [2]*50, # try right steps 33-36
+ },
+ }
+ },
+ {
+ "label": "S 420 und S 425 warten — IC 301 passiert zuerst",
+ "kpis": {
+ "local_delay": 32, # S420+S425 je ~16 min
+ "global_delay": 18,
+ "energy": 69,
+ "anschluss": 3,
+ },
+ "outcome": {
+ "hold_trains": ["Train_1", "Train_2"],
+ "hold_steps": 9, # released at step 32 (23+9)
+ # IC301 tries LEFT at junction steps 30-33 (indices 7-10)
+ # Flatland uses FORWARD if LEFT is not valid at that cell
+ "scripted_actions": {
+ "Train_0": [2]*7 + [1, 1, 1, 1] + [2]*50,
+ },
+ }
+ },
+ ],
+ }
+ ],
+}
diff --git a/usecases_examples/Railway/experiment_scenarios/scenario2.py b/usecases_examples/Railway/experiment_scenarios/scenario2.py
new file mode 100644
index 00000000..bfbabb74
--- /dev/null
+++ b/usecases_examples/Railway/experiment_scenarios/scenario2.py
@@ -0,0 +1,115 @@
+"""
+scenario2.py — Szenario 2: Fahrt auf Sichtweite
+
+Karte: maps/map2.json (25×25)
+
+Züge (alle Richtung Norden, dir=0):
+ P 205 (Agent 0): Station 1 (17,2) → Station 4 (6,22) dep=1
+ G 501 (Agent 1): Station 2 (15,12) → Station 5 (1,6) dep=15
+ P 312 (Agent 2): Station 3 (15,22) → Station 5 (1,6) dep=12 (+3 früher)
+
+Konfliktzone: Row 7 (P205 fährt ost, G501/P312 fahren west)
+"""
+
+SCENARIO_2 = {
+ "id": "scenario2",
+ "name": "Szenario 2 — Fahrt auf Sichtweite",
+ "map": "maps/map2.json",
+
+ "agent_defs": [
+ dict(start=(17, 2), target=(6, 22), dir=0, dep=1, arr=80, name="P 205"),
+ dict(start=(15, 12), target=(1, 6), dir=0, dep=15, arr=80, name="G 501"),
+ dict(start=(15, 22), target=(1, 6), dir=0, dep=12, arr=85, name="P 312"),
+ ],
+
+ "colearning_config": {
+ "trains": ["Train_0", "Train_1", "Train_2"],
+ "actions": ["vorfahrt", "warten"],
+ "invalid_actions": ["warten"],
+ },
+
+ # Marey link: start=Station3 (S18 start, right end), end=Station5 (left end)
+ "marey_link": {"start": [15, 22], "end": [1, 6]},
+
+ "events": [
+ {
+ "timestep": 5,
+ "type": "train_delay",
+ "train": "Train_0",
+ "duration": 8,
+ "card_title": "Streckenkontrolle — Vmax 40 km/h",
+ "card_description": (
+ "Im Bereich Rüthi wurde eine Unregelmässigkeit der Fahrbahn festgestellt. "
+ "Für P 205 gilt Vmax 40 km/h. Fachdienst wurde aufgeboten."
+ ),
+ },
+ {
+ "timestep": 22,
+ "type": "info",
+ "train": "Train_0",
+ "duration": 0,
+ "card_title": "Dispositionskonflikt — Kreuzungsreihenfolge",
+ "card_description": (
+ "P 205 ist verspätet und verursacht einen Kreuzungskonflikt "
+ "mit G 501 und P 312 auf dem Einspurabschnitt. "
+ "Bitte legen Sie die Zugreihenfolge fest."
+ ),
+ },
+ ],
+
+ "decision_points": [
+ {
+ "timestep": 22,
+ "description": (
+ "Durch die Geschwindigkeitsreduktion auf dem Streckenabschnitt "
+ "ist die ursprünglich geplante Kreuzungsreihenfolge nicht mehr möglich. "
+ "Welchem Zug soll Vorfahrt gewährt werden?"
+ ),
+ "options": [
+ {
+ "label": "P 205 Vorfahrt — G 501 und P 312 warten",
+ "kpis": {
+ "local_delay": 20, # G501+P312 je ~10 min
+ "global_delay": 14,
+ "energy": 78,
+ "anschluss": 2,
+ },
+ "outcome": {
+ "hold_trains": ["Train_1", "Train_2"],
+ "hold_steps": 10,
+ }
+ },
+ {
+ "label": "G 501 Vorfahrt — P 205 und P 312 warten",
+ "kpis": {
+ "local_delay": 29, # P205 10 min + P312 19 min
+ "global_delay": 20,
+ "energy": 72,
+ "anschluss": 3,
+ },
+ "outcome": {
+ "holds": {
+ "Train_0": 10,
+ "Train_2": 19, # 9 Schritte mehr als Train_0
+ }
+ }
+ },
+ {
+ "label": "P 312 Vorfahrt — P 205 und G 501 warten",
+ "kpis": {
+ "local_delay": 28, # P205 18 min + G501 10 min
+ "global_delay": 23,
+ "energy": 70,
+ "anschluss": 3,
+ },
+ "outcome": {
+ "holds": {
+ "Train_0": 18, # 8 Schritte mehr für Train_0
+ "Train_1": 10,
+ }
+ }
+ },
+ ],
+ }
+ ],
+}
diff --git a/usecases_examples/Railway/experiment_scenarios/scenario3.py b/usecases_examples/Railway/experiment_scenarios/scenario3.py
new file mode 100644
index 00000000..095b02eb
--- /dev/null
+++ b/usecases_examples/Railway/experiment_scenarios/scenario3.py
@@ -0,0 +1,135 @@
+"""
+scenario3.py — Szenario 3: Zugreihenfolge
+
+Karte: maps/map3.json (25x25)
+
+Züge:
+ S 17 (Agent 0): Station 3 (16,2) → Station 4 (2,17) dir=0 (Nord)
+ S 18 (Agent 1): Station 1 (6,23) → Station 3 (16,2) dir=3 (West)
+ IC 3 (Agent 2): Station 2 (15,10) → Station 4 (2,17) dir=0 (Nord)
+"""
+
+SCENARIO_3 = {
+ "id": "scenario3",
+ "name": "Szenario 3 — Zugreihenfolge",
+ "map": "maps/map3.json",
+
+ "agent_defs": [
+ dict(start=(16, 2), target=(2, 17), dir=0, dep=1, arr=60, name="S 17"),
+ dict(start=(6, 23), target=(16, 2), dir=3, dep=22, arr=77, name="S 18"),
+ dict(start=(15, 10), target=(2, 17), dir=0, dep=33, arr=68, name="IC 3"),
+ ],
+
+ # Co-learning: all 3 trains selectable, vorfahrt=priority, warten=invalid
+ "colearning_config": {
+ "trains": ["Train_0", "Train_1", "Train_2"],
+ "actions": ["vorfahrt", "warten"],
+ "invalid_actions": ["warten"],
+ },
+
+ # Marey link: start=Station1/S17-start (right end), end=Station4 (left end)
+ "marey_link": {"start": [16, 2], "end": [2, 17]},
+
+ "events": [
+ {
+ "timestep": 14,
+ "type": "train_delay",
+ "train": "Train_0",
+ "duration": 30,
+ "card_title": "Betriebsstörung — S 17",
+ "card_description": (
+ "Auf der Strecke wurde ein Hindernis gemeldet. "
+ "S 17 muss an der aktuellen Position anhalten und den Streckenabschnitt sichern. "
+ "Geschätzte Wartezeit: 30 Zeitschritte. "
+ "Der Streckenunterhaltsdienst wurde verständigt."
+ ),
+ },
+ {
+ "timestep": 25,
+ "type": "train_delay",
+ "train": "Train_1",
+ "duration": 18,
+ "card_title": "Signalstörung — S 18",
+ "card_description": (
+ "Im Streckenabschnitt von S 18 wurde eine Signalstörung gemeldet. "
+ "Der Zug muss gemäss Vorschrift auf Sicht fahren und an der nächsten "
+ "Haltestelle auf Freigabe warten. "
+ "Geschätzte Verzögerung: 18 Zeitschritte."
+ ),
+ },
+ {
+ "timestep": 40,
+ "type": "info",
+ "train": "Train_0",
+ "duration": 0,
+ "push_card": True,
+ "card_title": "Dispositionskonflikt — Zugreihenfolge",
+ "card_description": (
+ "Durch die Verspätungen von S 17 und S 18 ist die geplante Zugreihenfolge "
+ "nicht mehr einzuhalten. Bitte entscheiden Sie, welchem Zug Vorfahrt "
+ "gewährt werden soll."
+ ),
+ },
+ ],
+
+ "decision_points": [
+ {
+ "timestep": 40,
+ "description": (
+ "Aufgrund der Verspätungen von S 17 und S 18 ist die geplante "
+ "Zugreihenfolge im gemeinsamen Streckenabschnitt nicht mehr einzuhalten. "
+ "Bitte entscheiden Sie, welchem Zug Vorfahrt gewährt werden soll."
+ ),
+ "options": [
+ {
+ "label": "S 18 Vorfahrt — S 17 und IC 3 warten",
+ "kpis": {
+ "local_delay": 33,
+ "global_delay": 25,
+ "energy": 76,
+ "anschluss": 3,
+ },
+ "outcome": {
+ "holds": {
+ "Train_0": 18, # S17 wartet 18 Schritte
+ "Train_2": 22, # IC3 wartet 22 Schritte
+ },
+ "scripted_actions": {
+ "Train_1": [2]*14 + [3, 3, 3, 3, 3] + [2]*60, # try right steps 54-58
+ },
+ },
+ },
+ {
+ "label": "IC 3 Vorfahrt — S 17 folgt nach 5, S 18 wartet",
+ "kpis": {
+ "local_delay": 28,
+ "global_delay": 20,
+ "energy": 80,
+ "anschluss": 2,
+ },
+ "outcome": {
+ "holds": {
+ "Train_0": 5, # S17 wartet 5 Schritte
+ "Train_1": 18, # S18 wartet 18 Schritte
+ }
+ },
+ },
+ {
+ "label": "S 17 Vorfahrt — S 18 wartet, IC 3 wartet",
+ "kpis": {
+ "local_delay": 35,
+ "global_delay": 28,
+ "energy": 72,
+ "anschluss": 3,
+ },
+ "outcome": {
+ "holds": {
+ "Train_1": 15, # S18 wartet 15 Schritte
+ "Train_2": 29, # IC3 wartet 29 Schritte
+ }
+ },
+ },
+ ],
+ }
+ ],
+}
diff --git a/usecases_examples/Railway/experiment_scenarios/test_scenario.py b/usecases_examples/Railway/experiment_scenarios/test_scenario.py
new file mode 100644
index 00000000..5152c812
--- /dev/null
+++ b/usecases_examples/Railway/experiment_scenarios/test_scenario.py
@@ -0,0 +1,104 @@
+"""
+test_scenario.py — Testszenario: Technische Störung (Zug 3)
+
+Ablauf:
+ Schritt 15: Technische Störung Zug 3 — Zug 3 wird angehalten.
+ Schritt 30: Störung behoben — Zug 3 kann weiterfahren.
+ Entscheidungspunkt: Konflikt mit Zug 0 muss gelöst werden.
+"""
+
+TEST_SCENARIO = {
+ "id": "test",
+ "name": "Testszenario — Technische Störung",
+ "map": "maps/4city_map.pkl",
+ "scenario_index": 0,
+
+ "events": [
+ {
+ "timestep": 1,
+ "type": "train_delay",
+ "train": "Train_2",
+ "duration": 999,
+ "push_card": False,
+ },
+ {
+ "timestep": 15,
+ "type": "train_delay",
+ "train": "Train_3",
+ "duration": 15,
+ "card_title": "Technische Störung — Zug 3",
+ "card_description": (
+ "Zug 3 hat eine technische Störung und wurde angehalten. "
+ "Der Wartungstrupp wurde alarmiert. "
+ "Voraussichtliche Behebung: 15 Zeitschritte."
+ ),
+ },
+ {
+ "timestep": 30,
+ "type": "info",
+ "train": "Train_3",
+ "duration": 0,
+ "card_title": "Zug 3 — Technische Störung behoben",
+ "card_description": (
+ "Die technische Störung von Zug 3 wurde behoben. "
+ "Zug 3 ist wieder fahrbereit. "
+ "Zug 0 nähert sich demselben Streckenabschnitt — "
+ "bitte eine Lösung auswählen, um eine Kollision zu vermeiden."
+ ),
+ },
+ ],
+
+ "decision_points": [
+ {
+ "timestep": 30,
+ "description": (
+ "Zug 3 ist nach der technischen Störung wieder fahrbereit. "
+ "Zug 0 nähert sich demselben Streckenabschnitt — "
+ "ohne Eingriff kommt es zur Kollision. "
+ "Bitte eine Lösung auswählen."
+ ),
+ "options": [
+ {
+ "label": "Zug 0 für 15 Schritte anhalten — Zug 3 fährt zuerst",
+ "kpis": {
+ "local_delay": 15,
+ "global_delay": 20,
+ "energy": 76,
+ "anschluss": 2,
+ },
+ "outcome": {
+ "hold_train": "Train_0",
+ "hold_steps": 15,
+ }
+ },
+ {
+ "label": "Zug 3 weitere 15 Schritte anhalten — Zug 0 fährt zuerst",
+ "kpis": {
+ "local_delay": 15,
+ "global_delay": 12,
+ "energy": 81,
+ "anschluss": 1,
+ },
+ "outcome": {
+ "hold_train": "Train_3",
+ "hold_steps": 15,
+ }
+ },
+ {
+ "label": "Zug 3 über Alternativroute (links) umleiten",
+ "kpis": {
+ "local_delay": 22,
+ "global_delay": 16,
+ "energy": 63,
+ "anschluss": 2,
+ },
+ "outcome": {
+ "scripted_actions": {
+ "Train_3": [2, 2, 2, 2, 2, 2, 2, 1, 1, 1] + [2] * 60,
+ }
+ }
+ },
+ ]
+ }
+ ]
+}
diff --git a/usecases_examples/Railway/maps/4city_map.pkl b/usecases_examples/Railway/maps/4city_map.pkl
new file mode 100644
index 00000000..a88ba1cb
Binary files /dev/null and b/usecases_examples/Railway/maps/4city_map.pkl differ
diff --git a/usecases_examples/Railway/maps/drawn_environment_export.json b/usecases_examples/Railway/maps/drawn_environment_export.json
new file mode 100644
index 00000000..1b1f52f1
--- /dev/null
+++ b/usecases_examples/Railway/maps/drawn_environment_export.json
@@ -0,0 +1,388 @@
+{
+ "gridDimensions": {
+ "rows": 15,
+ "cols": 20,
+ "cellSize": 30
+ },
+ "grid": [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 4,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 5633,
+ 1025,
+ 1025,
+ 1025,
+ 4608,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 4,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1097,
+ 1025,
+ 1025,
+ 4608,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 49186,
+ 2064,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32872,
+ 4608,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 4,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 17411,
+ 1025,
+ 1025,
+ 2064,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 4,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 3089,
+ 1025,
+ 1025,
+ 1025,
+ 2064,
+ 0,
+ 0
+ ]
+ ],
+ "overpasses": [],
+ "stations": [
+ {
+ "id": 1,
+ "r": 0,
+ "c": 8
+ },
+ {
+ "id": 2,
+ "r": 14,
+ "c": 8
+ }
+ ],
+ "nextStationId": 3,
+ "lines": [],
+ "timetables": [],
+ "trainCategories": {
+ "IC": {
+ "id": 1,
+ "description": "High-Speed",
+ "speed": 1
+ },
+ "RE": {
+ "id": 2,
+ "description": "Express",
+ "speed": 0.8
+ },
+ "S": {
+ "id": 3,
+ "description": "Local",
+ "speed": 0.6
+ },
+ "C": {
+ "id": 4,
+ "description": "Cargo",
+ "speed": 0.5
+ }
+ },
+ "flatlandLine": {
+ "agent_positions": [],
+ "agent_directions": [],
+ "agent_targets": [],
+ "agent_speeds": []
+ },
+ "flatlandTimetable": {
+ "earliest_departures": [],
+ "latest_arrivals": [],
+ "max_episode_steps": 0
+ }
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/maps/map2.json b/usecases_examples/Railway/maps/map2.json
new file mode 100644
index 00000000..9043568d
--- /dev/null
+++ b/usecases_examples/Railway/maps/map2.json
@@ -0,0 +1,748 @@
+{
+ "gridDimensions": {
+ "rows": 25,
+ "cols": 25,
+ "cellSize": 30
+ },
+ "grid": [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 8192,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 16386,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 256
+ ],
+ [
+ 0,
+ 0,
+ 16386,
+ 1025,
+ 1025,
+ 1025,
+ 1097,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 5633,
+ 1025,
+ 1025,
+ 1025,
+ 3089,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 4608,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 128,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 128,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 128,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ ],
+ "overpasses": [],
+ "stations": [
+ {
+ "id": 1,
+ "r": 17,
+ "c": 2
+ },
+ {
+ "id": 2,
+ "r": 15,
+ "c": 12
+ },
+ {
+ "id": 3,
+ "r": 15,
+ "c": 22
+ },
+ {
+ "id": 4,
+ "r": 6,
+ "c": 22
+ },
+ {
+ "id": 5,
+ "r": 1,
+ "c": 6
+ }
+ ],
+ "nextStationId": 8,
+ "lines": [],
+ "timetables": [],
+ "trainCategories": {
+ "IC": {
+ "id": 1,
+ "description": "High-Speed",
+ "speed": 1
+ },
+ "RE": {
+ "id": 2,
+ "description": "Express",
+ "speed": 0.8
+ },
+ "S": {
+ "id": 3,
+ "description": "Local",
+ "speed": 0.6
+ },
+ "C": {
+ "id": 4,
+ "description": "Cargo",
+ "speed": 0.5
+ }
+ },
+ "flatlandLine": {
+ "agent_positions": [],
+ "agent_directions": [],
+ "agent_targets": [],
+ "agent_speeds": []
+ },
+ "flatlandTimetable": {
+ "earliest_departures": [],
+ "latest_arrivals": [],
+ "max_episode_steps": 0
+ }
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/maps/map3.json b/usecases_examples/Railway/maps/map3.json
new file mode 100644
index 00000000..b7eb4ab4
--- /dev/null
+++ b/usecases_examples/Railway/maps/map3.json
@@ -0,0 +1,743 @@
+{
+ "gridDimensions": {
+ "rows": 25,
+ "cols": 25,
+ "cellSize": 30
+ },
+ "grid": [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 8192,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 49186,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 256
+ ],
+ [
+ 0,
+ 16386,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 4608,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 32800,
+ 16386,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1097,
+ 1025,
+ 17411,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 1025,
+ 2064,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 32800,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 72,
+ 37408,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 128,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 32800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 128,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ ],
+ "overpasses": [],
+ "stations": [
+ {
+ "id": 1,
+ "r": 6,
+ "c": 23
+ },
+ {
+ "id": 2,
+ "r": 15,
+ "c": 10
+ },
+ {
+ "id": 3,
+ "r": 16,
+ "c": 2
+ },
+ {
+ "id": 4,
+ "r": 2,
+ "c": 17
+ }
+ ],
+ "nextStationId": 5,
+ "lines": [],
+ "timetables": [],
+ "trainCategories": {
+ "IC": {
+ "id": 1,
+ "description": "High-Speed",
+ "speed": 1
+ },
+ "RE": {
+ "id": 2,
+ "description": "Express",
+ "speed": 0.8
+ },
+ "S": {
+ "id": 3,
+ "description": "Local",
+ "speed": 0.6
+ },
+ "C": {
+ "id": 4,
+ "description": "Cargo",
+ "speed": 0.5
+ }
+ },
+ "flatlandLine": {
+ "agent_positions": [],
+ "agent_directions": [],
+ "agent_targets": [],
+ "agent_speeds": []
+ },
+ "flatlandTimetable": {
+ "earliest_departures": [],
+ "latest_arrivals": [],
+ "max_episode_steps": 0
+ }
+}
\ No newline at end of file
diff --git a/usecases_examples/Railway/record_route.py b/usecases_examples/Railway/record_route.py
new file mode 100644
index 00000000..6abfe759
--- /dev/null
+++ b/usecases_examples/Railway/record_route.py
@@ -0,0 +1,90 @@
+"""
+record_route.py — Records the action sequence for Train 3 taking the alternate left route.
+
+Run this from the Railway folder:
+ python record_route.py
+
+It will:
+1. Load the environment (same as the scenario)
+2. Run to step 30 with Train 3 held from step 15-29
+3. From step 30 onwards, try MOVE_LEFT for Train 3 at every step
+4. Let the dispatcher handle all other trains
+5. Print the full action sequence for Train 3
+
+Copy the output into test_scenario.py as "scripted_actions" for option C.
+"""
+
+import sys
+sys.path.insert(0, '.')
+
+from Corridor_environment import load_corridor_env
+from TimetableDispatcher import TimetableDispatcher
+from ScenarioManager import ScenarioManager
+
+# ── Load environment ───────────────────────────────────────────────────────────
+env, stations, junctions = load_corridor_env("maps/4city_map.pkl")
+manager = ScenarioManager(env, stations, junctions)
+timetable, train_infos, _ = manager.load_scenario_manual(0)
+dispatcher = TimetableDispatcher(
+ env, timetable,
+ train_infos=train_infos,
+ enable_random_delays=False,
+)
+
+# Flatland action constants
+DO_NOTHING = 4 # STOP_MOVING
+MOVE_LEFT = 1
+MOVE_FORWARD = 2
+MOVE_RIGHT = 3
+
+MAX_STEPS = 100
+HOLD_START = 15
+HOLD_END = 29 # inclusive
+DECISION_STEP = 30
+
+# Track Train 3's actions after decision
+train3_actions = []
+all_actions_log = []
+
+print("Running simulation...")
+print(f"Train 3 holds from step {HOLD_START} to {HOLD_END}")
+print(f"From step {DECISION_STEP}: Train 3 takes MOVE_LEFT at every opportunity\n")
+
+for step in range(MAX_STEPS):
+ # Get dispatcher actions
+ actions = dispatcher.get_actions(step)
+
+ # Hold Train 3 from step 15-29
+ if HOLD_START <= step <= HOLD_END:
+ actions[3] = DO_NOTHING
+
+ # From step 30: try MOVE_LEFT for Train 3
+ if step >= DECISION_STEP:
+ actions[3] = MOVE_LEFT
+ train3_actions.append(MOVE_LEFT)
+
+ # Step environment
+ obs, rewards, dones, info = env.step(actions)
+
+ # Print Train 3 position and action
+ a3 = env.agents[3]
+ action_taken = actions.get(3, DO_NOTHING)
+ if step >= DECISION_STEP:
+ action_name = {1: 'LEFT', 2: 'FWD', 3: 'RIGHT', 4: 'STOP', 0: 'NOTHING'}.get(action_taken, '?')
+ print(f"Step {step:3d}: Train3 pos={a3.position}, dir={a3.direction}, action={action_name}")
+
+ if dones.get('__all__', False):
+ print(f"\nAll trains done at step {step}")
+ break
+
+ if a3.position is None and step > DECISION_STEP:
+ print(f"\nTrain 3 reached target at step {step}")
+ break
+
+print(f"\n{'='*60}")
+print("COPY THIS INTO test_scenario.py option C outcome:")
+print(f"{'='*60}")
+print(f'"scripted_actions": {{')
+print(f' "Train_3": {train3_actions},')
+print(f'}}')
+print(f"\nTotal actions recorded: {len(train3_actions)}")
diff --git a/usecases_examples/Railway/requirements.txt b/usecases_examples/Railway/requirements.txt
new file mode 100644
index 00000000..3d1c4bff
--- /dev/null
+++ b/usecases_examples/Railway/requirements.txt
@@ -0,0 +1,7 @@
+# Flask brain dependencies for Railway use case
+flask==3.1.3
+flask-cors==6.0.5
+flatland-rl==4.2.4
+requests==2.32.3
+Pillow==11.2.1
+numpy>=1.24.0
diff --git a/usecases_examples/Railway/run_scenarios.py b/usecases_examples/Railway/run_scenarios.py
new file mode 100644
index 00000000..4f48811c
--- /dev/null
+++ b/usecases_examples/Railway/run_scenarios.py
@@ -0,0 +1,386 @@
+"""
+run_scenarios.py — Test runner for loaded Flatland maps.
+"""
+
+import argparse
+import os
+from typing import Dict, List
+
+from Corridor_environment import load_corridor_env, build_timetable_from_loaded_env
+from Timetable import Timetable, TrainSchedule
+from TrainInfo import TrainInfo, TrainType, calculate_priority
+from SafetyVerifier import SafetyVerifier
+from safe_resolver import resolve_all_conflicts_safe
+from FlatlandMapLoader import visualize_loaded_env
+from scenarios import make_scenarios
+
+
+# ============================================================
+# SCENARIO RENDERER
+# ============================================================
+
+class ScenarioRenderer:
+ def __init__(self, env):
+ self.env = env
+ self.renderer = None
+ self.fig = None
+ self.ax = None
+ self.img_artist = None
+ self._available = False
+ self._init_renderer()
+
+ def _init_renderer(self):
+ try:
+ import matplotlib
+ matplotlib.use("TkAgg")
+ except Exception:
+ pass
+ try:
+ import matplotlib.pyplot as plt
+ from flatland.utils.rendertools import RenderTool
+ self.renderer = RenderTool(self.env, gl="PILSVG",
+ screen_width=800, screen_height=600)
+ plt.ion()
+ self.fig, self.ax = plt.subplots(figsize=(10, 7))
+ self.ax.axis("off")
+ self.fig.tight_layout(pad=0)
+ self._plt = plt
+ self._available = True
+ plt.show(block=False)
+ plt.pause(0.1)
+ print(" Renderer: matplotlib window ready.")
+ except Exception as e:
+ print(f" Renderer: unavailable ({e}). Using ASCII fallback.")
+
+ @property
+ def available(self):
+ return self._available
+
+ def render_frame(self, step: int, pause: float = 0.3):
+ if not self._available:
+ return
+ try:
+ image = self.renderer.render_env(
+ show=False, show_observations=False,
+ show_inactive_agents=True, show_rowcols=True,
+ return_image=True,
+ )
+ if image is None:
+ return
+ if self.img_artist is None:
+ self.img_artist = self.ax.imshow(image)
+ else:
+ self.img_artist.set_data(image)
+ self.ax.set_title(f"Step {step}", fontsize=11)
+ self.fig.canvas.draw()
+ self.fig.canvas.flush_events()
+ self._plt.pause(pause)
+ except Exception as e:
+ print(f" Render error at step {step}: {e}")
+ self._available = False
+
+ def close(self):
+ if self._available:
+ try:
+ self._plt.ioff()
+ self._plt.close(self.fig)
+ except Exception:
+ pass
+
+
+
+
+# ============================================================
+# SCENARIO RUNNER
+# ============================================================
+
+def run_scenario(
+ scenario: Dict,
+ env,
+ stations: Dict,
+ junctions: List,
+ render: bool = False,
+ pause: float = 0.3,
+ max_steps: int = 150,
+ verbose: bool = True,
+ ignore_holds: bool = False,
+ rejected_resolutions=None,
+ enable_random_delays: bool = False,
+ delay_probability: float = 0.15,
+) -> Dict:
+ name = scenario['name']
+ timetable = scenario['timetable']
+ train_infos = scenario['train_infos']
+ priorities = scenario['priorities']
+
+ # CLI flags override scenario defaults
+ use_delays = enable_random_delays or scenario.get('enable_random_delays', False)
+ delay_prob = delay_probability if enable_random_delays else scenario.get('delay_probability', 0.15)
+
+ print(f"\n{'='*70}")
+ print(f" SCENARIO: {name}")
+ print(f"{'='*70}")
+ print(f" {scenario['description']}")
+ print(f" Trains: {len(timetable.schedules)} | Random delays: {use_delays}")
+
+ verifier = SafetyVerifier(timetable, max_steps=max_steps)
+ is_safe_before, violations_before = verifier.verify_safety(verbose=False)
+ print(f"\n Pre-resolution: {'✅ safe' if is_safe_before else f'❌ {len(violations_before)} violations'}")
+
+ result = resolve_all_conflicts_safe(
+ env, timetable, priorities, train_infos,
+ max_iterations=50, verbose=verbose,
+ stagger_spawn=scenario.get('stagger_spawn', False),
+ rejected_resolutions=scenario.get('rejected_resolutions', None),
+ )
+
+ verifier2 = SafetyVerifier(timetable, max_steps=max_steps)
+ is_safe_after, violations_after = verifier2.verify_safety(verbose=False)
+ print(f"\n Post-resolution: {'✅ safe' if is_safe_after else f'❌ {len(violations_after)} violations'}")
+
+ print("\n Final schedule after resolution:")
+ for tid, sched in sorted(timetable.schedules.items()):
+ hold_str = ""
+ if getattr(sched, 'was_held', False) and getattr(sched, 'hold_until', None):
+ hold_str = f", HOLD at {getattr(sched,'hold_at_cell',None)} until step {sched.hold_until}"
+ reroute_str = " [REROUTED]" if getattr(sched, 'was_rerouted', False) else ""
+ print(f" Train {tid}: dep={sched.planned_departure}, "
+ f"route_len={len(sched.route)}{hold_str}{reroute_str}")
+
+ print()
+ visualize_loaded_env(env, stations, junctions, step=0)
+
+ _simulate_and_render(env, timetable, train_infos, priorities, stations, junctions,
+ max_steps=max_steps, pause=pause,
+ ignore_holds=ignore_holds,
+ enable_random_delays=use_delays,
+ delay_probability=delay_prob,
+ render=render)
+
+ return {
+ 'name': name,
+ 'n_trains': len(timetable.schedules),
+ 'violations_before': len(violations_before),
+ 'violations_after': len(violations_after),
+ 'success': is_safe_after,
+ 'iterations': result.iterations,
+ 'resolutions': result.resolutions_applied,
+ }
+
+
+# ============================================================
+# SIMULATE AND RENDER
+# ============================================================
+
+def _simulate_and_render(env, timetable, train_infos, priorities, stations, junctions,
+ max_steps=150, pause=0.3, ignore_holds=False,
+ enable_random_delays=False, delay_probability=0.15,
+ render=True):
+ from TimetableDispatcher import TimetableDispatcher
+
+ print("\n Rendering simulation...")
+ env.reset()
+
+ for agent_id in range(len(env.agents)):
+ agent = env.agents[agent_id]
+ schedule = timetable.schedules.get(agent_id)
+ dep = schedule.planned_departure if schedule else 9999
+ if hasattr(agent, 'earliest_departure'):
+ agent.earliest_departure = dep
+ if hasattr(env, 'timetable') and env.timetable is not None:
+ try:
+ env.timetable.earliest_departures[agent_id][0] = dep
+ except Exception:
+ pass
+
+ dispatcher = TimetableDispatcher(
+ env, timetable,
+ ignore_holds=ignore_holds,
+ train_infos=train_infos,
+ enable_random_delays=enable_random_delays,
+ delay_probability=delay_probability,
+ delay_min_steps=5,
+ delay_max_steps=30,
+ )
+ dispatcher.reset()
+ renderer = ScenarioRenderer(env) if render else None
+
+ dispatcher.print_timetable_plan(train_infos=train_infos)
+
+ if renderer is None or not renderer.available:
+ for step in range(1, max_steps + 1):
+ actions = dispatcher.get_actions(step)
+ _, _, dones, _ = env.step(actions)
+ for evt in dispatcher.get_step_events():
+ if evt.event_type in ('departed', 'arrived', 'holding_at_cell',
+ 'delay_injected', 'deadlock_warning'):
+ print(f" {evt}")
+ if dispatcher.get_pending_replan():
+ print(f"\n [Re-planning at step {step}...]")
+ resolve_all_conflicts_safe(
+ env, timetable, priorities, train_infos,
+ max_iterations=20, verbose=False, stagger_spawn=False)
+ dispatcher._init_routes(preserve_active=True)
+ print(f" [Re-plan complete]")
+ if dones.get('__all__', False):
+ print(f" All agents done at step {step}.")
+ break
+ dispatcher.print_final_report(train_infos=train_infos)
+ _print_delay_summary(timetable, train_infos)
+ return
+
+ if render and renderer and renderer.available:
+ renderer.render_frame(step=0, pause=pause)
+
+ for step in range(1, max_steps + 1):
+ actions = dispatcher.get_actions(step)
+ _, _, dones, _ = env.step(actions)
+
+ if render and renderer and renderer.available:
+ active = [i for i, a in enumerate(env.agents)
+ if a.position is not None and i in timetable.schedules]
+ pending = [i for i in sorted(timetable.schedules)
+ if env.agents[i].position is None
+ and not timetable.schedules[i].planned_departure <= step]
+ renderer.ax.set_title(
+ f"Step {step} | Active: {active} | Waiting: {pending}",
+ fontsize=9)
+ renderer.render_frame(step=step, pause=pause)
+
+ for evt in dispatcher.get_step_events():
+ if evt.event_type in ('deadlock_warning', 'priority_blocked',
+ 'departed', 'arrived', 'holding_at_cell',
+ 'delay_injected'):
+ print(f" {evt}")
+
+ if dispatcher.get_pending_replan():
+ print(f"\n [Re-planning after injected delay at step {step}...]")
+ resolve_all_conflicts_safe(
+ env, timetable, priorities, train_infos,
+ max_iterations=20, verbose=False, stagger_spawn=False)
+ dispatcher._init_routes(preserve_active=True)
+ print(f" [Re-plan complete]")
+
+ if dones.get('__all__', False):
+ print(f" All agents done at step {step}.")
+ break
+
+ dispatcher.print_final_report(train_infos=train_infos)
+ _print_delay_summary(timetable, train_infos)
+ if renderer:
+ renderer.close()
+ print(" Render complete.")
+
+
+# ============================================================
+# DELAY SUMMARY
+# ============================================================
+
+def _print_delay_summary(timetable, train_infos):
+ print("\n" + "=" * 70)
+ print(" DELAY SUMMARY")
+ print("=" * 70)
+ print(f" {'Train':<30} {'PlannedArr':>10} {'ActualArr':>10} "
+ f"{'TotalDelay':>11} {'RerouteDelay':>13} {'InjectedDelay':>14}")
+ print(" " + "-" * 68)
+ any_delay = False
+ for tid, s in sorted(timetable.schedules.items()):
+ name = train_infos[tid].name if tid in train_infos else f"Train {tid}"
+ planned = s.original_planned_arrival
+ actual = s.actual_arrival if s.actual_arrival is not None else "—"
+ total = s.arrival_delay if s.arrival_delay is not None else "—"
+ reroute = s.reroute_delay_added
+ injected = s.injected_delay_steps
+ status = (" [incomplete]" if s.actual_arrival is None
+ else " ✓" if s.is_on_time else " ✗")
+ if not s.is_on_time and s.actual_arrival is not None:
+ any_delay = True
+ actual_str = str(actual) if actual != "—" else "—"
+ total_str = f"+{total}" if isinstance(total, int) and total > 0 else str(total)
+ print(f" {name:<30} {planned:>10} {actual_str:>10} "
+ f"{total_str:>11} {reroute:>13} {injected:>14}{status}")
+ print(" " + "-" * 68)
+ print(f" Weighted delay (Σ delay×priority): {timetable.weighted_delay():.1f}")
+ if not any_delay:
+ print(" All trains on time ✓")
+ print("=" * 70)
+
+
+# ============================================================
+# SUMMARY PRINTER
+# ============================================================
+
+def print_summary(results: List[Dict]):
+ print(f"\n{'='*70}")
+ print(" SCENARIO SUMMARY")
+ print(f"{'='*70}")
+ print(f"{'#':<3} {'Scenario':<40} {'Trains':<7} {'Before':<8} "
+ f"{'After':<7} {'Res':<5} {'Result'}")
+ print("-" * 70)
+ for i, r in enumerate(results):
+ status = "✅ PASS" if r['success'] else "❌ FAIL"
+ print(f"{i:<3} {r['name'][:39]:<40} {r['n_trains']:<7} "
+ f"{r['violations_before']:<8} {r['violations_after']:<7} "
+ f"{r['resolutions']:<5} {status}")
+ passed = sum(1 for r in results if r['success'])
+ print(f"\n Passed: {passed}/{len(results)}")
+
+
+# ============================================================
+# ENTRY POINT
+# ============================================================
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--map", default="maps/4city_map.pkl")
+ parser.add_argument("--render", action="store_true")
+ parser.add_argument("--scenario", type=int, default=None)
+ parser.add_argument("--pause", type=float, default=0.3)
+ parser.add_argument("--steps", type=int, default=150)
+ parser.add_argument("--no-waits", action="store_true")
+ parser.add_argument("--delays", action="store_true")
+ parser.add_argument("--delay-prob", type=float, default=0.15)
+ parser.add_argument("--quiet", action="store_true")
+ args = parser.parse_args()
+
+ if not os.path.exists(args.map):
+ print(f"Map not found: {args.map}")
+ return
+
+ print(f"Loading map: {args.map}")
+ env, stations, junctions = load_corridor_env(args.map)
+ scenarios = make_scenarios(env, stations, train_infos=None)
+
+ if not scenarios:
+ print("No scenarios could be built.")
+ return
+
+ print(f"\nAvailable scenarios ({len(scenarios)} total):")
+ for i, s in enumerate(scenarios):
+ delays_tag = " [delays]" if s.get('enable_random_delays') else ""
+ print(f" {i}: {s['name']}{delays_tag}")
+
+ to_run = [args.scenario] if args.scenario is not None else list(range(len(scenarios)))
+
+ results = []
+ for idx in to_run:
+ if idx >= len(scenarios):
+ print(f"Scenario {idx} does not exist.")
+ continue
+ result = run_scenario(
+ scenario=scenarios[idx],
+ env=env, stations=stations, junctions=junctions,
+ render=args.render, pause=args.pause,
+ max_steps=args.steps, verbose=not args.quiet,
+ ignore_holds=args.no_waits,
+ enable_random_delays=args.delays,
+ delay_probability=args.delay_prob,
+ )
+ results.append(result)
+
+ if len(results) > 1:
+ print_summary(results)
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/usecases_examples/Railway/safe_resolver.py b/usecases_examples/Railway/safe_resolver.py
new file mode 100644
index 00000000..e394b09a
--- /dev/null
+++ b/usecases_examples/Railway/safe_resolver.py
@@ -0,0 +1,478 @@
+"""
+Safe Conflict Resolution with Verification.
+"""
+
+from typing import Dict, List, Tuple, Set, Optional
+from dataclasses import dataclass
+from collections import defaultdict
+from copy import deepcopy
+
+from flatland.envs.rail_env import RailEnv
+
+from Timetable import Timetable, TrainSchedule
+from TrainInfo import TrainInfo
+from ConflictResolver import ConflictDetector, ResolutionGenerator, ResolutionType
+from CostCalculator import CostCalculator, get_priority_focused_weights
+from SafetyVerifier import SafetyVerifier, SafetyViolation
+
+
+@dataclass
+class ResolutionResult:
+ """Result of conflict resolution process."""
+ success: bool
+ iterations: int
+ resolutions_applied: int
+ remaining_violations: int
+ resolution_chain: List[dict]
+ final_timetable: Timetable
+
+
+def _resolve_spawn_conflicts(timetable, priorities: Dict[int, float], verbose: bool = True):
+
+ from SafetyVerifier import SafetyVerifier
+
+ verifier = SafetyVerifier(timetable, max_steps=10) # Only check early steps
+ _, violations = verifier.verify_safety(verbose=False)
+
+ # Find pairs with very early conflicts (steps 1-3 = spawn zone)
+ early_pairs = set()
+ for v in violations:
+ if v.timestep <= 3:
+ pair = (min(v.train_a, v.train_b), max(v.train_a, v.train_b))
+ early_pairs.add(pair)
+
+ if not early_pairs:
+ return 0
+
+ adjustments = 0
+ for pair in early_pairs:
+ a, b = pair
+ pri_a = priorities.get(a, 1.0)
+ pri_b = priorities.get(b, 1.0)
+
+ # Delay the lower priority train
+ train_to_delay = b if pri_a >= pri_b else a
+ schedule = timetable.get_schedule(train_to_delay)
+
+ # Push departure back by 3 steps
+ delay = 3
+ schedule.planned_departure += delay
+ schedule.planned_arrival += delay
+ if getattr(schedule, 'hold_until', None):
+ schedule.hold_until += delay
+
+ adjustments += 1
+ if verbose:
+ print(f" Spawn conflict {a} vs {b}: delayed Train {train_to_delay} "
+ f"departure by +{delay} (now departs at step {schedule.planned_departure})")
+
+ return adjustments
+
+
+def generate_resolution_id(option, conflict=None) -> str:
+ """
+ Generate a stable, human-readable ID for a resolution option.
+
+ IDs are content-based so they remain consistent across re-runs:
+ WAIT: WAIT_T{id}_at_({r},{c})
+ REROUTE: REROUTE_T{id}_len{n}_h{hash6}
+
+ Use these IDs in scenario 'rejected_resolutions' sets to block
+ specific options without affecting others.
+ """
+ tid = option.train_to_delay
+ if option.resolution_type == ResolutionType.WAIT:
+ cell = option.wait_at_cell
+ if cell:
+ return f"WAIT_T{tid}_at_({cell[0]},{cell[1]})"
+ elif conflict:
+ return f"WAIT_T{tid}_at_({conflict.cell[0]},{conflict.cell[1]})"
+ return f"WAIT_T{tid}"
+ else:
+ route = option.new_route or []
+ h = format(abs(hash(tuple(route))) % 0xFFFFFF, '06X')
+ return f"REROUTE_T{tid}_len{len(route)}_h{h}"
+
+
+def resolve_all_conflicts_safe(
+ env,
+ timetable,
+ priorities,
+ train_infos=None,
+ max_iterations=50,
+ verbose=True,
+ stagger_spawn=True,
+ rejected_resolutions=None,
+):
+ """
+ rejected_resolutions: optional set of resolution keys to skip.
+ Each key is a tuple: (train_a, train_b, resolution_type_str, route_hash)
+ where train_a < train_b, resolution_type_str is 'wait' or 'reroute',
+ and route_hash = hash(tuple(new_route)) for reroutes or 0 for waits.
+
+ Example — reject the WAIT between trains 0 and 3:
+ rejected_resolutions={(0, 3, 'wait', 0)}
+
+ This forces the resolver to find the next-best option (e.g. a reroute).
+ """
+ if verbose:
+ print("\n" + "=" * 70)
+ print(" SAFE CONFLICT RESOLUTION")
+ print("=" * 70)
+
+ resolution_chain = []
+ iteration = 0
+ # Pre-populate with any caller-specified rejections so they are
+ # skipped as if already attempted.
+ attempted_resolutions = set(rejected_resolutions) if rejected_resolutions else set()
+
+ # Pre-pass: spawn staggering
+ if verbose:
+ print("\n Pre-pass: checking for spawn conflicts (step 1-3)...")
+ if stagger_spawn:
+ spawn_fixes = _resolve_spawn_conflicts(timetable, priorities, verbose=verbose)
+ if verbose:
+ if spawn_fixes:
+ print(f" Fixed {spawn_fixes} spawn conflict(s) by staggering departures.")
+ else:
+ print(" No spawn conflicts found.")
+ else:
+ if verbose:
+ print(" Spawn staggering disabled for this scenario.")
+
+ # Direction fix BEFORE conflict detection so hold_at_cell is computed
+ # on the actual route that will be executed.
+ _fix_rerouted_route_directions(env, timetable, verbose=verbose)
+
+ # Main resolution loop
+ while iteration < max_iterations:
+ iteration += 1
+ verifier = SafetyVerifier(timetable, max_steps=150)
+ is_safe, violations = verifier.verify_safety(verbose=False)
+ if is_safe:
+ if verbose:
+ print(f"\n \u2705 All conflicts resolved after {iteration - 1} iterations!")
+ break
+ conflicting_pairs = verifier.get_conflicting_pairs()
+ if verbose:
+ print(f"\n Iteration {iteration}: {len(violations)} violations, {len(conflicting_pairs)} pairs")
+
+ detector = ConflictDetector(env, timetable)
+ detected_conflicts, projections = detector.detect_conflicts(0)
+ active_ids = set(timetable.schedules.keys())
+ detected_conflicts = [c for c in detected_conflicts
+ if c.train_a in active_ids and c.train_b in active_ids]
+ projections = {k: v for k, v in projections.items() if k in active_ids}
+
+ resolved_this_iteration = False
+ for conflict in detected_conflicts:
+ pair = (min(conflict.train_a, conflict.train_b),
+ max(conflict.train_a, conflict.train_b))
+ if pair not in conflicting_pairs:
+ continue
+ generator = ResolutionGenerator(env)
+ calculator = CostCalculator(env, timetable, weights=get_priority_focused_weights())
+ options = generator.generate_all_options(conflict, priorities, timetable, projections)
+ if not options:
+ continue
+ ranked = calculator.compare_options(options, projections)
+
+ # Print rejected options so user knows what was skipped
+ if verbose and rejected_resolutions:
+ for opt, _, _ in ranked:
+ rid = generate_resolution_id(opt, conflict)
+ if rid in attempted_resolutions:
+ rn = train_infos[opt.train_to_delay].name if train_infos else f"Train {opt.train_to_delay}"
+ print(f" [REJECTED] {rid}")
+
+ for option, cost, _ in ranked:
+ resolution_key = generate_resolution_id(option, conflict)
+
+ if resolution_key in attempted_resolutions:
+ continue
+ schedule = timetable.get_schedule(option.train_to_delay)
+ old_route_len = len(schedule.route)
+ if option.resolution_type == ResolutionType.REROUTE:
+ schedule.route = option.new_route
+ schedule.planned_arrival += option.delay_added
+ schedule.was_rerouted = True
+ schedule.reroute_delay_added += option.delay_added
+ elif option.resolution_type == ResolutionType.WAIT:
+ schedule.planned_arrival += option.delay_added
+ schedule.was_held = True
+ schedule.hold_until = option.wait_until
+ schedule.hold_at_cell = option.wait_at_cell
+ schedule.reroute_delay_added += option.delay_added
+ attempted_resolutions.add(resolution_key)
+ train_name = train_infos[option.train_to_delay].name if train_infos else f"Train {option.train_to_delay}"
+ resolution_chain.append({
+ 'iteration': iteration,
+ 'train': option.train_to_delay,
+ 'train_name': train_name,
+ 'type': option.resolution_type.value,
+ 'delay': option.delay_added,
+ 'conflict_cell': conflict.cell,
+ 'old_route_len': old_route_len,
+ 'new_route_len': len(schedule.route),
+ })
+ if verbose:
+ name_a = train_infos[conflict.train_a].name if train_infos else f"Train {conflict.train_a}"
+ name_b = train_infos[conflict.train_b].name if train_infos else f"Train {conflict.train_b}"
+ print(f" {name_a} vs {name_b} at {conflict.cell}")
+ print(f" -> {option.resolution_type.value.upper()} {train_name} (+{option.delay_added})")
+ print(f" ID: {resolution_key}")
+ resolved_this_iteration = True
+ break
+ if resolved_this_iteration:
+ break
+
+ if not resolved_this_iteration:
+ if verbose:
+ print(f"\n \u26a0\ufe0f No more resolution options available!")
+ remaining = verifier.get_conflicting_pairs()
+ print(f" Remaining: {len(remaining)} pairs")
+ for pr in sorted(remaining)[:5]:
+ na = train_infos[pr[0]].name if train_infos else f"Train {pr[0]}"
+ nb = train_infos[pr[1]].name if train_infos else f"Train {pr[1]}"
+ print(f" {na} vs {nb}")
+ break
+
+ # Final verification
+ verifier = SafetyVerifier(timetable, max_steps=150)
+ is_safe, final_violations = verifier.verify_safety(verbose=False)
+
+ if verbose:
+ print("\n" + "-" * 70)
+ print(" RESOLUTION SUMMARY")
+ print("-" * 70)
+ print(f" Iterations: {iteration}")
+ print(f" Resolutions applied: {len(resolution_chain)}")
+ safe_str = "✅ SAFE" if is_safe else f"❌ {len(final_violations)} violations"
+ print(f" Final safety: {safe_str}")
+ if not is_safe:
+ remaining_pairs = set()
+ for v in final_violations:
+ remaining_pairs.add((min(v.train_a, v.train_b), max(v.train_a, v.train_b)))
+ print(f"\n Unresolved pairs ({len(remaining_pairs)}):")
+ for pr in sorted(remaining_pairs):
+ na = train_infos[pr[0]].name if train_infos else f"Train {pr[0]}"
+ nb = train_infos[pr[1]].name if train_infos else f"Train {pr[1]}"
+ pv = [v for v in final_violations
+ if (v.train_a, v.train_b) == pr or (v.train_b, v.train_a) == pr]
+ steps = sorted(set(v.timestep for v in pv))
+ print(f" {na} vs {nb}: steps {steps[0]}-{steps[-1]}")
+
+ # Direction fix again on any newly rerouted routes
+ _fix_rerouted_route_directions(env, timetable, verbose=verbose)
+
+ # Final result
+ verifier2 = SafetyVerifier(timetable, max_steps=150)
+ is_safe2, final_viol2 = verifier2.verify_safety(verbose=False)
+
+ return ResolutionResult(
+ success=is_safe2,
+ iterations=iteration,
+ resolutions_applied=len(resolution_chain),
+ remaining_violations=len(final_viol2) if not is_safe2 else 0,
+ resolution_chain=resolution_chain,
+ final_timetable=timetable,
+ )
+
+def _fix_rerouted_route_directions(env, timetable, verbose=True):
+
+ from Corridor_environment import compute_route_bfs
+ DELTA_TO_DIR = {(-1,0):0,(0,1):1,(1,0):2,(0,-1):3}
+ DIR_NAMES = {0:"N",1:"E",2:"S",3:"W"}
+ fixed = 0
+
+ for agent_id, schedule in timetable.schedules.items():
+ agent = env.agents[agent_id]
+
+
+ if agent.position is not None:
+ continue
+
+ if not schedule.route or len(schedule.route) < 2:
+ continue
+
+ init_dir = int(agent.initial_direction)
+
+ r0, r1 = schedule.route[0], schedule.route[1]
+ dr, dc = r1[0]-r0[0], r1[1]-r0[1]
+ route_dir = DELTA_TO_DIR.get((dr, dc))
+ if route_dir is None:
+ continue
+
+ diff = (route_dir - init_dir) % 4
+ if diff == 0:
+ continue
+
+ if verbose and diff == 2:
+ print(f" Direction fix: Train {agent_id} route starts "
+ f"{DIR_NAMES[route_dir]} but initial_dir={DIR_NAMES[init_dir]} "
+ f"(180° mismatch) — recomputing route")
+
+ start = tuple(schedule.route[0])
+ target = tuple(agent.target)
+ new_route = compute_route_bfs(
+ env, start, target,
+ use_transitions=True,
+ start_direction=init_dir,
+ )
+
+ if new_route:
+ old_len = len(schedule.route)
+ schedule.route = new_route
+ schedule.planned_arrival = schedule.planned_departure + len(new_route)
+ fixed += 1
+ if verbose:
+ print(f" Train {agent_id}: route recomputed "
+ f"({old_len} -> {len(new_route)} cells)")
+ else:
+ if verbose:
+ print(f" Train {agent_id}: WARNING no route found with "
+ f"initial_dir={DIR_NAMES[init_dir]}, keeping original")
+
+ if verbose and fixed:
+ print(f" Direction fixes applied: {fixed}")
+
+
+# ============== TEST: CORRIDOR MAP (original) ==============
+
+def test_safe_resolution_corridor():
+ """Test on the handbuilt corridor with named stations."""
+ from Corridor_environment import create_corridor_env, compute_route_bfs
+ from TrainInfo import TrainType, calculate_priority
+
+ print("\n" + "=" * 70)
+ print(" TESTING SAFE RESOLUTION — CORRIDOR MAP")
+ print("=" * 70)
+
+ configs = [
+ {'id': 0, 'start': 'GENEVA', 'end': 'ZURICH', 'dep': 1,
+ 'type': TrainType.PASSENGER_EXPRESS, 'passengers': 400, 'name': 'ICE-101'},
+ {'id': 1, 'start': 'ZURICH', 'end': 'GENEVA', 'dep': 1,
+ 'type': TrainType.PASSENGER_EXPRESS, 'passengers': 350, 'name': 'ICE-102'},
+ {'id': 2, 'start': 'GENEVA', 'end': 'ZURICH', 'dep': 4,
+ 'type': TrainType.PASSENGER_LOCAL, 'passengers': 100, 'name': 'RE-201'},
+ {'id': 3, 'start': 'ZURICH', 'end': 'GENEVA', 'dep': 5,
+ 'type': TrainType.PASSENGER_LOCAL, 'passengers': 100, 'name': 'RE-202'},
+ {'id': 4, 'start': 'BERN', 'end': 'MILAN', 'dep': 2,
+ 'type': TrainType.PASSENGER_LOCAL, 'passengers': 150, 'name': 'EC-301'},
+ {'id': 5, 'start': 'MILAN', 'end': 'BERN', 'dep': 3,
+ 'type': TrainType.FREIGHT, 'passengers': 0, 'name': 'Freight'},
+ ]
+
+ n_agents = len(configs)
+ env, stations, junctions = create_corridor_env(n_agents=n_agents)
+ env.reset()
+
+ schedules = {}
+ priorities = {}
+ train_infos = {}
+
+ for cfg in configs:
+ route = compute_route_bfs(env, stations[cfg['start']], stations[cfg['end']],
+ use_transitions=True)
+ schedules[cfg['id']] = TrainSchedule(
+ train_id=cfg['id'],
+ planned_departure=cfg['dep'],
+ planned_arrival=cfg['dep'] + len(route),
+ route=route,
+ )
+ train_info = TrainInfo(
+ train_id=cfg['id'],
+ name=cfg['name'],
+ train_type=cfg['type'],
+ passenger_count=cfg['passengers'],
+ )
+ train_infos[cfg['id']] = train_info
+ priorities[cfg['id']] = calculate_priority(train_info)
+
+ timetable = Timetable(schedules=schedules, priorities=priorities)
+
+ print("\n INITIAL STATE:")
+ verifier = SafetyVerifier(timetable, max_steps=100)
+ is_safe_before, violations_before = verifier.verify_safety(verbose=False)
+ print(f" Collisions before: {len(violations_before)}")
+
+ result = resolve_all_conflicts_safe(
+ env, timetable, priorities, train_infos,
+ max_iterations=50, verbose=True
+ )
+
+ print("\n FINAL VERIFICATION:")
+ verifier = SafetyVerifier(timetable, max_steps=100)
+ verifier.verify_safety(verbose=True)
+
+ return result
+
+
+# ============== TEST: LOADED FLATLAND MAP ==============
+
+def test_safe_resolution_loaded(pkl_path: str = "maps/4city_map.pkl"):
+ """
+ Test safe resolution on any flatland-generated .pkl map.
+
+ Reads agent start/target positions directly from env.agents —
+ no hardcoded station names required.
+
+ Args:
+ pkl_path: Path to a .pkl file created by Make_map.py
+ """
+ import os
+ from Corridor_environment import load_corridor_env, build_timetable_from_loaded_env
+
+ print("\n" + "=" * 70)
+ print(f" TESTING SAFE RESOLUTION — LOADED MAP: {pkl_path}")
+ print("=" * 70)
+
+ if not os.path.exists(pkl_path):
+ print(f"Map file not found: {pkl_path}")
+ print("Run Make_map.py first to generate it.")
+ return None
+
+ # Load map — same interface as create_corridor_env()
+ env, stations, junctions = load_corridor_env(pkl_path)
+
+ # Build timetable from agent assignments
+ timetable, train_infos, priorities = build_timetable_from_loaded_env(env, stations)
+
+ if not timetable.schedules:
+ print("No valid schedules built — check route finding.")
+ return None
+
+ # Check initial state
+ print("\n INITIAL STATE:")
+ verifier = SafetyVerifier(timetable, max_steps=150)
+ is_safe_before, violations_before = verifier.verify_safety(verbose=False)
+ print(f" Collisions before resolution: {len(violations_before)}")
+
+ # Resolve
+ result = resolve_all_conflicts_safe(
+ env, timetable, priorities, train_infos,
+ max_iterations=50, verbose=True
+ )
+
+ # Final check
+ print("\n FINAL VERIFICATION:")
+ verifier = SafetyVerifier(timetable, max_steps=150)
+ verifier.verify_safety(verbose=True)
+
+ return result
+
+
+# ============== ENTRY POINT ==============
+
+if __name__ == "__main__":
+ import sys
+
+ if len(sys.argv) > 1:
+ # Pass a .pkl path as argument to test a loaded map
+ result = test_safe_resolution_loaded(sys.argv[1])
+ else:
+ # Default: run on the handbuilt corridor
+ result = test_safe_resolution_corridor()
+
+ if result:
+ print(f"\n\nFinal result: {'SUCCESS' if result.success else 'FAILED'}")
\ No newline at end of file
diff --git a/usecases_examples/Railway/scenarios.py b/usecases_examples/Railway/scenarios.py
new file mode 100644
index 00000000..4ab80423
--- /dev/null
+++ b/usecases_examples/Railway/scenarios.py
@@ -0,0 +1,221 @@
+"""
+run_scenarios.py — Test runner for loaded Flatland maps.
+"""
+
+import argparse
+import os
+from typing import Dict, List
+
+from Corridor_environment import load_corridor_env, build_timetable_from_loaded_env
+from Timetable import Timetable, TrainSchedule
+from TrainInfo import TrainInfo, TrainType, calculate_priority
+from SafetyVerifier import SafetyVerifier
+from safe_resolver import resolve_all_conflicts_safe
+from FlatlandMapLoader import visualize_loaded_env
+
+
+# ============================================================
+# SCENARIO RENDERER
+# ============================================================
+
+class ScenarioRenderer:
+ def __init__(self, env):
+ self.env = env
+ self.renderer = None
+ self.fig = None
+ self.ax = None
+ self.img_artist = None
+ self._available = False
+ self._init_renderer()
+
+ def _init_renderer(self):
+ try:
+ import matplotlib
+ matplotlib.use("TkAgg")
+ except Exception:
+ pass
+ try:
+ import matplotlib.pyplot as plt
+ from flatland.utils.rendertools import RenderTool
+ self.renderer = RenderTool(self.env, gl="PILSVG",
+ screen_width=800, screen_height=600)
+ plt.ion()
+ self.fig, self.ax = plt.subplots(figsize=(10, 7))
+ self.ax.axis("off")
+ self.fig.tight_layout(pad=0)
+ self._plt = plt
+ self._available = True
+ plt.show(block=False)
+ plt.pause(0.1)
+ print(" Renderer: matplotlib window ready.")
+ except Exception as e:
+ print(f" Renderer: unavailable ({e}). Using ASCII fallback.")
+
+ @property
+ def available(self):
+ return self._available
+
+ def render_frame(self, step: int, pause: float = 0.3):
+ if not self._available:
+ return
+ try:
+ image = self.renderer.render_env(
+ show=False, show_observations=False,
+ show_inactive_agents=True, show_rowcols=True,
+ return_image=True,
+ )
+ if image is None:
+ return
+ if self.img_artist is None:
+ self.img_artist = self.ax.imshow(image)
+ else:
+ self.img_artist.set_data(image)
+ self.ax.set_title(f"Step {step}", fontsize=11)
+ self.fig.canvas.draw()
+ self.fig.canvas.flush_events()
+ self._plt.pause(pause)
+ except Exception as e:
+ print(f" Render error at step {step}: {e}")
+ self._available = False
+
+ def close(self):
+ if self._available:
+ try:
+ self._plt.ioff()
+ self._plt.close(self.fig)
+ except Exception:
+ pass
+
+
+# ============================================================
+# SCENARIO DEFINITIONS
+# ============================================================
+
+def make_scenarios(env, stations, train_infos=None):
+ from copy import deepcopy
+ from Corridor_environment import build_timetable_from_loaded_env
+ from TrainInfo import TrainType, calculate_priority
+
+ base_timetable, base_train_infos, base_priorities = build_timetable_from_loaded_env(
+ env, stations, departure_offset=1, stagger_departures=True
+ )
+
+ def clone_timetable(base, agent_ids=None, new_departures=None):
+ t = deepcopy(base)
+ if agent_ids is not None:
+ for aid in list(t.schedules.keys()):
+ if aid not in agent_ids:
+ del t.schedules[aid]
+ t.priorities.pop(aid, None)
+ if new_departures:
+ for aid, dep in new_departures.items():
+ if aid in t.schedules:
+ s = t.schedules[aid]
+ route_len = len(s.route)
+ s.planned_departure = dep
+ s.planned_arrival = dep + route_len
+ s.original_planned_arrival = dep + route_len
+ return t
+
+ ids_all = sorted(base_timetable.schedules.keys())
+ agent_count = len(ids_all)
+ ti = base_train_infos
+ p = base_priorities
+
+ scenarios = []
+
+ # ── Scenario 1: junction wait — no random delays ──────────────────────
+ if agent_count >= 2:
+ a0, a_last = ids_all[0], ids_all[-1]
+ deps = {aid: 1 + i * 2 for i, aid in enumerate(ids_all)}
+ deps[a0] = 1
+ deps[a_last] = 1
+ scenarios.append({
+ 'name': 'Scenario 1: 4 trains, junction wait conflict',
+ 'description': (
+ f'Train {a0} departs step 1, Train {a_last} departs step 9, others staggered. '
+ 'Creates a junction waiting conflict that the resolver must handle. '
+ 'No random delays — pure conflict resolution.'
+ ),
+ 'timetable': clone_timetable(base_timetable, new_departures=deps),
+ 'train_infos': ti, 'priorities': p,
+ 'stagger_spawn': False,
+ 'enable_random_delays': False,
+ 'delay_probability': 0.0,
+ })
+
+ # ── Scenario 2: junction wait — with random delays ────────────────────
+ if agent_count >= 2:
+ a0, a_last = ids_all[0], ids_all[-1]
+ deps = {aid: 1 + i * 2 for i, aid in enumerate(ids_all)}
+ deps[a0] = 1
+ deps[a_last] = 9
+ scenarios.append({
+ 'name': 'Scenario 2: 4 trains, junction wait conflict with random delays',
+ 'description': (
+ 'Same as Scenario 1 but with random delay injection enabled. '
+ 'Each train has a 15% chance of a delay event (5–30 steps). '
+ 'Watch the Injected column in the schedule table.'
+ ),
+ 'timetable': clone_timetable(base_timetable, new_departures=deps),
+ 'train_infos': ti, 'priorities': p,
+ 'stagger_spawn': False,
+ 'enable_random_delays': True,
+ 'delay_probability': 0.15,
+ })
+
+ # ── Scenario 3: cancel button test ────────────────────────────────────
+ a0, a_last = ids_all[0], ids_all[-1]
+ deps = {aid: 1 + i * 2 for i, aid in enumerate(ids_all)}
+ deps[a_last] = 80
+
+ t3 = clone_timetable(base_timetable, new_departures=deps)
+ ti3 = deepcopy(ti)
+ if a0 in ti3:
+ ti3[a0].train_type = TrainType.PASSENGER_EXPRESS
+ ti3[a0].passenger_count = 400
+ if a_last in ti3:
+ ti3[a_last].train_type = TrainType.FREIGHT
+ ti3[a_last].passenger_count = 0
+ p3 = {aid: calculate_priority(ti3[aid]) for aid in ti3}
+ t3.priorities = p3
+
+ scenarios.append({
+ 'name': 'Scenario 3: cancel button test scenario',
+ 'description': (
+ f'Train {a0} is a high-priority express (dep=1). '
+ f'Train {a_last} is a freight train that departs very late (dep=80). '
+ f'Select Train {a_last} in the schedule table before step 80 '
+ f'and click Cancel Train to test live train removal and re-resolve.'
+ ),
+ 'timetable': t3, 'train_infos': ti3, 'priorities': p3,
+ 'stagger_spawn': False,
+ 'enable_random_delays': False,
+ 'delay_probability': 0.0,
+ })
+
+ # ── Scenario 4: Cooperative Learning Demo ─────────────────────────────
+ # Identical timetable to Scenario 1 (T0 dep=1, T_last dep=9) so the
+ # conflict is known and reproducible. The only difference: the user's
+ # manual resolution choice is saved to learned_resolutions.json and
+ # pre-applied automatically on every subsequent load.
+ if agent_count >= 2:
+ a0, a_last = ids_all[0], ids_all[-1]
+ deps = {aid: 1 + i * 2 for i, aid in enumerate(ids_all)}
+ deps[a0] = 1
+ deps[a_last] = 9
+ scenarios.append({
+ 'name': 'Scenario 4: Cooperative Learning Demo',
+ 'description': (
+ f'Same conflict as Scenario 1: Train {a0} dep=1, Train {a_last} dep=9. '
+ 'Switch to Manual mode and resolve the conflict — your choice is '
+ 'saved and replayed automatically every time you reload this scenario.'
+ ),
+ 'timetable': clone_timetable(base_timetable, new_departures=deps),
+ 'train_infos': deepcopy(ti), 'priorities': deepcopy(p),
+ 'stagger_spawn': False,
+ 'enable_random_delays': False,
+ 'delay_probability': 0.0,
+ })
+
+ return scenarios
\ No newline at end of file
diff --git a/usecases_examples/Railway/sessions.db b/usecases_examples/Railway/sessions.db
new file mode 100644
index 00000000..754ac165
Binary files /dev/null and b/usecases_examples/Railway/sessions.db differ