diff --git a/demos/smart_factory/.gitignore b/demos/smart_factory/.gitignore new file mode 100644 index 0000000..c5a750b --- /dev/null +++ b/demos/smart_factory/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +.env +.venv/ +venv/ +node_modules/ +# frontend/dist/ — intentionally tracked for Databricks Apps deployment +.databricks/ +*.egg-info/ +.DS_Store +uv.lock diff --git a/demos/smart_factory/Makefile b/demos/smart_factory/Makefile new file mode 100644 index 0000000..c55cc8a --- /dev/null +++ b/demos/smart_factory/Makefile @@ -0,0 +1,17 @@ +.PHONY: build-frontend deploy dev clean + +build-frontend: + cd frontend && npm ci && npm run build + +deploy: build-frontend + databricks bundle deploy -t dev + +deploy-prod: build-frontend + databricks bundle deploy -t prod + +dev: + cd frontend && npm run dev & + uvicorn app:app --reload --port 8000 + +clean: + rm -rf frontend/dist frontend/node_modules diff --git a/demos/smart_factory/README.md b/demos/smart_factory/README.md new file mode 100644 index 0000000..248a34f --- /dev/null +++ b/demos/smart_factory/README.md @@ -0,0 +1,152 @@ +# SmartFactory — IoT Streaming Demo + +A customer-facing demo showing how Databricks turns factory sensor data into actionable insights — from machine floor to decision — with no Kafka, no infrastructure to manage, and full governance out of the box. + +## What This Demonstrates + +| Capability | Business Value | +|---|---| +| **ZeroBus Ingest** | Eliminate Kafka and message bus infrastructure. Sensor data flows directly into governed Delta tables. | +| **SDP Streaming Pipeline** | Catch equipment anomalies as they happen, not in tomorrow's batch report. Continuous, serverless, pure SQL. | +| **ML in the Pipeline** | Every sensor reading scored for anomalies inline — predictive maintenance without a separate ML platform. | +| **Live Operations Dashboard** | Plant managers and technicians see machine health the moment it changes. Faster response, less downtime. | +| **Unity Catalog Governance** | Every table governed from the first byte. Lineage, access control, audit — ready for compliance on day one. | +| **Databricks Apps** | Full-stack app deployed and managed by Databricks. No separate hosting. | +| **DABs** | Entire demo — app, pipeline, dashboard — deploys with a single command. | + +## Architecture + +![Architecture](docs/architecture.png) + +## The Machines + +| Machine | Sensors | Fault Scenario | +|---|---|---| +| **CNC Mill** | Temperature, Vibration, Spindle RPM | Overheating, vibration spike | +| **Hydraulic Press** | Pressure, Temperature, Cycle Count | Pressure surge, cycle slowdown | +| **Conveyor Belt** | Belt Speed, Load Weight, Motor Current | Speed drop, overcurrent | + +## Demo Story (6 minutes) + +> **Pre-flight**: Start streaming + pipeline 30s before presenting. Confirm dashboard has data. + +### Act 1 — "This is your factory" (IoT Simulation tab) +> "3 machines, IoT sensors streaming every 2 seconds, directly into Databricks. No Kafka." + +- Gauges are already updating, event feed scrolling +- Point out "Streaming" and "Pipeline Running" in the header +- Expand ZeroBus info panel — highlight ≤200ms ack, 10 GB/s, Joby Aviation quote + +### Act 2 — "Here's the pipeline" (SDP in Databricks UI) +> "Declarative SQL. Streaming and batch in one pipeline. Fully serverless." + +- Switch to Databricks workspace, open the SDP pipeline DAG +- Show Bronze → Silver → Gold with streaming indicators +- Click into Silver SQL — "anomaly detection is a SQL JOIN. Any SQL developer can own this." +- Three SDP benefits: declarative, streaming+batch unified, serverless + +### Act 3 — "Let's break something" (Inject a fault) +> "Watch what happens when the CNC Mill starts overheating." + +- Click **Fault: CNC Mill** — watch temperature climb, gauges go red +- Event feed lights up with warnings and criticals +- Switch to **Operations Dashboard** — health scores dropping, anomaly log filling + +### Act 4 — "Everything is governed" (Unity Catalog) +> "Every table governed. Full lineage from raw sensor event to dashboard." + +- Open Catalog Explorer, click Gold table → show lineage graph +- "One command to deploy. No Kafka. No ML infrastructure. Just push and go." + +### Act 5 — "Clear the fault" (Resolution) +- Click **Clear All** — readings normalize, health scores recover + +See [docs/demo-script.md](docs/demo-script.md) for the full script with talking points and objection handling. + +## Quick Start + +### Prerequisites +- [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) v0.288+ installed +- CLI profile configured for your target workspace +- Node.js 18+ and npm installed +- An existing Unity Catalog catalog with cloud storage configured + +### One-command setup +```bash +git clone +cd smartfactory-demo +./setup.sh +``` + +This script handles everything: +1. Finds and starts a SQL warehouse +2. Creates the `smartfactory` schema and landing table +3. Builds the React frontend +4. Deploys all resources via DABs (app, pipeline, dashboard) +5. Detects the app service principal and grants all permissions +6. Starts the app and deploys code +7. Sets the SDP pipeline to continuous mode + +### After setup +1. Open the app URL printed by the script +2. Click **Streaming** in the header to start the data simulator +3. Click **Start Pipeline** to begin continuous SDP processing +4. Switch between **IoT Simulation** and **Operations Dashboard** tabs +5. Inject faults and watch anomalies flow through the pipeline + +> **Important: When you're done demoing, stop the streaming and pipeline!** +> Both consume compute resources. Click **Streaming** (to pause) and **Pipeline Running** (to stop) in the header bar. +> The simulator and pipeline start paused by default — you must manually start them each demo session. + +### Redeploying after code changes +```bash +cd frontend && npm run build && cd .. +databricks bundle deploy -t dev +databricks apps deploy smartfactory-app \ + --source-code-path /Workspace/Users//.bundle/smartfactory-demo/dev/files +``` + +## Project Structure + +``` +smartfactory-demo/ +├── setup.sh # One-command setup script +├── databricks.yml # DABs bundle (app + pipeline + dashboard) +├── app.yaml # Databricks App config +├── app.py # FastAPI backend (WebSocket + REST + pipeline control) +├── simulator.py # 3-machine IoT sensor simulator with fault injection +├── zerobus_client.py # ZeroBus SDK wrapper with SQL INSERT fallback +├── pipeline/ +│ ├── bronze.sql # Validated ingestion (streaming table) +│ ├── silver.sql # Anomaly scoring via threshold JOIN (streaming table) +│ └── gold.sql # Health KPIs + anomaly timeline (materialized views) +├── frontend/ +│ ├── src/ +│ │ ├── App.tsx # Tabbed layout (IoT Simulation + Dashboard) +│ │ ├── components/ +│ │ │ ├── FactoryFloor # SVG machine visuals with live sensor readouts +│ │ │ ├── MachineCard # Per-machine gauge cards +│ │ │ ├── SensorGauge # Circular SVG gauge component +│ │ │ ├── ControlPanel # Fault injection buttons +│ │ │ ├── EventFeed # Live scrolling event log +│ │ │ ├── DashboardView # Charts, KPI tables, anomaly log +│ │ │ └── PipelineBanner# Pipeline flow + UC governance badge +│ │ └── hooks/ +│ │ └── useWebSocket # Auto-reconnecting WebSocket hook +│ └── dist/ # Pre-built frontend (deployed with app) +├── dashboard.lvdash.json # Lakeview dashboard definition +└── CLAUDE.md # Development notes and known issues +``` + +## Tech Stack + +| Layer | Technology | +|---|---| +| Frontend | React 18, TypeScript, Vite, TailwindCSS, Recharts | +| Backend | FastAPI, Uvicorn, WebSocket | +| Ingestion | ZeroBus SDK (with SQL INSERT fallback) | +| Pipeline | SDP (Spark Declarative Pipelines), serverless | +| ML | SQL threshold-based anomaly detection in SDP Silver layer | +| Governance | Unity Catalog (lineage, access control, audit) | +| Deployment | Databricks Asset Bundles (DABs) | +| Dashboard | Lakeview (AI/BI) + in-app React dashboard | diff --git a/demos/smart_factory/app.yaml b/demos/smart_factory/app.yaml new file mode 100644 index 0000000..7408e48 --- /dev/null +++ b/demos/smart_factory/app.yaml @@ -0,0 +1,19 @@ +command: ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"] + +env: + - name: CATALOG_NAME + value: dilan_catalog + - name: LANDING_SCHEMA + value: smartfactory + - name: ZEROBUS_TABLE + value: dilan_catalog.smartfactory.raw_sensor_events + - name: WAREHOUSE_ID + value: "01c7fe04f060528e" + - name: PIPELINE_SCHEMA + value: dev_dilan_patel_smartfactory + - name: PIPELINE_ID + value: "4b993ed3-336f-40b5-8bb7-55ff9f056707" + - name: ENABLE_SIMULATOR + value: "false" + - name: SIMULATOR_INTERVAL_MS + value: "1000" diff --git a/demos/smart_factory/dashboards/smartfactory.lvdash.json b/demos/smart_factory/dashboards/smartfactory.lvdash.json new file mode 100644 index 0000000..1fef5e1 --- /dev/null +++ b/demos/smart_factory/dashboards/smartfactory.lvdash.json @@ -0,0 +1,145 @@ +{ + "pages": [ + { + "name": "machine-health-overview", + "displayName": "Machine Health Overview", + "layout": [ + { + "widget": { + "name": "machine_health_scores", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "machine_summary", + "disaggregated": false + } + } + ], + "spec": { + "version": 2, + "widgetType": "bar", + "encodings": { + "x": { "fieldName": "machine_id", "displayName": "Machine" }, + "y": { "fieldName": "avg_health_score", "displayName": "Health Score" }, + "color": { "fieldName": "machine_type", "displayName": "Type" } + } + } + }, + "position": { "x": 0, "y": 0, "width": 3, "height": 2 } + }, + { + "widget": { + "name": "anomaly_counts", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "machine_summary", + "disaggregated": false + } + } + ], + "spec": { + "version": 2, + "widgetType": "bar", + "encodings": { + "x": { "fieldName": "machine_id", "displayName": "Machine" }, + "y": { "fieldName": "total_criticals", "displayName": "Critical Alerts" }, + "color": { "fieldName": "machine_id", "displayName": "Machine" } + } + } + }, + "position": { "x": 3, "y": 0, "width": 3, "height": 2 } + } + ] + }, + { + "name": "sensor-trends", + "displayName": "Sensor Trends", + "layout": [ + { + "widget": { + "name": "sensor_timeseries", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "enriched_events", + "disaggregated": false + } + } + ], + "spec": { + "version": 2, + "widgetType": "line", + "encodings": { + "x": { "fieldName": "timestamp", "displayName": "Time" }, + "y": { "fieldName": "value", "displayName": "Sensor Value" }, + "color": { "fieldName": "sensor_name", "displayName": "Sensor" } + } + } + }, + "position": { "x": 0, "y": 0, "width": 6, "height": 3 } + } + ] + }, + { + "name": "anomaly-log", + "displayName": "Anomaly Log", + "layout": [ + { + "widget": { + "name": "anomaly_table", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "anomaly_timeline", + "disaggregated": false + } + } + ], + "spec": { + "version": 2, + "widgetType": "table", + "encodings": { + "columns": [ + { "fieldName": "timestamp", "displayName": "Time" }, + { "fieldName": "machine_id", "displayName": "Machine" }, + { "fieldName": "sensor_name", "displayName": "Sensor" }, + { "fieldName": "value", "displayName": "Value" }, + { "fieldName": "anomaly_status", "displayName": "Status" }, + { "fieldName": "critical_threshold", "displayName": "Threshold" } + ] + } + } + }, + "position": { "x": 0, "y": 0, "width": 6, "height": 4 } + } + ] + } + ], + "datasets": [ + { + "name": "machine_summary", + "displayName": "Machine Summary", + "query": "SELECT * FROM dilan_catalog.smartfactory.gold.machine_summary" + }, + { + "name": "enriched_events", + "displayName": "Enriched Events", + "query": "SELECT * FROM dilan_catalog.smartfactory.silver.enriched_events WHERE timestamp > current_timestamp() - INTERVAL 1 HOUR ORDER BY timestamp DESC LIMIT 5000" + }, + { + "name": "anomaly_timeline", + "displayName": "Anomaly Timeline", + "query": "SELECT * FROM dilan_catalog.smartfactory.gold.anomaly_timeline LIMIT 500" + }, + { + "name": "health_kpis", + "displayName": "Health KPIs", + "query": "SELECT * FROM dilan_catalog.smartfactory.gold.machine_health_kpis" + } + ] +} diff --git a/demos/smart_factory/databricks.yml b/demos/smart_factory/databricks.yml new file mode 100644 index 0000000..644539d --- /dev/null +++ b/demos/smart_factory/databricks.yml @@ -0,0 +1,53 @@ +bundle: + name: smartfactory-demo + +variables: + warehouse_id: + description: "SQL Warehouse ID for dashboard queries" + catalog_name: + description: "Unity Catalog name" + default: dilan_catalog + +resources: + schemas: + smartfactory_schema: + name: smartfactory + catalog_name: ${var.catalog_name} + comment: "SmartFactory IoT demo — all pipeline tables" + + pipelines: + smartfactory_sdp: + name: "smartfactory-sdp-pipeline" + catalog: ${var.catalog_name} + target: smartfactory + serverless: true + libraries: + - file: + path: pipeline/bronze.sql + - file: + path: pipeline/silver.sql + - file: + path: pipeline/gold.sql + channel: PREVIEW + continuous: true + + apps: + smartfactory_app: + name: "smartfactory-app" + description: "SmartFactory IoT demo — ZeroBus + SDP streaming with anomaly detection" + source_code_path: . + + dashboards: + smartfactory_dashboard: + display_name: "SmartFactory Health Dashboard" + file_path: dashboards/smartfactory.lvdash.json + warehouse_id: ${var.warehouse_id} + +targets: + dev: + default: true + mode: development + workspace: + profile: fevm-classic-stable-w6wiaf + variables: + warehouse_id: "01c7fe04f060528e" diff --git a/demos/smart_factory/docs/architecture.mmd b/demos/smart_factory/docs/architecture.mmd new file mode 100644 index 0000000..2138f08 --- /dev/null +++ b/demos/smart_factory/docs/architecture.mmd @@ -0,0 +1,45 @@ +%%{init: { + "theme": "base", + "themeVariables": { + "background": "#0a0e17", + "mainBkg": "#111827", + "nodeBorder": "#1f2937", + "clusterBkg": "#0d1520", + "clusterBorder": "#3b82f6", + "titleColor": "#d1d5db", + "edgeLabelBackground": "#0a0e17", + "nodeTextColor": "#d1d5db", + "lineColor": "#374151", + "primaryColor": "#111827", + "primaryTextColor": "#d1d5db", + "primaryBorderColor": "#1f2937", + "fontFamily": "ui-monospace, SFMono-Regular, Menlo, monospace", + "fontSize": "13px" + } +}}%% +flowchart LR + classDef appBox fill:#111827,stroke:#3b82f6,stroke-width:2px,color:#d1d5db + classDef rawBox fill:#111827,stroke:#1f2937,stroke-width:1px,color:#9ca3af + classDef bronzeBox fill:#1c1409,stroke:#f59e0b,stroke-width:2px,color:#f59e0b + classDef silverBox fill:#130d1f,stroke:#8b5cf6,stroke-width:2px,color:#8b5cf6 + classDef goldBox fill:#131008,stroke:#eab308,stroke-width:2px,color:#eab308 + classDef dashBox fill:#062016,stroke:#10b981,stroke-width:2px,color:#10b981 + classDef govBox fill:#0a0e17,stroke:#1f2937,stroke-width:1px,color:#9ca3af + + APP["🏭 Databricks App\nReact + FastAPI\n3 IoT Machines"]:::appBox + + APP -- "ZeroBus SDK\nNo Kafka" --> RAW + + subgraph UC ["🛡 Unity Catalog — Governed"] + RAW["📥 Landing\nraw_sensor_events"]:::rawBox + BRONZE["🟠 Bronze\nValidated"]:::bronzeBox + SILVER["🟣 Silver\nML Anomaly Scored"]:::silverBox + GOLD["🟡 Gold\nHealth KPIs"]:::goldBox + RAW --> BRONZE --> SILVER --> GOLD + end + + GOLD -- "SQL" --> DASH["📊 In-App Dashboard\nHealth · Anomalies · KPIs"]:::dashBox + + DASH -- "WebSocket" --> APP + + style UC fill:#0d1520,stroke:#3b82f6,stroke-width:2px,color:#d1d5db diff --git a/demos/smart_factory/docs/architecture.png b/demos/smart_factory/docs/architecture.png new file mode 100644 index 0000000..fd36d1f Binary files /dev/null and b/demos/smart_factory/docs/architecture.png differ diff --git a/demos/smart_factory/docs/demo-script.md b/demos/smart_factory/docs/demo-script.md new file mode 100644 index 0000000..5d6e944 --- /dev/null +++ b/demos/smart_factory/docs/demo-script.md @@ -0,0 +1,165 @@ +# SmartFactory Demo Guide +## 5-7 Minute Live Demo — ZeroBus + SDP Streaming + Unity Catalog + +**Audience**: Manufacturing/IoT decision makers, data leaders, platform engineers +**Goal**: Show how Databricks turns factory sensor data into actionable insights — catch anomalies as they happen, reduce downtime, and govern everything from day one. No Kafka, no infrastructure, one command to deploy. + +--- + +## Before You Start + +- [ ] App is open and on the **IoT Simulation** tab +- [ ] **Streaming is ON** — click "Streaming" so data is flowing +- [ ] **Pipeline is ON** — click "Start Pipeline" so SDP is processing +- [ ] **Wait ~30s** for data to flow through all layers +- [ ] Confirm **Operations Dashboard** tab has charts with data +- [ ] Switch back to **IoT Simulation** — this is your starting view +- [ ] Databricks workspace open in another tab (Catalog Explorer + Pipeline ready) +- [ ] All machines showing green/NORMAL — no faults injected yet + +--- + +## Act 1 — Set the Scene + +**Where**: IoT Simulation tab — gauges updating, event feed scrolling + +**Key points to hit**: +- 3 machines, each with IoT sensors, streaming every 2 seconds +- Data is already flowing into Databricks — point to the live gauges +- Streaming and pipeline indicators in the header show everything is live +- Transition: "Let me show you how this data gets here" + +**Personas**: Operations teams, plant floor staff — "the kind of data your operations teams deal with every day" + +--- + +## Act 2 — ZeroBus: Eliminate the Message Bus + +**Where**: Expand the ZeroBus info panel on the IoT Simulation tab + +**Key points to hit**: +- No message bus. Data pushes directly into governed Delta tables. No staging, no ETL, no waiting. +- Anything with an internet connection and a few lines of code can push data — just like this web app +- What this means: one less system to manage, one less team to hire, faster time to insight +- Bosch: 33% cost savings, 40% faster data transmission +- Joby Aviation: days of telemetry latency to minutes +- Scales effortlessly — thousands of devices, gigabytes per second, no infrastructure to tune + +**Personas**: **Platform engineers** (eliminate infra), **IoT developers** (simple SDK), **VP Data** (lower TCO) + +--- + +## Act 3 — The Pipeline (SDP) + +**Where**: Pipeline banner at the bottom of the factory floor + +**Key points to hit**: +- Walk the flow: Factory Floor → ZeroBus → Bronze → Silver (ML) → Gold → Dashboard +- Continuous pipeline — processes data as it arrives, no batch windows +- Silver layer scores every reading for anomalies — catch equipment issues before they become downtime +- Gold layer aggregates health KPIs for operations teams +- All in SQL. Serverless. No Spark expertise needed. + +**Personas**: **Data engineers** (build the pipeline), **SQL analysts** (can modify thresholds) + +--- + +## Act 3b — SDP Deep Dive + +**Where**: Switch to Databricks workspace — open the SDP pipeline + +**Key points to hit**: +- Show the pipeline DAG: Bronze → Silver → Gold with streaming indicators +- "Three things data engineers love about this": + 1. **Declarative** — SQL says what, Databricks handles how. No Spark code. + 2. **Streaming + batch unified** — same SQL for continuous processing and historical reprocessing + 3. **Fully serverless** — no clusters, auto-scales, pay per use +- ML runs inline in Silver — no separate ML platform, no serving endpoints +- Real-time streaming pipeline — no Flink, no stitching together five different tools +- Tie to business value: catching a bearing failure before it takes down a production line + +**Personas**: **Data engineers** (build), **SQL analysts** (contribute without Spark expertise) + +--- + +## Act 4 — Break Something + +**Where**: Back to the app — IoT Simulation tab + +**Key points to hit**: +- Inject a fault on the CNC Mill — click the fault button +- Gauges show raw sensor data drifting from green → amber → red +- Switch to **Operations Dashboard** — this is what the pipeline produced from that raw data +- Sensor trends spike, health scores drop, anomaly log fills with ML-scored anomalies +- "This isn't a batch report. Your maintenance team responds in minutes, not the next morning." + +**Personas**: **Plant managers / reliability engineers** (see the dashboard), **maintenance technicians** (get alerted immediately), **operations analysts** (drill into the anomaly log) + +--- + +## Act 5 — Governance + +**Where**: Databricks workspace — Catalog Explorer + +**Key points to hit**: +- Every table governed from the moment data lands — no "ingest first, govern later" +- Click a Gold table → show lineage back to the raw sensor landing table +- Role-based access control, audit logs for every query +- "When someone asks 'where did this number come from?' — one click." + +**Personas**: **Data governance / compliance** (lineage, audit), **IT security** (RBAC, encryption) + +--- + +## Act 6 — The Close + +**Where**: Back to the app + +**Key points to hit**: +- Clear the fault — show readings normalize, health recovers +- Rule of three: + 1. **No message bus** — sensor data straight to governed Delta tables. Entire infrastructure layer eliminated. + 2. **ML and analytics in one pipeline, real time** — 30-50% less unplanned downtime when moving from batch to real-time detection + 3. **Governed from day one** — lineage, access control, audit. Compliance covered. +- Closer: "Databricks brings it all together — ingestion, intelligence, and governance — so your teams can focus on outcomes, not infrastructure." + +--- + +## Objection Handling + +### "We already have Kafka / Confluent" +It works — but it's a separate system to manage. ZeroBus collapses that layer. Data goes straight to Delta. Fewer moving parts, lower cost. Bosch saw 33% cost savings. + +### "Our ML team uses Python models, not SQL thresholds" +This demo uses SQL for simplicity, but the Silver layer can call any MLflow model registered in Unity Catalog — Isolation Forest, XGBoost, neural nets. You can also use `ai_query()` to hit a serving endpoint directly from the pipeline. Same SDP, any model. + +### "How does this compare to AWS IoT / Azure IoT Hub?" +Those get data into the cloud. ZeroBus gets data into your lakehouse — directly into governed Delta tables. No intermediate storage, no ETL, no separate governance. Plus you get time travel, schema evolution, and ACID out of the box. + +### "What about edge processing?" +ZeroBus handles cloud-side ingestion. For edge, pair it with your existing gateway — Greengrass, IoT Edge, or custom. The ZeroBus SDKs run anywhere your edge compute does. + +### "What's the cost?" +$0.05 per GB ingested. A factory streaming 1,000 sensors at 1 reading/sec generates ~2-3 GB/day. That's about $0.15/day for real-time ingestion into governed Delta tables. + +### "Is this production-ready?" +ZeroBus is GA as of February 2026. Toyota and Joby Aviation run production IoT workloads on it. SDP is GA and serverless. Unity Catalog governs thousands of production environments. + +--- + +## Power Moves (Extra Time) + +### Show UC Lineage Graph +Catalog Explorer → Gold table → Lineage tab → full graph from landing to dashboard. + +### Multi-Machine Chaos +Fault all three machines at once. Dashboard becomes a sea of red. Clear them one by one. + +### Show the SQL +Open `pipeline/silver.sql` — the anomaly detection is a SQL JOIN. Simplicity is the point. + +### Show the Bundle Config +Open `databricks.yml` — entire infrastructure in ~50 lines of YAML. + +### Show the Setup Script +`setup.sh` — one script, new workspace, full demo running in 5 minutes. diff --git a/demos/smart_factory/frontend/index.html b/demos/smart_factory/frontend/index.html new file mode 100644 index 0000000..8cd938b --- /dev/null +++ b/demos/smart_factory/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + SmartFactory - IoT Monitoring + + + +
+ + + diff --git a/demos/smart_factory/frontend/package-lock.json b/demos/smart_factory/frontend/package-lock.json new file mode 100644 index 0000000..1592bb4 --- /dev/null +++ b/demos/smart_factory/frontend/package-lock.json @@ -0,0 +1,3037 @@ +{ + "name": "smartfactory-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smartfactory-frontend", + "version": "0.1.0", + "dependencies": { + "lucide-react": "^0.400.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.12.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "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/generator": { + "version": "7.29.1", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@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-globals": { + "version": "7.28.0", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://npm-proxy.dev.databricks.com/@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.28.5", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://npm-proxy.dev.databricks.com/@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/helpers": { + "version": "7.29.2", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "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-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "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/runtime": { + "version": "7.29.2", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://npm-proxy.dev.databricks.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://npm-proxy.dev.databricks.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/@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/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://npm-proxy.dev.databricks.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://npm-proxy.dev.databricks.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://npm-proxy.dev.databricks.com/@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://npm-proxy.dev.databricks.com/@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://npm-proxy.dev.databricks.com/@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/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://npm-proxy.dev.databricks.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://npm-proxy.dev.databricks.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://npm-proxy.dev.databricks.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://npm-proxy.dev.databricks.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://npm-proxy.dev.databricks.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://npm-proxy.dev.databricks.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://npm-proxy.dev.databricks.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://npm-proxy.dev.databricks.com/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://npm-proxy.dev.databricks.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://npm-proxy.dev.databricks.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/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/arg": { + "version": "5.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://npm-proxy.dev.databricks.com/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "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.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.9", + "resolved": "https://npm-proxy.dev.databricks.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/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/braces": { + "version": "3.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/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.28.1", + "resolved": "https://npm-proxy.dev.databricks.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://npm-proxy.dev.databricks.com/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://npm-proxy.dev.databricks.com/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/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/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/clsx": { + "version": "2.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/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/csstype": { + "version": "3.2.3", + "resolved": "https://npm-proxy.dev.databricks.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://npm-proxy.dev.databricks.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://npm-proxy.dev.databricks.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://npm-proxy.dev.databricks.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://npm-proxy.dev.databricks.com/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://npm-proxy.dev.databricks.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://npm-proxy.dev.databricks.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://npm-proxy.dev.databricks.com/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://npm-proxy.dev.databricks.com/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/fastq": { + "version": "1.20.1", + "resolved": "https://npm-proxy.dev.databricks.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/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/fraction.js": { + "version": "5.3.4", + "resolved": "https://npm-proxy.dev.databricks.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://npm-proxy.dev.databricks.com/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://npm-proxy.dev.databricks.com/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://npm-proxy.dev.databricks.com/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/glob-parent": { + "version": "6.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/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/hasown": { + "version": "2.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/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/internmap": { + "version": "2.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/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://npm-proxy.dev.databricks.com/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-extglob": { + "version": "2.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/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-number": { + "version": "7.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://npm-proxy.dev.databricks.com/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://npm-proxy.dev.databricks.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://npm-proxy.dev.databricks.com/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://npm-proxy.dev.databricks.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://npm-proxy.dev.databricks.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/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/lucide-react": { + "version": "0.400.0", + "resolved": "https://npm-proxy.dev.databricks.com/lucide-react/-/lucide-react-0.400.0.tgz", + "integrity": "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://npm-proxy.dev.databricks.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://npm-proxy.dev.databricks.com/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/ms": { + "version": "2.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://npm-proxy.dev.databricks.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://npm-proxy.dev.databricks.com/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/node-releases": { + "version": "2.0.36", + "resolved": "https://npm-proxy.dev.databricks.com/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://npm-proxy.dev.databricks.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/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/pify": { + "version": "2.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://npm-proxy.dev.databricks.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://npm-proxy.dev.databricks.com/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "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://npm-proxy.dev.databricks.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://npm-proxy.dev.databricks.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://npm-proxy.dev.databricks.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://npm-proxy.dev.databricks.com/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/react": { + "version": "18.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://npm-proxy.dev.databricks.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://npm-proxy.dev.databricks.com/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://npm-proxy.dev.databricks.com/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://npm-proxy.dev.databricks.com/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/recharts": { + "version": "2.15.4", + "resolved": "https://npm-proxy.dev.databricks.com/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://npm-proxy.dev.databricks.com/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://npm-proxy.dev.databricks.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "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/reusify": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/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/rollup": { + "version": "4.59.0", + "resolved": "https://npm-proxy.dev.databricks.com/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/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/scheduler": { + "version": "0.23.2", + "resolved": "https://npm-proxy.dev.databricks.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/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/sucrase": { + "version": "3.35.1", + "resolved": "https://npm-proxy.dev.databricks.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/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/tailwindcss": { + "version": "3.4.19", + "resolved": "https://npm-proxy.dev.databricks.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://npm-proxy.dev.databricks.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://npm-proxy.dev.databricks.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://npm-proxy.dev.databricks.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://npm-proxy.dev.databricks.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/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/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://npm-proxy.dev.databricks.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://npm-proxy.dev.databricks.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://npm-proxy.dev.databricks.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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://npm-proxy.dev.databricks.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://npm-proxy.dev.databricks.com/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://npm-proxy.dev.databricks.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/demos/smart_factory/frontend/package.json b/demos/smart_factory/frontend/package.json new file mode 100644 index 0000000..4a4b74d --- /dev/null +++ b/demos/smart_factory/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "smartfactory-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.12.0", + "lucide-react": "^0.400.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } +} diff --git a/demos/smart_factory/frontend/postcss.config.js b/demos/smart_factory/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/demos/smart_factory/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/demos/smart_factory/frontend/src/App.tsx b/demos/smart_factory/frontend/src/App.tsx new file mode 100644 index 0000000..21629be --- /dev/null +++ b/demos/smart_factory/frontend/src/App.tsx @@ -0,0 +1,253 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useWebSocket } from "./hooks/useWebSocket"; +import FactoryFloor from "./components/FactoryFloor"; +import MachineCard from "./components/MachineCard"; +import EventFeed from "./components/EventFeed"; +import ControlPanel from "./components/ControlPanel"; +import DashboardView from "./components/DashboardView"; +import PipelineBanner from "./components/PipelineBanner"; +import ZeroBusInfo from "./components/ZeroBusInfo"; +import { + Activity, Wifi, WifiOff, Factory, BarChart3, + Play, Square, Loader2, Radio, CircleOff, +} from "lucide-react"; + +type Tab = "factory" | "dashboard"; + +export default function App() { + const { + machines, + latestReadings, + faultStates, + eventLog, + totalEvents, + eventsPerSec, + connectionStatus, + injectFault, + clearFault, + clearAllFaults, + } = useWebSocket(); + + const [activeTab, setActiveTab] = useState("factory"); + const [pipelineState, setPipelineState] = useState("UNKNOWN"); + const [pipelineLoading, setPipelineLoading] = useState(false); + const [simRunning, setSimRunning] = useState(true); + const [simLoading, setSimLoading] = useState(false); + + const machineIds = Object.keys(machines); + const hasData = machineIds.length > 0; + + // Poll pipeline + simulator status + const fetchStatus = useCallback(async () => { + try { + const [pRes, sRes] = await Promise.all([ + fetch("/api/pipeline/status"), + fetch("/api/simulator/status"), + ]); + const pData = await pRes.json(); + const sData = await sRes.json(); + setPipelineState(pData.state || "UNKNOWN"); + setSimRunning(sData.running ?? true); + } catch {} + }, []); + + useEffect(() => { + fetchStatus(); + const interval = setInterval(fetchStatus, 8000); + return () => clearInterval(interval); + }, [fetchStatus]); + + const handlePipelineToggle = async () => { + setPipelineLoading(true); + const isRunning = pipelineState === "RUNNING"; + try { + await fetch(`/api/pipeline/${isRunning ? "stop" : "start"}`, { method: "POST" }); + setTimeout(fetchStatus, 2000); + setTimeout(fetchStatus, 5000); + setTimeout(fetchStatus, 10000); + } catch {} + setPipelineLoading(false); + }; + + const handleSimToggle = async () => { + setSimLoading(true); + try { + await fetch(`/api/simulator/${simRunning ? "stop" : "start"}`, { method: "POST" }); + setTimeout(fetchStatus, 1000); + } catch {} + setSimLoading(false); + }; + + const pipelineRunning = pipelineState === "RUNNING"; + + return ( +
+ {/* Header */} +
+
+
+ +

SmartFactory

+ + IoT Demo + +
+ + {/* Tabs */} +
+ + +
+ +
+ {/* Simulator Control */} + + + {/* Pipeline Control */} + + + {/* Connection Status */} +
+ {connectionStatus === "connected" ? ( + <> + + Live + + ) : connectionStatus === "connecting" ? ( + <> + + Connecting + + ) : ( + <> + + Disconnected + + )} +
+
+
+
+ + {/* Main Content */} +
+ {/* Factory Floor — always mounted, hidden via CSS */} +
+ {!hasData ? ( +
+
+ +

+ Connecting to factory sensors... +

+
+
+ ) : ( +
+ + +
+
+
+ {machineIds.map((machineId) => ( + + ))} +
+ +
+
+ +
+
+
+ )} +
+ + {/* Dashboard — always mounted, hidden via CSS */} +
+ +
+
+
+ ); +} diff --git a/demos/smart_factory/frontend/src/components/ControlPanel.tsx b/demos/smart_factory/frontend/src/components/ControlPanel.tsx new file mode 100644 index 0000000..f6bef49 --- /dev/null +++ b/demos/smart_factory/frontend/src/components/ControlPanel.tsx @@ -0,0 +1,88 @@ +import React, { useState } from "react"; +import { MachineConfig } from "../types"; +import { Zap, ZapOff, RotateCcw, Trash2, Loader2 } from "lucide-react"; + +interface ControlPanelProps { + machines: Record; + faultStates: Record; + onInjectFault: (machineId: string) => void; + onClearFault: (machineId: string) => void; + onClearAll: () => void; +} + +export default function ControlPanel({ + machines, + faultStates, + onInjectFault, + onClearFault, + onClearAll, +}: ControlPanelProps) { + const anyFault = Object.values(faultStates).some(Boolean); + const [resetting, setResetting] = useState(false); + + const handleReset = async () => { + if (!confirm("Reset all demo data? This clears all tables for a fresh start.")) return; + setResetting(true); + try { + await fetch("/api/reset-data", { method: "POST" }); + } catch {} + setResetting(false); + }; + + return ( +
+
+

+ Fault Injection +

+
+ + +
+
+
+ {Object.entries(machines).map(([machineId, config]) => { + const isFaulting = faultStates[machineId] || false; + return ( + + ); + })} +
+
+ ); +} diff --git a/demos/smart_factory/frontend/src/components/DashboardView.tsx b/demos/smart_factory/frontend/src/components/DashboardView.tsx new file mode 100644 index 0000000..06fc9d7 --- /dev/null +++ b/demos/smart_factory/frontend/src/components/DashboardView.tsx @@ -0,0 +1,379 @@ +import React, { useEffect, useState, useCallback } from "react"; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + LineChart, Line, Legend, Cell, +} from "recharts"; +import { RefreshCw, AlertTriangle, Activity, Database, BrainCircuit } from "lucide-react"; +import PipelineBanner from "./PipelineBanner"; + +interface HealthRow { + machine_id: string; + machine_type: string; + avg_health_score: string; + total_criticals: string; + total_warnings: string; + worst_sensor_health: string; + last_activity: string; +} + +interface KpiRow { + machine_id: string; + sensor_name: string; + avg_value: string; + max_value: string; + min_value: string; + health_score: string; + critical_count: string; + warning_count: string; + total_readings: string; +} + +interface AnomalyRow { + machine_id: string; + sensor_name: string; + value: string; + anomaly_status: string; + timestamp: string; + unit: string; +} + +interface TrendRow { + machine_id: string; + sensor_name: string; + value: string; + anomaly_status: string; + timestamp: string; +} + +const STATUS_COLORS: Record = { + CRITICAL: "#ef4444", + WARNING: "#f59e0b", + NORMAL: "#10b981", +}; + +const MACHINE_COLORS = ["#3b82f6", "#8b5cf6", "#06b6d4"]; + +export default function DashboardView({ totalEvents }: { totalEvents?: number }) { + const [health, setHealth] = useState([]); + const [kpis, setKpis] = useState([]); + const [anomalies, setAnomalies] = useState([]); + const [trends, setTrends] = useState([]); + const [landingCount, setLandingCount] = useState(0); + const [loading, setLoading] = useState(false); + const [lastRefresh, setLastRefresh] = useState(null); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const [hRes, kRes, aRes] = await Promise.all([ + fetch("/api/dashboard/health"), + fetch("/api/dashboard/kpis"), + fetch("/api/dashboard/anomalies"), + ]); + const newHealth = await hRes.json(); + const newKpis = await kRes.json(); + const newAnomalies = await aRes.json(); + if (Array.isArray(newHealth) && newHealth.length > 0) setHealth(newHealth); + if (Array.isArray(newKpis) && newKpis.length > 0) setKpis(newKpis); + if (Array.isArray(newAnomalies)) setAnomalies(newAnomalies); + setLastRefresh(new Date()); + } catch (e) { + console.error("Dashboard fetch error:", e); + } + setLoading(false); + }, [landingCount]); + + const fetchTrends = useCallback(async () => { + try { + const res = await fetch("/api/dashboard/trends"); + const data = await res.json(); + if (Array.isArray(data) && data.length > 0) setTrends(data); + } catch {} + }, []); + + useEffect(() => { + fetchData(); + const mainInterval = setInterval(fetchData, 5000); + const trendsInterval = setInterval(fetchTrends, 3000); + return () => { clearInterval(mainInterval); clearInterval(trendsInterval); }; + }, [fetchData, fetchTrends]); + + // Health bar chart data + const healthChartData = health.map((h) => ({ + machine: h.machine_id.replace("_01", "").replace(/_/g, " "), + "Health Score": Number(h.avg_health_score), + Criticals: Number(h.total_criticals), + Warnings: Number(h.total_warnings), + })); + + // Sensor unit lookup + const SENSOR_UNITS: Record = { + temperature_c: "°C", vibration_mm_s: "mm/s", spindle_rpm: "RPM", + pressure_bar: "bar", cycle_count: "cpm", + speed_m_min: "m/min", load_weight_kg: "kg", motor_current_a: "A", + }; + + // KPI data for sensor breakdown + const kpiChartData = kpis.map((k) => ({ + name: `${k.machine_id.split("_")[0]} / ${k.sensor_name}`, + machine: k.machine_id, + sensor: k.sensor_name, + unit: SENSOR_UNITS[k.sensor_name] || "", + avg: Number(k.avg_value), + max: Number(k.max_value), + min: Number(k.min_value), + health: Number(k.health_score), + criticals: Number(k.critical_count), + warnings: Number(k.warning_count), + readings: Number(k.total_readings), + })); + + return ( +
+ {/* Compact stats bar */} +
+
+ } label="Events" value={(totalEvents ?? landingCount).toLocaleString()} color="text-blue-400" /> + } label="Machines" value={String(health.length)} color="text-emerald-400" /> + } label="Anomalies" value={String(anomalies.length)} color="text-purple-400" /> +
+
+ + {lastRefresh ? lastRefresh.toLocaleTimeString() : "—"} + + +
+
+ + {/* Sensor Trends — full width, top position */} +
+

