From 8b86a0574c5015cda5eca3ed7cf72b70a8179dc2 Mon Sep 17 00:00:00 2001 From: Dilan Patel Date: Tue, 26 May 2026 14:36:33 -0500 Subject: [PATCH] Add smart_factory demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end IoT streaming demo: factory sensor simulator → Zerobus → SDP bronze/silver/gold → Lakeview dashboard, with React frontend and Databricks App deployment via setup.sh. --- demos/smart_factory/.gitignore | 11 + demos/smart_factory/Makefile | 17 + demos/smart_factory/README.md | 152 + demos/smart_factory/app.yaml | 19 + .../dashboards/smartfactory.lvdash.json | 145 + demos/smart_factory/databricks.yml | 53 + demos/smart_factory/docs/architecture.mmd | 45 + demos/smart_factory/docs/architecture.png | Bin 0 -> 83540 bytes demos/smart_factory/docs/demo-script.md | 165 + demos/smart_factory/frontend/index.html | 13 + .../smart_factory/frontend/package-lock.json | 3037 +++++++++++++++++ demos/smart_factory/frontend/package.json | 27 + .../smart_factory/frontend/postcss.config.js | 6 + demos/smart_factory/frontend/src/App.tsx | 253 ++ .../frontend/src/components/ControlPanel.tsx | 88 + .../frontend/src/components/DashboardView.tsx | 379 ++ .../frontend/src/components/EventFeed.tsx | 90 + .../frontend/src/components/FactoryFloor.tsx | 274 ++ .../frontend/src/components/MachineCard.tsx | 92 + .../src/components/PipelineBanner.tsx | 68 + .../frontend/src/components/SensorGauge.tsx | 118 + .../frontend/src/components/ZeroBusInfo.tsx | 122 + .../frontend/src/hooks/useWebSocket.ts | 131 + demos/smart_factory/frontend/src/index.css | 23 + demos/smart_factory/frontend/src/main.tsx | 10 + demos/smart_factory/frontend/src/types.ts | 50 + .../smart_factory/frontend/tailwind.config.js | 37 + demos/smart_factory/frontend/vite.config.ts | 19 + demos/smart_factory/pipeline/bronze.sql | 20 + demos/smart_factory/pipeline/gold.sql | 59 + demos/smart_factory/pipeline/silver.sql | 57 + demos/smart_factory/pyproject.toml | 16 + demos/smart_factory/requirements.txt | 6 + demos/smart_factory/setup.sh | 198 ++ demos/smart_factory/src/__init__.py | 0 demos/smart_factory/src/app.py | 546 +++ demos/smart_factory/src/setup.sql | 14 + demos/smart_factory/src/simulator.py | 195 ++ demos/smart_factory/src/zerobus_client.py | 145 + 39 files changed, 6700 insertions(+) create mode 100644 demos/smart_factory/.gitignore create mode 100644 demos/smart_factory/Makefile create mode 100644 demos/smart_factory/README.md create mode 100644 demos/smart_factory/app.yaml create mode 100644 demos/smart_factory/dashboards/smartfactory.lvdash.json create mode 100644 demos/smart_factory/databricks.yml create mode 100644 demos/smart_factory/docs/architecture.mmd create mode 100644 demos/smart_factory/docs/architecture.png create mode 100644 demos/smart_factory/docs/demo-script.md create mode 100644 demos/smart_factory/frontend/index.html create mode 100644 demos/smart_factory/frontend/package-lock.json create mode 100644 demos/smart_factory/frontend/package.json create mode 100644 demos/smart_factory/frontend/postcss.config.js create mode 100644 demos/smart_factory/frontend/src/App.tsx create mode 100644 demos/smart_factory/frontend/src/components/ControlPanel.tsx create mode 100644 demos/smart_factory/frontend/src/components/DashboardView.tsx create mode 100644 demos/smart_factory/frontend/src/components/EventFeed.tsx create mode 100644 demos/smart_factory/frontend/src/components/FactoryFloor.tsx create mode 100644 demos/smart_factory/frontend/src/components/MachineCard.tsx create mode 100644 demos/smart_factory/frontend/src/components/PipelineBanner.tsx create mode 100644 demos/smart_factory/frontend/src/components/SensorGauge.tsx create mode 100644 demos/smart_factory/frontend/src/components/ZeroBusInfo.tsx create mode 100644 demos/smart_factory/frontend/src/hooks/useWebSocket.ts create mode 100644 demos/smart_factory/frontend/src/index.css create mode 100644 demos/smart_factory/frontend/src/main.tsx create mode 100644 demos/smart_factory/frontend/src/types.ts create mode 100644 demos/smart_factory/frontend/tailwind.config.js create mode 100644 demos/smart_factory/frontend/vite.config.ts create mode 100644 demos/smart_factory/pipeline/bronze.sql create mode 100644 demos/smart_factory/pipeline/gold.sql create mode 100644 demos/smart_factory/pipeline/silver.sql create mode 100644 demos/smart_factory/pyproject.toml create mode 100644 demos/smart_factory/requirements.txt create mode 100755 demos/smart_factory/setup.sh create mode 100644 demos/smart_factory/src/__init__.py create mode 100644 demos/smart_factory/src/app.py create mode 100644 demos/smart_factory/src/setup.sql create mode 100644 demos/smart_factory/src/simulator.py create mode 100644 demos/smart_factory/src/zerobus_client.py 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 0000000000000000000000000000000000000000..fd36d1f6100514438a0d6bc53fbe6a029f0d252c GIT binary patch literal 83540 zcmeFYby!qg-!?p`2ug_((wC!vv`Dw$3?0%9(%s!CDoBbnICQ6UhrrMwIfO&^&>b`I zZN0AR{+|2$`}^m4k9Qw)9DDX+vG=UC&h?A)cka(`6r~96Q{D%GKm;<<5ET#ze*px- zD#X17v?MGl3E^#5S+RfO`N#RG=d zCYDtC9EMgPz^F~wlNSwI9Zhg4&9rEnBAX-;dhB6?o34V?gq71I+Kid%s7b}a)t#@y z?D-l$zo2liad3PZzl1UK^Pkt~+7sg8owz@#%YVOps%ziE$y^v&i}&}coio10{$~V4 zDo|o|(io_kIti3hS8dQtv;wiWK#RIoh_|u-G6RFEyD5;zWIhiq)Y#jhges@{nKxLg zFrUy#i=Ov2A&^9SjTW}e=To7#YwTwacHa{fijK6O&E3C$zrk!pq4|oSSmPsku0iv8 z;~w?ccOy(&;HwxJHfCl;8HKC;EU3-o_3>`XQL8ov6O1QzYKfKLw*A6-yr+wRDzsb@k2t+=yrUYA&C$ z~pFOStJ2emYp z?ap3V6ZiHFoVrJl$CZ`s#VE<2o*byh_I+0RGdPgmi~nHJR7Gv%mX)~*7Ghl08YMa{EuwQpBY2@(b$?j=U&|0gqE70>_!Ht3TMu5oKdY}#t=DbHom17h!5eAakc*no6(8I& zy<~do;JWd1(IrAgPKW_~MfA+D?WA^Zs@|`ANEX)8(t^1<85-6?e+4U5^Jt9*4&;R3^&KXtZ3-foO<0PC(<6psY zeWEU>Oq{x|V>iCuKQ~&J{ebQWOPKCwMSQ6!5nmu}ytUpn-jy=BYD!{Q&+}dLF8i&tn{0$^B zJ2y2+k!xjbwfW^9Pvyn@`KCtRa@vI-&YxenPxk+UiZcaPSTCHO8%~5IUBjiR&2`8_N%i4AV0TY`qD*_G61btga9=oGh@4>9U}iF18v|q?u`?Q}nV` zX{R5wF2q+?Z}iwPt}=w(TzW}f&s(J7oL^{em+O=a#(cedyf<3`b%81~(^~&bzs7S( zD;eJ;BQf2ZYC!_`US2jH7A~|2_>QzLlGAnH`Z;G^ooXQu7zy=eZ+FxTJuB-mTC>qo za^5{`B=KfOcb(!l*i|VgcIm=H0iTn=4vYEvaQLKCL(W%k8@S`|D}15LtBaGp*>2BI zz-;l@%#8az&$UOUA{!~~QlfSW4Mc1ZghB|CA3uHoyK&;8LJ>g9I^`oODxvg%dAVes z<^^^9zYVOT#!VNI_}}Q%<1Xw~$<)zHHfNq+Q$?_8AlEEpr`H3}Uk0<~wiOyOpJ#?h zi~IXqUF4>bcjU0?hEw?I8cT3Nm22#gldXpH?{4?SSbKS0+z!TPVPhgCj_DOPp8A5i zOMpvo53mQj0mui$7c0v|U=-vrg2HPoB*lT69n7B4Z~C&ntkngPDzw)!9D?Ilv++6% z&I(q{tDKE%9%eQFS&fM}Y3S>t_Bq`ZQkupSyEg5w+1U+#)HTnS9zf?&BZD|`Yqj%q zTzW=azgB1vL+kYuS6RCxo@c(~Vf4KJEWgoG7FpP8&ieW@1~lz=7k+AoCd^!W6d7jm zXfjFo}B{a#M4pOvh#u#kzV0Edv4gjiuL z&1W&^*RLkAZ$(`v+jA4yl7e4hY_0Wr ztvyPNI69s!If}x~Mn&a)=5w5{F?qtgy;CvAVXG`2d((Wt1aE9S56;?dq?>Fx{$*>D z7-BnHW3ugcrpe0B#A&n^Zgtu=pYZEyvAa4-?4qiu{m}F8r;Lz=sUzH=%6)z8?$RWG z_RvCF1nKzS`(3zabySvB)Qnw$yF@rM*Gkr^oc6Ka%(bjk5X7k-G6o?gmQhkd=#kgK zE!^~HD&XI=xx!tOL*#ue+W`kpMxJ1q%m8-xSt#Bd3y-cem!|>N`{$Q0p?R&RmnlaE z9EJfl9bF2;qGwCn0vz}h{x1Ih?|yqY^yRGgQy2qydU_gzL#RQLlbwA$^CON+=~IoJ zE>%QK-srGlegEpO7Qf(;U6(=i+hsd>&-*rnc3T>TtLICv4U}UEcV_3DuI#`SChMgZ zX+8(%pJOCn>`t#6_@362&wHPV5GM()mUn(IulMruD{c0lim0WPQK;5pHQ9Tt?-Ws5 zDJySzIz)A}1@CS?T@%u8Fyy(6YzzwxYnOOU-h|PCudXgF8yk8VDLP8;qa0esmJ3CL z9O?>+2Z*Vt#+p&2$VP*aN*}~jV&aj~$`gd1Tii%bZ*ReF{Zj`@(aq>!35oaJBK!NR zNbkXKm~W-1dkNgShnwA9myRC4+~(gCkx`0)6!WsyC;X64nnZ7x4)^zuxna6=?fFKn zqU3!Ye>I!%+0QsD%)dK}4EUb@{f+uWxkn_$&sGP($_&9U;X>}btK~A$(Qe@=SJaFd z&_63H3j%>$oOgHt7R1@bB_eAug^{q{|4ZY-F0!HJa@2*M9^T(&txeMLz9AeyDY zHI>Jt&}ZQMsw=Y5!#1hIX}L!^!2@+NDTG%%S;g_@rdiK1KL^UR6w`X+x>51$r{N{j*Li(H-oa|UH8;vnU>{9S$ zc-)BlT^c4N82$xjOJ<}DW)MbOMipJg#>cA*V`eAj%g~25Sw7`QrrTh90bF?O%$dAQ5;B}rh5#|2oV}tw&}l$>n1Z~i zSoP{?nBS#~TV1{N=DM4R+`rgr2Dqy|g2y6|-EYm}T&-Yh4q*Nd)u(P@OI;%>Y)_;<(8__%e+Lev* z-eYv&f=Q^lY$#NjM5>_Dpz^)ANgpKxQx3g~x$QDIK#t;xXTd6HvAB=;>ix;?H7( zX3wmnowGSUN{XiD0*mpla2OFy>ut)0)dBn%Z(Fi&p-?A9J@eGkE zK^wteN_iRNrKkTyCMCt0ajU7jbtmc#74C`%uK%{a7L>w}rt7gn6oRZA1;=F*7Axt`DvSiMCUBshx zuzSruk4D6t+t_4F19R6#&s-zET-U}?x}=hDs@&x)64vPBx3X3QU^I@J(CmVBih+UE0F%uJ1 zsI;`>{vaQLhhbl{r-7%Imc3%>mDKJ4{h%Vm9;vhRz_xL^IH9q zh5*j1h(9`HqaoFI|7Z4RqLYS-#=^qZlKID`L=`$Wo+;z)nP4C7=^_r1v{+9W8JSJQ zyswYlO-q1hJn`tm=+UpEp_r@F9WKh~Xky9bg^Z^PlzjK^5fb9#zEIIVSy~Q%jpDNv zM8Ug}K8OX>U5pWd`q@aPk^unyoS`5bB0l@6H<0!Y>$LS>?O!t=+#`!VK8}%8Lgqgw zW@0I7YC%TF9=d8SOA;VqMJ*WjSFJ6)Db)e6H^>-V%Q`>bB1KP!uZfuQ;n#1z`@2`P&!4mEl^?4X@oW$&$;-<6I_2<;I!rs8ujeYH0NKRYluV`F>?8`|AdISSE&H}U8u4*8 zyn6K(nz@@UikgWiA6B@}=7WjwxIEj*dfpfD)LyH`7tY2vpBNKkvOQ$=B543T3NQu9 zbt9FP7nf?`_^AaWn{1XH?GhrI{}6EhvU;AA1>wcx9ah8%4EBTl))o`$mFlhv`Q(&xQOL!@gf|aPGI{OhoY9@VJ%Yp~>~@@& zHPK{YCwc{}we)hWbyq{2yeaCCQaXOeYbu9So9xWzs%huL;o2_u!$QegZWO;D{m=KK zqqBDNy}dZyP%Ve;)avSD7q$m@9D_xdpEoHeemwaB9<5O*QlCd7kb85FU60ld@h68{ zOH2%k_Rin9WKhR~{n+JqZ~pkL4S&9((fW zH2;h0(Z?s@meHB$xn0B=rAgbU84EK3SJKd#`*fE=N<`PsCkV~$sE7g^?iPJa^9XLm zETh=rTX&69AaWV-cHjLF%&70Ba3i$az`@0W5=ON0n-3)0a6e?Xa`#&AFZ9&V(Mdia zpb@o|hF>Q(xJ5Y31IC}_bl9iFR|)~_3_;is(X|K5@NrBYq|>-uF-o@u`9#SfQg;kqeX^0u7!TVbTUHMOj<;%@byid_9 z>nJ|sbAq@?9oBfSvZlAyI%EMY#m22sQBgjDjbpIg`=#prNUKUMXs0d4PfJ|6<{Fwf;3<_XEh`8b*}4EaZ7o`uCO^-XSPw z6%Tia`;h{$kyI^T57ZrZxXOs8{Do=m&Sa)dn0Gk{0%PQ&RLl8M=`qk-lq@~dag9T zAxr!g|F`fa&EZreBzwu*`O}V&ySb@3<$N^(6`v4wWT@17vq6i5LGY;^IlJdoA1zWx zf99~4kwGc#vh#XnW%R2Garrlc6tLmA}jM=kMMFaAFRuiTc4Vmn$tYCDjA6WviK}L{NI4YoF19LX-$HjB9aYg5EdW_#xGyip1 z^m?t|EJ0txKTp$jQ`oJ~q>C(mH(ez~BcZ{;>-5{Yo4zIqaQn3y2ZRRX^0?))ki`eE z0dqP=DI@3S{3c2hwFUuhn@i9ejP=FLUaR&IvQXWSz1dm>=fIwGSs243CK55o9BigYDxI9~_);?&hR_-ya;i3I!Otd5>Xzhk*dA zeYYUAFD7>|(G2}f-n+@SdmBUd5fCPuWv*b|yI$|M*eDxx+3OW?kh! z#t>$Gd4L$)%zJ5V8Y)RZC4AWvM1Vq+RdwfdD4(A26{RKdbR(Bhs7B}MJhJ{hnS!Ov zrS7zEynZvWp+yBW0$(f4cQ`tN3EU0eAWnuw4G+;_Wt1%O5zQu5PP?jO7Ny!(s&E-n z=!Me>qChH&u=QZl8VUcN5fn^BqsLMguVQ4PsE+{BXs(k@i_u707Y~4E_}T0+Z-vkD zoYmL$@R5j*E6vZcvJiCLTAoal0_UMTUBJG*qxAPu)NJh@wa+{Td^j+sNbptu2u__>$ z8VbPdAbn2NgjeHJ9GZ6GQ<$$^caXIK1Nvt$w|On7vPPMXKl4+Yp6@9`I&HsW-pC3TnSj3pz{0*jQHgo?`=%qmJ&oTwP#8isy7?U*$n4VL>m$x+q~~hvW>13d zYHaHrOcVme0~Rpm@;KG;rg>LR05+sOELwY5q`u3-XZ33XzHUJ^9LV_=2*)BG^NyeY z0Bq@*CQz`z7HI?^axQ1-*2nKVm`V=j*xn7!ir3`m?+#ybc9b6sz^) z5s_OzTVT_+a}9PeO0Wy-Xsc5&a6}BpI)RhgA(!j3tiNWB_Ue2u~cBIzJ!jU4*j3 zxVX4HwtxAR0!GuHMyLvpsI1wqyA+7^)y-I@wp7%s-B5B=eAccj(tW4~_``p-01ha= z*tj^D00R@}6DB53dU`jc1FD3qf8XeJd>MM{{NM=ryD)uC_Gj)jfBX8tR=Ph0i2$2u zxAqj?&m052g!@E2?5wj*Zh5sVhp}d9n6!Yv0D#`|1WKVhfUipRv>gQ~1Q_}}#)201 z;=08K=eRkudm;e#C*{~zg8sShEf>l<)N$m*+n*yTL$-kE8i8S(D|EP~YYAZ)Z@*k#K#8atJ1*>Fw(Ss<#St+1j(b zC9-m|-6H%Wvpj#-6F}tm{#7cB<0tfQ+6z&edCYN@8V|R2i!9Umo+jlz1-D~-G2X5y zW~9GZmHE!FYOVqY8`Fuo-`@jlgF^lZn?O7!tC~ga9UVe$wrN1(v#MzbgvW>O&^afd zK8E@fDJ?5sSExCh*f0Ws1)!*Qrz;(Hlv=ZMbG#Y_|GwKoCitJKY9Qqi5CYBV@4u@p zI+vSjFYWe`;NSH~gO|!}wpO8u?%LbN?2(fItfltUXe*SEjkOMW8q8 z9dm@2_GJ@*0wwO_EGrch(Q70fQ*a4c-8LX1yW8C_Ft6Baz(E8z zcpNY&NLPfMRYzm08eb1{s5I$g2jZ1`CeN`(inRhmW%cv`Oy>VM7Ks1legAEw{kH=C zZ^QrX@@FRg@4Wn;hW|ss&}PG}aO8e3|E~U-`#(PB&!VG||Nj`Ze{4-tn|ZnLc!By@ z#Vw>U88LiQp141}A@h33oe?s#!*x$X_{DE-|KS|bR7s>c z=>r)F@tetDaOhZcUeW;bp1`d(_*)gTH(!t9YleEBi_Pb+O$xFpyekjoc|{Ty60$o^ zgI$WQYKzgeK}^pS7|aDz4TjZd$$pzFr2++tiIY}G*PBqey>0%I%X!Zz`X1J)WzocZ zb}}jPh`zT~Mtnfi_DZSui95HIXCYG3Y}0;SiJj@leG#-DWz5_Ag?DK7sxCA6{BlyJ z;#1WdKNAv{^vpyk1fkzh`HDrR(!wFkvZ_Q5h4#=$6w|X{ck*ns2Zn?m{6k8ddz5RJ zM7i0H^f8=N8zYT~p~S=qy=sW=)5#nHcT%A|TcI!gl4+m?&Ud_+mZ3=hLH$#28PNlz z!l@RN2bKJq8VYU*33(oB%!|YK5@(_oT74pb#!Lk&M06Yri0M>>k~ky~f!aQPXKt|@ zygj2;YBvgQ@n6>g25GJAnzv!L#7w$OCzhgcVr5Afb{1hpN^+vAwUN6qV|LIgD9Sde zy+Hx8{)ChY$gg#I>yXfFl&J(OEp!Oa*BF6il2dU=l0cP<$tEVJsI}(F=DPTng6|RQ zK3uBWlw+m%*z_2PV%t9c@nZXXOQU+wOung#p)&ube%lcq1qejl&J12YTVt!R@n*}_ ztFenR_qi;P836hMK+v%}ba{gMoJlP&x-l^dLZcGZC=k;ltvM|D%0jxn9`KbS^0d(t z7c{&mzi?uAcEdVmur*hAHug?8sb z57=hf&_Q|qGTJ-%j`sNu-G_BGXlo&iy8lZ_Z|63EgQg%Leel$7K0iM=i@NGC0+O^8 z0hd!~zQyUbMX`o%4v?+O$;llZ9YxV9TzOCMbw|>+;>M}PCMNDLynd9Km1)OoqhUTd zq~F+ba~(V|P+Xvzwmp`Y7IM5bQK^(BYLTj?rWP9$69c43$;l1gCmv>IaUOT;*Hgc0cSl*C#;xfb2_Yhf5}*qkuEYcYC?fQ zA3`0Og+s()HNKTJcqJz*cQbHub%LUL@YvD(M$^y_28+X3l=@=5f4?o_eQG&Y7zekW zZu07H|42uLxpyz&mvjFhxMIH}iVrATL>JxKcW}3RU+IzAZ%|gxQy5{8oY$zh>D*%Y z<#}dUuXVv#`!4hD;xEs?Iu3>;70|Tno}22rpRBFQe%o^PzVo;vW&mCcCn5AjXY*UqZ=`ireUS-;5oKY z)NzZ7{~MesZ<3~0&trdOhW2yRwqS$s#R}1`(^J7#I^uCwF0qS<3BQO(R8Uu&*^SAm zpS)m+R|IWhT|eE2#QpTdBVt%H?4XD6=!@?4=VWryVd-7;%`mDnI*%b;zr|K{Tgtqs zO_qBn^xUs(d70SbUU#pCr2(IncNxwrADWsgC^_WSpnC{yiZRVHx}1=nc86hsmtaV% zO(6aTtEh$H1?v3YnqDf{sq)fs*$Mc^PfaH|hno6Th!zKNhq2Qc>J?g$$tJ0`@5ly5 zfx*eS0gZA2ju{49&vNgA_Cp@aL6lZqe9oV?~4kgiY z`;6h_@$SrN*F=K?=se7ZvHB{VOT4y?mcTSanKn4n8t(%<1gXJd1u3And#b zlf_6QyZcZy0CqTj+hGGTVp?`^nqR{?Ji(x}t|cM!d4G%6Lo!NcaDkQ8(b`}u{|T#h zgKh7q8J+Lu^&l43n0joKqN}fVj&JUJ%U6YBWsCdx~!C8m? zFr2SMlmrQD+@n@t6)mkQz*S)`3AD;H(amc`Wm`Pn#~Y!HxoOO;^Wnue+SAuVLfuvN z$ina69rqWgPIqSa-d){p>dmk^idr9tva9*)`YWRcb>DDVFX>Q4MZd?9a9p%#N1g`%OxLeM^AC$<#~Rd0*sKB71D-c?gZvAX3O_GUxB5gx8J zc^=QblC83Jf_kWhcEsCZPKy0+?#xP$qC@_2vn3j}0tF^(%9Xf$-UDkg{;>#VvaM{`jL=tHs_uy|g3yT^Ts6O4T+b)sPjye9H0!UT9{W4~hI z%Ys|$wN62$8*0+;>X>uJCC%r$*oiaBnSZ#x>uP)2MGWouE(9QdTX5q#;oYC5rKML_ zXQJqYYK%H^^W->_%oXi$4ibY0nRZuTZb?c74F}K8RRNnedsi$Q}##xzkT`M#B zViqgtT1Jr9{Ue0g+%U|tKGX2$yhDTaAt%nb2D?M9okGc0ShTi>?3Y$B_$Yx{cj#bo z-mbUrFzqD!l3Yb?0^z(2djf2tvOnhdv~yXtukV_X!2IYSNd!fa)oeF=I!w+XfcpI8 z#}67I_n!XY`R_(1PEJnn*P)rNdn0~(sUlWZQh)_Y=C(pFN7*pL0G}9CcYYp0#k;n$ zqE@uP)Wgb z{@F_zX8kSsByX->-z1@@$omtL2hEA=&r*G8{ag?0B_nD5X7iP60resgo&TdvZgdHc zz1ChXF`}g&u0uUOFpS8}H;3}Pw+tm#S1r*{CMhYFN4Py5WXeP(=h_S{sRRq~d99F6 zCuwWj1Wa#is`&ZZx@|#bcHRMAXIk6h0^b(d;O3?;cggxhprSf<|Nd~x zk=3hLujG-Id1G$dod@m7Q#7P)V!C59$=p5IFC?@pH6fkm zlS15T?%A73*yK%$DRZo{pYdX73cPiA!%j|>Qig-y|H!-NK3wz~D2w}QvrtM07^gK|YszBU`xM$IdA<-X21m&E{@FnfX* z^t|~15;tZ>i(p`|V#4+5{bl|9N}t6?NnY#IMA#Q78G@As&ef^uxpa{m*q!suofKJ( zmTi75_*WopTM!56!M6VULp|KWLO!`=X=NcK*!g^p!qjl zJw0KQt^tRZi<(euA`0P7%k1o2>tr&Z;BPr1Q8Z!gs+e@RQm8&J34!=tZh~WaOcA z!$Aj;Sj%Bw6LY*>Pw$}q(Q>jqvhlNYo+8aKk1hJ#>2)w6J3BKiFS;v+QAxw*{z6%< zRn~co0R2@|HqK<&$bf%gZ+7B9XeM(gxGSwgg_u~scy*IoAK|8e_;xcq=hrQz?282o zsPtjOe3c`Tk^cDXfj2=jAMvI%$0+1`x|SvQ{aYz0@f~6-Sp*$4^si=I+$Ya67+!g^ zC+KlB9$lGmB+!2S@}Td^o9G?+{w6X2 zy^9kS3zRMb^+liCdVAOZ-r*<`g zhMdW`z#hx6K^LJ)Qg&xU{CP76GRSvL#nXmAeA)PQZEC7(Lj?L#k`WV-Sx!{A^ZFI; z6EznSupwu(llP}vY>Ye^zIQ?%XR{YuV9yr0p5f;TCv{GXZ(YkGnsL|zJn6ks?3M#C zwbBkMYi9!=u3Q~R1?LbY%7uqpm&>pFwGjo&!h3Ysy$^0~+@V%-IBojW! zvk&80SF4kDAN0h4#?Z1skHLv-CPiR570&@FErbuUk9@-RUP=mw(3oO`2*>9*B$aM^ zf)GS#pntfxLtus-_&_2G+!kE%5L6hD9o>m*1Ro!3&J>Z~vJp8{w$r5Y6ooe6Nu1qZFD#;wPpvsQ>f(nk z@BjMnz#AW6R%V;LxH5ztcHbMn&cb?yu-Dft(Xo255lloUVA}KjLnj2ZFl8MEL9BX5__I#@@g`MkZ|YT>NQgdcYOd2+%gab>mQThe1}%6sTyWzCWnu*5TtELM!8?`LAtOSEDyu#{ z0fDx>#+oaW%*9QA5K?jmqmGyovU1!+dfs^?n!7Q9po=M0QoYG zCiZEBa$-`nf;_$ue|pl$F)E;Y!N)rSd<0sRKRszj1;3trN8vv7?CI>+DEG0AvwDR8 zqE{+2b?;soF~|DWIsO9)+=&-BU2z%Up*!C`3%BFpa@3fxIBm=y?afNOGlTdg`yb09 z#F|%?B@}W6#Z3lC`e0xSk1+pv#Li+?T~i?)YCV0HK6C zC8$_P@P(XTP~NY?AmoSbDzYcbpQ^4G50Ud?Hy_5YWQWp69jTu%x}X$1`HOH~YN<~HdEh4UP+-_k zj_U-YPtC|FMc_tv7F%KJkjLEn%`pqT7GE!Tr~~NjG^5-%7_6eAV$So3Ekd$rpqGNj zI$Wa&J5V`G@-7Ze0{&(-MVb9{g;U3;li6B~`|-wNfhyzeK)CV6YJy?FjbBQMb}!b{ zu{Y-WHcqH|josK?kpGPx)$ozW?i}o;S#nMI=ObHrhOrH06SaEmqc83oKd;-~HJ3HL zEVz|s$y90(Q62YM;&mvS9fCdAVO|vU-R6vTK=Ji3=(Cm{UmK%9clL<&fY&BNNhpw+ zR7P{96bI-k^SyXK&4XEnX2H@Ydzz zM4gj~cD#t=!dqY8TcBU9z>C2pv-ceovqlC|8XeDe!7jIH1P;%dyzoXx$FN?p5_92K zzWtF&FBczh*qhf5>1tn;d*OTjCfkb%JblB?Yuu98(YaIQI-!s z$0BZ7U=2yo9l&+d4^@kGcTxszQx$m?o(8l)OWBPKEagE9m$zHS9wwJyfkOO13hN{4 z0lIsa<3~?8=?*IWS8rN|IHm|e-@U=a<)&j+{!GqGiaNr%Whn(3G4WeKD-W z0PMW){F$HftiCG|8x*M$r5Nq7x}_3na5FHlS;=u+5&zerokHkK^ee#x+z6{O*y9VpmN zbvVOry&8_M^1(A++~|RRDSc)h>3bn0;F)^sRdAQ7<@%xXxYj5CE5Q)0!$J4+b+KEZ za*rOIB9^OnzkMHW##Zn>>))I2;&=pviM{KcgdoT|brAu4}TS_4cbUvO%ncEl1tgRx4GkUYQ*@mpdyU7F#(2*q10FsuK zB_p&1IiCLV%J?gx5%(jzzF@DZN*LVvc!N{-MzhplRW94v)WRYm9P+qq#P(&t^+m$n zAaTq1n3&rjMa9S(^Y&LZn^xEY#pQ_7dmq;ZQ^XF}-#zd)FF5|NoUUUa_?k2?Pnjxz z>R}?aP#DUr+k-0**!M^ON31{?`F+Z~^&jOZ zVNlOb??%m}sA?<;2;Y2W&qZ|j73eRflwZSGpf@W~bq~@cH0ju;c0(qAa=yn3R3n8| z!o7c`B7;F~JMyEgOt>6*Xy3q3*ZfjM61XDR^1cTwx7DEv$4`{%NFVt)AfkeO@(Px0 zc}_6iiYTY-cQ=8?Y)4h3G_&h!?Pn5C1i$~KEGYQ)LAP${Gi|zXt4E@7W)SVIl#Dsp zhlD6yl5Sm4o6H&$u>j~*>QDn*f8Cs7(945MX}3|BL)WE9MJVKf1Zm#ng9SPs&BxC{ zM1p?MfZ$p4iIqB)_*|CFUp$USqIa$yF2?14;HbF;lHbMyIWCJnpTM5gshQUCxA63$ zf{oHqZ-4HUN1(Oecr8%xN4>0NlHmEgej%kqTk?qdDV`kddYNoLz zzB@X4cUKhii%nNS!^l8i67q6CmKHivcWd)Y zs;vA7S5YXcG!q3$KL7H2FiXRlnjA#9F?d@Q>sUW-mszE=R< zLZk&o5`#c?9Y!5YAP^T3=#|%km3XBd4K{XkXtAz9K=8L(bB5Q6Z%;nXga9uR5iMnFJm0Rd@5k?!tLQo1{(1SLd3y18_BcQ+#4 z(%oHe`2O#``%}aV_r#o;9c!<>h7`t`zjA;Hm6lFsNyn+Gsy$b`PLvaHcOpd1d#B}x+JbnXNzQqM8z4;L(IB5>a;AC7tAlhioh7I3mL;97iJiK=yU_MJkl_?IM% zDk8`Gf$;ittI>4q_d)YRns5Fh2+&S9OUZaEQn;*(GBTb(-1Xcny0^=-Xli#&_3xk~=7A+3#J%TN*7Kosz?F;!Vnk z9O{@xf(G>6<6@17e9~hC>0nXWQQINUO*q9}1gzO&>8%yb4`BIiA0>M~7cv`1PU}~N zkaE#br8kX!;kUgtydab3gtI>9u}NTRD$xQ{J+5$Bo3D4?3HVlD zTPyKOF{2k5va&+`tne>10dt;1XtOr@XQu-d4$nFF-8O>&xm5`G}?& zRV~w(+9-G6L{49$OAABzr3ld9S6zc1KT#YT;UFr%C1r~(GPgc@NMRk{H62Vh5I_i^ zuu_nKbwxpG*vX}b5-Ihr=3!~pTJ86^qA~3gh=SsP!J@N$p0_Bxx!Jo^ge0_TwMN*8 z$8}p-RrR-g)hl-uc`(>d5YewM7Jsy9MTm1459EkYFw>bt%f+zl{DIjo$9v!NER>rZ zAYD)(TW64R6eyz?YF1*Q0+1BO382sRz{bSm7T)=;zqyf?wSLF~jzaB1@#C1fXOcIA zC!$4eJ1}dSN}UnG%!rgmjOV$+Lp|C=acI<$Xe<*$(WOe88k6FB*%$5;S9UHP-`~in z4R7DAB(PLj6JX=v$?q+{xq6a$b$R`mivTqrv2gMP!H4!{4Dvi7Yo`A1!c>rrO-b>- z_z!N6L)PIFrhZT}MurU8wo+SJ;NHuI{c-a6h@AF|8OCn*T796!Ud3C3DRr{FIXC?G zx@ynGw_MAzgd)rvqmAvKf{Zyj5eb@Gt`kB|(OI@P#vBUVIM9@B@}f5^bwR;+UfVMz z&*2A_(5A#s!>080G8Z~ar|R#N5dWduyMwUWbit~<4l5SUhjg1~VlodM-i^zb$jh($ zgKqH#^9bZDj%7bL4RL;sgi?*BE;&@Rm#29;3j`p#M4`M1L6Cv^;~s@*qWnD;KXF&Y z2ywMxs!9*mQ?t@SE2R2LX_VE8f>}M4YF3ajexT-dc^MUN(vazJh-K`lZx>3woubJ(D*X_pTVs|=2{nhXZwOBa5@ut09QO8+e(R>{!%&O6`=r&Xo7#o+n zUIs-}J+GlxqLGVbE&8YRVY7F6X=$!Op(E955b5j-U#ip?99c%zY3Q|;O;XrGop$K& z6>cGV%J;~hk(W_2*p6TG%PgXwCNI$5c2=d`%?399g&n2pT57#D{iOFqL+_xGt9x=% zjX7pGp64SI_QAaKr^TixD*K@;++izF%XDw;U&H`fbWc3zeVV(=6%y#n9G%-}`cp7Y zFLJErcP4S0uD7+GBg0WDht<0dOdC!Z!Y3nz<@f#AXf^ch$Bez)Ci)=rX*rn9+Yp|1 z>!a1%`O0^h(CAkY&6$19qy~kp_N9KyR0D)3Q1A4qpQVUubF`HwU%tuF9sV1K6Nl&6 zaFqVZ@EY33jw^yR_YpM4oC+V)x1=Ql#N=^He<3$7pwqu|?Rm1c)?V80Usf%(mR!S# z95|*Uv48`7taE412*^K=K=U{&LHIgh#huYulL)(+}O^vQ} z?@WZZLy!q`X$%9u?@Ubd*F)BjpjRL3l{x-$?*y3aY?=p12qQq!#%_F-r}l8{VocAU zLUOKKN$f5dUmrmMWp~yO&wyD0EPcy5=jtKDi}A_D%-C{XuX*{!c zy2lF3l)iKL-(&R1BO(;~>NvaQ60uMVwW=AadRiCYAZFbL!+gp$jIZP*#-13L0FV`; zd@Z0tS3|FDISBeB<$p@P6k2W%I0^V$2Rvo_w?>cEo<$+gVe5L&dEe1s29=o)nMcEzk|!e6xRZv6xctv{(0? zA`DAtm{#)h#WRH>L?(iztGos#f)TUNCJjA`p3@~mY}@OfUh7G_i8p8C6h7;!WU&)I z>utWXe_0e&*@wL@ve76GwHT&lG-dNwI?XcpGrORK5TtV{LSHmvKv$YWSAr*z^K-n% z35STcN!&j4TnZJ_1K%qQ7b4Gz@8^X*gl7MiEF{&)*6ZzapMF3MC}`;D`)1&H|JzY$ z_zhV#$>7Xo6jk`NVXig2Ph&^r$Gb_iN%#JYc!Xz@BxMk#KRI7=Tl`)6oI`3`(C_F$ z*(VT&gXHCr+=TYBpRE(Q2546CuztxsqqfOGqa)!UiVDFcXNI{)s5Ic?K{)%-)o#WZ zkdL$wq9dJ=eYzRxci_js+*2W;$pILEWm$Q-*6k5h+H+EOAWN|KYPN>EzTxeSj8PR1 z#Az}0Lb_wCNPc&r(SE9g5<4OS0G#$a)(P}5BusK<{=B$v-^7-^{uOsG0x$)7iPM`Y zfopO0=FJ{C$;GS!qDzE{aYO9d9;YDN8mRgZolQjwWMT+I44K z7&Quh&E=h(0s85QE@#EUPN8tb5Q#~;6TGDW?R-VoY5!M8p(p%*&8UZM1B!$clUL4Y zlwh2%qkH{*g_c$AOqG>af42JJ{KJKOC@1tX2NT}nuirht`~#(l8k-Q};5;WLrXN_+ za8Vs;+dY0*Pv&=3q-)?#ZMF%5AT=&E)ed-{9}P=a8gWTyHk6Tl7SU87{D@5_t`qW2 z5>~W9CgHa6$;dD;R;6iY>+QWzQdAtoZ|41h<<|6Sm9<$1pZHWIadGU6&%AB=kGSZj zTzy*lMqQe$W?2KGK*(o7oGYbjp$$8$6Aq$?q1u~FN!6r&>cdHiwXR~*Y@)1%2;nGh zo!!E@8iAHH#naf=4h(|_+K*wIdi6W=CzXS^sL-3w9i3swT6ktnyN%E6NEyb``$!}(g3czqrP z;$HX_ndjZ?|Pb;r19Ohh6pqr;K#;-^8)FZvgwwJiM{U-nIvHv%&T{xySo?U-h)-WOT4xY(*3Oz3d@;A!;G`9vfdw)viG zyWWJzLHO6Yg$YSP%%9-tG)UHp20R*NfAYwG?IF~lRFVk%6dE6&IfJ_PTxD;h9({vy znvMB%SwKt-A?0dcbUu_i`bDky-t_u37Xm^Khu1q#dv)zMUSnnb-uYYVCqGu8!TuZH zR{O(D$6W2nWTBRUi3ykOKZGRNX2+caO3DwQT9O*obUs5`)jCVUX+cCxOd0hJgZMc) zfz!kN6QLI`SWBcfhQh)y+8~1zCLOP}K76eWNu#iY1am(`1tg4JcB0ed_)YkNFpyy#+s;jkbToh6nCbweGZXg_eIyKDgYufo{LHmwAk8 z_a&|;zuQ1#K3=1nh6#3PNQjM{onroYn^stP!^8a?;BUeEC(F7e0rN|b*LW{>;y`09 z7k+`**KM=`%L~D4=@(J$o)2dG$6tuz1CK?g9xN{@($5WAp}A>rfUi?|I51Tj#h)x6o+b_yuQ zEPvTr+v;RRA3lMWwzep7f}xh~NwqKDyHiK8u|#TWwaQd+0ym+IY6d(%U=opCLYi9d zeIDu$nmqq?wbJnO1S*b`aTsN2Iv!!DtB0~A)C9q`{xi2 zJwZVzhmib!Xx4h#DQ?5HamZ$Os?<9y1^(v<27}b=fOD-{weZD+8 zzpFW}4i5oQphZ%vw066mB-K3CXKnYMiIHVSS4tEank;NDp|6*HS9ti?G;8SRhER(J zqMdu*p9pJFT*{J@YEv_2;Y)bR7r0|zypJVm%kBVxjE}Un^!x8T7@MD}13z!S`dDX< zrG%@UM?O|d&4-Ac$4A8~ODSF&JV#40<&TP(yffjpX)7s3B{d=*&A}j*>+p7FTyfDH zQYq~zay^WVbsK~GG#pSZO%OUF-O)4m+z$kUO<}^TpUTv~g`GQAhAnKZ%&@-7zWg+t zZCylCa7hj^&t1W$(xn~SI{CEX5uYLX*rYHoznfT|3N_x-QxnAL^RfyJ5b{h(HqFad znc@TCw(mI7lxUiEb8>?^k#H^5Hv@R+(`f6ccWkkf+O=WFK*W*MXAf&$cWwliU+sE- z@cA~g{5~xsLoT(WrYxEi;mc@G%dJ>W%|u8MBF;m5Zc9&pEPg#cT$z2L-}_H*Gk3}t z35^s1C8i|R2qE$NQ%Jo!JY@fa=Ui}O)a?<<-46fTgfwd{Eb@?G3`(ST*`%0lEH}1g za{s~1-Jw%_?&D7Az(ywOYn*ok8{k{qj>!oYYm|%9HBUVROj|d<$}~cjPy>tkyt(lP zS_&6*2CJy5k`A`$-E7t#sAu`ss;CICT#t^Kxi1r>vy4(a>YNwfE$`hwc_@EItBMYb zTHy{=jl@c+xmaT@nSg)*7$7>p52nc}ZLx9Pi7|SLdub_EpPE*u*3NM#@j{saACive_zcJq zFpNS&&ICIr%{$X>w6NZuJ<24rxUXI(08DCMa z@;ghJsH1~~(c$3v#m;Dc1guEvrcnDI@;~$!H?v$`=6|<19puuxI6CtB!XCD=;Cb;R zjo;-k708t$uhlz8g4Hi*`Uk=Lf%kd0<@|Adb+{VP=tL{NoYec~`_Xqh;M*(%DCX2T z?%?8Di@*FOQ7ny|r!`r%xzySY>e^UR9g;>vqZ!>p$pV7b3+`cIVO5B3jrWs!Tr!E= z3r^_EuJ`MMT3pui`{RH{)0ZI@zBlXqg8QY*%->$4y-Ao<4437M;)LSqUt6c>6aHdF z;|Zr1avX%jDTkk{6>GBr&P> z@13Nl-V|Htl!TOCz(#JcpSvXYbE)S0e%{8cHtet_IsbyUUt6Vbyt)>L=^b!;FehYa zhMb_t^`8ggMb~eQ9phCK^C4qe zvT}4~&rsQK68q$J7VX^hbKg{;lP$j5CYzldXdrD)ic6o!?t6J`I*%dIiBTnjsI(B@ ztT#f$#!wl)X?o;#P|g-yF0boVQ`zdPCe`P#HS4f(OaDSkHqDIeekvb3nc1Ni!9Jy^ zp#z)2h7J@ghNh5@Y zICg@#Ue^@!PJiPmznQg#b#hRRTpyil<KL*&HR|L4U*f|gFVit-NYhe9gw7uAsI6q7BqSQ+S8 z|8I`~%mQoMk|%+$V>Q(;cTt1z){}Us^+d?ML7L%lu+1^ppfs zdRbg60D`NDMaa#g-L!Yn1o!B6{MmQrOn193nO2K21jXP za09FP|LFevU04fKUVM6c?Z*7sSF;nG$iRT*Yp&<(VvI^|dLQhMabmQn4Noh0N}OnK z2FVphcT2^l>Ow@3J}9&1IQ~pTtR8zoojs5&!emJz3*Y!99*4QZZ$UYjsA2BQ54TUO zty|`)Z-2(jjQJY|Nwqx`n?0w=Q6YHD_psc&mhe)5ds6=AdOtUPvN4ogJhNTvT3df{ zt1$loZukTQg+}fP$;tO)ayzJ8HROay5YUPPFZ|uH2};=MXocuLO1Pi(g?ponGO zL|nPXFaKGdLnw8?{AUz_t|){ICl!s;$sh|;M0r9rqkQ{bLD9-;0oMPt{7}2*@E}7( zE!FGpkXeT><(;P}aD_JNjGYp%}8Xz7#}MWtLOv`+)+f1%3J zAM(cz*t|-*S|l5V4ICqq=HqX<9(Eg1{_D0S=F|Fket$t!P z)hc$16-X`CeNC*zFey^YDGi|BC~B|WQQ-;9V(d-HMqAkD4@1)bFGSD`d@NmnZ-)z- zA9$$siQ|+tE#4P@8(NkpYR5{_^*-KRL#^;Ue@4tk1l``=N;3cI%>+14o&5#|MoJ)7 zUTVJNxh4kbejAsvO)_-eVwnid0SV=|?0Pg;ys3VFd5En;JZQflyEu~1&td zoK~M*PNm`|A9}^MfpcVtkTydb(_d{hzq`9@$Uy?2*IlZZZvo##MMc8M_z{Ts?t{c{ZhFxlmsMp zyg{it2F3l&Or?3aGx#X7z`=hQA(jy4$FHm%gM;nk<2rFk5z%q2lZO`J$O5Ham&@Hp zxLt1Ut~}f`)#ijsE~9bmCe8=Us+#IlXqEPXJ8*2JM6-7!+`zw8h64&7N7xr+jNxvAa35bLJe=w zRuL7=>;00mHv*Q609iryqZW?72&%K3GbW3Qtdp^EeC-Gs_oIDb5Q2Olm_MssZG!fG z)|e^VAH_2_OE8Rp49Y^2RVW`6@}1l9+F@BF#rs~Q6rmCjJ-Dpqh#)&?Ddz#OGiruWB!N1&4@B&_~f zf~?mJMTl$9Du8F2r{S+YhDIlELg@NM-QPOteJN4+1GsmF58TmMO){%R-$zQsDfGO5 zcOrek29?O453k?*_%t>e$cxu_e=1QnR-3g2BlWu8_T;xbk${`Bp`qcjh*>2sJA1j! z;LSlZBc5vUeQp5;m*oNzo~}n}vo>?_*Pq~;((+m!%QKHiC!COvyd+%EoC-Dx`PQBi zk~I^kA+yVgD`rJ0O0L;~OQxe(v83@vzpKIS1ado?4W735QBd2Y^ zQ-11Kb#idPz{uTD%Ptb8`tR3sCOSGhfg^-|ioZ0UC~(`WR6`4Ye|yv|_3+M4nXaC@ z9@*Xjz6EUU(6@aEJg(3;|DRQsGk+;9(q$?D>fpDh$!` zK~{J=kzWDqB@`_m93lJsiRPr?OK@c?1MwCa$D)>EUz1gFa{so^hC`o;;QcADhyv3l zB!>OH*B(9;63)`4Aq2gRLwnIoS*jC44MkxINx-_$OKt@e>9Bl&H8=ZG{IqQf(Fpro zyry(@dp|ikP9dZ9h5v{_SDA$jg>i~{#T*N?6Ot13FZ88*W3 z&vyiA{!0cG$Z00pjX{CNdHH*EXh7i8^L{s1OvSW6{eJ zh}c7!T4;&VZ9nn#Ux2~yGezXbqB@QwsI99L%1ag!O3@9U@Wc2)a1oWw4~shHAGYV0 za}8T@3EN=e@3wv3ZBKw^vdmMI2vu9p?r$x>GamS|Tv%Q{R%Rq+Jzpnaz3-s~NT(d8 zf5qvD&;ySz)C)&lTwI{{jfW+9UXN~G{%yXnujbUq;9Fu8fLwB~oeBQB=PIwtcrV9T zDx7|JShPo%>@dMY^7shl4oAX4(NjUeHyZ~yDD4a=HWfJhXH4zysw{1Vn&La#Yze~3 z4->(^ai$lqjoBfOhr2ugSH0G1>HrnU3QQD~g&I@9x+VoW=tyZ@gM)9I8Jlh`w<;#0 zz97+d$IsD%=bMAW5CIMh&NwVB+U~ci6@`S#55ogwx&dw6R#sD$0n8>@)BFOp5M6ii zLH=+EZXbHP59<}m`}>!4FTI)Fgi!GBUr7i3614x$s)H&;^vMS<@H3GRC~%_rv!b`N zp;P0$LGLeP`b-odK2XNgUxv*0lwu9=3h zF)C%irObpqzPF;EB!%gNl|D2mBj??+;>Q98H*AB#+>EKcj7rsTP#HH^UA3c(s(f0# zV0xw!;1>;N?U;lW){C+^u4m(0-uIP?yrh3u5wLD z9CbzAzPF|p@1=sdZo9-Y)L{#Wu>GNfHFao9N)^ zv`;bYeIG9!HSKSF(tG-p=r|{K%6;8qGQgn*CntwRDKgwL^0BXo>d)P+&c-Hzb(>?C z!}^iKhCd}P!ijo$#?M{izqf}`c8H(Kr+S33?VJx@@>S!!;RCg0)C6u@kKR7e+k_Hu zd5Mt5dQv$#Xp9SSSj|m!Pgege(sdt8K(&29oYF@_DG>@n;Pn04mf=i!?QP#Y8&F|6{Kw`(qE$`v?_Dx}J2MRs6l-&ZZSYj^=PX%^?@4!_VKH&(A(|cYA2M6OsY) z+q=~pz+XScRjkrS?srPrvQGB)PkoxhZ$0;SuiAbXD(1=~qvmNv)<2G2bkTPu4=Qlg zn^zxvE$JPRtZ-zZw^7i66CzSy6~Lk*Bm zKG~lI)G;n1boBP`!l3W!A<<26t@PjxxULP1|59|JP0D}zEaOg~P0Zg6j{M;<2jIQs z1SWNZ*bD82zr2XNOwoqE?XMAQ32Q$T2m0N2)kex4FZXy|0;Xcfk* zX2kLDMa<*rQU7;E;9D@z|Nl$4mYLc1_u}U1vNJB|@c4Ke;7>6z)tX7}SI5zeYWn^0 ze_pj-+!o{bpDU%?d3 z_>h#40bml-~%sUt^yE`pk5ugaP!k&yZRy3?SYGj|K!>Q?f%V zxr1TCzv$O~rukT!5#w42qq?JSanzrV+rkWH;NAzMi-2FtBZK4Y$j?YH5Vu4+HaxNm zF4)0V0v68SOsNFZvsNp>3+8MpIm_9yc!Dh8ubPhwI-On!2zc&HEUFb;8qe41G3(T6 zt2)KYSnO8bvtZQNZ-gCA3=LUr{Rw9zeSchdxyr|-+$RloNQ&;86Y%tHqoPjNns^o} z@8Tl$@(=3iBjH2fdDx@lm8%AgKlpgcljlwc3!VURQ7N0=(DPu>|MC1}vgqy#IPd%! zQImmKasmR?0>z9`kHgl6UzCqDE3@S`%>0tHY3s)6gMg;0wCq7+cHq6*cV{YZ+_bgF zeIElxEH^tB?)F5lOvAfB0vuXOhgC&LNu!NDPmD=rM;?Aj9Yj)Acq_5hvZEXsLZ4Q) zd})*GMcSNK)S=kM;F}dY`dNTw#fE*ST$#mnrlnyI2gIyCJ~g8XIqKM%fZUCCsAYXD zG0y$=4T#@x^9(w6n3Qb74<(O>m>l)5@+**u4_=;BG&V-KP3h(x+M8>a8R>7Q-Sm0q z6T~)aoh--@JhwfzE*G{Q?f7&bl1gLw{`g;K=0zDHL7~jPmT^}7c>9j)BA`b_3_tupbhCh0mRtv z+Y=$@78-q7G1h_7AvLhPAx7j ze*gh;N1~-Atd0DUKQPqV+d1Cbi4TP!Hu7?bC1w9Hv!f5!$bOu($dB;v=9bf2&WXXn zN~P>D{Zb-Gv*L@_!NOe)POnzBZ1Vnmy@|3i9<;ia3)o?izv30wLX!o(TfxRg&Ih_} zqHs$opdQI@D@peN(xt%!E;JMrpw`q)o5<^!hpH9FVcM43`UJEB%{nuBUjprzjP!Ji zgvb08%Z-yL281WG-n;HeDHg) z(p*fgLoEO1_bn0f3CO6IYe`bxDI%*!_v01n?t1EO#;P!j1REdQ(rc^TfgrAZzh>Hy z1A{#z2lVFJ2MK9J+_yg4@|nm=6Wlq%NFJ!LoHE!50b z$16Q?8IVf*gEH)Y3R4sbkdE7K# z{u=${5)}$61^YlDf_CgH@Tpf{!~d?n~%vs<4tQXgHiU?&5c|l z_t%XpmZnQoNY~Y9dt#CEX7VWi_YZmy5co$*z%sf+G#jt8XD|>zlKEHkcpUEXisF9N zL$m`dO}D@z9>h+s6GQW!`sS~GQRiR8!#a+}yqFhKOPckUi`DDwm>fbG7L2=}GE{Gf zFj>4)S{KM0zG&aL*@|#(9WG>OuGihcu*F;I!2Hfg z&!g|1+{X30qGwQQkqK$QN1Ljufm!!AXW6rTq;erjW>zR`y9w3h99)Ri*W{3*y}ol| z$`zdgPj7=?BXBGEB`Cw9zBA|cW3Q;Xxo(n+yWZ)2xz25)e@VYHH=%^~`3UE;DY`h9!O z1^#H?#i7y?J`scp(`LMsDeB&1==fOceJdK2)R>|9UiBkTjtduxt@VXQ9wvN+wYLh< zw2o}E3#m~ckE%{yr}!oB?9b;mSat`Zp2~(T<$i=ct!O2t?<{ddp`~$_t%x5Ty~$x) zBE}8C^cmjW9G-Drw_E*+lAYM|i!61x0Ju_R7{(m;M8@5`{~H;YYc;@^oC@uJntJR$z62EfZlbxBpqsuv&zgyz$mGPg}()S+dtY zxu6VQEK{ku_mi?c|9281v^NWo>Wo&p23QmZDX)HC)E1@| zCI}3kg8bnb@@X+h0EEKBCFQD;s)r&*({$qoNuK)j3N21+r%iMGIg!j=rPr`W_u_^p zxToa$RQ6USuKXiSmQp=dGipfgp*($_*(PY0OIC?CfPN0=~)@_~(5}FQVXGuCCoDb8m7=A6-J;e<-bhpNEG>)8%dXuQP%l0e|x5 z%$e{_IMJT=5uWnscN~@)bZuFCnnEO-pY3b>fkN@8{r5 zi4o-4+252acNOZq>U=ceU#v|Q2Y3af^JAu_6B%<>=>oVhZq zhqS*d%5elb!_MHpBc0HFjy=KT{O#M{UgAL#YAQaY`|U{TE2F_;=(-|<+CBrFv(+yO zU9aKidVLJY0;F`X@wcD8wpef)!LsLMYe>A$7XP|CS8VgY=m60!A<=W}d4BGOul3o* z>P+<6NJ2gkoFu)xvmAvSg@-61m~GUL*pJX~lz{gwJ)UN@>D$jjYs{!z<`Ya=mRiA? z4UylDWdKKGeSp|4=^n%zCPv25V~YF#z{WQTa4d~P%GIZ0hCl)x{(Wg$8e_B$ZGJ_CG!3I;t~!;~R0&*vnUGmbOpMyAi}Akk@wdEnX+TYan*5D0l>*q9 zOZd(W@Ht<_ygQ9lpu-!}OX^~U-AWcO0j$JL(#U*i_1TVG(w2@C4_4z$DAdyT@#$Og zAuL=(9qJEOYJJw3WnNst8Z)nP5;I{;1aB%FkuCKw6EltOcNt7F4w@k{>c09O`R?fR z?}67%EkD`3|B74+g(EJlX_s@2gD8vm({jj$brE4-VLMHX#S3g6jv_uL`W@}6YuQlI zKP?X;Zy4`R{<*i!iaLHP$s9mKqY_n}qz;4_8_s@<)lN;)XWP@pRTrBZ1m{GRqd=d6 zxJMe__KU;$P*SnPh&yNh6Z|!w)FzNb-KVaTX@gv}gT1m&1poH7F*4u%1zz1-4>c>j zRndaDu|=TD*5%eQPtWvz^pC>2hUzdnwrf9W9S>r3B^uC=K<&m#Ga{SJLdV9}(b;`% zy?HR9&EUjrj*Fmf?fOx$ng(Ygxp+$s6*K~7>d)}{I$21s!32mv2b&X9<-s1PF**(7{1qk5QIIpFhOq{ z&-|6LAw;|2WB2>V|tkEnv_hraBL;s&-e{{ur-#Din> zy;g_Aw&2J`{qFN8f=c|``xieEgh(hU8?~98Ym+v@8?_lEf`q;PZQLOhH%?C{O+Txh z+w*b?Tpv#0bLn|8hW(}s=;H913^j=HQu!e213{!hz)ISiVOTw0Jl`*`tbS&`7#8&F z*DsKS{+XC?0-P*aStGT64w6A2x%v8D6KszuO!ao5|8t1vQD@)JrPLtud6{Tp1E9KA z?{*enY&~|g(jD9^S#7;|wVrUWJtYvf($&b#`GWO57Q#`CIF`Tt#?Zms)sNhv)DKU1 z9gx}YzT@NJ0bu!hb=8mx6$9h@=EC6Me4%=_`_7_Q0}II1i78 z?#5GJ#WXxpHX?MKV8i>{r&*|MaSk0p%lG&9k}x4AdIpA>2G^ZJtvCOqxVR?C1*kgi z0Lyy=kfJq(^rr`<`vStN4_FP`I1^keel`U?%{!U-q zMFhZ((`12WYz4qxy%rc#yVaNrX{xol6QxpQh+d%$VN9u)5f|5Pw)*(t^JlW*kmlwG zAO+AV1QWU%73&Y$JccpORz0ZiY{C0vw-DZ_cV|7v4*k;YS)xWK_|iwQiwgeAT*8;z zHanY4>jdXuMO~FrtUX!pbw}2?LrT}|SDBsOi)ZVZI97iy+JeibmwU8_CS=xy1YY^J zxFq8usJ^98wa=*6Tf5Z`$tQTWy0|$UlruOh^x=(|@RNk{SJW=Bfy-|h9W4uUycMPsYXJuMUU%H~O2oN`>o3|t) z?uo`8ta@0wDgP;=8Jdr;J^v+v_m%ufG0lFC^59TgcIYr#Yx2ie4X6E2bUdHeJE0|J zN96}^e$d^mGiXOp<=hPQkNk!-V7k6AS;i3?on8B|)5$}x5z`qnVN7o6p^JV*!W>E0 z#K$|bP$sdsE``TtPt`Y>Wi2Q`Pc4{bwDM*iUN2GtPdIaYtx$lIqnHl1d&@yG_PVD= z^*Pg?$6TPn=y-@Ii(M7Q^I6w~Em#XW64hd`|&0 z-%sJ2tuq@om{mVpg||cG|0^d`2b=>x8!Otz&ile-<~qc_;`oQ`G-34V&Jt(85l(<= zOtqc2$&;~O*!rE8$I{NsSVep6-;8EG-JJ1_cu7N?aUa!`%Q^=x}I4fp28t z?mL+Vr{Zh>{;A%j_B4E+aL;43PAtX$d^)~%bx%{3vihe$v;RLu@@tWm$~U{Prmjjd z-{n}rKSpdv%dLHR@n3cC*?2#=>E00Bbz~x zT!Kcqu8f=pDQqQ9OeBrq))Hmj9j^f`gI_yrZ!H#g>l7u!k-et*<@;YtG zUx*U2=&Hv!kYK;oy7rQn|74|Wwb*o*;_2idq@ONLlko1&=mA=>AWZQ*?>t^(w6n7h z%BiVwxjM%0k14*?zxr-=alVzE^4&GQvH5PHPz&=-(nPa&b6R_#&GU;tm%#$==kFXg zS-WjNf8NWMEnJVY^YLD2a4ilDs8s7&6aL#Ek`pUSO{JzrNEOd!^eZxQr80j~*V}Wn z4?mxSBs4B=IwX$ehvU2J&6Tz`q2vwMWT2Nf21=cntX}eEx-8sEyIH}MqjKqAkeWFg?)&6W>`+1NW%Qzh3-m+PS*9}W)=VsoLRqFRiW)z%a!Gk`An zVTAMhUq+7~AIo|z)QN%*_yE37iz~m3-n#ug6Zx^=wzKDaFc++rtBgw!9q5mezKC-q0Y~B;H1_J6-Yyb+PdGEe=p7@3dxiu(j1~yx!^ju>gh}855ZJ!|5o2LuJca9z0WE?uciW`QMq)w z{B0Fc)ObBew77yVY*#UKap4JxL<{ijI0j}yX5VFGv0OS4RIK1PS+lAn`Hp>%`9^S< zmV6uY4dLM_)2_}(Gud&oN{;KJ1B+3)uIjADnCX12sR9}43~H+Q*>CU2si!G=HJLfl zwU`Z8*>nAGmNc5=#HWY6W7|SeR#@g?BO-z8KLQquSz+kZOYqR&bnz6&*S3V%5-z6Z z2ZVja|9DK*$0Xvesw2ypIloZMS!!t{)TadBg#~qZ2j_T>1xM#iYg~qGRp?4*V=K@$ zZ+U7p#1zYnk$w;E`7mGMzGS|}%R;)ix4|i++YnK45h?Kc-A&=(VAK;c_Knh{Y6N$T zS5#=Ql__SecrFt9uKpRWB#xRvlMDM|TEj$YDk+#jGl5h+;=mUEv9YLbfqG6YL5aVs3&P*L4OTpBn!>R77E!w>8cK zlRn))q}fR?k#QOJCik0Dav@C;&5WmQQ_YKI*l>k58@+yUdF9b^Bhpnh>UWA*v%Z&k zz1Ypw$K)qBYXKDZB!_pSt-`Bjq}TeBmRU!`z)=v7nfgW6P0t7hderoL@8rfH7$cvN z+$&xV|5N{Rr^&oSz&$zICpT~syv1>(te)UbXSkg!fLS|TsqXD@neDDc-f;a6`>M`? zV$9ah)aGUMz&h+u&qJqf`?fk-J66S{XS7@RJ}U)}h?!UUU$oKkCM zr9!oA+tb!3AqXwhX>qN$`!-a*WNWd?$5j1OBhG$yp~$ql`9Y*yjjb>YdvyNBVu&HW z{QG^Ku4?OivX_qN#5xmaaPa5i0E4a1e$^_kBL7r5(=q+5w}}kZk%Z45p7b}Ve{+Nu z_M${-(k%D(&Ky|MP@O*CZd-!|gA2B!PJ~oh^jELqF!#H=wVQ3%wC#kny>vOT=~axw zn8{^sKay}gD=Bp@h+~OrA!t>vYg6ZbVV7!X(-&})lo1|PpUk(-)gTD0qY@JY@ zn9<)m-xCzDunr}Ct66>T?$^rdyl`u8GMvztEtA5q&Tl2U_%gkE!7W>>+9p?d2FN77 zHE+s2vw=uvX04ky_V?eJPoUePmsmI7HIG z9v%~LKa*#OQdQlan`@YBXeiXGl1pZb2;%3T1N+nm)4ieJ`q$YHy3&_ibr~Y#NRH2~ zn2_7*97#{Ff7Lk`yPn8X@p>;)88GqjxzAsRS~oXNRa80dr~>K|mzD5?<25?APCWro z1KCgeh;+g)=+RMf(5&Ztx=CTb(YU?4`ZBWLCE5FC@1jDp5Oh$R7Z0wvHhol|T9Z-> z>S>N^R%Vcq79SYwr%vzb$!CkRSna{i3QQ(@BVDK}=yLdJ_M9R($N`84Pd=Wxtu5XA z!E3TeM^0AO%l?QD$d!a_Vt)_@I*A9bHMUpwz^;W-Z=S0Xa#nFJFY5rucc~hgnAo7- znFmxruttNh+lzG@Rpr%dOiw0ES>!0GXuDA(-y=Ll{2di_dpRb1w05}s5#R1Aj7)H` z7FSLfbVS>6q9kN6}w+JuoaxWhvt7%ryqnV1-IzPq|0Pag(N4|`r0e9+XhwU zv7zOxCG6I+x|xfw-7r#w|J!A%`Eg{iMoE=kAY`>^x4c{b>)6rE@^tlenQAGFIw*)L za_T<+pHpJOvjn1{jW1I>f;iZO(L(KNsNDpTL0`6PtR7TEg;#j(Ej??}_0RuQ3kA1e zkLGKslUlE*cCkAGAds7^oOjBe6}!@$?OuAfI_pCZyZ|D-;W~(*rboz-&3SuYL}^P+ zNeExq7a6U0&uVa1RUU#Euqj1}0Ir9rrqRxO<6MR&0BH!jU1jn4R*H&^7L7va&UIuG zOs-_(XS;$_XWueh@r=fGrlIcDN>{hAF#KK}t9#kFmlj&F*X!S1*(nSoS%q_pwY8Jw zp7G66!ZJzq-tL*+#;ulABvadw(1jN>=LgR9IK_to&4>hwesc z&vf#4P}u*W?JL8g+}^!$>sCRD0SQUzln!aZ5v03IxwE2IrA~Tmq1!pwVXoBe*T&l~9>TNN zM$DlvNLeSQXYZ_b>vp<5+AgR2J)R^Y&e_!_>n9&&8|kpc&S09Iu|e(}oh3~~i0z_& z*qK~h@wQHv+RO9ok-qU+h~{z8OX`FO-^4qh#Up}bejgF_vd~&tt_Z5CWpp#u&M~f9 zb0GRzfERV{rsAy$ZJ|lJZYxRg?d{yn``lYGsNh%pX-(I;2A4zdR|&WIjjc$>eFeQORG3ne8WqBW-1YM*bA~HAj|j7)gcN{X;P`P7#eq z=n%Va{3A~yB^Z_-I~fpRjBXJKr|NsUnsj%FOXYS;9r$2uOWQV?Y3rUzO?|Q(V^^x6 z*-y%e)2Z!-46Ui}BdB*T(Tqk>NWc7&k4O$rNe&;7qEh8{wqWFk!$bV0r$w#%2?`?= zR@%bHW(6A2AA8xT(oE2P7;fsu*%3rR5y*%Dlf~Ydgz=B8^5Kh~o`Do#q%ooA({7g( z0`E^4E2NPwGC3^!{ac<9jtc=`%^77^7V0yggbCD<7&yj!QE{4XO3uw6$QZJRkqWXi zGWK$6n5sN}Cjq1|B(v*2&0juu(bg`i{g|yPJj@2zfu)-8uP~Ve0hQ+&(IhR69^B*H zY;5H@*u~Y8EusaADlXQK7bUv7Sm(WgwMN3FkJAcfVnA0%hgfj@e6wwzyHHmj7Ji?y z2?F5;eL;k;27+sS{h?~>#BWbRa5TW}SF04_K}Q=sE(e8)QdevzO7`Y1>j^{F!>?%~ zyJ2R#Cxc#hXEp701{8cA=Shm@w`W@ur7V!_TwxH(`l$mk@TGlnW{@!9=jYvWx|S2Z z(|(RH0nOBOW@<`9QJ~J-VJbZ!y z5Cp!k?-(Svb1s$vbd)1+FbBef_vo1D8o*Q0kE-C`-3Ydxcsi?AZ+&Vz89ZqK$P9*P z&0=n(tf#YcXh?{Ynb~2O@IKa~hmfNNsw%gx_MlivNl94OY>U8~Hy@HnN2?f1PVIw0 zo}wAar_{-zRIDk7k0QY3tYdWW+=Sup4WsrNbE`LGgAj#0T3(H#1*AT4D$}|U4%uCp zjN$T02kOfXTC}2mb)_F49 z$fW;KN-5qhhWHtEt}0qPHD^7(9Mn>-EW*ArffON}%cxBRl=p0LYYI05pr|1(=^@nG3zkyGa>%A)Q!5{iA5zemFyV&uvb4Ax>whcQb1| zFgkI=q+R=y)WM)FPKvrnr0Hi5VTx%to9_gT6dAQQ;w=fXFZh`rj}ALzCKo@9kfMD~ zo$;4-o#?nad1cYnk4$VACv$Eo>viIF{wqMjTHacN+0L7-?p5JL+nbD8rBv`meu#RyDF>y^=>1+b5&dGeH_#D|UVO#+V3EwQgyLRo zEJ}@pYic~=%&H#}-sJn(nbSLhM+NO!#%q6in(*M%D=AtWSGo13FM>H}KLOS|2)1csoDD>Ln|$ttAb!4&f(kXOE$APQf&lO}6H~SI&5^ zA>s1U7IL8878HJ$yTUVXF332G;bmp7GJgO5@%wj0z*I%01MEZL7-87J$5#|z)UU`1 zxcuHbJ5z-d2z!c&>Ed?k5b^B$MXg4q`bggTLF^12W<*S)%)L zKIkpYX=4Sywg9V$+jI@C)bp?1wAR_Dh6?isCI7gwhY_)-3Eg29d9Y>BcuY=Y>U~ zvf~e+Mp%-Zd?WGcH@p+gg3}A9!!zYL5M%t=;;NG_-(;OzQ|$0Wna$$SZ#j{XDK3$D zyBwbzvukR9=WFi#e$)HmR*(<=@+9=xni{ z-8B0%ba%!2bVq{l2(N_U-9z^*e#(2ruOI392C2_~zaZ|kYy0rAuYaWeLpqe=18yhXO0w2X~%XEJ^)fpS)9mlL}h(fa% z_2IIyO3h*n>JI!(Ui+xy(Aw*tFs1T@TMXSk-t>vVdF zHu;WK><+Kzw7AH!(2KI_XbLncANW5}S8cvt@WUBE}*3oV(FtZ}^+H~gr z+KBJA2I5kI58%!+9r$>rR7STdvHkCP4)%g%1c!$X)-b%4+9Q5hH>o2%_>)Ce(ri?B z)^#!Tp~~8dS38QKGz!{fl~$tpPsp5OtX?++(5X4t&XuRDOau~-dP9|qSeR0+9)9*= z5yqs*m=ztr8%q#s74BLR^_=uriBF}C{girV7vNs))+Y!dID!h?AErsZ4>0F6d$6b3 z(WlZ8Jl*9o)$co9)6YU-$37yyw-kYXI_21Ll9Zfq^NGEmtbN_*y^oH#82X|us?At> z%uX%wQ83OU6LaHly$?YWnh_XFproK6w9~7rqZy+vB9@7&(l6$9mS?K+!@_tV!0{<# z)FXdwLNkuYG^!F_-rd9OQ8e}9s;76b)bJf&vdwB2+f22MvlFNr0VQh(;$l5L8L`(? zRI9x#vAsqWf*w9MAE*3-%iSN1f_yFni{LHPwh?{_Qk#Io3i{R3xGQtZY)sbCp52Sp zOHq}VueH6`eKfkbkRg*eJ}AUL`;oOst7fSqWb^0G&jkuXhY4yj%?feianf>tbX-#$Y zRHH|jd5ShZ{ssWCk5=bu0<ooLTJfEq z-`}8kriVP5X%=A!kLyq<{PK!`S-WQv5wsvTKib;FQ#No*D z)8Yt1S+#eZPf+CKz1c3rIfI_A!ZxFIqY`?L1ZuX+(r=Tam#odUo;J1p%qhkXolPTs zFj{!L5LjnlK|v8A#Y>UCp2%}%sZ2vnVR3%Qqd5Z27*Kr?dHov{5#+x`@~Azxtx15@ zT@LmPQ6VNDr`Exze>#SlUueT2*{?1{*aMaq$1ccLRa_Z5q;2@K_VA&j)26#yaa>`9 zwiWV(N(L6$Ctd}tvmRe)`hjrQ*qWfxi1@46xRC9#@iA^rKH~-3SiG30(;c!T+XV84 zRJU#pDy2W#$Jh6TWOX=B4r-^rlhz|O`nB?nsvAY|@7?u7^V@zp(%LXLCwqHZZ4@uR zv9NJ2ULKK?bj`o4*v4|qeq;B87hZ6z9OyuJUm7G^mU zrd2emdUSMT*x96hK!cJ3TTdqkb0P^A<5SLlhP)%RNO@y-e6`WpwOaAx$KpO@X=td% zzQUnxX&l6e^-ens;U7ZD_;tgCE-zgdn!HRw{R3f8?_KtSZKL1Kmx4kwhm4! z*GJA=&Yz>)CkibLQv4MFdt9^3$=B0my0IUn1IBevOz01+q2FsQ#Wy||q}|^<4aNF9 zzu2fDV*BknZpP1SI4eK%YS^{Td?LG}*+j?XzV&>5(Jn&HJMyOBOY^hjT8(~EDuFGw z#wWm=YgsXD&3wPGKWUGNC~UuM;8j#obK_F^E<)?)?QDie>@ts<&e*(*K@)f5v`W6l zgtpbU{#9<7MpWVPh5%)X3zwbxjb%&GcD8xNiW1_0YdVTeCkp#& zv|u^m_;_1SC~ceN=2Y!LuiClY_O=y?`Y%Z0$&5Azf)oZo(064T5d|K z{?cK4h`-Gxz@FZshRg48@NqD`Gefw0WY$5Rhnj-X;X+%+KH5v*E;9UqfU!Y@L_;3C z9m|(H(QW zg1_A;X|%6ty3D5Ha~>l$Ck%ZuX>xupaVVK=dbJX%hb78AIp&SiYmf1ox=8_#ABM1e z0Y6}*DPX5)Etm>x<&gk#*dRmXmklq>b*Wrb1!Vx@q`7v-1XZQDBJ6iW+R>)+ zf$-_>$lRDHWsdIl_8|OrKZQ*3XqJoP(--&{BtnXYksa02VQ|212+vHBlH}H{P2w~M zqKT0`wrpmI@KtPr!M3fk7kecbalO=@Y5FZboI-( zZfGR(fAnN+bu~fK{^WRnUgi~r-a-yAKJif@PG+X>zHFK(H1%!t^2-$y3vpM~o8$jf zOr<>~_unb+)Jaa}bs;>FZ4%vt`Nq#Xhf`gb*3B z*JSw$ZV2(EWwKAuEX7X;n1%AC#XLJWq8AREVq4kkv31G|9`r)LK*$!DDQsJejUR3A zq&&=AEL2l%-D87epOiI?hKJ@qF2vH7MOsqV(^?Wz>^BiVc~CHSD4kT^@aP4l^K)FDwJd0!x!8c_;Hh|Gl3d?%F9CxgcG%+loS&MFbH!sm2N|CYJ7i7=^ zj!&h>^@@oItnhjHB%L_*cRcLJA$qlSPnvA<8F>2WGgHWe(2L?=q^mFMhnlw93Op9K zoik9dS>XcjMNkh*LJ#;`{nZS|Ue&N_C);=!nAby0v-dyjTWgNpL|m1}5CMfI84 z=%{d(li>mi{}a=A?Oz0pU4v3Gfz57bqE?Qp-%dMu1HusqUdWWXmYPoVj~`{1{WXO# zJ|JhwmmCVa^-O&H=uuoxXVaS)7?6GeCR3JX3VmN7;iCDhxP;HCGsOjM3GX(uV|6`u ziraJGiup$x^8l|735QbXR67Q!1E>^)bRNFI;nf~%A0G@eve)p#lQO;I;NhUmFds2l zUt5PLeEhfw!iNx-=2cOz2gl1#5SByIHxcunBf$N-xw`_V+9uX>#Ni=x4qc)$4J}2}vXc;- z5F^Wl)ZW(C(&{P|^Tx)8xK6Vy6nJVvqf(HI1AF|?EM>a(-zj;Zx+{2XGu?VFnhM79GLn{Rvlf_4Sy9oPcQ<0AODEFZKi-yIf6Wv3!LYLSzDd1485#h(e~<-~e&8QnG`gJT&Xoj8{|vxVzV`1Ldm5IrX9g^-rqNI+76zYXdrqxeM3?7e5366! z*{z5|Uo9|psmqWX?^g%In4&v8IGoI@(i~A@@pPe37IArgvhykL737neb)O^=om;xD z5IoE*r^w4nm%LhEe)n&;WnjaIX@)^;h zo7vkGeynBg6*T;ilSgGms?}LQZ~T#39aC&vwBFjzY^Gy+ZlkdhgEuDhnlrL-xBX zyk}KgH6yYx4p-UGN&IED+3CK_e22C?j0GP1=k+nQL9h}kD}2TpGtS{&p`z2%o?Rxw zAmL>3qU4xU)EJ*1V2SR=G*2 zMohgq{+{Ryf`lh0$dDP#YFo?V+^{{d6oYOLk7q|zj6Vx}Ltz2%Mx$_Ek8&PL3P0&0 zRT<3J{T94pN`I>$gk8)vw3~T8C|P;NneJ%47^*ma$;xs^npK^*=HXjcg^DN=l|cl1 zG;!W(87!*aOD*ffBshbmrtem#D|Bc{%xs~6I3I=#!V5or0-ttyn zuW3c4MM6F?h@Qv(cw@UJQDNtYG-k$zHT#%p$)y4fHVo{1Y1XH1yc*puD;bo7ot;p? z@!UQ;BfdD+7Z4EObJ?c~^)o?higLM~JPIN5$y3go*b%-yH0&Vgv;%uH{eUjd$G7)F zjUKpocuj{bwS<~gHI+LfotH9#uYN96%yvmUe%IaNXPjQuimGT8R#9u zo`EXXTIV^xPQp=wpyhfJq7uIu@w>$FO^F>#6V?z%Mw>idJfCwPfyhkd1&>mC95#T^ zBJ2@4pUwMkJ-tBTn(lEfP|1UB&Nq(6#i>*9xgLH3I3x99oqBE#y?7zJM;g)ioR?R(d zp@%}>`C|aLLEOO;0P$uCC1HHpqEDwn;m==OMD2J!K1n=G62g8ss6HPC_8|p;4XUi=trJp?3 zKhfeH>1My<@#YFmg#6k%3x<6a2-bq1(9bl;Y}s4du83L@UiALGf}z&*voyJ_krx)q zvYcGx3Qo%ioS<6V!HN7K>@SQACeTsJAHwCZ4woeg9D4R}46&Cr=x3|V$v+R0+QatR zo&}rOV#D&Fi+Fpi^D2a6DfeEaHD<5&;#<$qxRGAB`e-hjPM{8o(le>aX|p=2&>}^| z@63&hRs~8eF@pKT)Ce)3Ufx&uq#} znkdyyUnck5_R2@eowy8s7$|#HYyhgKSzeI=U;%ub?(5M7Hx6BqF-N3Zt4(TMV?)@E z%WNs}>eORHI(Ka>pIh;2y(CNGx3|_vSr>w!Tii(~h!Pca{@(UfrAI?>^FIZj+M!0Cao#3r~LdLc3w zI4P#R^h-+GF1a=?7;OW$co;b*03fT0`*2!ni64~0os+!c!@t1nIVHqP@5vwl_l0SB zlkGPgPT3SPSSyCHS4w?rkU*eJU0JTo8$>&+ZKKyjdqcSZN&F%4;t!q*zVSVRFuJ^QyHxADUP2j3Q>c+)_Z=e8i#Ks#fr_7tLk%0^ z#v570*|Waal7$UIC;`iB--ErLX@Z4s=JMhU!h3p}c%}&%Q5y&ry*1SVlm0|P#z(H@ z&e@HG!|{DlnEk^&R92o_EAb`HeKXmj{X8j|4@d|gN5ngG-(j0K3wa;>ZK+Yt9Kp)k z_vAte`=G5;x7mx4;atpUG@GL)gPCSFv?4(}L6HrAuj9Tg&m31nb1_Iya zR~>3&p0A&vT#{v-Z>`PWP}=b_y34iLmU2QF8=wZ(hPKw0wkGM^G8N`@^xcE~J^lU6 zUEO$b@=lB*SfssKkwiQtJbpTDb2c;8UTZxnDwaS3P=J#&ODfi8?HSNlI6LYjD^O-o zl2BDWuQKPr#>OTcUoKE#)T%DJ_5Z`w{+*T%<#g>zo6t+tb7Jh6bTI6YO+2rlk8%bK z_jnEyFStpxe|`O{r+S{{%ekq}0Il_%M#i(p7T`N;ZRW1=J3>{oqK$h`-RJlZm|n6Q zeY#Q()GhV6zcc3M=B_kpq(HtxLqh|$j(`YB`4|(_jDQxNdfCUN*3d$eV8NEMD|8u) zbu59sq}n2uKA7&zn#4sHY{iX8>xl>!n*9z3V%4W@r*yEghNYh8)zcOL9ZD7U5lMnM zBX4Z~gbAx?NUQdkdry39rR@T;!L~89Q z-CMttwX{r6xYwn9$+Ir{Tcqn(Wg=IFB3jKn7qVv+x18J7P#~mpW$kIgTq4=6AC_0C zD8kqDX8l+fqhpX-3yB?r>|t&m?9JBJg%BNO|9o`q#9gWw<@E^>-SgOcK(@JkDw^nh z+AZb|969T2&ta7kXspVBw>p4tHZ|Tn>C&n0%{Df>IrjwQA8g-Ai626vGWAPuUnMnK z7YX0LOd!xbNASE>Be>y`>Y}qOSHto`64TBXVKXMxN>EN8Q9pJ*BrF!7nsqxAKH=S6 z*qS*S&5l13XLT|< zbMWqD#PKGMUG#O}P>$H!Vud4Sy*)4V`fza#B;4N}AsKBoPF%I97f82K zH9pmQb~Ad(C3g3&CkG>3HaAXiOR9WbF?@2`BP4GS#P^x0T7$<#>lXWa0{n*3ooAA6 zQyUv`qpiZJi4suz{Pr^E!iL?M1XWx-(_KDcTKbcQOMA^h;Zc);9Qf7rDO}uNdb_;F z6NgX-aQGhTZ5}ISClsE$oZ0R3Rqe$tzwGJ@yT6*1X7N976ve|C%Gc6MZa}b8d=im5 z*pSZ6Q`fsZLBylJ^obUxqb>X#+u{NMrO52det3$6RQAbeia&Fw?OByAqb#c?^0%{I zTp_f@WW^@rRV})BNvxNfa^b8P0N|QxIRCwV2*+drTtDwfXBisDXGx~+FQco(sg-Wt zxb#otqy}k`#UDn}D?h>Hs6Wsc@j1TdAJ*0ST1buQbk)lV*DyaeN9&zfT+!naN*uVZ zF7-4~WY6m?ghF3?Owhw|kNr%t9@2vqyPs z&)qcZ9`||%ir-9)2=1LIGhLt&Nz;o7*SKOK46r(+2!$oomM2SnU|w-_uQs6NRUZlR z>M20|>&60qVmQ|6DFfr^_GGQ`3%B~?C1Sk+Vr#}1F$9w-O0zAt0kCCRkDrdaF_ee`;IQNnkJ0mv8lcvOmSE}EH*b%odG5%k z<1MRdBWt2BLRy|+^INFov*z59zyc6*2da>2I&1s;czAd;(UPT5-H@iCGQB|%qZ4u+ zgP@RsmR6j29-&iT4LAwIJ7-~qsFsX{Khx4Y=;@DK4g#sk$;rDVBs9WOwAn=77BgeX zdoXDyB^%poNcISR`}GPV7G$mBqN23nIl2D+Kh^E+{ZxMa5(x+hK%Ct{-tYn$+iZ;) z_4;~5zH&iTRh31nI>-YE$#}lWs7DQ0J~*aSV88_mgJoqu+uPfRh7@u~<`7=FX?3WK z(P-}@b7Z0bZ(E?X<1F4wx?wS6*P0jar=TzjyZ=8Yx)?cVEYo`{Nl zKzxu4Oxq$QS3;(@QDIH4b72k<@;*~^@7q^{6x$t z+|0;!NZIsv*4ixHu<-GFOsVLg%IW6XtV8bsxCJKkmi~il`&|-U2=hogjdBMgHpif# z#k+frr+H%}e_7lUu+War#xl|wnc6CfM4#;L{8F~Bvx@})q3FKO=#shoyUt$@eS_a@ zsKb9E$AtOE+d}qdDQ(rz%%IR0FU3(+n&#f3gNJTLeZYnC`QGacBX10u1kRGmEhi1x z@M3sKqu+GLX_`uTH605rBM*P&hm+=;!@O5d$8ttsbiBN4eoNLH)@DCn3D;&rw=uAk?XWallkHXZ$X0<2U>q1Iat*PKBt@T54v5D`CPzlO!<+O#W>=N}dOA~5 zjztPOcIWuu=LodW#|*GOZ07KV{RoAFECTOU!H;Nxh;ItU`~oCu{ep3IoCRYJ<>q6% zdW9RZ^X!cUCL7d$xtwz+H(G0ZY&>w;5IN&|fU+noYWh1Wgz>!g{?+DlLc2T=Yks}- zS1Xk!SE?}+xE4E}uge;Nb-jA=2bwm#ZEurHQfmE-BBQT`Ki<#9q9jt~6Hnoyjn27h z@omN^_#$LbzNE*>HW44Me+`1d@E;x2S%Rk_Tr~O1C{~*Lny(MuiT$;bfkP&TnM%jZ z><6P`_FGo8C`CsB&B-62D&FeY3FNBIs83C66_;)FiLXOrP3Y*|hDjcwU!5Luegy7}bmS|uO9- z+Yg{07n>ZOnn?NdJMA1l%4cy^W#y>oc#YVE1PYl}mt6gq&%g7LS~<(gwgSY_pO%8{ z)9$IHp0t-{V-Hw&0D&V{$N!&e+|xYGdw}x8yn)k_h`)FEX!!Y8a$0^$7Dh@ImPFKD z2^V0ki8L@UFf=eob!0)36NZ?aCD^7ovi#ToUo_9;FazZI!z_>gygi4dzTh*v%_Wb0 z{GTiQ&p*FEfv5R=pRw&=uJO_hL_cmCT>xwGzncIaxSgLqsGS;`GwNED|LWvW?k#}V zA>WOTS6h(X54#C)I8<^RJER&hhiqKYj&Y7Y>=fK0{oj3}+|IlLThaY%UX7Os-gr6g zfA>oE<Hebwlv}lr&bWL+9Nr&d} zO#k$AMiz7`JxE!bn<8OWn_56vGcvW8OhC%-z44m8M)%KjO-re(UrJHAZlaCvy ze-8^%BBX@NV!m-sl+yJ>W)?Z-ST(v>#>-tRA$~1-rKmr^m4XZkW=1LI$h(zfYdT!K>j4i%qsB1LM zGSO=Yc{RE9kW^AgT-mF5H`(teRx~iFl$?hX9-lh;777Qc; zO-+o)F62q$lZ*GV0$3cM9c0LNMMfxv-r8k&-?-)24o-bifjdO8TV#Sz!swy96y-({D698xa(SV`J=V`cS z%dnP=zuM0p)g3)u@dk8FtLX->k@Y?Q^LvUvVE(SMjYMq zv*nS?dRtXwvh&W@m0HrddTfQ0-yfqDRkB`NY~uZE=pdbt0bg>EN=d{;{O)DC ze!$z|we5Q0V|*UKgi$<0tFRdBo3BtYo3D~Uls05soY7{ezP_b7w;0Zkk2n2Pd0B3F zV$#3Z$~RHEl|f-B#=yfOr;YV!Npf{8zEbyQH%R5{E3EKb|C>M-?b zzAEa`LUAoywW+(!$WEU*e6skwi5M(}@_DTJY-1=axquZpsE0|VQ>*nu3ksdLIp3SE z*<6^K82C|TdNZg%yX;F5x>D=TY0df+Qw#1f*feI@gdcmCcoj;(owr}l#iR6ESD=~n z4$_8@y|)8eD`aNIeQ}VSHL%4aAHM$UAA<$ucIQ>v`J|nyMen>|D}YVQrruJ!Pq>KM z3jnQ$RWxI$D-oI8$%=TjuHH>KvW1>v7WW^a*VK|zVVogH>=FIC1sGXI-5lhL-z@lAroMNEY@#x^vinbz?E@b3txl=-5Bs3jn9elB;_tN0egd{w+>o^iGFHCC&W{qFOps-S!#yQOqyAE3-AfYkrl zbmcsk7^Fu@!LKzVrc3IA69LO}DTPf{>Z;O5=U7~f4AV#_Ddf@H+dEHJd51`w1Q*?v zd80$t*4NQV&jH-~x~J;F!|THawv{ddei!8Mj%|Yud=9t=F}azhOBx5~B)@UHE_R6t z=)Ftb60{g)b>UHK)b-(EGhf3e3mC&=@}yu4@$!OLX4c+Cp~U zHy&($v}HgzF*@&C+Ty))qv=`O-7#CIKf3h!Qtse*Gi2YEU+fOA73Qoh zroCk&JiETtV8}k6IpA^LVOtEswS;v|b#I*v3Q=VwB_%}{HUZ|<*V=B(#A=_W+d?@Z zEeR;VL=$sv#^0MBG-Vd%RSSPU;8=@^O8g;;J&cksVFt;fqo8O+gMEDe-V=ta0Pzr4 zfV}tq3i~MXKJc%=zoGBn2A5Yb)`+AaKuC#0K zS&GMAD?$AMQv3#;pxSs(&_!9DlZxuHe?srnBo{ISPv&nt8MlO<{{D6m zLgHboY$RCeJz0MsT@k(3?Q}5i1pJ&#oHhMWOD$a1lEQK)P@lW5Bh*R`gg!219KiC- zM>5;eLaAs*_@zQIxXQLFXwS?l)q7|cR4qPwdk%+ltP%J?Oo?fsd0orQJ}VigrV zx2^c=@*%G0h20(hnpMnPALI+IzY=|C_D$bP&_7hQD<_%Zj6zdP@XccmaUh>fSRX&_ zVAF3bu?{CF=F5r*?$Z{@BKkhESAtJCS??e!nl+s+@pkUV=jl038(q?kS*wXsG8rhQ zFso(+6?q%}$@(+$raK!Adr1aJdj}LJymw2p!tn~o8uzPt>y}$|53iXdm)-1>NIy~}$UVlB4>vL%rHqYaIeWiwL5=pg%Finl3+PcFuK_b>!2Ck~M zDmi5+Gp^4J3k00*CO4-lg;<{(-bM~K@wyev?9t0iqiIw4{zb)akKmd#zO1$S1hcHZB}6tg^Hp|6gC(!I)Vx8_6sv{x zxeSM7(!RUCh?mWWsNjgKRZfvM^mm_@#%D#oc+b^WSgx;K! zf$1}=ZTr^5gd5U*{1W+*SWb>M12FL1Jd&7sAX7a$xE4KKZ`f>ZN3O2?zNa^HtbIc# zLUP~OHm=cBh9^c^de_*kq&nBb%|}c_iGvxrln4I({T>w?qEAsa$u+6pDl%taqj*Y9m8sUD<`t7sb z`Qof9qxQgszRs6qUIyHJdUNUajWve!`6eqtZYQP#35Dj@$h#-BLVJk!Rr;;hCR5df z-~SbCXzNA=HGc6S8u**%`2afjUQ6zqvo#b>{^fypFk7q^Ep{+L_-{wbyimObB5ZW# zB-s!Lkv55^u_aeP#Bzr7&TdB!UeD7JH@1!7bp-lMP|k}HmWiUPoy~G1itSm%NOBFj z?P#99Ob+4A&r9G92~;fK#zgDCdxAkK@N;m&AC{*`7_BwZBizL1w%uYYL=BkaPbL4v31%}%?5x(?xcH^SyvByn@vzi*ZirW|dvaRJuE}uQj%VqmrhL~AZn!~?u9)MB zftu1nj#KYuUY9MtH*#dKS@vczv)i`WrQ;js=E95xoJsvMlgH5N9{G)yy>k2e_q+l= z$F84O3rx&pgHEkhyR-0#8HCf!dDUq!?2xt6Xd0*^TM{^T?EU+VQQQv|bTY`w)>;U{RS9!WibwdPf=CYdz0A zT_YEkz2N0`kQZ*6bTSU%feig7f2dllzI~TZL}(C-QEx6U!z~8&$DnrOaYO3=b8ex; z(kROp8f!l#?}KuQe1*ZvE5xxA^xq_ZFX%~n*2?~KM1(>nnSd*u`LHF6ufJciT>&r*0!zkI3^YE^%dU4} z0t#Tsn#*v*lM30V8grCUQOO-~!w_dZAa#0lIYH$*IDZGceaSO&X@X~57UliWte;>~ zqt_d+J{y!Z8>!2V&vD*?r7!8Hvo!NvIpT0zPrqR*+DAV0$79x1)Hl1WeWI*_j*4M3 zUnCJ78>0Fn!F|?BxrE3Cf@T+$i6X^$sU!3>Ze38@?22=!g*k5aZFr8aS1|@&v`|{=6<5RAgc}u#5q*KNE z>HwP>?%j3%ASp>pp&3&LlQ+bnKsbBO>)@ejJx=17>JoErLT}d_2=NP}nkl|2+nb4N znKl1;*Sq!w?|X-iZedfad7e>N(^m2U`a?EN44uuu?i&wheRt}#-$8jC>-Q@Mn@buT-=<}%^!kEeAi8n{W_Img+4l&J_j;KG zLtI2e#1G|Qg5$mTdu#;-y8!*o4Xl48B|h2cx-YkXXB!G8U=uS!CbJn(J=#ceEBS)c zM!*(;x=i%}3zgh=5sKiYCG|!I1bID0sZB~EPkuUp$5T6I!9~p0oc*Mz7o$&+(&fe9 z#r>#Ervb-jn9s`s_rqn$6o~g&O3)dp=hmK<;zw9=s@Kpa{DXi}$fpC}epgxMK+dzR<)-@Og zfWrM{&T7@+R9ne4y6DfJb9$8QkN&a>u62X8F4?ZD`T6_DM|VgEGR5lcl-prq4}obk zezzuJqx7QvW&(})0Y>H0#sOl)VZBJMFRQAo>^E;^ZjBkl7OvsHAaI%7U7AHI*W2IA z=<^`2Gz=3W$iAQ7@>P@~&~kov$bX+j`2L%rxW|lbBS*hgRu*yTF-NG+sYu5prtq!y znwh4in2il7IGmAsts;9K02krIYA>(dVoe&Xp}sI7-Wt~bJw!h{jATiOR^}8hui&h# zV6QCy*Y}la&M?b4ff5;Q(V1F2wGL|Sm$AROnUOuU>%}X8RVQs0DStlF;MVKw+ZN>Y z7fNkk+E>ljIHDigZr>TsE-7H+oY!G%aL#q#w)(aTQHyIHA0t=`&s+o|fC7zKyv z$$u)dvP@FRyH>}g2+HBVo2veqCQz7GiCC$WJJ|Pb5;D1o#!nTpKUD`$l-Yrv$YbeV zVK!x3sOPn}cMPwsKA3beHn3DtY1U&G=3!~JF@xopum8S~S~oQ|#u-WxOsRi!K;pCk z!UZ|0!N~?UOMU%t;JPgBIC>$q;V^gTa_KPe(vbYuuhhZhqmw`tsF6v;d#G-PF-%eh)ID-j$1Ic^|A&i9FuPAD?JUm-Y$Yg!|VUIC(-v#GL zC)+&cG#}o)+MsE;?MX~J@%Hk-QqXkoC|FQcLpJ=`3!Y^mX7?GIo2ew2YkI9*_U!xj z$sgh~>u$e36oWpzzR<07zWW~H(_6v5wl_g7At@o)WVCgRb~4KPk63SUwpLqKB^>uX zc>qKr7-C2TlWp7xVZ+w`LBZjZNCkVeq5U*gYmiUD{TlHArQkCN3r#+yztsNp&iL!2 zSlEkYb~c*YHrLR|Vm^qv@c6~2CI_h!A8jT({SU#)@%h-1Fr#j!AM~}0%9Z>=D=P_& zHv>Bb1@0bvP8JLKN*)~6g=J-%Kfk;K=LTk-^Ocf055@+5hw)i+r4<#>xbkEd{W(m@ zcE#(1C`I~` z=jeKeA9Hge5jK7YKkDyR3NSm3^G|iR*EUF0eXT{~d3Dvf9!SJ(<9+U@rX!0FL2oMm zGmSVp`qGcW1mA3@@IOXJjCtOAZ+TWeB7`_?+4mnG;GgdEk2XkGrj;G0)Z3gismsf= z9>uE-k8l8;nPrwck`y$Hvz^Y{(7(wOo06_BgedgNh;#>V!c>SI#0c)Wc{D}?goslgG$1HsympCf`(1*>sKu0eLb&8 z8T~D{>HFd?1_m~&c*h!2!dH4YUPsOtD6zDEcIg2>a$kLdl4WeM*-tZb+0-QlB@^YF z+LKK7+EuMNgd+cst+#+`b88y5dC|-)SDeh9-3IvL~Yw_X`T+`B0f;YI+QZ#sx z;3@8;XmDEGDHb&3zt8u6-}67`oOi9jN*0T(d&|t8J#)>>b+Vlgl=S?gjH@fTQzL?% zlmJnDjG;+D=s|Cb>oPqH5)&aOSC&HKN&UE;=735 zZ2}wBHe3tG7SZBo_o4cGs!D3JNc>ALFPni6e&HQ>psE~=uuD0lp=m$(rr$NJ+tV0>2Dq+)hH>F1rW9ZJ!{DIjvn z$VAhaHDL3cpbDO|Dm&Gxe;0Kw`7ra&@GHd2&0F8It`Y>(0@{1M^%b zN;5lI*7UV;ulYwpB_I`i=f`-MEA(vN&!&$VyIR%?NQ-Lf|IK~Mlce}d zwnP}BAl{h}IVZLs28$t`xVF>U)`X*Hu$8kn`GGX~{x3)<+{6^`pr$fHkHP@`l$>VJ z9B_FsI6AlURkX89CIzbCqZJb2)9cRK5c*;GG1=UpFlDHsfeWZ&<@3^Wz9hajay{Xd z<~&udn8PSjs>>Jdt<9$)%1PHAf&VgP*VXQ|g|X%!)*6tiut<;4ThC?MRbM}6R)jq< zzqj6YN}G`0V;E3#Fu3XE#DP3mDK!=!D*nJ(EIGrQA#YNdtjw*x5az+D^3aEM-b5gy zYqpOtG7Z7oG*PXgCLlxyfmVNv%&M&|hv-@7tAx^$83(7%pjyURW$z!Cm3cd*jPsh( zjcY|dXq=*XH}m1I(Z**>?+zqPw8!qByqKiT6`W|cBltbH!A7zP=BP9K>C!IUiC$cE zKD)fkg8su2{q9f7nEE>Y)8hncK2lN-xqGgYYGeydQei)3N!wMemO~jq&tQZeK4AuD zd*rR+@(7yGHLi-_aWm+*XNb!6%T}J9Q}MI}vNxdT1e}L! z9mB2e+i8g%R`thh7yEi8UF`tH7a+47reh&L1D)2_gstbLfY-p3l=8rxi4P7I70jU7 zSznb9tg#W&C!%As++dyKy|{4zv-wWT%d+!VuJBuD`@prGCgkWR+&7w+0nnBH1HZoj zmccsZKMYAtj$7-b@P5XmROR^o$7G#h5eYvT>wlWwlL;l{mJoFeO<>tF-7_J-&6KU6 zuI0k8LB3GRoFBMd&2geX3-TcFQ-I^dot!>4!754z{pA5+?v097s*{$5Ac+Y$P2!j?; zP}&*kR=SsAn>a5a%&=(j8+$u%_Du()!k+4vOFdb)>ra2W|M2sAYL0?UV*Z;=g|f_C zWlka8$Ol5R@i}JduvPIm^ZHc;OY~TFVuKf^G{?vbMr$74_s!PX0^?~hKGPmB6e4Hu zueU87V=*)PzWT#0!MB7~z*U`C;@Hn6?9ad3eB<}S@*Xl}q^v;YC~4p_dERvvfmyf< z5ce&TtU*FjS>EbTPvGZNGE1cuXA;jTwZd;j9Gikb3>;ApmXufkg;&MaQqBm%WF}3* z=V6MwX&VS;nW4quAWqQp99ZwKj`ka8RjO;FXucgzKG5lo#SR&e=|oG}TYir!e6;0j zJ3`U>W-2PHa81`}OI0yCYyy6@D+h0WlLPBwD-%}U{ zO7{j{-Qts9>hVnVNT(H%7AJi88sSVU-j-?HmGwyJVTZi_pA?CoDq_OUqQ0h4AW7(Z zZ{W@jXc>!e15aiCZy4J#yki*KVl;qXC;#J*E6j&8;Y?n4bl1Xdph4yf8oQrRoj*)l ze=l0R9RRLq>O%25{Di!ZSw{7=AG+RalW(|sq0lKxHRrj>LQu>pSLRdqjNHfpW4OCu z9Qj~Uc+VGlQh3s84oL`DEApH~2Q9y=&KOxNXEy&*u~>+&HK3bOI;0llUGkrJX+47oEVxH3Q6;P z*E$&(;0Fx`cUBXc>a>!j-3Eh0gz`O`A#xmj=)H?$1IiV*O-wB8IP2B?K99iiM96NU zxl1Y{fJkpktOtERg%M~w2vqCkN?`2MV)<>g|F_5z;^{3aJLdHB4n_B854dgQr*;>d zb$~RmM3+!LKsJJZ6axr{m&LbF;NZShTl%=g(ceA5RDzwY+^)lYy(r@HBNhB{4 z&}yuE3OYR;QvK8l$*_HW7o_d_M(-JiN=rlaXcb$vNd(N9a=SBwQTnoPgqr}IH+0$Z z(Q^Hq#1E33_Ia%(O?MZX6re_{(Z`G#l@3=E7nKKE3tW4WV3^6nzrrXj@44v7HT&f%q=5@l<@QH#7^CoR~S;jaKr zzBb^q$TjzF}QRI zzucbS1oaO3RZH)Pf5@DakD^xlr47|#L--5Ap zzoPvi)3*Sh-k1ngVZ*`!XM;UX`(nlPivyTZ_#Otc%j19cd|kz&!H$y!#5@4mzGLsf z8c{rNOszznm@Egr*l!vP$@ld9(!ct=w!$DqfkEdaD28;cE7WFr#r1F6 z-HYUfuSR%GMlqt!>wT4A1Oe&v?Q$i;2-q~|Bwx#rjeLWWq-Y&Ci34=U%2d;_4m+R^ zil=Q|`TY#E-igjkp!5rQe!!QXK$$EhtpTixQ?~{<&&wH6<1`qhY*r1mY;5xaUG&DiwMR=@hRo`YQVwM)373AHwCWDu<X>iAo!$hPPT} zjK9!!n0^aeAsRZ)r*VAjBW3ZDTyy~234;-|dEU=|3SEY+FV-LI$fE08N_*3c!a1wH zjK7HAR{@p=*zvl?9(HP;t+lW;Y8(G92__RjH_Niyjc;CR*PKJsEfx%GJLmv)FtWBn zb&ohPR~Du?3jzAah2_tr1F4xF+Ldyym{d23?;0y)aNY+VAfPRpjeohGt$x?s4fFvm z*rZmDF`o>MU1b0>k}J^#2$DDq2qmNVC#L)~_n;H!K_>Ni|54$?X01B+q5Q}HN2PGP zO*4gCT(Ir^&!==D;txOz319`TdhY#dC?3lo>bz~Onwac(JG&XAZbMGaTkVs+9;pF( z)*nCP=*E*tM&i(j3@0H_vqmU88mULcC5`L2-CF^N73SF(1uz}vY6s7fxW(03m5i42 zk+4b5=qhSH2QAGFezm&j`yhUb+#VVfpwwF5X&+7^r`#}F2e(eM2JaEBS?6DoTIwXl z4vp`X4YWqBS0Byy4Bq#w^LYQ4%8Z(<8h2i|uI5lL^VHwt*Vo!%%uUD{AIQ%+96eOC z3r+WbQS4u{eiA)*i1H3Tp+DIY0mWx19LC&bt*BqAiVJh)RV#cdo$sVl>ts|cRJ=oF zfW`Si*4QjQ%cT1IA{2+X7kLgN(Pt+=`9p90lb(8uzb_M7&pIIb97G!GsV<8(4eMi! z5Mh!3l}V@}M8l$`?_6e@R$}~GS1{w5qU5H7D`tC63v~TP zMBtaSS8g^r+&OV$^&pWua z->{`=OZBC+cr|;FoAPESI#GRyQ`px|C~D=m-d*n>Qn-^Ktabd2AQ)^PC@}0Hkz%z? z3MZ!k5f%7bgQthj7HmNAOV+>v`v`v2RROkZn=VT++b|N&jn#6ENSIY<6#r!tG_RP1)v{ju^yAOJn2xah<=1ju}J;Rongm0SRGsv=OALvz7nj1?` zPr3ljY@BX%3Q$}l{Z(;?c!tF|;50toM9UXP!B*C^CO8drWJ0nZ?kF>ZtWhlM8ZAQC zi9;e1NfT$Sx*%;aq30lTU1NElnKW*S)gDQu5ao(a#B&?K0Hyhl_WIA^@DmjpIFOqr zohd?GvdA$MFlF9y92f9=vraIR8m| z0YY;2_s$LFlE&1uDofO~W?&uVWF+Q+@=53i2*jaQf`Id)%zi{^JJ2b?m9*t_`Uu|3 zBV{{(f78P*)@)THz;Q3j_GKVw6thp(X9Ui}*TzwilfmceM*1is5{c9z6c-aXM~?=Z zlp_=mtE($t0@sD@K4*apz1Bw2^XcN9n*j;4s@>PR*#@d$h~nENNlrL9-IBwti!3Z* z$LdXje-Wj43YMtcg9KlHhyZmAEMPs%?m&n`Vf0;{=_mu*)odw4Gn{75}ndu^Gfa_uDP_+axInuH*zQbyVH2!G8 z!O2ciweiUNyB>HYz53hIwG*Y!eTEWkeQ(u>DE-FTc;m?w6JgCezBTHP9h;iO6KtPL z79NNBU&OIEp*Ay!QXg)^xF=ce%q}+T?9H_AXa%y&bQ6=h&Xe)FJ|ZM$1btzLjIn|s z&;xN20uW#;f~|a7cuA^WT38p2%8XEZS~n&u7kNS%#& zeC)_w+S~ormu*Ne+D*rGOfyj>+uF2#9SEBP_NWmhv*voZhgn=>ST$rqAS^A)8~!uK@Ir9$a|9+P-bo&AKsZehRgwsgsADbGB6 zMp!KjxBWKFo zWy-Sk=1`G~FB-DOS963^rH5rnELS>>yOuS_Y3sslQ`hq(n%)+!HbNKLJAtGY?B;g`T=gA|UYd48gHH#)dD`B&DEql;>C@IpfmZk3^XYrfn!r3g0zSi;4a z+TV8X8=woh!KYMX8rPBM*UR}lXr$!02Jqd|;&VGMVzb@VF0&o7okgvEzrho^h40ya zD}50&n(XS1uk5q$XSJ(_X(NH+Aa$?q^ge8tL~TzsN-@l>XP+lj3HDWY?vVq>QYrDV zC4ybJm_-=yI#dtsKYao8bjhoj3TV`Lm_qn}Zd`v`$18(-Z9{?LeNU~xvhE$cUh_Za zp7QgAzqmy$yU{rO@ckgIpbR4re_z+bu#3|Ho`g(SMT@F3*|>uq?$b;PK{5FN^S)pP zU54fCmUhXe#_6*!UKl)AO0Hb$eES&0Pa!19zQi&$MF_9qa-Cx@A)A!2u~>$I&JN$e z`6xq876#sp@`q#Ie=GJP1KauaC36=dIlw;BCK1s>TL0dYIGey2$aE&Tu35ewh#MYR0{? zbnUs3$EJ4f3cCE)WRzQ{r2`{Q*8DRUwZ3C zj{;(Xlt2HsDKpfIvV3gwI8?$QV5_m*DZP9xAIIl>!v z=c3z#>+Gz*#N+e?TxG(O62&m)Aly8h`b8Z*eZBunn(nw^;#3|d@M$9TVo!Ci~g1qJmQ zf#;+U91NC?B!9{EGIU0sjEwB$$IVl{jTaBh4GmLsa&pp=TDDFFsW+*q9DMPgvLze6 zQNJ1mKJL!d+7Hk9<9lP(1Aa9ScyE84!+$?xf3@ACAkFLJ6{ha`yM1YCF0)zC2|x4g zWMycQIJQMtNT@*8-9zi(h(`^93;u^YEOVig4NXP@PwFjAWo~)J0K7;@!V*hj>o$oS zx8v^bGt(6mPF}qaY;oUVCxH)Xi(CGo$;DV?_Ci$D-vqh4Gusph2G>t_i1K)gl~G3J zvVy%9TYQ6&E8)J2m#Clt<@7$-6CwTQvtz@@c_C(VmuX-#T5B_;LxGO-a6)8zSmo3;{S603hWj@aA_~SFP zrp)dV2N##%fUAGmg^M>(7B3HD{`9YKD6pMy$4C89-;j5^qY)OHuKinKVd0a-WPbV; zd2|}$^FJajpl%6MjYku^Xvg-gi51Pi=nopR@|KGU%p#?1CimznR##2HI+j+g6`&$f zS62FdhN+B78d3+n3?-O!I5joPpn{#9{_ekWC>;BN`)N@N?;k}3x|&UA58fN>T{+K$+skD1^mcU*_s->=CbFsirh9=8KP>>eCWU)wSBNH((}4PQ zt&zCz9#8}y^YioP{;$zh3NcTxThqHU!&!(1kWPFa2bsy~)^u5jv)B3c$A{Y}fb5K} zI+E&XI669sD5cWU)coC@Q8?F2{pjc7m*Ckyae)R?M}1VdtMc8s;%)D&n>VhwC~K|5 zO9ro>&~If2?mf-rL7BgnT99!5J-rt+f1$SE^S+u>=6}fvc*h~dqeT?{oR{~zv)gRz z*Y-CrKU;6jCTwA()6Tti4EwD$B(l=dT%@o&BfIeMo+%e0x0eA5-I8DFS-IIxu!b&^ zeHEywT=`~UOAc|C_2NQ{85I6Cntq;H;s7@7R#jA@&&iYo$Vg9Um$y@gN(itG-W+QAhTj*^zdt&j0|CGx=Dq3=NP(m&h6)Krh#NKEmhU=9)U$+3j#voz{7*^ zU7^`m4A2=seD+&UpOJ)xhpGA7$NQ2>OQX67rJSbue*y8x*4E3nJvl+cnf?HHOBvPZ zbh^N%Q&?zk^9WwPty7?$!|#2;;)yzAk(LFZ&p&aOx*I_U4TVd~=|CFA16s&{i|>Lj zr>MN%#HGUGM5zOrz-P~Obzx$fpcO+1>d%flMD$e~v-$BW=W^y9axrwCJS4cqa0bs| zgNpNkovX!-reVjUuX_z0&g7Joz*lCD{cElNsNeY#I>WTm-#H1S{dx?g0|nlV^z^EC zraR(PAIT;X2mWec(bLNkysQ>qx!vl7C;nHbhI*ljR$oD&V`&K^&dTsC4h3 ztN9#!yz4vg@xdMg9R)Rw=b`Liibdvz_33n_#}P0Q>R=$;sxB5#D&rj8m1{m<+yQ_M zg?V{Cs4q@_hL6{+0QR`Twa%;yyE@#K4?NwixF)=H_coUW z_902%H!E|M^nj~+U)|&+^{g4Pk#z6vqoezWNB5agxa-UPRvB=Xn7OTb$ny7AaLsi^ zjC>ap_;}$`;EAB~BMJ&7pb~qry+k5{oerq@+t)Y$&0CxV=)ZY+u`NcXRgz55Duq2f zS!W0OKO;@54aWc?xnX-KHx->UC6$QM4A~JUtq<@^sg=g!%{ze z`V0V25sH}FjE@hFkCV;p0ao}Vv*8Hnc{EjQeFJSh!>>%Yg>SWMfqRhM34rV3S^Ua+pCm2KX4T~)aGRQJ+{;Yb{pTZ+ zzDQCMdP;KL!mA?7jePaOdArNNBcW=bOlXH)$FJYQG(s6ke{m*FR;z9R?ng4h;<+v< zc~JF)e!b4*BS$1Zw9V>>ZyCG>gz29MmGUT!nmu>R5el{w+`n!Psw!}>vwQXza)vs* zFv$>dDgHJ3&sq2bFQus2^uZKUW)VesV5h{}G@orSG4Y9ubZMv?mJy!3k4E!61&%ve zQ2OO&iqqtk_p#wO|JBw13$~vBtaF)t1f0)YTphFL>*=U#Q-Lez)1XCMoc<2AB&2gl z?k~i^xe%0l6&e~&FKHC8i}PLlM9~oR{YQo?@xzCrkQbs)e|VtHkiPpqS<~o#?PL8i zV>(Q8+Pqzc;1O;J==l^9xCN954<~A>{zy~rp~;{&)@oGQDgKKkXE^X2=w>nAboy|D zrRX8*B>QYlZTr@3yUgC125Vne^97^qf2Lsm@pJJGpwyFr`rur~%{ISdZUTTYRStc? z*e*qRlJn@w{p!}4?`=q30d$+3Qip5f!LYaj&Qd)d=0#VOUuP4{d2kA;-3PW7P62b zbc86GQIMako^?)OK|%0SI^~7OCnrpxj3H(v()BB^1l5q`d>8!}8r-&e%h1rzu$w7U=LC@azqtDIY)S%;{pJ${0wH~MHn4(RAd>~$q@GQ+B*@r+ zl`nj_%aeG<2-uAI^xBvx!$QbCMa+0jteP1tn(`mhPdgNM`-KmSrOA*XVlZ9;=b4esG+b& zN!G?SFyC8e8fjv|KR(S~8>9Mm9uN~Y*>mu%x1dXa9bZ^(G z`X=~T4{3;!k{o=_X@<29duEh8)Q!V8t?!AuJRIMBA& z1!y~Ir#~IIuYO2GCz>8~<@bbyEt3DJqu6?{smZ^Zxx;x|jaCME9l`t5;Ks7>C7ob$FdrFj5RFKZ|=BuL%iS;GowE!)<8cwdqHv8V< zr04kPgpbCjr1nDbpECet+XFIVgS$9BUgMgVJQQ++10e0%xxvv9A*{D&?`UG3VUa(m zi28hvup&)ZT{+(CsIm=z+9r;t3HAm1P4!zq7n|IqNn94I1_s+MLi(=GEr!w#u}<+T zY||&W_;W7Obh43>iWV1jE97hr6x)}R!V`jV733o3#ry-QOs_%bAi@1*02F0m zVW{Yf0I=-C@iCr>E5afK1U>JY>H>x0?bI?dxetK^24HKw3#J^*X$qPa2Dk*_K)^oS z3|nVTOiWB?x$s-N1PmVV_~($)oSceBR&DJ}h2p>*Spz0ZG@sen#5d>a`w-8g!H{3sm(C7iGc~6YX+Vl0Rkpr?82b-_^bPdjZSAgZqAd%<==>7Dtw&qrg z>w$p^b)i`S`=N;!LxWm`2(q(ZgVSg!JVG=vDJl50=x)RtfO`T+v0X?*i@=+7>_QE- zG_HAcR={lmV`r6|%NmHKPrd$Cnx4tQ!C^mdLF0Hvq7c`3RLDSivav7`WD^}Mg3DSR zXpM+-(iasG=0TUTzXAg|C~$4*aBE;;nn(~5cDZ;3Ej4$l`wH6TN|p9KKP8kpKf7s$ zDOAmB^~CJ46W0a|HSSXmN5w`mf}r7eV0hBkQ*soOxHsM7fD+mWj*cun-DF{i4Lp8l z5n?K_2d`SVmbrO(4NMB$cR|~K9hl*hAp_6ti6~wM9NqOI563^Bg!5$L* zjvqqH3C+9nW3#4}Trz5cjkz*bR@*>#z}oHWel!oKv`tkJaGUWqTWoNOOIxBe;%RrR z1~MS>+8F`l8wqpE&I}0Z=th~T8?5T33%O(_Cj<8i_jejPI)K1c@o9a~Ej(TeN%;+|FNG^Z(SR3cw+WRTpd9m`k;Y*=JLg3O3G56e$Jo>?3o|#e-(JPDj zu@?6~Lz>KfJ4uATE~M8e;fM6Znu+`DAncNYFNrc2g`A9(l^p?8@l@+xmF4o{>Z_su z1LF$x<@>Py|6};;-Pqdf7Xh^*^;B?Q%a<;D(CRs-ehpKv^IP!Z-W zWcGFJ2EFHKU7IYP>Ki+XeqU(wlTA(xu6y7+5NCJz@X~3Q(f!Z=B^VK3NVD|aqrt^G z-^{ui_^$@&G>Bhv)8fkgqqEk;I|DvF!N56{Nt(Ob8q&)~kAxnWi#YN>-N6-&sA}{dXKwRwubRONpwD*og@;l)-~`Esrk|p}m}s*o?Lw zmgN^cs(!WT_kGIiWj%n^R3GMtk`DY-U;E_2tA_Waq7H>ce@#>VgqmroJx$FW@7l@s zBQp+TU>S>O zP(<(ff_6X8!5!ENr=)1q1J{NI0TKt4E02Zyc94Ou<8TTp*+jT9c2mb+*N|en4AWp= z@e~AyuLpRy2APF=gHvXVTMD+$*bex>d016190%$69K+R9XT<*ah~9%1q;4ysxN0-L z0Z3||0v>fL0ch%`2wL**+B1?SgBQ*jsgr0N&1r01tx#QKt`p>nrvn`8SXrwU0_aE@ zQt)Na%al({z;@og8JvnRezou!2uxKU%(DggM(PT`beR7HGtH@3q9HbK7LZd@q6++n zcMa%jVcq|!0jO{NKPTCdsYblFkHJ))Q#U2g}4(p~XS#@!FF(IMtLLRr)#dB=>KI#U#$w7H z)hV|$?|K*g*qmRHR$%=$!?bW&H233zVc~n&^9Nt=Xr2w*d| z2|Gsy5fCHtK)JY`dkgIo?;VnFlB?HPkf8qN^^vdEV#=|syn~h3-l#3KuSZoAK1y@pO`4=WKTlb@5tY&-uQ{INMWL+ zXYG=&`YmRkG}$9d$0?QKb7|FX{RZAt`HlUnm>^Nv#Y)jZJ` zT~p~R{)wso!-q8=Nnp)jm8lD2QR6fz( zn~&5z{88Y>{^nIxt~rz6-lhB9*3rukCbJ&OzHbTafzh(0(PHX^)p(Zm zUV^IRFIj_b?K;&e78&T{gg~-LXX-hb)^An1$(voC_syz9D8ol3tHZ<+w@uixG6eLv z{0~%$%|D%B%LnbAfwB#Dg522h?FKMRh97;b5RM_*>jM^1JF@$vfX20Qn2FrI$gTAX z2<2m{`ZC&5JNI->u^;gEyF-9zlV|=+MV7GbkKSHS+#*DI;J~^5e<9$0UbW&JfcV~Q z!h5CoOgC;Zm))AJ^O-?Z)Oe;qcfWx!a&D$8(C3ST1XJPF>AR@g6#ptobG@2KV?0g2 zQ;4f-I%`YI=NS|j^-Mz;_Z-8QatHsY#p`VedPp#poc&5;(iogXvhX!ch!qeMO1e1k zWy81A>r@vLRKe|=K58aw>2-r=*7oO*z0YcsGs=IxQh#i84iF9>5G@rF%dSR#pZO~# zoeam8%uBUC(2{<}*QCJ)QKaxufhsFQFx__jJ3F5zB*nNHBe#8aNaTkC5;!@vGc}}E z7>I}MPFp%6CoewYx`+o^L;MP`r!s84i`Six8mt9Cxi2lY$=)Y%AJ_$?u4gR@X`$MtiKBSLlSy9j^IDo$Q`t|H<5z;LzlcDVN*7LwmRW>Pug+ii&`Wke zPPUEr>&+K^^3L8-cM%V{2Onez?-Fy1{aeaSL&h`24X)u8ZbtHN-o}a5KSy)_penO< zRm)p#TrhHuHQZ}%*ZSZm}<#3)2B#;C9V6xn#18~o9 zBTXr=E4kLil>{auDia-KnFnyiqfFP)ud1UrtvAOegdrOhwKiUpCQVK454NG3j=2%cR`D0}cWO`~$>K8c8Hh8NBT?QhxUyCcX_CGw?+K@6 zK-BgFc4;uPi;d7ZAdK$!Nf>|-HXT9y*!;+y8wHyehp4bIHL!AZsv}VekLh+0gb2-1 z{(}eU`kAqFYa0u&+-bw-3*?v`3UH^*G=Cl$N#DO%yVW%5^0*1?@0ThlpUEcIqQ3q1 z>a?jw?7{mqP8#vpOtamZZ&5}@V7l35;q>AU7n2=9m@0v0Up+F=l6WkynmzaYnRNs(EpqRf4wmc$(#DY9wLii1vaYdfK6KAsxsGhV2D-62g}s3)(>j7vcEHB zLHw~csTIG!IkBeUn)@|HS-2i;HUJ#}d4(AGd2UJ`Ipw>8Kv@Zo?1_ku^+>Hwq)&NM zTzxjA-S}wmKq_^Ql2*PGEwn_eXEV)GG1LZ}dk<)xb(^uMBp1YP)qF?QNfz_gL5q(g z66h$Mn``w4g|53AMzyx&Ubo&gwmy~cVpj_=Rx8o-|03d=%Qd( zdUbG_b!ZC)n<@iYiM3V*3~Fss2||RhHJlh8>=CRwmd;;VF0-%m zjc#r`IP{JU=g~WA31FwdAE!)hRA42>T=|4<4DK8!pSgZ{lLSrW#J+22B>eA%`EDL1 zd2%qERj~YI+I7dIK*z=KDabL)bi0AYPV58~74th~sYdAq3ALDv$4n=~cbCrW>=(n^EM4a5R*o3nTE8sqcR}e5f#M`eAp$G{Lgy&kNtH%+%BWSK z?*`IL*;i4YtXxwqn_I zW;(L}B92VHbF?LkRoQAOjw0_tstdzZEQd+mWXI={YEZI7E5KtagAeA`vLvU^_o(7CV9a)rs+R zzNua|#~&S2&;P7)o}fotAVvR;TD7BSOD#;~I@rJA=HR?B3o5mCIrFf4w|*iyz!`Tu zzSqYOb9-lx>s=h8LaZ!?R{h)k6lE$ z30hJK%xPak0l#lMuFZDdq5_SIUKdiOfB`gLEMtsBlACfM|=B=G1QqE(mkvwq#rL5FO?;ES!HrYFf)R>^f%U%h{9DtsTFYkYU{8oy!9u*3JY z&Mn^JnmgT962Ic5pr%K(1^PQycDSoxuV-8dz&!Fo3 zGf&_5Z+2#p>;*H*gS^0%orb*1bWZPZE`GUEddk9M%4+|L#UXEuh-A&Y$BF67IF$0u zfRGJ7=^|L(ae|pdYo5q+Rw1yd6zf|xq8ZE08aR{}spzsLa;7sz^j3M`~zPzxu<7KC-QZSM?XnpFeW?G@}eCncrNDZ+OTO{)LQhwnN_`Jlo(Jbs6Q)K=Vq63`_jNrr} z6Dr+oZ(;>AtF0W|Z&!x}FcLmVq|*R@3VDtWSX*UsH@HQ--@D|*5e5@-D|Fdq#}4}z zu@^h!laN%^xRutGrUdtme$CqQjDQ6=OAfp(?`}vd&Pf69L3^g|4tqJbnDI0ty%Wb$ zt^!=II$Wr0UCX#qa?FQ6+6A6;rZw5%K*;00*HW90fx@JHD=Y%fJ6Nc31(YYJb`H7( z{~Tnn3ArjVwZEZxx9C-$a&%W=!B^u+^W8FGtXBGDvY7u`E8UK7PEPtsVf;9M1gp+v zHz=1Qa&saQLK)q{M8aDSHyX#CKBVY=lcx?>xgf4UZ8*0Z!=FG zuE+j9-@=|m>H1sETuI-#(M*CJ1m$Jm`j^f@uJwJVMTZQwpsil4=;DxtdREo=1qcp z_zF5o9()?^a}dxtKd+5s0ZtHKCc$7{O3XK|}Des^P0i zD&|&;)II6)s^vYxOkxWw$x}w9&4W|uBlt?G*Id7klJM-Okl4a<-mHc0jjChOg%FH? zfCL-J@oTz5;Y^XwTK2`Fyh6wI)XSc)DO-R)0ql}W^nvXaW-xkAs%1fa-ENP`OmDDd zCgaJ(s1DooL^34OD$|M$RJVQEt91^_jY>_YOXB|e3p=B#ww))Aq@czBWGQ211!0}g zFKV6gI3nZss}_EkXyjXIKsmp>3An1;$e3}zlq*)-O7x7nv-zj;*BOO7!-|@~G=7r| zNDJQhV>TvOehl0>o)>F9_44`0{K)|g4Y&A{VJ^GcWDW3mP@JpAt0vM#ZbcW>9975N z*%48%x7NM40m;luF4GvA%l=h8?~$!NYtLyVR*((rWC=oHq~9KHAguP{7u?mkdZ)8L zkns&)yby90pr7X3fz;}Zc^z;!6+4w2cM83V!Zc(wvEDZKo=tWuSkAC>>n>OebP>i< z6zYLH*R=^D$GYsbyBHw^eCc=+@tv0hj`ej})N&u`#p>yk{-Mk4mLRZw^2D_!ic-Vq zsH<$kj6{=Qa5z-xJ>I^C>`PfxPqy(q#s5=y?mryFUdLDeH2m6PGiLz#>dq~f4a+G9b%z_-N%Rdq7HX1V*l(NmKZ6ak?US2@wwa=q`@n}mnd#d z0e%SDq(JDQvoMxVNKwUwGd3p3va4;uRxdGsS7R83BRYtn^JV zM&==4nvti{@4k(brO)2UN-ky(PRV`kb;ig>u;~+ZlvdJjs%B8{9@F6co&l1lHAjwdSpLUld1< z%}SA3@%t0X9mwM>C!e= z_ptD>0wDxj?wjp87&Opb7MVuTBv0IxWGtog&LEDZX2OJ#w&X*P$m11}<2SP8@?{AX zYhr>>Lz8NGr2<4#X-B`*-SI(+$M0|Uta{A_V_bS_otC}CZnk}I_9qONJSZ)2Xi6JJ z0;IV*4@=7LK~EmHx%y&|aLe8dN4l_^F&xgJUm9>yTV*RoBUA)EQp4CvZ8cu^T5BXF zC-2$OA!l(v-pi5OUgF0>&XYz;Fr{UR51TO6>zgSSJ_jQw+Y!AQ(s-bW(jL|!C~&7J zlXjmX#UfzX|0Whc`Cq0v?Mfn8*`jKscPEjo=J1sllyR&Awy}MmBeiFg@McoR9N^Zj z%By2+AvDOB{z|H{<%G05-N;bJo^t<6#l8}ye!!HRXsAsBKM5;73A^-9ct2cc(RhJ~ zDb~}bkmlSGxZ!x0L{D!q52yq{*Dt@T`bu{W71L5ZZwc~*xHu4n^wp2<5H{Is#;EZ*` z)R|z0PZv&m@gbLHU}hld483j-44>l$?atIqFCTl;2tAH*7imT!g-izBP)8xmS#{OA z?4r2YeKS|gPnz=iiBvF%IzoZYr>SarX2RfMkjt^jYHV}RYy4oc^N69BTgm*_`}EH2 zAVR!h&F^QiX@3KP5Ru07x1YMKA|{0AAt`Wm0o6;#j~?x7jVCl%5y_`|pvyb$s=5 z_ch7ATa-LT$@5W?oKxUoL{_Od-I#yhcPHHSPx`{1PSWy&xMd!+WBm%P?Y zjl)Tt=fw+WP3U?FWmE&s!{zdL&tG?53&13WapNAmh? z%gw!4Z5zu`5{I?flIpYmu3tYs{3v~{I28IMEnATh(qnd-60w&xxtkdH@oG#&T^H}N-o~}gd~3BSPXt8*25H=KMvfRT#I2IF1ejhl!{x<*tq)NNCqvMg5+@}U zJd@TfO|oN#0aIru6Ep3(megdKk;{sf6etBgpC3_ScY+tpP9+{kfPr{;6q*=K@6+LJ zASrRYpeVxsJ8~O+{R+PdE!QN^_pP8uE(DpS-DarAm$$YA0svgtb4!wga|k^!lI2md zj*sRc(_QEO{?p5gv_}vOH;WM`C5h;HXKb9rF^{}fj6Gk*5CD|AV}BC3#YwfyvbnY3 zTq~-%{jd!w-e$g{S-@irP6cqW^avckd&b7)4Q$(hV=;Ufm4#Zq_9f2o|5-|ZZJM`o z5B{uuuf8ZHx#%BwY)!ma$Qp@G5eOKMvW=~r%2A(ov!yO zpL+-%w>@hTYk)vE8n4f(bYigHcwbv=zKJVPjWQd0Facq~iXBDIndZQvbAHQ!;F*F> z!&4wM4o~cQP)s-70tzWNzf=RdTh|{Xpu(f8V4ks zv7c|Q@N`Ghp%yDB|680yIv;6@Z>+4ZGm-s&wS9M3lUegNu7!0~Ttq<;L4y>j(mPlZ zPh z%A9*Z$^=KDIAzM0|WH*wpSpy$1}H0DP;7YPU}|ak?9H zX8s3-*1|U+o(LiWWUuS-UGLdomT=jgOXDRLnwYN?aHzyz+No?oC0vwqBxawzwz1>( zyjAY5i@|xD!7>F2=P}E5K6q_F*T_ydbaHPsn5X;;{O+?Zbd@GlJ8yd5_tf(Olg*F^ z4ug>+`bDNTqVfkzs4Y%);e{Qi_5{T_t&Iid$PQY%-=jAc#Z5il)wG(MMG>p}(r?Sr zKdgg8dULfNHwOAKAFu_jJIvaF!?=GN%QXP~9<5Cximi?t8+ZHqyz zo@Q;iT5l2NR@MiXz_p1z^PgVBZC;w{%cgEmc*gDdSbjh*lkDJG~Tu z)79oBFht^>D{|Kdu4Jo`ZYC2ZZdTrRmr8-SO@?2gHS;PZ8+5bg>>M0vQuzJXW3XwI zV57JqUCm;pH?Cn2Biw04i``A4!kC=h6z>;dEZ(Mz z#f_<@RVbEm`4j~d%xdtUBmTfa6hSO9y1x-GX8FM}$6E%b^)g%SGFzaBT+?`Pz@8Tf zIuxoD9gb7GEaAEInNlR6{|>R&rTaYfIYjQdEw&ppKp%LAXY= zq4Q>glcgFIs)8v~5O+ep>8aiwLZ3oKv^T)Pr2vmTjH`5@SuUf}8fSw&Mt9F_$$gzS z4=x-YZvXH8Ws=Fg(x=!UK45QHSQ+?g#jEDIVo`U(Xj4%0xFWkD^Ux}Dhk>cKu=m(6 znFrk_wBsIAo)H=K@?Ik=c5pZH@$S>X0L0#X%`vb4l5v~h)joYA0s>L$&)0$37(9`{ zS&14~AWG2|XBBp*MY>;vL3?FN`(>~zuZW5)fgTMLB1EMe5Qp)I@Xd zxUdno;Xm^TjW@hiuQOkWX2hchmuwjP+0?Pun&le5&!OG+Vnd=_wmcs+d$jfRTx62afg($Jqv_y zj#*c#NVolZ)7SkaO6DS)ji~MZTxfg#n}UAFfg;n2q0gJDdi~*SL1la7L&>_Tu6LO)X`Y{;YH9U_}CTHsoB{+uPe76z29JqZA6c203$?HE=~?*Vept z?}bT&Syb5iN`K(|(Ywzxjg=yU+`n8PUe*E8zepOMV?Xzk?%c6HI!2)Hy^B{sNP6c= z{lUOHKum|S1&>ufB&_6IuHmLHeT7f#k{DxQi{gT284+4>mG-Hmru7&Kr0gLf`}ipe zx3?JkN;bBAOzzR3Q~7d9=^!sV?qZ~mtkA^h+4cfKh%I3gaUqrm!id6v7+;9C=AXek zZ;!NMMn;m~Ntl|{c@_3p^@cw2QaI=f4IMKn9@CPNUk%(E^KENkqRj2FMS@6AM-#dD z&dFKl~DoN81v>7#5%SHGVuDPey@;!udWYFvDqjALE;Tp9>$oIuhJSGVl9 z=MLs-5&~CdGsl7b{8uahyvYn@)w>!n)vET5Y5y8z0_jt1_8)T%d5=BatK{P5{NpXU zn!u$+!KQ9$J<=3sU}N$u9?iTAh3qVS z6I`q<;%*0uS^-#O^rYb&JAffi-%mImuEwV%XzOj%Iu_p?0yDBl%F!>OfO`))4eala zbdJKNw+7uosy7~pPEKIvVr`FuaH4C&j$fv8xse9REvARER4y3Vsv>wgZF}W&_-dmO zyF&MT&FYv;m%?g+q39+sP)uvwDtdgu{vNxuHDnh%CI@^ilT)YCf-SOoT&IVjSvt5W zkvJAYL3oek#1lHj=4CfOSr__;HOW%)#XY`?RXWcV^Y!_K-8H01$1-~=)p#?>Bd6F^ z?viqi=DN7#MfOX9z0_Ojyr_n*w1#*<*7n__s(`ktHUOz(Vbh6rin?DxKE$6|=#dsu zq29aK%d5t54uZ^sjqS?Pcc>3|xv&Pny)>?$^PR$z+0>Hi_rfL2f;|sWT?Q|&c*&rk z5Q$O0s^}R+u<0-^^o-}{<;47!77nWN24KZouA4R{w_1O|={_)P(K>6lrLg_kwcK_U z8v=YN`K`{9rNEDn=#RjG6Q&zkBGk=|dXKpcNHHm8(9_qXk1K${^<}RRcCg-b;l__SqZl|n^Mhh#|h~;G!>do z4UJGy9ub)1isiI6IEQLsJJ|XP#27tVaEJ?A2Z~7ZS{*=6gHO=l4S( z=IUt5x`^P)f>_3a4?5YXn25}Tzwv2L5_BnCi3m}C;Klgn(tguI(LXaEadGtcz4<%E(X)V^KhoUA8T_ehalxj`DF^JUDe^+DTEJBldI}1 zVM(hpAqv=*`))4PP(A)^OHBIJrj9nQ@Qlvgz^+=)#j1paDY8%fQxlYNm3-y>*k{rA z*5?wNTn4ob;hwA$zvfPqK!4QT32C@DY9;(iZU^H)< zSdCIh)P|u9G5~6}Q4E=XTTNDHpbx(PVC`idZAsCZ^Pu)Nu)n-kw)P$e1#3cs&|>}) z=MRxefah@*QGJxxkSZ&I7pH7qxrw3`|tDGAw0K#^!TrL zX>$CR8K$AZ9{bO!7)TBj_rK+Sase*?_>k`}^d9|B`Ji;=|1C50zw|!M|DiKBgHA6N z1b&{IFUSW)E5FdBOXpz`6*9|Sq5Uf*!&k&Zn-LXx*!x6Jm=@zc#od$Xr}#!1w!9Vq}z7KVY7aUW(xN2se zT4%%y)AE2bXhG@edkDcg&yS(Q!niXQGr0Rr1%8^Jx7S{TFo&?CAQ4525g99e@Y!-? zBiGMw2m+-GzkJL<6)ib9il#Kn{X82qPjs3OP&n6%jv?)>uCO z^7H-ZcxM+t|LM5Z2>OENBPG5nq0M=VyIo4Y3$@%4z{q-J?0E{&ulB);5|eR@JfP`N zlBE1|t~W_tz%U{|+{c{Dj~B4-D~Ka3)5EQCVM&nPN8@%w%j)JqXIx#*pz&szwkgbF zi`OT~Ax$66--zil=T`Rqrz_t->BZDM=))D8FB1hjdMksdw68`qAE zbsDAB7Uf4I$?>u9oC#HyC^A`g^LKZ1oSvRdGE7yXQGX$I&r{}RCMLfpCyY%(ucYfSp@Aoy$&WZ z`RqsnNgJ9sFACRK&Ew>SyA(@Ej4yUn85cIzGlMT<`(Zyz@|IjNf3F-ex+PAEq_;%F%~%&O)68OtQ8 zrO$57pq|`~M%qv9iiwAcaB+2`(R0XJFp7@lLY-m@zH{WgQ%751(MShRgP*-Q&8O6h zFGRrJquD3$mx)Gv`ru>H=v6mgCpslHrG})6MUKhR2kDZU+AP2AimBzVSpFD$$Wb+| z^HG8t__i|!-W&k7?od(w2^3;hZDTbNABk<7Zfolx&(ydFFy`|YD=q_}sd%Z+%y;tT zdD^3}HfTmI>fe!IlRIK#(FR}ec))+5oxQ+nBde*EOKD%mEAOBxw^5bc*vZZ9dMRHZ zFoTxl-L?8gS-2}ph}?ib2rdQ-KiVTPlhC-GkvX-Njvve3c%b%yEl74=Lm<}3V*h9^%7W^T2ivnLy)wS{4km~l z&lrWn)*1eg{`3r8y^Yy{kN9HJrcY$5q3EQbz&ew2q@q+m1I-I+DY`FLW5RH#N*BJN zS}i@j1TI``>|$G6g|V|@^Y+|EdXwZu?vru5gZlqg!=vS@Yfgzp+j&clvZf)lBM*9k zaB;w%_S^#^LQ5l=4g}-vyL$vts}526(!ndN#ofyrQgUYKQHydJL?0GCsE;?-ex-UH zTlc+4-)E-Uq8osX)nn-rRnyTMy9s(9aui2e^9l4F8wo(;UDiKL2_424`gdY5*?3~G ziEn!QF`6a8KzOP4#5?L39x$O4Eqgu=;DdQ1CO!N|Sks2lQ^?v#=l%uOQAx(|V~;ri za%o;ocgR~P`uAaFCB_6C7;Hydp+$wUNDAimVfIGw)4T6}<1ZCTcRsCa5l$1LtVi}> z25=<#a=w=Jw-a~Xz0E$8k<`hm@uCAAJJnGx#UdV>sfd?`oTEFga+CveJDB^U!n+@# z$?5If5yL4{cs!8iM1GQKQa0)JgYc%TPv)k%n9qhsXs%NGuZh9LP-5-`-=BEaq(8Jo zSH*=}CfC%P<#bGW_#y`K(t5Qg9;Az2s#FO zQPB4pi~VrnAEHCYVT)=3!a$y_ZIA~Ai#!l{S>xs`d;+I;RYc+WE6hh|Xm3!r+Qps? zWUeR+&uH1h6f9P~in!eN(BFNDE&-$-K%?#U{qN+8UgSCRMCI{>rY2Xx5BLg!BWp)m zQ-AdeRna=1DH5j&U}~S^DdvxJl#-J)B}&wZ`I_Zm?2w)%stH+yi6z2aqK20}{p#sL z9f!LR8JIW7YEjb#!80cyuhX;14L>Nh5cU*olB|@xRg-9WOAIMW@jcj~H#LDaa3 zlRilm&puEX$aNT^Q@M2i6WA^9!igbf0xMv1?;uiu*?bSxPDW-gDJJos{I#t z-~M4$E(7$>Sh8tOM`kXzClB*RGf!b&knZJqj&tONB6=F+Ju3bvF{%;n0D+iiGRo(U zXrA$^sTTkrvniSE5=sDS)dWy(*4K5Jsp#E{bhsI(tnWSXV*zrlBQmOGse?e{Q%O-c ztJY20*Xe0@eQ0R*!hvnbQsV(Xn{c8T^Nt@qfC|G;c+`#n03x{!FM&mv+uVH~U1}(L zjwkZTqX|ui>!#Rp2NCMlx*LgiY5Mx;n_YKel}}H`LJ@|g!GB8`qdguV(olPf3ew1J z9o-hvTJ?w&n2w3{-0XE*huWghyYC*{2S~>f`7FOqmFDflW6+DM{ER1Ykcr7cG=b|6 zX*gX1&SJXX$qS8N>BzTOddC>0m=Udc{oD(xlF5r#*7IJb4v_9XIH&NNfHB3B0_^9T zK}qF>U^2{R*RDHN#AReyaj*5Vt9{Spt4_=XUj4@P-Z1#+%;$K1+Hs!)GOo%%ZzEPi ze$?ugnqEIh7oMq-&{b2@Xbq`A^kaehl_YD^J{5Hqy*twv&Y&=|M>~qC9~rrPVjOAP z%_@#|Uas{7vIUwhJxniIpm=!7v$jD!L21L9N|uo=>FB+aB|k^qD9spCgq_ug{2`FrF`eaA$a zkY=j+_BIt0({}5=R(jUQZTpJhD|89XBri!gUCyv&g;9aHhPsPwfaLk-O6;YEe5{cl zETauYu?_(f)SZ0GNF|=HVPhcph3`}a$TuJUak53lEYS01Sb=eaA2<}@=HX)&op+&i<)m;b*Kmwuj6g%NJD=|HuX>+KRc4`MOhIlvi#wh|(gqgNfaGho8Jz3_+mJG7tb;ih6D|$5omh|2JXMvir9kiYjl;whex-uOr zY+{IYj)c=ChJ3c=H4mp+c@2s>8;8nlRLTc|7|zvca?wEwkaPj)UU&@+@uJb z@BtK`*|gPA<(w?F7ma?-HA%J>P7ULV3HK4E2ONwskCB=NRb73#wYNb$guSN^B}^ct zhbkN;T+Md5HQ>a#t`s{K8oxFS)7iZ(hQV@y29J=i=a(fKAyxOTpSy5r0Hb1;*;CeE zs?eRB7)TnlOUthfNfLhjR3W3k50bm&o+3aMoEs^*zWL+|Zc z6+XtuX8;?x8P6y+zxI*U4+NIjpOsNZ#Oy}t;@~G~bWb{(O)UsagvBaz-lc+R_sSb% zXGU-vZ$m21SX2xzdF2dRimh74((q$AV#rY0cva3AvJd2vMk_-&R5ngETRR+jT>Kuj z7sy|CR=X9p=6m4K6L6ugT{5#ay9$Tq0c17S?rGWNqc>sFo{l4 zpSKpRH|s9tHxU?%zvu{_(Z;jLL2C{8be9j@h!jvctV5 zN7^&Y_Tp$5l<+3Oj9KX6WN0e}Oa;felc*NU3S-=J-{pp_vEZGV=)_~gB^DY^V{fGV zZ-h1r1#B~bY6Yc(y}{7);m}kc%Wa8q)oxbX@?moyN}|{D(2WSobtiE6yjOG$pb`I; zv#B46Lt@m!%Cx5GFA$``NPeBAb}nWvR9PlMG||@SHAM1}^KalPQv9 zIQ#&B3B zKnLbCxXhjfZWfx5hD^zZ3LeQf2k?As^+lRoV;Cx@U}tgwaB~+jl2jQ{^hV`LAZybg zqS6>|o8RIjl!%{o4!)>EpJy$M!K}Z`CgtfW6DnTz^m(YMU_{jjZ(hdtit>-ft_;Nb zDHA-@h;KTdeRsZj5{eunGMJYIbJW{5W+FZ|9M&F!EFconO}hvzaZzYUVgKut+}3`F zy~07~R~-;%L)a@g*JKU_AQZgc7FsKCR`#X`&^nK4$B|>cbG8|9#rICLooUdl>a5Sz zDwRy;KBR+iwu{TB1nR#O$+&i#{&wt9^D{Y0^I3%z(|Wv1+WsJyS@B##Vw1e2M{d7m zpu-20T9POuc(gb1Yddc-bF(aKcTSqRn{RjN5s1spfZPt`ERNENKDj*tH8vQ>G@S8+pR_YGT=aK^lIun~@*FUl2 zlf*8|?*rbH1Ja!4=eN*Tyt_{7uUNd1q9JbtZG92w*fGEldkPy5+-ksL<96>-p-mIm zuLW2vLKyH?Vu`vy{5gR&smcjzuapluiKA5+%Uhg;6Y;jFw)&1y>zHsTgkI%CWD^cR zCLeR868qAN6Eu?0B7wYN4nsvFy8S;=h9PrbIh-&#CTjWHJ7(4^By zN*uhvW*s)DINa#*gcCj0h%hM81I`5ZfD>T^Pw+Pn`19Lk%P~FdfQ+y-{p6|DYfz~c zy6Ttt~YxfAZx1@QO0}&M6=5WQ&9@xAHPm$NH`9>(aYDD8cTL8szG$VDoqTpX&k8< zXH~H6G|Xw<=?!B75nY@Ri4mWJP`1EiWe%8iuTm_6vF{?#+gA0&#Y^)0;lK=&-+uc^ zc+QP3ciRGfz%yP+b~J~fH$E-4Ee=ICnu=FZAO{icQV{UJ?C43IYq*6 zeG#&@SbS(wgV38&j4aT1BfI&JsaG{#&)2;w|T)H^l=*a9+q*vTV*UoTNYv zS^4r6SmZnr+z}brhzxE(J3_G7FzyI!ZUzsQdsrYJo54JF9wwBR-LPkT51=0BIe?;Z zK)(*tMTFxR!hImD06g_JUxJm6YS=!y_hraDzw8`Mu3Kq_LVn#bU1s)ooc_DsqIy~v zViDSE5%={j5GsoWocDf-&A@VkZ<4Fv4bJ{7o(OD`8a4ylT3sM`Gf`J3PT?Zx9`brP zj6E@A$i%hL8LVXmA`gqrc6cnn#tqS+W25ZO_xSqOh@%Tt=DH3KcIE_9WQLt81snH! zUm0emE7o}}L$8AfuIlPwKhiq+Rd1;kZYgA_&%wN5cfk=dsj*#LJ;`IWD2&DrH0(K; z2dBs_4KjThc5Asi{3sU8JPl7;wqs16w)V-xva02Lt=5F> zm+J=E=V85vT2*j*t@E(w?_wwl@bj=95xLq7Yxo8F`-0f8lE>6!$S?O8l^M^lPlT-h7&UsVg3jt;H$ zl53f-A!WZy)7a@{`?|HzPnf@PW9%AZ_M#sjBYAz}yaLP0i!OJ@ zM-H+!Yckau6`V8acw!%hdFS@Xkt#UACO%T`9cuA6yP{`3`Qa>p)M()HfoZ==?`yYz z%*wrDV-dK+gHAz9rAmB^9n;;}*0wdHF7%}`ORe#?{x;uc`4z2}hbkc2IWD8*DV)vO zFev2ivyr{GkI!CWy8t8gl-Grw@8iDdw?Hg5x5fAN{`qh$SF1!Q-z!D>0=KO@A0tDw zS%Y8dUX**gsaRqsq_^I;ydhBWuP&zh0ZR6-&fdPp8YKv?9J%KKf;c9MD;!n!N$h+I z=w$V*l!vXPq-?d_?byx&Oh^1B9hbW0S>aQaj>MIkxzs`Y6;P1TUep%93x}GjD-p8Q z0Bj~&h-7!aTH9e79&qA?0C!PBzS3nB+xehBE9~R5Gnb*$-4}SDcP&y@lZ2ahx0>e3 z7Ev}^A12Aeu1$on`7sz@e5!tw(Ocrl}RtH0UL{;+=zo#+@D z#?xLoXi%KovJc*cvR!K9r#GtH*RTo}ZmyRCi`5i#Q08tNkpZ;wQL;~fMAc$1raTPf zBiUOm7fB(Sy7zanAMTB(MYhIpsKBN{(jZ~^RFB?p8^zjW0s2Ha{}*9=h9#fomwGb& zH#re5Rg2Fq7okPG%@S+eXFIQm%$}jw((~-gjTT4{aR=3!@es>W!?b`k^3n}>U25;^ zW-^GP=veWPT&nY>x=WyaxkY7Ns4e7i3s4)*PrAV&?APY|i5g}zb~H4{=86UxYTKacAJOiAznMO-FnJ+^bEAO|3vDr>nYZyp7dK7%k$gr?AhOyA=#TO;tq0=1{&!GqAaUQOgj% zUHxzh2C~`MI*-=sL4iDiIIqzLCGY(HA&g1_*y|5U4doBkMXcgmoapJ%{X7sWT%%3a zjHv+evTJ>+k?-_JgUHr4uC#T^9z&OGxl$zu4{5l zlRrz>ppdBQwdnOM+hH&vNy2e=pjzSr>_&N?gn6*Y`fH`{xq zY-+Ka=lSc7Dli^AToxlWh}GZY zbAnDz#62^Yl{s@&{qr2rVZectVU053i@zk&SY}wzdL&z745@Sd9DTG_rkMk=2%hK} z(>A>`2QR58(S(9PnC%t+)rSUZi#fSeKM|T ztz9K?U;kMKH$b*!Kxq^;QCd?H5P*_mG7osSxMA(bsj;&kj!Wwda2lsn2akHY9{6nt z%Ozz;vK@BiYGsPZsiDPv_m-=I4gccBBoRmTUSsM_BCH$icy?LDwn zI*QyE`Wy}^TT_s=w@0^9JoU#rF(C@b4*n_u6N+87-G?wHklp1f1n9Vig?2}A*Fowj z%lFog`e&PCm1>TN(bte$7nLHJ3m5$oDehfT{yS?kz>ym5e+0l``BSOY&5QdaT$=Je zC53i+X4XskwGObunfQ%m?YfeMWlC#IyMR*N0}eyjJwWBD%PdnZ8`{T|8uz*Gv?jo7 z#`_b1#o&;WvanfgP;C6|TW6w7g0nqXr{PLxigDd$f_adb`t*o|Ktj3uT$h+5$l=t4 zoOu6KX1oZXE@Qbbm;uG=$RFKi0<_ilrW*x6-lu5Vr^-h{Bf=y z^7L<11)X?%=f{ICG!L%*n|Y+<_wS7A_ih^g`Zpef-}rt?OT+k6Zo5IsY8qIJb3i~06OXSz5oCK literal 0 HcmV?d00001 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