+ Sensor Trends — Live +

+ + { + const byTime: Record> = {}; + [...trends].reverse().forEach((t) => { + const time = new Date(t.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + const key = `${t.machine_id.split("_")[0]}-${t.sensor_name}`; + if (!byTime[time]) byTime[time] = {}; + byTime[time][key] = Number(t.value); + }); + return Object.entries(byTime).map(([time, vals]) => ({ time, ...vals })); + })()} + > + + + + + + + + + + + + +
+ + {/* Two-column layout: Health (left) | Anomalies (right) */} +
+ {/* LEFT: Health */} +
+ {/* Health Scores Chart */} +
+

+ Machine Health Scores +

+ + + + + + + + {healthChartData.map((entry, i) => ( + 70 + ? "#10b981" + : entry["Health Score"] > 40 + ? "#f59e0b" + : "#ef4444" + } + /> + ))} + + + +
+ + {/* Sensor Health Table (condensed) */} +
+

+ Sensor Breakdown +

+ + + + + + + + + + + + {kpiChartData.map((row, i) => ( + + + + + + + + ))} + +
MachineSensorAvgMaxHealth
{row.machine.replace("_01", "")}{row.sensor}{row.avg} {row.unit}{row.max} {row.unit} + 70 ? "text-emerald-400" : row.health > 40 ? "text-amber-400" : "text-red-400" + }`}> + {row.health} + +
+
+
+ + {/* RIGHT: Anomalies */} +
+ {/* Anomaly Counts Chart */} +
+
+

+ Anomaly Counts +

+ + + ML in SDP + +
+ + + + + + + + + + + +
+ + {/* Anomaly Log */} +
+
+

+ Anomaly Log +

+ + + Real-time + +
+
+ + + + + + + + + + + + {anomalies.length === 0 && ( + + + + )} + {anomalies.map((a, i) => ( + + + + + + + + ))} + +
TimeMachineSensorValueStatus
+ No anomalies yet +
+ {new Date(a.timestamp).toLocaleTimeString()} + + {a.machine_id.replace("_01", "").replace(/_/g, " ")} + + {a.sensor_name} + + {Number(a.value).toFixed(1)} {a.unit} + + + {a.anomaly_status} + +
+
+
+
+
+ + {/* Pipeline Banner */} + +
+ ); +} + +function MiniStat({ + icon, + label, + value, + color, +}: { + icon: React.ReactNode; + label: string; + value: string; + color: string; +}) { + return ( +
+ {icon} + {label} + {value} +
+ ); +} + diff --git a/demos/smart_factory/frontend/src/components/EventFeed.tsx b/demos/smart_factory/frontend/src/components/EventFeed.tsx new file mode 100644 index 0000000..33e76b2 --- /dev/null +++ b/demos/smart_factory/frontend/src/components/EventFeed.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { SensorReading, SensorConfig, getAnomalyStatus, AnomalyStatus } from "../types"; +import { MachineConfig } from "../types"; +import { AlertTriangle, AlertCircle } from "lucide-react"; + +interface EventFeedProps { + events: SensorReading[]; + machines: Record; +} + +const anomalyBadge: Record = { + NORMAL: null, + WARNING: { + icon: , + cls: "text-amber-400", + }, + CRITICAL: { + icon: , + cls: "text-red-400", + }, +}; + +const SENSOR_SHORT: Record = { + temperature_c: "TEMP", + vibration_mm_s: "VIBR", + spindle_rpm: "RPM", + pressure_bar: "PRES", + cycle_count: "CYCL", + speed_m_min: "SPEE", + load_weight_kg: "LOAD", + motor_current_a: "CURR", +}; + +export default function EventFeed({ events, machines }: EventFeedProps) { + return ( +
+

+ Live Event Feed +

+
+ {events.length === 0 && ( +

+ Waiting for sensor data... +

+ )} + {events.map((event, i) => { + const machineConfig = machines[event.machine_id]; + const sensorConfig = machineConfig?.sensors?.[event.sensor_name]; + const status = sensorConfig + ? getAnomalyStatus(event.value, sensorConfig) + : "NORMAL"; + const badge = anomalyBadge[status]; + const time = new Date(event.timestamp).toLocaleTimeString(); + + return ( +
+ + {time} + + + {SENSOR_SHORT[event.sensor_name] || event.sensor_name.slice(0, 4).toUpperCase()} + + + {machineConfig?.display_name || event.machine_id} + + + {event.value.toFixed(1)} + + + {event.unit} + + {badge && ( + {badge.icon} + )} +
+ ); + })} +
+
+ ); +} diff --git a/demos/smart_factory/frontend/src/components/FactoryFloor.tsx b/demos/smart_factory/frontend/src/components/FactoryFloor.tsx new file mode 100644 index 0000000..7c966ce --- /dev/null +++ b/demos/smart_factory/frontend/src/components/FactoryFloor.tsx @@ -0,0 +1,274 @@ +import React from "react"; +import { MachineConfig, SensorReading, getAnomalyStatus, AnomalyStatus } from "../types"; +import { Radio, Shield, BrainCircuit } from "lucide-react"; + +interface FactoryFloorProps { + machines: Record; + readings: Record>; + faultStates: Record; + totalEvents: number; + eventsPerSec: number; +} + +function getMachineOverallStatus( + config: MachineConfig, + readings: Record | undefined +): AnomalyStatus { + if (!readings) return "NORMAL"; + let worst: AnomalyStatus = "NORMAL"; + for (const [sensorName, sensorCfg] of Object.entries(config.sensors)) { + const reading = readings[sensorName]; + if (!reading) continue; + const status = getAnomalyStatus(reading.value, sensorCfg); + if (status === "CRITICAL") return "CRITICAL"; + if (status === "WARNING") worst = "WARNING"; + } + return worst; +} + +const statusColors: Record = { + NORMAL: { fill: "#10b981", glow: "#10b98133", ring: "#10b98177", bg: "#10b98110" }, + WARNING: { fill: "#f59e0b", glow: "#f59e0b44", ring: "#f59e0b88", bg: "#f59e0b15" }, + CRITICAL: { fill: "#ef4444", glow: "#ef444455", ring: "#ef4444aa", bg: "#ef444418" }, +}; + +const machineOrder = ["CNC_Mill_01", "Hydraulic_Press_01", "Conveyor_Belt_01"]; + +// Bigger, more detailed machine icons +const machineIcons: Record = { + cnc_mill: ( + + + + + + + + + + + ), + hydraulic_press: ( + + + + + + + + + + + + ), + conveyor_belt: ( + + + + + + + + + + {/* Packages on belt */} + + + ), +}; + +const SENSOR_SHORT: Record = { + temperature_c: "TEMP", + vibration_mm_s: "VIBR", + spindle_rpm: "RPM", + pressure_bar: "PRES", + cycle_count: "CYCL", + speed_m_min: "SPEED", + load_weight_kg: "LOAD", + motor_current_a: "CURR", +}; + +export default function FactoryFloor({ machines, readings, faultStates, totalEvents, eventsPerSec }: FactoryFloorProps) { + const ids = machineOrder.filter((id) => machines[id]); + + return ( +
+ {/* Header */} +
+
+

+ Factory Floor Simulation +

+ + Simulated IoT Devices + +
+
+
+ + Sensors streaming via ZeroBus +
+
+ {eventsPerSec} evt/s + | + {totalEvents.toLocaleString()} total +
+
+
+ + {/* Factory visualization */} +
+ {ids.map((machineId, i) => { + const config = machines[machineId]; + const machineReadings = readings[machineId]; + const status = getMachineOverallStatus(config, machineReadings); + const colors = statusColors[status]; + const isFaulting = faultStates[machineId] || false; + + return ( +
+ {/* Machine visual */} +
+ {/* Outer glow */} + + {/* Pulsing background */} + + {status !== "NORMAL" && ( + + )} + + + {/* Ring */} + + + {/* Sensor dots around the ring */} + {Object.entries(config.sensors).map(([sName, sCfg], si) => { + const angle = -90 + si * 120; + const rad = (angle * Math.PI) / 180; + const cx = Math.cos(rad) * 48; + const cy = Math.sin(rad) * 48; + const reading = machineReadings?.[sName]; + const sStatus = reading ? getAnomalyStatus(reading.value, sCfg) : "NORMAL"; + const sColor = statusColors[sStatus].fill; + return ( + + + + {sStatus !== "NORMAL" && ( + + )} + + + ); + })} + + {/* Machine icon */} + + {machineIcons[config.type]} + + + + {/* Fault indicator */} + {isFaulting && ( +
+ 🔥 +
+ )} +
+ + {/* Machine name */} +

+ {config.display_name} +

+ + {/* Status badge */} + + {status} + + + {/* Live sensor readouts */} +
+ {Object.entries(config.sensors).map(([sName, sCfg]) => { + const reading = machineReadings?.[sName]; + const val = reading?.value; + const sStatus = val !== undefined ? getAnomalyStatus(val, sCfg) : "NORMAL"; + const sColor = statusColors[sStatus].fill; + return ( +
+ {SENSOR_SHORT[sName]} + + {val !== undefined ? val.toFixed(1) : "—"}{" "} + {sCfg.unit} + +
+ ); + })} +
+
+ ); + })} +
+ + {/* Pipeline + UC banner */} +
+
+ +
+ +
+ + + + +
+
+ Silver + + ML + +
+ Anomaly Scored +
+ + + + +
+
+ +
+

Governed by Unity Catalog

+

End-to-end lineage, access control & audit

+
+
+
+
+ ); +} + +function PipelineNode({ label, sublabel, color }: { label: string; sublabel: string; color: string }) { + return ( +
+
{label}
+ {sublabel} +
+ ); +} + diff --git a/demos/smart_factory/frontend/src/components/MachineCard.tsx b/demos/smart_factory/frontend/src/components/MachineCard.tsx new file mode 100644 index 0000000..bedbdc2 --- /dev/null +++ b/demos/smart_factory/frontend/src/components/MachineCard.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { MachineConfig, SensorReading, getAnomalyStatus, AnomalyStatus } from "../types"; +import SensorGauge from "./SensorGauge"; +import { Cog, ArrowDownUp, Gauge } from "lucide-react"; + +interface MachineCardProps { + machineId: string; + config: MachineConfig; + readings: Record | undefined; + isFaulting: boolean; +} + +const MACHINE_ICONS: Record = { + cnc_mill: , + hydraulic_press: , + conveyor_belt: , +}; + +function getMachineStatus( + config: MachineConfig, + readings: Record | undefined +): AnomalyStatus { + if (!readings) return "NORMAL"; + let worst: AnomalyStatus = "NORMAL"; + for (const [sensorName, sensorCfg] of Object.entries(config.sensors)) { + const reading = readings[sensorName]; + if (!reading) continue; + const status = getAnomalyStatus(reading.value, sensorCfg); + if (status === "CRITICAL") return "CRITICAL"; + if (status === "WARNING") worst = "WARNING"; + } + return worst; +} + +const glowClasses: Record = { + NORMAL: "animate-glow-green border-emerald-500/40", + WARNING: "animate-glow-amber border-amber-500/50", + CRITICAL: "animate-glow-red border-red-500/60", +}; + +const statusBadge: Record = { + NORMAL: { label: "Healthy", cls: "bg-emerald-500/20 text-emerald-400" }, + WARNING: { label: "Warning", cls: "bg-amber-500/20 text-amber-400" }, + CRITICAL: { label: "Critical", cls: "bg-red-500/20 text-red-400" }, +}; + +export default function MachineCard({ + machineId, + config, + readings, + isFaulting, +}: MachineCardProps) { + const status = getMachineStatus(config, readings); + const glow = glowClasses[status]; + const badge = statusBadge[status]; + + return ( +
+ {/* Header */} +
+
+ {MACHINE_ICONS[config.type]} +

+ {config.display_name} +

+
+ + {badge.label} + +
+ + {/* Sensor Gauges */} +
+ {Object.entries(config.sensors).map(([sensorName, sensorCfg]) => ( + + ))} +
+ + {/* Machine ID */} +
+ {machineId} +
+
+ ); +} diff --git a/demos/smart_factory/frontend/src/components/PipelineBanner.tsx b/demos/smart_factory/frontend/src/components/PipelineBanner.tsx new file mode 100644 index 0000000..0b4fdb8 --- /dev/null +++ b/demos/smart_factory/frontend/src/components/PipelineBanner.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Shield, BrainCircuit } from "lucide-react"; + +export default function PipelineBanner() { + return ( +
+
+ {/* Pipeline Steps */} +
+ + + + +
+
+ Silver + + + ML + +
+
Anomaly Scored
+
+ + + + +
+ + {/* UC Governance Badge */} +
+ +
+

+ Governed by Unity Catalog +

+

+ End-to-end lineage, access control & audit +

+
+
+
+
+ ); +} + +function Step({ + label, + sublabel, + color, +}: { + label: string; + sublabel: string; + color: string; +}) { + return ( +
+
{label}
+
{sublabel}
+
+ ); +} + +function Arrow() { + return ( + + ); +} diff --git a/demos/smart_factory/frontend/src/components/SensorGauge.tsx b/demos/smart_factory/frontend/src/components/SensorGauge.tsx new file mode 100644 index 0000000..e49471d --- /dev/null +++ b/demos/smart_factory/frontend/src/components/SensorGauge.tsx @@ -0,0 +1,118 @@ +import React, { useMemo } from "react"; +import { SensorConfig, getAnomalyStatus } from "../types"; + +interface SensorGaugeProps { + name: string; + value: number | undefined; + config: SensorConfig; +} + +const SENSOR_LABELS: Record = { + temperature_c: "Temperature", + vibration_mm_s: "Vibration", + spindle_rpm: "Spindle RPM", + pressure_bar: "Pressure", + cycle_count: "Cycle Rate", + speed_m_min: "Belt Speed", + load_weight_kg: "Load Weight", + motor_current_a: "Motor Current", +}; + +export default function SensorGauge({ name, value, config }: SensorGaugeProps) { + const displayValue = value ?? config.min; + const status = value !== undefined ? getAnomalyStatus(displayValue, config) : "NORMAL"; + + const statusColors = { + NORMAL: { stroke: "#10b981", text: "text-emerald-400", bg: "bg-emerald-500/10" }, + WARNING: { stroke: "#f59e0b", text: "text-amber-400", bg: "bg-amber-500/10" }, + CRITICAL: { stroke: "#ef4444", text: "text-red-400", bg: "bg-red-500/10" }, + }; + + const colors = statusColors[status]; + + // SVG arc gauge + const radius = 40; + const strokeWidth = 6; + const cx = 50; + const cy = 55; + const startAngle = -225; + const endAngle = 45; + const totalAngle = endAngle - startAngle; + + const pct = useMemo(() => { + const range = config.max - config.min; + if (range === 0) return 0; + return Math.max(0, Math.min(1, (displayValue - config.min) / range)); + }, [displayValue, config.min, config.max]); + + const valueAngle = startAngle + pct * totalAngle; + + function polarToCartesian(angle: number) { + const rad = (angle * Math.PI) / 180; + return { + x: cx + radius * Math.cos(rad), + y: cy + radius * Math.sin(rad), + }; + } + + function describeArc(start: number, end: number) { + const s = polarToCartesian(start); + const e = polarToCartesian(end); + const largeArc = end - start > 180 ? 1 : 0; + return `M ${s.x} ${s.y} A ${radius} ${radius} 0 ${largeArc} 1 ${e.x} ${e.y}`; + } + + const bgArc = describeArc(startAngle, endAngle); + const valueArc = + pct > 0.01 ? describeArc(startAngle, valueAngle) : ""; + + return ( +
+ + {/* Background arc */} + + {/* Value arc */} + {valueArc && ( + + )} + {/* Center value */} + + {displayValue.toFixed(config.max > 1000 ? 0 : 1)} + + + {config.unit} + + + + {SENSOR_LABELS[name] || name} + +
+ ); +} diff --git a/demos/smart_factory/frontend/src/components/ZeroBusInfo.tsx b/demos/smart_factory/frontend/src/components/ZeroBusInfo.tsx new file mode 100644 index 0000000..c9adc6f --- /dev/null +++ b/demos/smart_factory/frontend/src/components/ZeroBusInfo.tsx @@ -0,0 +1,122 @@ +import React, { useState } from "react"; +import { ChevronDown, ChevronUp, Zap, Gauge, Quote } from "lucide-react"; + +export default function ZeroBusInfo() { + const [expanded, setExpanded] = useState(false); + + return ( +
+ {/* Collapsed: headline metrics */} + + + {/* Expanded: detailed metrics */} + {expanded && ( +
+
+ {/* Speed */} +
+
+ + Speed +
+
+ + + +
+
+ + {/* Throughput */} +
+
+ + Throughput +
+
+ + + + +
+
+ + {/* Why it matters */} +
+
+ Why It Matters +
+
+

· No message bus — straight to Delta

+

· Fully serverless — zero infra management

+

· Open standards — Delta, UC, gRPC/REST

+

· SDKs: Python, Java, Go, Rust, TypeScript

+
+
+
+ + {/* Customer proof */} +
+
+ +
+

+ "ZeroBus reduced our telemetry resolution latency from days to minutes." +

+

+ — Joby Aviation, Flight Telemetry +

+
+
+
+
+

33%

+

cheaper

+
+
+

+ 33% cost savings and 40% faster data transmission vs Kafka. +

+

+ — Bosch, Manufacturing IoT +

+
+
+
+
+ )} +
+ ); +} + +function MetricRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/demos/smart_factory/frontend/src/hooks/useWebSocket.ts b/demos/smart_factory/frontend/src/hooks/useWebSocket.ts new file mode 100644 index 0000000..47e1e45 --- /dev/null +++ b/demos/smart_factory/frontend/src/hooks/useWebSocket.ts @@ -0,0 +1,131 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { WSMessage, MachineConfig, SensorReading } from "../types"; + +interface UseWebSocketReturn { + machines: Record; + latestReadings: Record>; + faultStates: Record; + eventLog: SensorReading[]; + totalEvents: number; + eventsPerSec: number; + connectionStatus: "connecting" | "connected" | "disconnected"; + injectFault: (machineId: string) => void; + clearFault: (machineId: string) => void; + clearAllFaults: () => void; +} + +const MAX_LOG_SIZE = 50; + +export function useWebSocket(): UseWebSocketReturn { + const [machines, setMachines] = useState>({}); + const [latestReadings, setLatestReadings] = useState< + Record> + >({}); + const [faultStates, setFaultStates] = useState>({}); + const [eventLog, setEventLog] = useState([]); + const [totalEvents, setTotalEvents] = useState(0); + const [eventsPerSec, setEventsPerSec] = useState(0); + const recentCountRef = useRef(0); + const [connectionStatus, setConnectionStatus] = useState< + "connecting" | "connected" | "disconnected" + >("connecting"); + const wsRef = useRef(null); + const reconnectTimeout = useRef(0); + + const connect = useCallback(() => { + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const ws = new WebSocket(`${protocol}//${window.location.host}/ws`); + wsRef.current = ws; + setConnectionStatus("connecting"); + + ws.onopen = () => { + setConnectionStatus("connected"); + reconnectTimeout.current = 0; + }; + + ws.onmessage = (event) => { + const msg: WSMessage = JSON.parse(event.data); + + if (msg.type === "init" && msg.machines) { + setMachines(msg.machines); + } + + if (msg.fault_states) { + setFaultStates(msg.fault_states); + } + + if (msg.events) { + // Track event counts + setTotalEvents((prev) => prev + msg.events!.length); + recentCountRef.current += msg.events!.length; + + // Update latest readings per machine+sensor + setLatestReadings((prev) => { + const next = { ...prev }; + for (const reading of msg.events!) { + if (!next[reading.machine_id]) { + next[reading.machine_id] = {}; + } + next[reading.machine_id] = { + ...next[reading.machine_id], + [reading.sensor_name]: reading, + }; + } + return next; + }); + + // Append to event log (keep last N) + setEventLog((prev) => { + const updated = [...msg.events!, ...prev]; + return updated.slice(0, MAX_LOG_SIZE); + }); + } + }; + + ws.onclose = () => { + setConnectionStatus("disconnected"); + // Exponential backoff reconnect + const delay = Math.min(1000 * Math.pow(2, reconnectTimeout.current), 10000); + reconnectTimeout.current++; + setTimeout(connect, delay); + }; + + ws.onerror = () => { + ws.close(); + }; + }, []); + + useEffect(() => { + connect(); + // Calculate events/sec every second + const epsInterval = setInterval(() => { + setEventsPerSec(recentCountRef.current); + recentCountRef.current = 0; + }, 1000); + return () => { + wsRef.current?.close(); + clearInterval(epsInterval); + }; + }, [connect]); + + const sendAction = useCallback((action: string, machineId?: string) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send( + JSON.stringify({ action, machine_id: machineId }) + ); + } + }, []); + + return { + machines, + latestReadings, + faultStates, + eventLog, + totalEvents, + eventsPerSec, + connectionStatus, + injectFault: (id) => sendAction("inject_fault", id), + clearFault: (id) => sendAction("clear_fault", id), + clearAllFaults: () => sendAction("clear_all"), + }; +} diff --git a/demos/smart_factory/frontend/src/index.css b/demos/smart_factory/frontend/src/index.css new file mode 100644 index 0000000..13deb0d --- /dev/null +++ b/demos/smart_factory/frontend/src/index.css @@ -0,0 +1,23 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + sans-serif; +} + +/* Scrollbar styling for dark theme */ +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: #111827; +} +::-webkit-scrollbar-thumb { + background: #374151; + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: #4b5563; +} diff --git a/demos/smart_factory/frontend/src/main.tsx b/demos/smart_factory/frontend/src/main.tsx new file mode 100644 index 0000000..9b67590 --- /dev/null +++ b/demos/smart_factory/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/demos/smart_factory/frontend/src/types.ts b/demos/smart_factory/frontend/src/types.ts new file mode 100644 index 0000000..76a5755 --- /dev/null +++ b/demos/smart_factory/frontend/src/types.ts @@ -0,0 +1,50 @@ +export interface SensorConfig { + unit: string; + min: number; + max: number; + warning_threshold: number; + critical_threshold: number; +} + +export interface MachineConfig { + type: string; + display_name: string; + sensors: Record; +} + +export interface SensorReading { + machine_id: string; + machine_type: string; + sensor_name: string; + value: number; + unit: string; + timestamp: string; + is_fault: boolean; +} + +export interface WSMessage { + type: "init" | "sensor_data"; + machines?: Record; + events?: SensorReading[]; + fault_states?: Record; +} + +export type AnomalyStatus = "NORMAL" | "WARNING" | "CRITICAL"; + +export function getAnomalyStatus( + value: number, + config: SensorConfig +): AnomalyStatus { + const { warning_threshold, critical_threshold } = config; + + // High-value fault sensors (threshold ascending) + if (warning_threshold < critical_threshold) { + if (value >= critical_threshold) return "CRITICAL"; + if (value >= warning_threshold) return "WARNING"; + return "NORMAL"; + } + // Low-value fault sensors (speed drops, cycle count drops) + if (value <= critical_threshold) return "CRITICAL"; + if (value <= warning_threshold) return "WARNING"; + return "NORMAL"; +} diff --git a/demos/smart_factory/frontend/tailwind.config.js b/demos/smart_factory/frontend/tailwind.config.js new file mode 100644 index 0000000..2af46c3 --- /dev/null +++ b/demos/smart_factory/frontend/tailwind.config.js @@ -0,0 +1,37 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + colors: { + factory: { + bg: "#0a0e17", + card: "#111827", + border: "#1f2937", + accent: "#3b82f6", + }, + }, + animation: { + "pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite", + "glow-green": "glow-green 2s ease-in-out infinite alternate", + "glow-amber": "glow-amber 1s ease-in-out infinite alternate", + "glow-red": "glow-red 0.5s ease-in-out infinite alternate", + }, + keyframes: { + "glow-green": { + "0%": { boxShadow: "0 0 5px #10b981, 0 0 10px #10b98133" }, + "100%": { boxShadow: "0 0 10px #10b981, 0 0 20px #10b98155" }, + }, + "glow-amber": { + "0%": { boxShadow: "0 0 5px #f59e0b, 0 0 10px #f59e0b33" }, + "100%": { boxShadow: "0 0 15px #f59e0b, 0 0 30px #f59e0b55" }, + }, + "glow-red": { + "0%": { boxShadow: "0 0 5px #ef4444, 0 0 10px #ef444433" }, + "100%": { boxShadow: "0 0 20px #ef4444, 0 0 40px #ef444466" }, + }, + }, + }, + }, + plugins: [], +}; diff --git a/demos/smart_factory/frontend/vite.config.ts b/demos/smart_factory/frontend/vite.config.ts new file mode 100644 index 0000000..3a39324 --- /dev/null +++ b/demos/smart_factory/frontend/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: "dist", + emptyOutDir: true, + }, + server: { + proxy: { + "/api": "http://localhost:8000", + "/ws": { + target: "ws://localhost:8000", + ws: true, + }, + }, + }, +}); diff --git a/demos/smart_factory/pipeline/bronze.sql b/demos/smart_factory/pipeline/bronze.sql new file mode 100644 index 0000000..c71dd3a --- /dev/null +++ b/demos/smart_factory/pipeline/bronze.sql @@ -0,0 +1,20 @@ +-- Bronze: Validated raw sensor events from ZeroBus landing zone +-- Source table is fully qualified; output uses pipeline's target schema + +CREATE OR REFRESH STREAMING TABLE raw_sensor_events ( + CONSTRAINT valid_value EXPECT (value IS NOT NULL) ON VIOLATION DROP ROW, + CONSTRAINT valid_machine EXPECT (machine_id IS NOT NULL) ON VIOLATION DROP ROW, + CONSTRAINT valid_sensor EXPECT (sensor_name IS NOT NULL) ON VIOLATION DROP ROW, + CONSTRAINT valid_timestamp EXPECT (timestamp IS NOT NULL) ON VIOLATION DROP ROW +) +COMMENT 'Validated IoT sensor events from ZeroBus landing zone' +AS SELECT + machine_id, + machine_type, + sensor_name, + CAST(value AS DOUBLE) AS value, + unit, + CAST(timestamp AS TIMESTAMP) AS timestamp, + COALESCE(is_fault, false) AS is_fault, + current_timestamp() AS ingested_at +FROM STREAM(dilan_catalog.smartfactory.raw_sensor_events); diff --git a/demos/smart_factory/pipeline/gold.sql b/demos/smart_factory/pipeline/gold.sql new file mode 100644 index 0000000..0c2bf4e --- /dev/null +++ b/demos/smart_factory/pipeline/gold.sql @@ -0,0 +1,59 @@ +-- Gold: Aggregated health KPIs for dashboards + +-- Anomaly timeline — streaming table for instant anomaly visibility +CREATE OR REFRESH STREAMING TABLE anomaly_timeline +COMMENT 'Real-time anomaly events for timeline visualization' +AS +SELECT + machine_id, + machine_type, + sensor_name, + value, + unit, + anomaly_status, + warning_threshold, + critical_threshold, + timestamp, + processed_at +FROM STREAM(enriched_events) +WHERE anomaly_status != 'NORMAL'; + + +-- Machine health scores (MV, refreshes on pipeline interval) +CREATE OR REFRESH MATERIALIZED VIEW machine_health_kpis +COMMENT 'Machine health KPIs for dashboards' +AS +SELECT + machine_id, + machine_type, + sensor_name, + COUNT(*) AS total_readings, + COUNT(CASE WHEN anomaly_status = 'CRITICAL' THEN 1 END) AS critical_count, + COUNT(CASE WHEN anomaly_status = 'WARNING' THEN 1 END) AS warning_count, + ROUND(AVG(value), 2) AS avg_value, + ROUND(MAX(value), 2) AS max_value, + ROUND(MIN(value), 2) AS min_value, + MAX(timestamp) AS last_reading_at, + GREATEST(0, 100 + - (COUNT(CASE WHEN anomaly_status = 'CRITICAL' THEN 1 END) * 10) + - (COUNT(CASE WHEN anomaly_status = 'WARNING' THEN 1 END) * 3) + ) AS health_score +FROM enriched_events +WHERE timestamp > current_timestamp() - INTERVAL 5 MINUTES +GROUP BY machine_id, machine_type, sensor_name; + + +-- Overall machine health summary (MV, one row per machine) +CREATE OR REFRESH MATERIALIZED VIEW machine_summary +COMMENT 'One-row-per-machine health summary' +AS +SELECT + machine_id, + machine_type, + MIN(health_score) AS worst_sensor_health, + ROUND(AVG(health_score), 0) AS avg_health_score, + SUM(critical_count) AS total_criticals, + SUM(warning_count) AS total_warnings, + MAX(last_reading_at) AS last_activity +FROM machine_health_kpis +GROUP BY machine_id, machine_type; diff --git a/demos/smart_factory/pipeline/silver.sql b/demos/smart_factory/pipeline/silver.sql new file mode 100644 index 0000000..859dc9d --- /dev/null +++ b/demos/smart_factory/pipeline/silver.sql @@ -0,0 +1,57 @@ +-- Silver: Enriched events with anomaly scoring +-- Uses a threshold LIVE TABLE joined against streaming events for anomaly detection + +-- Sensor thresholds reference table (the "ML model" — simple, explainable, reliable) +CREATE OR REFRESH LIVE TABLE sensor_thresholds +COMMENT 'Anomaly detection thresholds per machine and sensor' +AS SELECT * FROM VALUES + -- CNC Mill + ('CNC_Mill_01', 'temperature_c', 55.0, 75.0, '°C'), + ('CNC_Mill_01', 'vibration_mm_s', 4.0, 6.5, 'mm/s'), + ('CNC_Mill_01', 'spindle_rpm', 4200.0, 4800.0, 'RPM'), + -- Hydraulic Press (some sensors fault LOW, so we use value < threshold) + ('Hydraulic_Press_01', 'pressure_bar', 210.0, 270.0, 'bar'), + ('Hydraulic_Press_01', 'temperature_c', 70.0, 90.0, '°C'), + ('Hydraulic_Press_01', 'cycle_count', 6.0, 3.0, 'cycles/min'), + -- Conveyor Belt + ('Conveyor_Belt_01', 'speed_m_min', 9.0, 6.0, 'm/min'), + ('Conveyor_Belt_01', 'load_weight_kg', 350.0, 430.0, 'kg'), + ('Conveyor_Belt_01', 'motor_current_a', 12.0, 17.0, 'A') +AS t(machine_id, sensor_name, warning_threshold, critical_threshold, unit); + + +-- Enriched streaming table with anomaly status +CREATE OR REFRESH STREAMING TABLE enriched_events +COMMENT 'Sensor events enriched with anomaly detection scores' +AS +SELECT + r.machine_id, + r.machine_type, + r.sensor_name, + r.value, + r.unit, + r.timestamp, + r.is_fault, + r.ingested_at, + t.warning_threshold, + t.critical_threshold, + CASE + -- Sensors where HIGH values indicate faults (temp, pressure, vibration, load, current) + WHEN t.warning_threshold < t.critical_threshold THEN + CASE + WHEN r.value >= t.critical_threshold THEN 'CRITICAL' + WHEN r.value >= t.warning_threshold THEN 'WARNING' + ELSE 'NORMAL' + END + -- Sensors where LOW values indicate faults (speed, cycle_count) + ELSE + CASE + WHEN r.value <= t.critical_threshold THEN 'CRITICAL' + WHEN r.value <= t.warning_threshold THEN 'WARNING' + ELSE 'NORMAL' + END + END AS anomaly_status, + current_timestamp() AS processed_at +FROM STREAM(raw_sensor_events) r +LEFT JOIN LIVE.sensor_thresholds t + ON r.machine_id = t.machine_id AND r.sensor_name = t.sensor_name; diff --git a/demos/smart_factory/pyproject.toml b/demos/smart_factory/pyproject.toml new file mode 100644 index 0000000..44a304b --- /dev/null +++ b/demos/smart_factory/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "smartfactory-demo" +version = "0.1.0" +description = "SmartFactory IoT demo — ZeroBus + SDP streaming with anomaly detection" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.100.0", + "uvicorn[standard]>=0.23.0", + "databricks-sdk>=0.61.0", + "pydantic>=2.1.0", + "python-dotenv>=1.0.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/demos/smart_factory/requirements.txt b/demos/smart_factory/requirements.txt new file mode 100644 index 0000000..164650a --- /dev/null +++ b/demos/smart_factory/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.100.0 +uvicorn[standard]>=0.23.0 +databricks-sdk>=0.61.0 +databricks-zerobus-ingest-sdk +pydantic>=2.1.0 +python-dotenv>=1.0.0 diff --git a/demos/smart_factory/setup.sh b/demos/smart_factory/setup.sh new file mode 100755 index 0000000..f4c5271 --- /dev/null +++ b/demos/smart_factory/setup.sh @@ -0,0 +1,198 @@ +#!/bin/bash +set -e + +# SmartFactory Demo — One-time setup script +# Usage: ./setup.sh [catalog_name] +# +# Prerequisites: +# - Databricks CLI v0.288+ installed (/opt/homebrew/bin/databricks) +# - Profile configured with workspace access +# - Node.js + npm installed (for frontend build) + +CLI="/opt/homebrew/bin/databricks" +PROFILE="${1:?Usage: ./setup.sh [catalog_name]}" +CATALOG="${2:-dilan_catalog}" +SCHEMA="smartfactory" +PIPELINE_NAME="smartfactory-sdp" + +echo "=========================================" +echo "SmartFactory Demo Setup" +echo "=========================================" +echo "Profile: $PROFILE" +echo "Catalog: $CATALOG" +echo "Schema: $SCHEMA" +echo "" + +# --- Step 1: Find or start a SQL warehouse --- +echo "[1/8] Finding SQL warehouse..." +WAREHOUSE_ID=$($CLI -p "$PROFILE" warehouses list --output json 2>/dev/null | python3 -c " +import sys, json +data = json.load(sys.stdin) +for w in data: + print(w['id']) + break +" 2>/dev/null) + +if [ -z "$WAREHOUSE_ID" ]; then + echo "ERROR: No SQL warehouse found. Create one in the workspace first." + exit 1 +fi +echo " Warehouse: $WAREHOUSE_ID" + +# Start warehouse if stopped +$CLI -p "$PROFILE" warehouses start "$WAREHOUSE_ID" > /dev/null 2>&1 || true +echo " Warehouse starting (or already running)..." +sleep 5 + +# --- Step 2: Create schema and landing table --- +echo "[2/8] Creating schema and landing table..." +run_sql() { + $CLI -p "$PROFILE" api post /api/2.0/sql/statements --json "{ + \"warehouse_id\": \"$WAREHOUSE_ID\", + \"statement\": \"$1\", + \"wait_timeout\": \"30s\" + }" 2>&1 | python3 -c "import sys,json; d=json.load(sys.stdin); s=d.get('status',{}).get('state','?'); e=d.get('status',{}).get('error',{}).get('message',''); print(f'{s} {e}' if e else s)" +} + +run_sql "CREATE SCHEMA IF NOT EXISTS ${CATALOG}.${SCHEMA}" +run_sql "CREATE TABLE IF NOT EXISTS ${CATALOG}.${SCHEMA}.raw_sensor_events (machine_id STRING NOT NULL, machine_type STRING NOT NULL, sensor_name STRING NOT NULL, value DOUBLE NOT NULL, unit STRING NOT NULL, timestamp TIMESTAMP NOT NULL, is_fault BOOLEAN) USING DELTA COMMENT 'Raw IoT sensor events ingested via ZeroBus'" + +# --- Step 3: Build frontend --- +echo "[3/8] Building frontend..." +cd frontend && npm ci --silent && npm run build 2>&1 | tail -1 +cd .. + +# --- Step 4: Update databricks.yml with correct values --- +echo "[4/8] Configuring bundle..." + +# Detect current user for dev schema prefix +USERNAME=$($CLI -p "$PROFILE" current-user me --output json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); u=d.get('userName','').split('@')[0].replace('.','_'); print(u)") +DEV_SCHEMA="dev_${USERNAME}_${SCHEMA}" + +echo " Dev schema will be: ${CATALOG}.${DEV_SCHEMA}" + +# Write a local env file for the setup +cat > .env.setup </dev/null | python3 -c " +import sys, json +d = json.load(sys.stdin) +# The SP client ID is in the app's service principal +sp = d.get('service_principal_client_id', d.get('effective_service_principal', {}).get('client_id', '')) +print(sp) +" 2>/dev/null) + +if [ -z "$SP_ID" ]; then + echo " WARNING: Could not detect app service principal. You may need to:" + echo " 1. Start the app: $CLI -p $PROFILE apps start smartfactory-app" + echo " 2. Re-run this script, or manually grant permissions" +else + echo " App SP: $SP_ID" + + # Warehouse access + $CLI -p "$PROFILE" warehouses set-permissions "$WAREHOUSE_ID" --json "{\"access_control_list\":[{\"service_principal_name\":\"$SP_ID\",\"permission_level\":\"CAN_USE\"}]}" > /dev/null 2>&1 + + # Catalog + schema + table access + for stmt in \ + "GRANT USE CATALOG ON CATALOG ${CATALOG} TO \`${SP_ID}\`" \ + "GRANT USE SCHEMA ON SCHEMA ${CATALOG}.${SCHEMA} TO \`${SP_ID}\`" \ + "GRANT CREATE TABLE ON SCHEMA ${CATALOG}.${SCHEMA} TO \`${SP_ID}\`" \ + "GRANT MODIFY ON TABLE ${CATALOG}.${SCHEMA}.raw_sensor_events TO \`${SP_ID}\`" \ + "GRANT SELECT ON TABLE ${CATALOG}.${SCHEMA}.raw_sensor_events TO \`${SP_ID}\`" \ + "GRANT USE SCHEMA ON SCHEMA ${CATALOG}.${DEV_SCHEMA} TO \`${SP_ID}\`" \ + "GRANT SELECT ON SCHEMA ${CATALOG}.${DEV_SCHEMA} TO \`${SP_ID}\`"; do + run_sql "$stmt" > /dev/null + done + echo " Catalog/schema/table permissions granted" +fi + +# --- Step 7: Start app and deploy code --- +echo "[7/8] Starting and deploying app..." +$CLI -p "$PROFILE" apps start smartfactory-app > /dev/null 2>&1 || true +echo " Waiting for app compute..." +sleep 30 + +$CLI -p "$PROFILE" apps deploy smartfactory-app \ + --source-code-path "/Workspace/Users/${USERNAME}@databricks.com/.bundle/smartfactory-demo/dev/files" 2>&1 | tail -1 + +# --- Step 8: Find pipeline ID, grant SP permissions, set continuous --- +echo "[8/8] Configuring pipeline..." +PIPELINE_ID=$($CLI -p "$PROFILE" api get /api/2.0/pipelines 2>/dev/null | python3 -c " +import sys, json +d = json.load(sys.stdin) +for p in d.get('statuses', []): + if 'smartfactory-sdp' in p.get('name', ''): + print(p['pipeline_id']) + break +" 2>/dev/null) + +if [ -n "$PIPELINE_ID" ]; then + echo " Pipeline: $PIPELINE_ID" + + # Grant SP pipeline permissions + if [ -n "$SP_ID" ]; then + $CLI -p "$PROFILE" pipelines update-permissions "$PIPELINE_ID" \ + --json "{\"access_control_list\":[{\"service_principal_name\":\"$SP_ID\",\"permission_level\":\"CAN_MANAGE\"}]}" > /dev/null 2>&1 + echo " Pipeline permissions granted to SP" + fi + + # Set continuous mode via API + PIPELINE_SPEC=$($CLI -p "$PROFILE" api get "/api/2.0/pipelines/$PIPELINE_ID" 2>/dev/null | python3 -c " +import sys, json +d = json.load(sys.stdin) +spec = d.get('spec', d) +spec['continuous'] = True +for k in ['pipeline_id','state','creator_user_name','latest_updates','cause','cluster_id','run_as_user_name','last_modified','budget_policy_id']: + spec.pop(k, None) +print(json.dumps(spec)) +") + echo "$PIPELINE_SPEC" > /tmp/smartfactory_pipeline.json + $CLI -p "$PROFILE" api put "/api/2.0/pipelines/$PIPELINE_ID" --json @/tmp/smartfactory_pipeline.json > /dev/null 2>&1 + echo " Pipeline set to continuous mode" + + # Update app.yaml with pipeline ID (for next deploy) + echo "" + echo "NOTE: Add this to app.yaml env vars for pipeline control:" + echo " - name: PIPELINE_ID" + echo " value: \"$PIPELINE_ID\"" +else + echo " WARNING: Pipeline not found. Deploy may still be in progress." +fi + +# --- Done --- +APP_URL=$($CLI -p "$PROFILE" apps get smartfactory-app --output json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('url',''))" 2>/dev/null) + +echo "" +echo "=========================================" +echo "Setup complete!" +echo "=========================================" +echo "" +echo "App URL: $APP_URL" +echo "Catalog: $CATALOG" +echo "Schema: $CATALOG.$SCHEMA (landing)" +echo "Pipeline: $CATALOG.$DEV_SCHEMA (pipeline tables)" +echo "Warehouse: $WAREHOUSE_ID" +echo "Pipeline ID: $PIPELINE_ID" +echo "" +echo "Next steps:" +echo " 1. Open the app URL above" +echo " 2. Click 'Start Pipeline' to begin continuous processing" +echo " 3. Inject faults and watch data flow!" +echo "" +echo "To redeploy after code changes:" +echo " cd frontend && npm run build && cd .." +echo " $CLI bundle deploy -t dev -p $PROFILE --var='warehouse_id=$WAREHOUSE_ID' --var='catalog_name=$CATALOG'" +echo " $CLI -p $PROFILE apps deploy smartfactory-app --source-code-path /Workspace/Users/${USERNAME}@databricks.com/.bundle/smartfactory-demo/dev/files" diff --git a/demos/smart_factory/src/__init__.py b/demos/smart_factory/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demos/smart_factory/src/app.py b/demos/smart_factory/src/app.py new file mode 100644 index 0000000..5ed8348 --- /dev/null +++ b/demos/smart_factory/src/app.py @@ -0,0 +1,546 @@ +""" +SmartFactory FastAPI Application. + +Serves the React frontend, runs the IoT sensor simulator, +pushes data via ZeroBus, and streams updates to the UI via WebSocket. +""" + +import asyncio +import json +import logging +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from pydantic import BaseModel +from databricks.sdk import WorkspaceClient + +from src.simulator import SensorSimulator, get_machine_configs +from src.zerobus_client import ZeroBusClient + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +# --- WebSocket Connection Manager --- + +class ConnectionManager: + """Manages WebSocket connections for real-time UI updates.""" + + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + logger.info(f"WebSocket connected. Total: {len(self.active_connections)}") + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}") + + async def broadcast(self, message: dict): + dead = [] + for conn in self.active_connections: + try: + await conn.send_json(message) + except Exception: + dead.append(conn) + for conn in dead: + self.active_connections.remove(conn) + + +manager = ConnectionManager() +simulator = SensorSimulator() +zerobus: ZeroBusClient | None = None +simulator_task: asyncio.Task | None = None +workspace_client: WorkspaceClient | None = None +pipeline_id: str | None = None + + +# --- Simulator Loop --- + +async def run_simulator(): + """Background loop: generate sensor data, push to ZeroBus + WebSocket.""" + interval_ms = int(os.getenv("SIMULATOR_INTERVAL_MS", "2000")) + interval_s = interval_ms / 1000 + + while True: + try: + events = simulator.generate_tick() + + # Push to ZeroBus (async, for pipeline ingestion) + if zerobus: + await zerobus.push_events(events) + + # Push to WebSocket (instant UI update) + await manager.broadcast({ + "type": "sensor_data", + "events": events, + "fault_states": simulator.get_fault_states(), + }) + + except asyncio.CancelledError: + raise + except Exception as e: + logger.error(f"Simulator tick error: {e}") + + await asyncio.sleep(interval_s) + + +# --- Pipeline Management --- + +def _find_pipeline_id() -> str | None: + """Find the SmartFactory SDP pipeline ID.""" + global pipeline_id + if pipeline_id: + return pipeline_id + # Check env var first + env_id = os.getenv("PIPELINE_ID") + if env_id: + pipeline_id = env_id + return pipeline_id + if not workspace_client: + return None + try: + pipelines = workspace_client.pipelines.list_pipelines( + filter="name LIKE 'smartfactory-sdp'" + ) + for p in pipelines: + pipeline_id = p.pipeline_id + logger.info(f"Found pipeline: {p.name} ({pipeline_id})") + return pipeline_id + except Exception as e: + logger.error(f"Failed to find pipeline: {e}") + return None + + +def _get_pipeline_status() -> dict: + """Get current pipeline status.""" + pid = _find_pipeline_id() + if not pid or not workspace_client: + return {"state": "NOT_FOUND", "pipeline_id": None} + try: + p = workspace_client.pipelines.get(pipeline_id=pid) + return { + "state": p.state.value if p.state else "UNKNOWN", + "pipeline_id": pid, + "name": p.name, + "last_update": p.latest_updates[0].update_id if p.latest_updates else None, + } + except Exception as e: + logger.error(f"Failed to get pipeline status: {e}") + return {"state": "ERROR", "pipeline_id": pid, "error": str(e)} + + +# --- App Lifespan --- + +@asynccontextmanager +async def lifespan(app: FastAPI): + global zerobus, simulator_task, workspace_client + + # Startup + enable_sim = os.getenv("ENABLE_SIMULATOR", "true").lower() == "true" + + try: + workspace_client = WorkspaceClient() + logger.info("Workspace client initialized") + # Self-heal: ensure SP has warehouse access (gets dropped by bundle deploy) + if WAREHOUSE_ID: + try: + sp_id = os.getenv("DATABRICKS_CLIENT_ID") + if sp_id: + workspace_client.warehouses.set_permissions( + warehouse_id=WAREHOUSE_ID, + access_control_list=[{ + "service_principal_name": sp_id, + "permission_level": "CAN_USE", + }], + ) + logger.info(f"Warehouse permission confirmed for SP {sp_id}") + except Exception as e: + logger.warning(f"Could not set warehouse permission: {e}") + except Exception as e: + logger.warning(f"Workspace client init failed: {e}") + + try: + zerobus = ZeroBusClient() + logger.info("ZeroBus client initialized") + except Exception as e: + logger.warning(f"ZeroBus client init failed: {e}. Running without ingestion.") + zerobus = None + + if enable_sim: + simulator_task = asyncio.create_task(run_simulator()) + logger.info("Sensor simulator started") + + # Start dashboard cache refresh + global cache_task + cache_task = asyncio.create_task(_refresh_dashboard_cache()) + logger.info("Dashboard cache started (5s refresh)") + + yield + + # Shutdown + if simulator_task: + simulator_task.cancel() + try: + await simulator_task + except asyncio.CancelledError: + pass + if cache_task: + cache_task.cancel() + try: + await cache_task + except asyncio.CancelledError: + pass + logger.info("SmartFactory app shutdown complete") + + +# --- FastAPI App --- + +app = FastAPI(title="SmartFactory", lifespan=lifespan) + + +# --- API Models --- + +class FaultRequest(BaseModel): + machine_id: str + + +# --- API Endpoints --- + +@app.get("/api/machines") +async def get_machines(): + """Return machine configurations and current fault states.""" + return { + "machines": get_machine_configs(), + "fault_states": simulator.get_fault_states(), + } + + +@app.get("/api/status") +async def get_status(): + """Return current simulator status.""" + return { + "simulator_running": simulator_task is not None and not simulator_task.done(), + "zerobus_connected": zerobus is not None, + "websocket_connections": len(manager.active_connections), + "fault_states": simulator.get_fault_states(), + } + + +@app.post("/api/fault/inject") +async def inject_fault(req: FaultRequest): + """Inject a fault into a specific machine.""" + success = simulator.inject_fault(req.machine_id) + if not success: + return {"error": f"Unknown machine: {req.machine_id}"}, 404 + logger.info(f"Fault injected: {req.machine_id}") + return {"status": "fault_injected", "machine_id": req.machine_id} + + +@app.post("/api/fault/clear") +async def clear_fault(req: FaultRequest): + """Clear a fault from a specific machine.""" + success = simulator.clear_fault(req.machine_id) + if not success: + return {"error": f"Unknown machine: {req.machine_id}"}, 404 + logger.info(f"Fault cleared: {req.machine_id}") + return {"status": "fault_cleared", "machine_id": req.machine_id} + + +@app.post("/api/fault/clear-all") +async def clear_all_faults(): + """Clear all faults.""" + simulator.clear_all_faults() + logger.info("All faults cleared") + return {"status": "all_faults_cleared"} + + +# --- Simulator Control Endpoints --- + +@app.get("/api/simulator/status") +async def get_simulator_status(): + """Get simulator (data streaming) status.""" + running = simulator_task is not None and not simulator_task.done() + return {"running": running} + + +@app.post("/api/simulator/start") +async def start_simulator(): + """Start the sensor data simulator.""" + global simulator_task + if simulator_task and not simulator_task.done(): + return {"status": "already_running"} + simulator_task = asyncio.create_task(run_simulator()) + logger.info("Simulator started via API") + return {"status": "started"} + + +@app.post("/api/simulator/stop") +async def stop_simulator(): + """Stop the sensor data simulator.""" + global simulator_task + if not simulator_task or simulator_task.done(): + return {"status": "already_stopped"} + simulator_task.cancel() + try: + await simulator_task + except asyncio.CancelledError: + pass + simulator_task = None + logger.info("Simulator stopped via API") + return {"status": "stopped"} + + +@app.post("/api/reset-data") +async def reset_data(): + """Reset demo by stopping pipeline, clearing data, and doing a full refresh.""" + if not workspace_client or not WAREHOUSE_ID: + return {"error": "No workspace client or warehouse"} + try: + # Stop pipeline first so it releases the streaming source + pid = _find_pipeline_id() + if pid: + try: + workspace_client.pipelines.stop(pipeline_id=pid) + logger.info("Pipeline stopped for reset") + await asyncio.sleep(10) # Wait for pipeline to fully stop + except Exception: + pass + + # Now safe to truncate the landing table + workspace_client.statement_execution.execute_statement( + statement=f"TRUNCATE TABLE {CATALOG}.smartfactory.raw_sensor_events", + warehouse_id=WAREHOUSE_ID, + ) + logger.info("Landing table truncated") + + # Trigger a full refresh of the pipeline (resets streaming checkpoints) + pid = _find_pipeline_id() + if pid: + workspace_client.pipelines.start_update( + pipeline_id=pid, + full_refresh=True, + ) + logger.info(f"Pipeline full refresh triggered: {pid}") + + # Clear simulator fault state + simulator.clear_all_faults() + logger.info("Demo data reset complete") + return {"status": "reset_complete"} + except Exception as e: + logger.error(f"Reset failed: {e}") + return {"error": str(e)} + + +# --- Pipeline Endpoints --- + +@app.get("/api/pipeline/status") +async def get_pipeline_status(): + """Get SDP pipeline status.""" + return _get_pipeline_status() + + +@app.post("/api/pipeline/start") +async def start_pipeline(): + """Start/trigger the SDP pipeline.""" + pid = _find_pipeline_id() + if not pid or not workspace_client: + return {"error": "Pipeline not found"}, 404 + try: + update = workspace_client.pipelines.start_update(pipeline_id=pid) + logger.info(f"Pipeline started: {pid}") + return {"status": "started", "pipeline_id": pid, "update_id": update.update_id} + except Exception as e: + logger.error(f"Failed to start pipeline: {e}") + return {"error": str(e)} + + +@app.post("/api/pipeline/stop") +async def stop_pipeline(): + """Stop the SDP pipeline.""" + pid = _find_pipeline_id() + if not pid or not workspace_client: + return {"error": "Pipeline not found"}, 404 + try: + workspace_client.pipelines.stop(pipeline_id=pid) + logger.info(f"Pipeline stopped: {pid}") + return {"status": "stopped", "pipeline_id": pid} + except Exception as e: + logger.error(f"Failed to stop pipeline: {e}") + return {"error": str(e)} + + +# --- Dashboard Data Cache --- + +WAREHOUSE_ID = os.getenv("WAREHOUSE_ID") +CATALOG = os.getenv("CATALOG_NAME", "dilan_catalog") +PIPELINE_SCHEMA = os.getenv("PIPELINE_SCHEMA", "dev_dilan_patel_smartfactory") + +# Background cache — warehouse queries run here, frontend reads from cache +dashboard_cache: dict = { + "health": [], + "kpis": [], + "anomalies": [], + "trends": [], +} +cache_task: asyncio.Task | None = None + + +def _run_sql(query: str) -> list[dict]: + """Execute SQL and return results as list of dicts.""" + if not workspace_client or not WAREHOUSE_ID: + return [] + try: + result = workspace_client.statement_execution.execute_statement( + statement=query, + warehouse_id=WAREHOUSE_ID, + ) + if not result.result or not result.result.data_array: + return [] + columns = [c.name for c in result.manifest.schema.columns] + return [dict(zip(columns, row)) for row in result.result.data_array] + except Exception as e: + logger.error(f"SQL query failed: {e}") + return [] + + +async def _refresh_dashboard_cache(): + """Background loop: refresh dashboard data from warehouse every 5 seconds.""" + while True: + try: + # Run all queries sequentially (one at a time, not competing) + health = _run_sql(f""" + WITH kpis AS ( + SELECT machine_id, machine_type, sensor_name, + COUNT(CASE WHEN anomaly_status = 'CRITICAL' THEN 1 END) AS critical_count, + COUNT(CASE WHEN anomaly_status = 'WARNING' THEN 1 END) AS warning_count, + GREATEST(0, 100 + - COUNT(CASE WHEN anomaly_status = 'CRITICAL' THEN 1 END) * 10 + - COUNT(CASE WHEN anomaly_status = 'WARNING' THEN 1 END) * 3 + ) AS health_score + FROM {CATALOG}.{PIPELINE_SCHEMA}.enriched_events + WHERE TRUE + GROUP BY machine_id, machine_type, sensor_name + ) + SELECT machine_id, machine_type, + MIN(health_score) AS worst_sensor_health, + ROUND(AVG(health_score), 0) AS avg_health_score, + SUM(critical_count) AS total_criticals, + SUM(warning_count) AS total_warnings + FROM kpis + GROUP BY machine_id, machine_type + """) + if health: + dashboard_cache["health"] = health + + kpis = _run_sql(f""" + SELECT machine_id, machine_type, sensor_name, + COUNT(*) AS total_readings, + COUNT(CASE WHEN anomaly_status = 'CRITICAL' THEN 1 END) AS critical_count, + COUNT(CASE WHEN anomaly_status = 'WARNING' THEN 1 END) AS warning_count, + ROUND(AVG(value), 2) AS avg_value, + ROUND(MAX(value), 2) AS max_value, + ROUND(MIN(value), 2) AS min_value, + GREATEST(0, 100 + - COUNT(CASE WHEN anomaly_status = 'CRITICAL' THEN 1 END) * 10 + - COUNT(CASE WHEN anomaly_status = 'WARNING' THEN 1 END) * 3 + ) AS health_score + FROM {CATALOG}.{PIPELINE_SCHEMA}.enriched_events + WHERE TRUE + GROUP BY machine_id, machine_type, sensor_name + """) + if kpis: + dashboard_cache["kpis"] = kpis + + anomalies = _run_sql( + f"SELECT * FROM {CATALOG}.{PIPELINE_SCHEMA}.anomaly_timeline LIMIT 100" + ) + dashboard_cache["anomalies"] = anomalies + + trends = _run_sql(f""" + SELECT machine_id, sensor_name, value, unit, anomaly_status, timestamp + FROM {CATALOG}.{PIPELINE_SCHEMA}.enriched_events + WHERE timestamp > current_timestamp() - INTERVAL 30 MINUTES + ORDER BY timestamp DESC + LIMIT 500 + """) + if trends: + dashboard_cache["trends"] = trends + + logger.debug("Dashboard cache refreshed") + except asyncio.CancelledError: + raise + except Exception as e: + logger.error(f"Dashboard cache refresh error: {e}") + + await asyncio.sleep(5) + + +# --- Dashboard Endpoints (serve from cache, instant) --- + +@app.get("/api/dashboard/health") +async def dashboard_health(): + return dashboard_cache["health"] + +@app.get("/api/dashboard/kpis") +async def dashboard_kpis(): + return dashboard_cache["kpis"] + +@app.get("/api/dashboard/anomalies") +async def dashboard_anomalies(): + return dashboard_cache["anomalies"] + +@app.get("/api/dashboard/trends") +async def dashboard_trends(): + return dashboard_cache["trends"] + +@app.get("/api/dashboard/landing-count") +async def dashboard_landing_count(): + return {"count": 0} + + +# --- WebSocket --- + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + await manager.connect(websocket) + try: + # Send initial state + await websocket.send_json({ + "type": "init", + "machines": get_machine_configs(), + "fault_states": simulator.get_fault_states(), + }) + # Keep connection alive, listen for client messages + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("action") == "inject_fault": + simulator.inject_fault(msg["machine_id"]) + elif msg.get("action") == "clear_fault": + simulator.clear_fault(msg["machine_id"]) + elif msg.get("action") == "clear_all": + simulator.clear_all_faults() + except WebSocketDisconnect: + manager.disconnect(websocket) + + +# --- Serve React Frontend --- + +FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist") + +if os.path.isdir(FRONTEND_DIR): + app.mount("/assets", StaticFiles(directory=os.path.join(FRONTEND_DIR, "assets")), name="assets") + + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + """Serve React SPA — all non-API routes return index.html.""" + file_path = os.path.join(FRONTEND_DIR, full_path) + if os.path.isfile(file_path): + return FileResponse(file_path) + return FileResponse(os.path.join(FRONTEND_DIR, "index.html")) diff --git a/demos/smart_factory/src/setup.sql b/demos/smart_factory/src/setup.sql new file mode 100644 index 0000000..d6fcffc --- /dev/null +++ b/demos/smart_factory/src/setup.sql @@ -0,0 +1,14 @@ +-- One-time setup for dilan_catalog workspace +-- Schemas already created; this creates the landing table for ZeroBus + +CREATE TABLE IF NOT EXISTS dilan_catalog.smartfactory_landing.raw_sensor_events ( + machine_id STRING NOT NULL, + machine_type STRING NOT NULL, + sensor_name STRING NOT NULL, + value DOUBLE NOT NULL, + unit STRING NOT NULL, + timestamp TIMESTAMP NOT NULL, + is_fault BOOLEAN DEFAULT false +) +USING DELTA +COMMENT 'Raw IoT sensor events ingested via ZeroBus'; diff --git a/demos/smart_factory/src/simulator.py b/demos/smart_factory/src/simulator.py new file mode 100644 index 0000000..bb6ed54 --- /dev/null +++ b/demos/smart_factory/src/simulator.py @@ -0,0 +1,195 @@ +""" +SmartFactory IoT Sensor Simulator. + +Simulates 3 factory machines generating realistic sensor telemetry. +Each machine has 3 sensors with configurable normal/fault distributions. +Supports fault injection for live demo scenarios. +""" + +import random +import time +from datetime import datetime, timezone +from dataclasses import dataclass, field + + +MACHINES = { + "CNC_Mill_01": { + "type": "cnc_mill", + "display_name": "CNC Mill", + "sensors": { + "temperature_c": { + "normal_mean": 45, "normal_std": 3, + "fault_mean": 85, "fault_std": 5, + "min": 20, "max": 120, + "unit": "°C", + "warning_threshold": 55, "critical_threshold": 75, + }, + "vibration_mm_s": { + "normal_mean": 2.5, "normal_std": 0.5, + "fault_mean": 8.0, "fault_std": 1.0, + "min": 0, "max": 15, + "unit": "mm/s", + "warning_threshold": 4.0, "critical_threshold": 6.5, + }, + "spindle_rpm": { + "normal_mean": 3500, "normal_std": 100, + "fault_mean": 4800, "fault_std": 200, + "min": 0, "max": 6000, + "unit": "RPM", + "warning_threshold": 4200, "critical_threshold": 4800, + }, + }, + }, + "Hydraulic_Press_01": { + "type": "hydraulic_press", + "display_name": "Hydraulic Press", + "sensors": { + "pressure_bar": { + "normal_mean": 180, "normal_std": 10, + "fault_mean": 280, "fault_std": 15, + "min": 0, "max": 350, + "unit": "bar", + "warning_threshold": 210, "critical_threshold": 270, + }, + "temperature_c": { + "normal_mean": 55, "normal_std": 4, + "fault_mean": 95, "fault_std": 6, + "min": 20, "max": 130, + "unit": "°C", + "warning_threshold": 70, "critical_threshold": 90, + }, + "cycle_count": { + "normal_mean": 12, "normal_std": 2, + "fault_mean": 3, "fault_std": 1, + "min": 0, "max": 30, + "unit": "cycles/min", + "warning_threshold": 6, "critical_threshold": 3, + }, + }, + }, + "Conveyor_Belt_01": { + "type": "conveyor_belt", + "display_name": "Conveyor Belt", + "sensors": { + "speed_m_min": { + "normal_mean": 15, "normal_std": 1, + "fault_mean": 5, "fault_std": 1.5, + "min": 0, "max": 25, + "unit": "m/min", + "warning_threshold": 9, "critical_threshold": 6, + }, + "load_weight_kg": { + "normal_mean": 250, "normal_std": 30, + "fault_mean": 450, "fault_std": 20, + "min": 0, "max": 500, + "unit": "kg", + "warning_threshold": 350, "critical_threshold": 430, + }, + "motor_current_a": { + "normal_mean": 8, "normal_std": 1, + "fault_mean": 18, "fault_std": 2, + "min": 0, "max": 25, + "unit": "A", + "warning_threshold": 12, "critical_threshold": 17, + }, + }, + }, +} + + +@dataclass +class MachineState: + is_faulting: bool = False + fault_progress: float = 0.0 # 0.0 = normal, 1.0 = full fault + drift_rate: float = 0.1 # how fast fault develops per tick + + +class SensorSimulator: + """Generates realistic IoT sensor readings with fault injection support.""" + + def __init__(self): + self.states: dict[str, MachineState] = { + mid: MachineState() for mid in MACHINES + } + + def inject_fault(self, machine_id: str) -> bool: + if machine_id not in self.states: + return False + self.states[machine_id].is_faulting = True + return True + + def clear_fault(self, machine_id: str) -> bool: + if machine_id not in self.states: + return False + state = self.states[machine_id] + state.is_faulting = False + state.fault_progress = 0.0 + return True + + def clear_all_faults(self): + for state in self.states.values(): + state.is_faulting = False + state.fault_progress = 0.0 + + def get_fault_states(self) -> dict[str, bool]: + return {mid: s.is_faulting for mid, s in self.states.items()} + + def generate_tick(self) -> list[dict]: + """Generate one tick of readings for all machines (9 events total).""" + events = [] + now = datetime.now(timezone.utc) + + for machine_id, config in MACHINES.items(): + state = self.states[machine_id] + + # Advance fault progress + if state.is_faulting and state.fault_progress < 1.0: + state.fault_progress = min(1.0, state.fault_progress + state.drift_rate) + elif not state.is_faulting and state.fault_progress > 0.0: + state.fault_progress = max(0.0, state.fault_progress - state.drift_rate * 2) + + for sensor_name, sensor_cfg in config["sensors"].items(): + value = self._generate_value(sensor_cfg, state.fault_progress) + events.append({ + "machine_id": machine_id, + "machine_type": config["type"], + "sensor_name": sensor_name, + "value": round(value, 2), + "unit": sensor_cfg["unit"], + "timestamp": now.isoformat(), + "is_fault": state.is_faulting, + }) + + return events + + def _generate_value(self, cfg: dict, fault_progress: float) -> float: + """Generate a sensor value blending normal and fault distributions.""" + normal_val = random.gauss(cfg["normal_mean"], cfg["normal_std"]) + fault_val = random.gauss(cfg["fault_mean"], cfg["fault_std"]) + + # Blend based on fault progress + value = normal_val * (1 - fault_progress) + fault_val * fault_progress + + # Clamp to sensor range + return max(cfg["min"], min(cfg["max"], value)) + + +def get_machine_configs() -> dict: + """Return machine configs for the frontend.""" + result = {} + for machine_id, config in MACHINES.items(): + sensors = {} + for sensor_name, sensor_cfg in config["sensors"].items(): + sensors[sensor_name] = { + "unit": sensor_cfg["unit"], + "min": sensor_cfg["min"], + "max": sensor_cfg["max"], + "warning_threshold": sensor_cfg["warning_threshold"], + "critical_threshold": sensor_cfg["critical_threshold"], + } + result[machine_id] = { + "type": config["type"], + "display_name": config["display_name"], + "sensors": sensors, + } + return result diff --git a/demos/smart_factory/src/zerobus_client.py b/demos/smart_factory/src/zerobus_client.py new file mode 100644 index 0000000..5758490 --- /dev/null +++ b/demos/smart_factory/src/zerobus_client.py @@ -0,0 +1,145 @@ +""" +ZeroBus Ingest Client Wrapper. + +Pushes IoT sensor events to Unity Catalog Delta tables via ZeroBus. +Falls back to SQL INSERT via statement execution if ZeroBus SDK is unavailable. +""" + +import logging +import os +from datetime import datetime + +from databricks.sdk import WorkspaceClient + +logger = logging.getLogger(__name__) + +# Try to import ZeroBus SDK +try: + from zerobus.sdk.aio import ZerobusSdk as AsyncZerobusSdk + from zerobus.sdk.shared import RecordType, StreamConfigurationOptions, TableProperties + ZEROBUS_SDK_AVAILABLE = True +except ImportError: + ZEROBUS_SDK_AVAILABLE = False + logger.info("ZeroBus SDK not installed, will use SQL INSERT fallback") + + +class ZeroBusClient: + """Wraps ZeroBus ingest API with SQL INSERT fallback.""" + + def __init__(self, table_name: str | None = None): + self.table_name = table_name or os.getenv( + "ZEROBUS_TABLE", "dilan_catalog.smartfactory.raw_sensor_events" + ) + self.w = WorkspaceClient() + self._use_fallback = not ZEROBUS_SDK_AVAILABLE + self._warehouse_id = os.getenv("WAREHOUSE_ID") + self._stream = None + self._zerobus_sdk = None + + async def _init_zerobus_stream(self): + """Initialize ZeroBus async stream.""" + if not ZEROBUS_SDK_AVAILABLE or self._stream is not None: + return + + try: + server_endpoint = os.getenv("ZEROBUS_SERVER_ENDPOINT") + workspace_url = os.getenv("DATABRICKS_HOST", "") + client_id = os.getenv("DATABRICKS_CLIENT_ID", "") + client_secret = os.getenv("DATABRICKS_CLIENT_SECRET", "") + + if not server_endpoint: + # Derive from workspace URL if not set + # Pattern: .zerobus..cloud.databricks.com + logger.warning("ZEROBUS_SERVER_ENDPOINT not set, falling back to SQL") + self._use_fallback = True + return + + self._zerobus_sdk = AsyncZerobusSdk(server_endpoint, workspace_url) + options = StreamConfigurationOptions(record_type=RecordType.JSON) + table_props = TableProperties(self.table_name) + + self._stream = await self._zerobus_sdk.create_stream( + client_id, client_secret, table_props, options + ) + logger.info(f"ZeroBus stream created for table: {self.table_name}") + except Exception as e: + logger.warning(f"ZeroBus stream init failed ({e}), using SQL INSERT fallback") + self._use_fallback = True + + async def push_events(self, events: list[dict]) -> bool: + """Push sensor events to the landing table.""" + if not events: + return True + + try: + if not self._use_fallback and self._stream is None: + await self._init_zerobus_stream() + + if self._use_fallback: + return self._push_via_sql(events) + else: + return await self._push_via_zerobus(events) + except Exception as e: + logger.error(f"Failed to push {len(events)} events: {e}") + return False + + async def _push_via_zerobus(self, events: list[dict]) -> bool: + """Push events via ZeroBus async SDK.""" + try: + offset = await self._stream.ingest_records_nowait(events) + logger.debug(f"ZeroBus: pushed {len(events)} events") + return True + except Exception as e: + logger.warning(f"ZeroBus push failed ({e}), falling back to SQL") + self._use_fallback = True + return self._push_via_sql(events) + + def _push_via_sql(self, events: list[dict]) -> bool: + """Fallback: push events via SQL INSERT using statement execution.""" + try: + values_clauses = [] + for e in events: + ts = e["timestamp"] + if isinstance(ts, str): + ts_sql = f"TIMESTAMP '{ts}'" + else: + ts_sql = f"TIMESTAMP '{ts.isoformat()}'" + + values_clauses.append( + f"('{e['machine_id']}', '{e['machine_type']}', " + f"'{e['sensor_name']}', {e['value']}, '{e['unit']}', " + f"{ts_sql}, {str(e.get('is_fault', False)).lower()})" + ) + + sql = ( + f"INSERT INTO {self.table_name} " + f"(machine_id, machine_type, sensor_name, value, unit, timestamp, is_fault) " + f"VALUES {', '.join(values_clauses)}" + ) + + self.w.statement_execution.execute_statement( + statement=sql, + warehouse_id=self._warehouse_id or self._get_warehouse_id(), + ) + logger.debug(f"SQL fallback: inserted {len(events)} events") + return True + except Exception as e: + logger.error(f"SQL INSERT fallback failed: {e}") + return False + + def _get_warehouse_id(self) -> str: + """Find an available SQL warehouse.""" + if self._warehouse_id: + return self._warehouse_id + warehouses = self.w.warehouses.list() + for wh in warehouses: + if wh.state and wh.state.value == "RUNNING": + self._warehouse_id = wh.id + return wh.id + raise RuntimeError("No running SQL warehouse found") + + async def close(self): + """Close the ZeroBus stream.""" + if self._stream: + await self._stream.close() + self._stream = None