diff --git a/CLAUDE.md b/CLAUDE.md index 4c822d39..b874c1da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,16 +1,34 @@ ## Claude Guidelines for this Project +### Core Rules Summary + +- **DB**: Use the single consolidated DB (CRAWLSY_DATABASE_URL → DATABASE_URL); no dual pools; schema_migrations must exist; apply migrations 031/032/033. +- **Images**: No MinIO. Save to local /images/products//-.webp (and brands); preserve original URL; serve via backend static. +- **Dutchie GraphQL**: Endpoint https://dutchie.com/api-3/graphql. Variables must use productsFilter.dispensaryId (platform_dispensary_id). Mode A: Status="Active". Mode B: Status=null/activeOnly:false. No dispensaryFilter.cNameOrID. +- **cName/slug**: Derive cName from each store's menu_url (/embedded-menu/ or /dispensary/). No hardcoded defaults. Each location must have its own valid menu_url and platform_dispensary_id; do not reuse IDs across locations. If slug is invalid/missing, mark not crawlable and log; resolve ID before crawling. +- **Dual-mode always**: useBothModes:true to get pricing (Mode A) + full coverage (Mode B). +- **Batch DB writes**: Chunk products/snapshots/missing (100–200) to avoid OOM. +- **OOS/missing**: Include inactive/OOS in Mode B. Union A+B, dedupe by external_product_id+dispensary_id. Insert snapshots with stock_status; if absent from both modes, insert missing_from_feed. Do not filter OOS by default. +- **API/Frontend**: Use /api/az/... endpoints (stores/products/brands/categories/summary/dashboard). Rebuild frontend with VITE_API_URL pointing to the backend. +- **Scheduling**: Crawl only menu_type='dutchie' AND platform_dispensary_id IS NOT NULL. 4-hour crawl with jitter; detection job to set menu_type and resolve platform IDs. +- **Monitor**: /scraper-monitor (and /az-schedule) should show active/recent jobs from job_run_logs/crawl_jobs, with auto-refresh. +- **No slug guessing**: Never use defaults like "AZ-Deeply-Rooted." Always derive per store from menu_url and resolve platform IDs per location. + +--- + +### Detailed Rules + 1) **Use the consolidated DB everywhere** - Preferred env: `CRAWLSY_DATABASE_URL` (fallback `DATABASE_URL`). - Do NOT create dutchie tables in the legacy DB. Apply migrations 031/032/033 to the consolidated DB and restart. 2) **Dispensary vs Store** - Dutchie pipeline uses `dispensaries` (not legacy `stores`). For dutchie crawls, always work with dispensary ID. - - Ignore legacy fields like `dutchie_plus_id` and slug guessing. Use the record’s `menu_url` and `platform_dispensary_id`. + - Ignore legacy fields like `dutchie_plus_id` and slug guessing. Use the record's `menu_url` and `platform_dispensary_id`. 3) **Menu detection and platform IDs** - Set `menu_type` from `menu_url` detection; resolve `platform_dispensary_id` for `menu_type='dutchie'`. - - Admin should have “refresh detection” and “resolve ID” actions; schedule/crawl only when `menu_type='dutchie'` AND `platform_dispensary_id` is set. + - Admin should have "refresh detection" and "resolve ID" actions; schedule/crawl only when `menu_type='dutchie'` AND `platform_dispensary_id` is set. 4) **Queries and mapping** - The DB returns snake_case; code expects camelCase. Always alias/map: @@ -20,7 +38,7 @@ 5) **Scheduling** - `/scraper-schedule` should accept filters/search (All vs AZ-only, name). - - “Run Now”/scheduler must skip or warn if `menu_type!='dutchie'` or `platform_dispensary_id` missing. + - "Run Now"/scheduler must skip or warn if `menu_type!='dutchie'` or `platform_dispensary_id` missing. - Use `dispensary_crawl_status` view; show reason when not crawlable. 6) **Crawling** @@ -33,7 +51,7 @@ - `/scraper-schedule`: add filters/search, keep as master view for all schedules; reflect platform ID/menu_type status and controls (resolve ID, run now, enable/disable/delete). 8) **No slug guessing** - - Do not guess slugs; use the DB record’s `menu_url` and ID. Resolve platform ID from the URL/cName; if set, crawl directly by ID. + - Do not guess slugs; use the DB record's `menu_url` and ID. Resolve platform ID from the URL/cName; if set, crawl directly by ID. 9) **Verify locally before pushing** - Apply migrations, restart backend, ensure auth (`users` table) exists, run dutchie crawl for a known dispensary (e.g., Deeply Rooted), check `/api/az/dashboard`, `/api/az/stores/:id/products`, `/az`, `/scraper-schedule`. @@ -44,3 +62,143 @@ - Brand images: `/images/brands/-.webp`. - Store local URLs in DB fields (keep original URLs as fallback only). - Serve `/images` via backend static middleware. + +11) **Dutchie GraphQL fetch rules** + - **Endpoint**: `https://dutchie.com/api-3/graphql` (NOT `api-gw.dutchie.com` which no longer exists). + - **Variables**: Use `productsFilter.dispensaryId` = `platform_dispensary_id` (MongoDB ObjectId, e.g., `6405ef617056e8014d79101b`). + - Do NOT use `dispensaryFilter.cNameOrID` - that's outdated. + - `cName` (e.g., `AZ-Deeply-Rooted`) is only for Referer/Origin headers and Puppeteer session bootstrapping. + - **Mode A**: `Status: "Active"` - returns active products with pricing + - **Mode B**: `Status: null` / `activeOnly: false` - returns all products including OOS/inactive + - **Example payload**: + ```json + { + "operationName": "FilteredProducts", + "variables": { + "productsFilter": { + "dispensaryId": "6405ef617056e8014d79101b", + "pricingType": "rec", + "Status": "Active" + } + }, + "extensions": { + "persistedQuery": { "version": 1, "sha256Hash": "" } + } + } + ``` + - **Headers** (server-side axios only): Chrome UA, `Origin: https://dutchie.com`, `Referer: https://dutchie.com/embedded-menu/`, `accept: application/json`, `content-type: application/json`. + - If local DNS can't resolve, run fetch from an environment that can (K8s pod/remote host), not from browser. + - Use server-side axios with embedded-menu headers; include CF/session cookie from Puppeteer if needed. + +12) **Stop over-prep; run the crawl** + - To seed/refresh a store, run a one-off crawl by dispensary ID (example for Deeply Rooted): + ``` + DATABASE_URL="postgresql://dutchie:dutchie_local_pass@localhost:54320/dutchie_menus" \ + npx tsx -e "const { crawlDispensaryProducts } = require('./src/dutchie-az/services/product-crawler'); const d={id:112,name:'Deeply Rooted',platform:'dutchie',platformDispensaryId:'6405ef617056e8014d79101b',menuType:'dutchie'}; crawlDispensaryProducts(d,'rec',{useBothModes:true}).then(r=>{console.log(r);process.exit(0);}).catch(e=>{console.error(e);process.exit(1);});" + ``` + If local DNS is blocked, run the same command inside the scraper pod via `kubectl exec ... -- bash -lc '...'`. + - After crawl, verify counts via `dutchie_products`, `dutchie_product_snapshots`, and `dispensaries.last_crawl_at`. Do not inspect the legacy `products` table for Dutchie. + +13) **Fetch troubleshooting** + - If 403 or empty data: log status + first GraphQL error; include cf_clearance/session cookie from Puppeteer; ensure headers match a real Chrome request; ensure variables use `productsFilter.dispensaryId`. + - If DNS fails locally, do NOT debug DNS—run the fetch from an environment that resolves (K8s/remote) or via Puppeteer-captured headers/cookies. No browser/CORS attempts. + +14) **Views and metrics** + - Keep v_brands/v_categories/v_brand_history based on `dutchie_products` and preserve brand_count metrics. Do not drop brand_count. + +15) **Batch DB writes to avoid OOM** + - Do NOT build one giant upsert/insert payload for products/snapshots/missing marks. + - Chunk arrays (e.g., 100–200 items) and upsert/insert in a loop; drop references after each chunk. + - Apply to products, product snapshots, and any "mark missing" logic to keep memory low during crawls. + +16) **Use dual-mode crawls by default** + - Always run with `useBothModes:true` to combine: + - Mode A (active feed with pricing/stock) + - Mode B (max coverage including OOS/inactive) + - Union/dedupe by product ID so you keep full coverage and pricing in one run. + - If you only run Mode B, prices will be null; dual-mode fills pricing while retaining OOS items. + +17) **Capture OOS and missing items** + - GraphQL variables must include inactive/OOS (Status: All / activeOnly:false). Mode B already returns OOS/inactive; union with Mode A to keep pricing. + - After unioning Mode A/B, upsert products and insert snapshots with stock_status from the feed. If an existing product is absent from both Mode A and Mode B for the run, insert a snapshot with is_present_in_feed=false and stock_status='missing_from_feed'. + - Do not filter out OOS/missing in the API; only filter when the user requests (e.g., stockStatus=in_stock). Expose stock_status/in_stock from the latest snapshot (fallback to product). + - Verify with `/api/az/stores/:id/products?stockStatus=out_of_stock` and `?stockStatus=missing_from_feed`. + +18) **Menu discovery must crawl the website when menu_url is null** + - For dispensaries with no menu_url or unknown menu_type, crawl the dispensary.website (if present) to find provider links (dutchie, treez, jane, weedmaps, leafly, etc.). Follow “menu/order/shop” links up to a shallow depth with timeouts/rate limits. + - If a provider link is found, set menu_url, set menu_type, and store detection metadata; if dutchie, derive cName from menu_url and resolve platform_dispensary_id; store resolved_at and detection details. + - Do NOT mark a dispensary not_crawlable solely because menu_url is null; only mark not_crawlable if the website crawl fails to find a menu or returns 403/404/invalid. Log the reason in provider_detection_data and crawl_status_reason. + - Keep this as the menu discovery job (separate from product crawls); log successes/errors to job_run_logs. Only schedule product crawls for stores with menu_type='dutchie' AND platform_dispensary_id IS NOT NULL. + +18) **Per-location cName and platform_dispensary_id resolution** + - For each dispensary, menu_url and cName must be valid for that exact location; no hardcoded defaults and no sharing platform_dispensary_id across locations. + - Derive cName from menu_url per store: `/embedded-menu/` or `/dispensary/`. + - Resolve platform_dispensary_id from that cName using GraphQL GetAddressBasedDispensaryData. + - If the slug is invalid/missing, mark the store not crawlable and log it; do not crawl with a mismatched cName/ID. Store the error in `provider_detection_data.resolution_error`. + - Before crawling, validate that the cName from menu_url matches the resolved platform ID; if mismatched, re-resolve before proceeding. + +19) **API endpoints (AZ pipeline)** + - Use /api/az/... endpoints: stores, products, brands, categories, summary, dashboard + - Rebuild frontend with VITE_API_URL pointing to the backend + - Dispensary Detail and analytics must use AZ endpoints + +20) **Monitoring and logging** + - /scraper-monitor (and /az-schedule) should show active/recent jobs from job_run_logs/crawl_jobs + - Auto-refresh every 30 seconds + - System Logs page should show real log data, not just startup messages + +21) **Dashboard Architecture - CRITICAL** + - **Frontend**: If you see old labels like "Active Proxies" or "Active Stores", it means the old dashboard bundle is being served. Rebuild the frontend with `VITE_API_URL` pointing to the correct backend and redeploy. Clear browser cache. Confirm new labels show up. + - **Backend**: `/api/dashboard/stats` MUST use the consolidated DB (same pool as dutchie-az module). Use the correct tables: `dutchie_products`, `dispensaries`, and views like `v_dashboard_stats`, `v_latest_snapshots`. Do NOT use a separate legacy connection. Do NOT query `az_products` (doesn't exist) or legacy `stores`/`products` tables. + - **DB Connectivity**: Use the proper DB host/role. Errors like `role "dutchie" does not exist` mean you're exec'ing into the wrong Postgres pod or using wrong credentials. Confirm the correct `DATABASE_URL` and test: `kubectl exec deployment/scraper -n dispensary-scraper -- psql $DATABASE_URL -c '\dt'` + - **After fixing**: Dashboard should show real data (e.g., 777 products) instead of zeros. Do NOT revert to legacy tables; point dashboard queries to the consolidated DB/views. + - **Checklist**: + 1. Rebuild/redeploy frontend with correct API URL, clear cache + 2. Fix `/api/dashboard/*` to use the consolidated DB pool and dutchie views/tables + 3. Test `/api/dashboard/stats` from the scraper pod; then reload the UI + +22) **Deployment (Gitea + Kubernetes)** + - **Registry**: Gitea at `code.cannabrands.app/creationshop/dispensary-scraper` + - **Build and push** (from backend directory): + ```bash + # Login to Gitea container registry + docker login code.cannabrands.app + + # Build the image + cd backend + docker build -t code.cannabrands.app/creationshop/dispensary-scraper:latest . + + # Push to registry + docker push code.cannabrands.app/creationshop/dispensary-scraper:latest + ``` + - **Deploy to Kubernetes**: + ```bash + # Restart deployments to pull new image + kubectl rollout restart deployment/scraper -n dispensary-scraper + kubectl rollout restart deployment/scraper-worker -n dispensary-scraper + + # Watch rollout status + kubectl rollout status deployment/scraper -n dispensary-scraper + kubectl rollout status deployment/scraper-worker -n dispensary-scraper + ``` + - **Check pods**: + ```bash + kubectl get pods -n dispensary-scraper + kubectl logs -f deployment/scraper -n dispensary-scraper + kubectl logs -f deployment/scraper-worker -n dispensary-scraper + ``` + - K8s manifests are in `/k8s/` folder (scraper.yaml, scraper-worker.yaml, etc.) + - imagePullSecrets use `regcred` secret for Gitea registry auth + +23) **Crawler Architecture** + - **Scraper pod (1 replica)**: Runs the Express API server + scheduler. The scheduler enqueues detection and crawl jobs to the database queue (`crawl_jobs` table). + - **Scraper-worker pods (5 replicas)**: Each worker runs `dist/dutchie-az/services/worker.js`, polling the job queue and processing jobs. + - **Job types processed by workers**: + - `menu_detection` / `menu_detection_single`: Detect menu provider type and resolve platform_dispensary_id from menu_url + - `dutchie_product_crawl`: Crawl products from Dutchie GraphQL API for dispensaries with valid platform IDs + - **Job schedules** (managed in `job_schedules` table): + - `dutchie_az_menu_detection`: Runs daily with 60-min jitter, detects menu type for dispensaries with unknown menu_type + - `dutchie_az_product_crawl`: Runs every 4 hours with 30-min jitter, crawls products from all detected Dutchie dispensaries + - **Trigger schedules manually**: `curl -X POST /api/az/admin/schedules/{id}/trigger` + - **Check schedule status**: `curl /api/az/admin/schedules` + - **Worker logs**: `kubectl logs -f deployment/scraper-worker -n dispensary-scraper` diff --git a/backend/dist/auth/middleware.js b/backend/dist/auth/middleware.js index b3f449a3..280a8cf7 100644 --- a/backend/dist/auth/middleware.js +++ b/backend/dist/auth/middleware.js @@ -39,18 +39,66 @@ async function authenticateUser(email, password) { role: user.role }; } -function authMiddleware(req, res, next) { +async function authMiddleware(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.substring(7); - const user = verifyToken(token); - if (!user) { - return res.status(401).json({ error: 'Invalid token' }); + // Try JWT first + const jwtUser = verifyToken(token); + if (jwtUser) { + req.user = jwtUser; + return next(); + } + // If JWT fails, try API token + try { + const result = await migrate_1.pool.query(` + SELECT id, name, rate_limit, active, expires_at, allowed_endpoints + FROM api_tokens + WHERE token = $1 + `, [token]); + if (result.rows.length === 0) { + return res.status(401).json({ error: 'Invalid token' }); + } + const apiToken = result.rows[0]; + // Check if token is active + if (!apiToken.active) { + return res.status(401).json({ error: 'Token is disabled' }); + } + // Check if token is expired + if (apiToken.expires_at && new Date(apiToken.expires_at) < new Date()) { + return res.status(401).json({ error: 'Token has expired' }); + } + // Check allowed endpoints + if (apiToken.allowed_endpoints && apiToken.allowed_endpoints.length > 0) { + const isAllowed = apiToken.allowed_endpoints.some((pattern) => { + // Simple wildcard matching + const regex = new RegExp('^' + pattern.replace('*', '.*') + '$'); + return regex.test(req.path); + }); + if (!isAllowed) { + return res.status(403).json({ error: 'Endpoint not allowed for this token' }); + } + } + // Set API token on request for tracking + req.apiToken = { + id: apiToken.id, + name: apiToken.name, + rate_limit: apiToken.rate_limit + }; + // Set a generic user for compatibility with existing code + req.user = { + id: apiToken.id, + email: `api-token-${apiToken.id}@system`, + role: 'api' + }; + next(); + } + catch (error) { + console.error('Error verifying API token:', error); + return res.status(500).json({ error: 'Authentication failed' }); } - req.user = user; - next(); } function requireRole(...roles) { return (req, res, next) => { diff --git a/backend/dist/db/migrate.js b/backend/dist/db/migrate.js index a795c19d..5af42b0c 100644 --- a/backend/dist/db/migrate.js +++ b/backend/dist/db/migrate.js @@ -3,8 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.pool = void 0; exports.runMigrations = runMigrations; const pg_1 = require("pg"); +// Consolidated DB connection: +// - Prefer CRAWLSY_DATABASE_URL (e.g., crawlsy_local, crawlsy_prod) +// - Then DATABASE_URL (default) +const DATABASE_URL = process.env.CRAWLSY_DATABASE_URL || + process.env.DATABASE_URL || + 'postgresql://dutchie:dutchie_local_pass@localhost:54320/crawlsy_local'; const pool = new pg_1.Pool({ - connectionString: process.env.DATABASE_URL, + connectionString: DATABASE_URL, }); exports.pool = pool; async function runMigrations() { @@ -94,6 +100,99 @@ async function runMigrations() { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); + `); + // Add variant column to products table (for different sizes/options of same product) + await client.query(` + ALTER TABLE products ADD COLUMN IF NOT EXISTS variant VARCHAR(255); + `); + // Add special tracking columns (DEPRECATED - not used with new approach) + await client.query(` + ALTER TABLE products ADD COLUMN IF NOT EXISTS special_ends_at TIMESTAMP; + ALTER TABLE products ADD COLUMN IF NOT EXISTS special_text TEXT; + ALTER TABLE products ADD COLUMN IF NOT EXISTS special_type VARCHAR(100); + `); + // ====== NEW SCHEMA ADDITIONS ====== + // Add array columns for product attributes + await client.query(` + ALTER TABLE products ADD COLUMN IF NOT EXISTS terpenes TEXT[]; + ALTER TABLE products ADD COLUMN IF NOT EXISTS effects TEXT[]; + ALTER TABLE products ADD COLUMN IF NOT EXISTS flavors TEXT[]; + `); + // Add new price columns (regular_price = market price, sale_price = discount price) + await client.query(` + ALTER TABLE products ADD COLUMN IF NOT EXISTS regular_price DECIMAL(10, 2); + ALTER TABLE products ADD COLUMN IF NOT EXISTS sale_price DECIMAL(10, 2); + `); + // Migrate existing price data + await client.query(` + UPDATE products + SET regular_price = original_price + WHERE regular_price IS NULL AND original_price IS NOT NULL; + `); + await client.query(` + UPDATE products + SET sale_price = price + WHERE sale_price IS NULL AND price IS NOT NULL AND original_price IS NOT NULL AND price < original_price; + `); + // Make slug NOT NULL and add unique constraint + await client.query(` + UPDATE products SET slug = dutchie_product_id WHERE slug IS NULL; + ALTER TABLE products ALTER COLUMN slug SET NOT NULL; + `); + // Drop old unique constraint and add new one on slug + await client.query(` + ALTER TABLE products DROP CONSTRAINT IF EXISTS products_store_id_dutchie_product_id_key; + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_store_id_slug_unique') THEN + ALTER TABLE products ADD CONSTRAINT products_store_id_slug_unique UNIQUE (store_id, slug); + END IF; + END$$; + `); + // Product Categories (many-to-many) - products can appear in multiple categories + await client.query(` + CREATE TABLE IF NOT EXISTS product_categories ( + id SERIAL PRIMARY KEY, + product_id INTEGER REFERENCES products(id) ON DELETE CASCADE, + category_slug VARCHAR(255) NOT NULL, + first_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(product_id, category_slug) + ); + `); + await client.query(` + CREATE INDEX IF NOT EXISTS idx_product_categories_slug ON product_categories(category_slug, last_seen_at DESC); + CREATE INDEX IF NOT EXISTS idx_product_categories_product ON product_categories(product_id); + `); + // Price History - track regular and sale price changes over time + await client.query(` + CREATE TABLE IF NOT EXISTS price_history ( + id SERIAL PRIMARY KEY, + product_id INTEGER REFERENCES products(id) ON DELETE CASCADE, + regular_price DECIMAL(10, 2), + sale_price DECIMAL(10, 2), + recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + await client.query(` + CREATE INDEX IF NOT EXISTS idx_price_history_product ON price_history(product_id, recorded_at DESC); + CREATE INDEX IF NOT EXISTS idx_price_history_recorded ON price_history(recorded_at DESC); + `); + // Batch History - track cannabinoid/terpene changes (different batches) + await client.query(` + CREATE TABLE IF NOT EXISTS batch_history ( + id SERIAL PRIMARY KEY, + product_id INTEGER REFERENCES products(id) ON DELETE CASCADE, + thc_percentage DECIMAL(5, 2), + cbd_percentage DECIMAL(5, 2), + terpenes TEXT[], + strain_type VARCHAR(100), + recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + await client.query(` + CREATE INDEX IF NOT EXISTS idx_batch_history_product ON batch_history(product_id, recorded_at DESC); + CREATE INDEX IF NOT EXISTS idx_batch_history_recorded ON batch_history(recorded_at DESC); `); // Campaign products (many-to-many with ordering) await client.query(` @@ -138,10 +237,50 @@ async function runMigrations() { last_tested_at TIMESTAMP, test_result VARCHAR(50), response_time_ms INTEGER, + failure_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(host, port, protocol) ); + `); + // Add failure_count column if it doesn't exist + await client.query(` + ALTER TABLE proxies ADD COLUMN IF NOT EXISTS failure_count INTEGER DEFAULT 0; + `); + // Failed proxies table + await client.query(` + CREATE TABLE IF NOT EXISTS failed_proxies ( + id SERIAL PRIMARY KEY, + host VARCHAR(255) NOT NULL, + port INTEGER NOT NULL, + protocol VARCHAR(10) NOT NULL, + username VARCHAR(255), + password VARCHAR(255), + failure_count INTEGER NOT NULL, + last_error TEXT, + failed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(host, port, protocol) + ); + `); + // Proxy test jobs table + await client.query(` + CREATE TABLE IF NOT EXISTS proxy_test_jobs ( + id SERIAL PRIMARY KEY, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + total_proxies INTEGER NOT NULL DEFAULT 0, + tested_proxies INTEGER NOT NULL DEFAULT 0, + passed_proxies INTEGER NOT NULL DEFAULT 0, + failed_proxies INTEGER NOT NULL DEFAULT 0, + started_at TIMESTAMP, + completed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + await client.query(` + CREATE INDEX IF NOT EXISTS idx_proxy_test_jobs_status ON proxy_test_jobs(status); + CREATE INDEX IF NOT EXISTS idx_proxy_test_jobs_created_at ON proxy_test_jobs(created_at DESC); `); // Settings table await client.query(` diff --git a/backend/dist/db/run-notifications-migration.js b/backend/dist/db/run-notifications-migration.js new file mode 100644 index 00000000..008b33d1 --- /dev/null +++ b/backend/dist/db/run-notifications-migration.js @@ -0,0 +1,56 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const migrate_1 = require("./migrate"); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +async function runNotificationsMigration() { + const client = await migrate_1.pool.connect(); + try { + console.log('Running notifications migration...'); + const migrationSQL = fs.readFileSync(path.join(__dirname, '../../migrations/005_notifications.sql'), 'utf-8'); + await client.query(migrationSQL); + console.log('✅ Notifications migration completed successfully'); + process.exit(0); + } + catch (error) { + console.error('❌ Migration failed:', error); + process.exit(1); + } + finally { + client.release(); + } +} +runNotificationsMigration(); diff --git a/backend/dist/dutchie-az/config/dutchie.js b/backend/dist/dutchie-az/config/dutchie.js new file mode 100644 index 00000000..f9b2088b --- /dev/null +++ b/backend/dist/dutchie-az/config/dutchie.js @@ -0,0 +1,106 @@ +"use strict"; +/** + * Dutchie Configuration + * + * Centralized configuration for Dutchie GraphQL API interaction. + * Update hashes here when Dutchie changes their persisted query system. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ARIZONA_CENTERPOINTS = exports.GRAPHQL_HASHES = exports.dutchieConfig = void 0; +exports.dutchieConfig = { + // ============================================================ + // GRAPHQL ENDPOINT + // ============================================================ + /** GraphQL endpoint - must be the api-3 graphql endpoint (NOT api-gw.dutchie.com which no longer exists) */ + graphqlEndpoint: 'https://dutchie.com/api-3/graphql', + // ============================================================ + // GRAPHQL PERSISTED QUERY HASHES + // ============================================================ + // + // These hashes identify specific GraphQL operations. + // If Dutchie changes their schema, you may need to capture + // new hashes from live browser traffic (Network tab → graphql requests). + /** FilteredProducts - main product listing query */ + filteredProductsHash: 'ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0', + /** GetAddressBasedDispensaryData - resolve slug to internal ID */ + getDispensaryDataHash: '13461f73abf7268770dfd05fe7e10c523084b2bb916a929c08efe3d87531977b', + /** + * ConsumerDispensaries - geo-based discovery + * NOTE: This is a placeholder guess. If discovery fails, either: + * 1. Capture the real hash from live traffic + * 2. Rely on known AZDHS slugs instead (set useDiscovery: false) + */ + consumerDispensariesHash: '0a5bfa6ca1d64ae47bcccb7c8077c87147cbc4e6982c17ceec97a2a4948b311b', + // ============================================================ + // BEHAVIOR FLAGS + // ============================================================ + /** Enable geo-based discovery (false = use known AZDHS slugs only) */ + useDiscovery: true, + /** Prefer GET requests (true) or POST (false). GET is default. */ + preferGet: true, + /** + * Enable POST fallback when GET fails with 405 or blocked. + * If true, will retry failed GETs as POSTs. + */ + enablePostFallback: true, + // ============================================================ + // PAGINATION & RETRY + // ============================================================ + /** Products per page for pagination */ + perPage: 100, + /** Maximum pages to fetch (safety limit) */ + maxPages: 200, + /** Number of retries for failed page fetches */ + maxRetries: 1, + /** Delay between pages in ms */ + pageDelayMs: 500, + /** Delay between modes in ms */ + modeDelayMs: 2000, + // ============================================================ + // HTTP HEADERS + // ============================================================ + /** Default headers to mimic browser requests */ + defaultHeaders: { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'en-US,en;q=0.9', + 'apollographql-client-name': 'Marketplace (production)', + }, + /** User agent string */ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + // ============================================================ + // BROWSER LAUNCH OPTIONS + // ============================================================ + browserArgs: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-blink-features=AutomationControlled', + ], + /** Navigation timeout in ms */ + navigationTimeout: 60000, + /** Initial page load delay in ms */ + pageLoadDelay: 2000, +}; +/** + * Get GraphQL hashes object for backward compatibility + */ +exports.GRAPHQL_HASHES = { + FilteredProducts: exports.dutchieConfig.filteredProductsHash, + GetAddressBasedDispensaryData: exports.dutchieConfig.getDispensaryDataHash, + ConsumerDispensaries: exports.dutchieConfig.consumerDispensariesHash, +}; +/** + * Arizona geo centerpoints for discovery scans + */ +exports.ARIZONA_CENTERPOINTS = [ + { name: 'Phoenix', lat: 33.4484, lng: -112.074 }, + { name: 'Tucson', lat: 32.2226, lng: -110.9747 }, + { name: 'Flagstaff', lat: 35.1983, lng: -111.6513 }, + { name: 'Mesa', lat: 33.4152, lng: -111.8315 }, + { name: 'Scottsdale', lat: 33.4942, lng: -111.9261 }, + { name: 'Tempe', lat: 33.4255, lng: -111.94 }, + { name: 'Yuma', lat: 32.6927, lng: -114.6277 }, + { name: 'Prescott', lat: 34.54, lng: -112.4685 }, + { name: 'Lake Havasu', lat: 34.4839, lng: -114.3224 }, + { name: 'Sierra Vista', lat: 31.5455, lng: -110.2773 }, +]; diff --git a/backend/dist/dutchie-az/db/connection.js b/backend/dist/dutchie-az/db/connection.js new file mode 100644 index 00000000..e3b32e39 --- /dev/null +++ b/backend/dist/dutchie-az/db/connection.js @@ -0,0 +1,79 @@ +"use strict"; +/** + * Dutchie AZ Database Connection + * + * Isolated database connection for Dutchie Arizona data. + * Uses a separate database/schema to prevent cross-contamination with main app data. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getDutchieAZPool = getDutchieAZPool; +exports.query = query; +exports.getClient = getClient; +exports.closePool = closePool; +exports.healthCheck = healthCheck; +const pg_1 = require("pg"); +// Consolidated DB naming: +// - Prefer CRAWLSY_DATABASE_URL (e.g., crawlsy_local, crawlsy_prod) +// - Then DUTCHIE_AZ_DATABASE_URL (legacy) +// - Finally DATABASE_URL (legacy main DB) +const DUTCHIE_AZ_DATABASE_URL = process.env.CRAWLSY_DATABASE_URL || + process.env.DUTCHIE_AZ_DATABASE_URL || + process.env.DATABASE_URL || + 'postgresql://dutchie:dutchie_local_pass@localhost:54320/crawlsy_local'; +let pool = null; +/** + * Get the Dutchie AZ database pool (singleton) + */ +function getDutchieAZPool() { + if (!pool) { + pool = new pg_1.Pool({ + connectionString: DUTCHIE_AZ_DATABASE_URL, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + }); + pool.on('error', (err) => { + console.error('[DutchieAZ DB] Unexpected error on idle client:', err); + }); + console.log('[DutchieAZ DB] Pool initialized'); + } + return pool; +} +/** + * Execute a query on the Dutchie AZ database + */ +async function query(text, params) { + const p = getDutchieAZPool(); + const result = await p.query(text, params); + return { rows: result.rows, rowCount: result.rowCount || 0 }; +} +/** + * Get a client from the pool for transaction use + */ +async function getClient() { + const p = getDutchieAZPool(); + return p.connect(); +} +/** + * Close the pool connection + */ +async function closePool() { + if (pool) { + await pool.end(); + pool = null; + console.log('[DutchieAZ DB] Pool closed'); + } +} +/** + * Check if the database is accessible + */ +async function healthCheck() { + try { + const result = await query('SELECT 1 as ok'); + return result.rows.length > 0 && result.rows[0].ok === 1; + } + catch (error) { + console.error('[DutchieAZ DB] Health check failed:', error); + return false; + } +} diff --git a/backend/dist/dutchie-az/db/migrate.js b/backend/dist/dutchie-az/db/migrate.js new file mode 100644 index 00000000..a4ea4eae --- /dev/null +++ b/backend/dist/dutchie-az/db/migrate.js @@ -0,0 +1,30 @@ +"use strict"; +/** + * Dutchie AZ Schema Bootstrap + * + * Run this to create/update the dutchie_az tables (dutchie_products, dutchie_product_snapshots, etc.) + * in the AZ pipeline database. This is separate from the legacy schema. + * + * Usage: + * TS_NODE_TRANSPILE_ONLY=1 npx ts-node src/dutchie-az/db/migrate.ts + * or (after build) + * node dist/dutchie-az/db/migrate.js + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const schema_1 = require("./schema"); +const connection_1 = require("./connection"); +async function main() { + try { + console.log('[DutchieAZ] Running schema migration...'); + await (0, schema_1.createSchema)(); + console.log('[DutchieAZ] Schema migration complete.'); + } + catch (err) { + console.error('[DutchieAZ] Schema migration failed:', err.message); + process.exitCode = 1; + } + finally { + await (0, connection_1.closePool)(); + } +} +main(); diff --git a/backend/dist/dutchie-az/db/schema.js b/backend/dist/dutchie-az/db/schema.js new file mode 100644 index 00000000..493692a3 --- /dev/null +++ b/backend/dist/dutchie-az/db/schema.js @@ -0,0 +1,405 @@ +"use strict"; +/** + * Dutchie AZ Database Schema + * + * Creates all tables for the isolated Dutchie Arizona data pipeline. + * Run this to initialize the dutchie_az database. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createSchema = createSchema; +exports.dropSchema = dropSchema; +exports.schemaExists = schemaExists; +exports.ensureSchema = ensureSchema; +const connection_1 = require("./connection"); +/** + * SQL statements to create all tables + */ +const SCHEMA_SQL = ` +-- ============================================================ +-- DISPENSARIES TABLE +-- Stores discovered Dutchie dispensaries in Arizona +-- ============================================================ +CREATE TABLE IF NOT EXISTS dispensaries ( + id SERIAL PRIMARY KEY, + platform VARCHAR(20) NOT NULL DEFAULT 'dutchie', + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL, + city VARCHAR(100) NOT NULL, + state VARCHAR(10) NOT NULL DEFAULT 'AZ', + postal_code VARCHAR(20), + address TEXT, + latitude DECIMAL(10, 7), + longitude DECIMAL(10, 7), + platform_dispensary_id VARCHAR(100), + is_delivery BOOLEAN DEFAULT false, + is_pickup BOOLEAN DEFAULT true, + raw_metadata JSONB, + last_crawled_at TIMESTAMPTZ, + product_count INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT uk_dispensaries_platform_slug UNIQUE (platform, slug, city, state) +); + +CREATE INDEX IF NOT EXISTS idx_dispensaries_platform ON dispensaries(platform); +CREATE INDEX IF NOT EXISTS idx_dispensaries_platform_id ON dispensaries(platform_dispensary_id); +CREATE INDEX IF NOT EXISTS idx_dispensaries_state ON dispensaries(state); +CREATE INDEX IF NOT EXISTS idx_dispensaries_city ON dispensaries(city); + +-- ============================================================ +-- DUTCHIE_PRODUCTS TABLE +-- Canonical product identity per store +-- ============================================================ +CREATE TABLE IF NOT EXISTS dutchie_products ( + id SERIAL PRIMARY KEY, + dispensary_id INTEGER NOT NULL REFERENCES dispensaries(id) ON DELETE CASCADE, + platform VARCHAR(20) NOT NULL DEFAULT 'dutchie', + + external_product_id VARCHAR(100) NOT NULL, + platform_dispensary_id VARCHAR(100) NOT NULL, + c_name VARCHAR(500), + name VARCHAR(500) NOT NULL, + + -- Brand + brand_name VARCHAR(255), + brand_id VARCHAR(100), + brand_logo_url TEXT, + + -- Classification + type VARCHAR(100), + subcategory VARCHAR(100), + strain_type VARCHAR(50), + provider VARCHAR(100), + + -- Potency + thc DECIMAL(10, 4), + thc_content DECIMAL(10, 4), + cbd DECIMAL(10, 4), + cbd_content DECIMAL(10, 4), + cannabinoids_v2 JSONB, + effects JSONB, + + -- Status / flags + status VARCHAR(50), + medical_only BOOLEAN DEFAULT false, + rec_only BOOLEAN DEFAULT false, + featured BOOLEAN DEFAULT false, + coming_soon BOOLEAN DEFAULT false, + certificate_of_analysis_enabled BOOLEAN DEFAULT false, + + is_below_threshold BOOLEAN DEFAULT false, + is_below_kiosk_threshold BOOLEAN DEFAULT false, + options_below_threshold BOOLEAN DEFAULT false, + options_below_kiosk_threshold BOOLEAN DEFAULT false, + + -- Derived stock status: 'in_stock', 'out_of_stock', 'unknown' + stock_status VARCHAR(20) DEFAULT 'unknown', + total_quantity_available INTEGER DEFAULT 0, + + -- Images + primary_image_url TEXT, + images JSONB, + + -- Misc + measurements JSONB, + weight VARCHAR(50), + past_c_names TEXT[], + + created_at_dutchie TIMESTAMPTZ, + updated_at_dutchie TIMESTAMPTZ, + + latest_raw_payload JSONB, + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT uk_dutchie_products UNIQUE (dispensary_id, external_product_id) +); + +CREATE INDEX IF NOT EXISTS idx_dutchie_products_dispensary ON dutchie_products(dispensary_id); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_external_id ON dutchie_products(external_product_id); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_platform_disp ON dutchie_products(platform_dispensary_id); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_brand ON dutchie_products(brand_name); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_type ON dutchie_products(type); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_subcategory ON dutchie_products(subcategory); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_status ON dutchie_products(status); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_strain ON dutchie_products(strain_type); +CREATE INDEX IF NOT EXISTS idx_dutchie_products_stock_status ON dutchie_products(stock_status); + +-- ============================================================ +-- DUTCHIE_PRODUCT_SNAPSHOTS TABLE +-- Historical state per crawl, includes options[] +-- ============================================================ +CREATE TABLE IF NOT EXISTS dutchie_product_snapshots ( + id SERIAL PRIMARY KEY, + dutchie_product_id INTEGER NOT NULL REFERENCES dutchie_products(id) ON DELETE CASCADE, + dispensary_id INTEGER NOT NULL REFERENCES dispensaries(id) ON DELETE CASCADE, + platform_dispensary_id VARCHAR(100) NOT NULL, + external_product_id VARCHAR(100) NOT NULL, + pricing_type VARCHAR(20) DEFAULT 'unknown', + crawl_mode VARCHAR(20) DEFAULT 'mode_a', -- 'mode_a' (UI parity) or 'mode_b' (max coverage) + + status VARCHAR(50), + featured BOOLEAN DEFAULT false, + special BOOLEAN DEFAULT false, + medical_only BOOLEAN DEFAULT false, + rec_only BOOLEAN DEFAULT false, + + -- Flag indicating if product was present in feed (false = missing_from_feed snapshot) + is_present_in_feed BOOLEAN DEFAULT true, + + -- Derived stock status + stock_status VARCHAR(20) DEFAULT 'unknown', + + -- Price summary (in cents) + rec_min_price_cents INTEGER, + rec_max_price_cents INTEGER, + rec_min_special_price_cents INTEGER, + med_min_price_cents INTEGER, + med_max_price_cents INTEGER, + med_min_special_price_cents INTEGER, + wholesale_min_price_cents INTEGER, + + -- Inventory summary + total_quantity_available INTEGER, + total_kiosk_quantity_available INTEGER, + manual_inventory BOOLEAN DEFAULT false, + is_below_threshold BOOLEAN DEFAULT false, + is_below_kiosk_threshold BOOLEAN DEFAULT false, + + -- Option-level data (from POSMetaData.children) + options JSONB, + + -- Full raw product node + raw_payload JSONB NOT NULL, + + crawled_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_snapshots_product ON dutchie_product_snapshots(dutchie_product_id); +CREATE INDEX IF NOT EXISTS idx_snapshots_dispensary ON dutchie_product_snapshots(dispensary_id); +CREATE INDEX IF NOT EXISTS idx_snapshots_crawled_at ON dutchie_product_snapshots(crawled_at); +CREATE INDEX IF NOT EXISTS idx_snapshots_platform_disp ON dutchie_product_snapshots(platform_dispensary_id); +CREATE INDEX IF NOT EXISTS idx_snapshots_external_id ON dutchie_product_snapshots(external_product_id); +CREATE INDEX IF NOT EXISTS idx_snapshots_special ON dutchie_product_snapshots(special) WHERE special = true; +CREATE INDEX IF NOT EXISTS idx_snapshots_stock_status ON dutchie_product_snapshots(stock_status); +CREATE INDEX IF NOT EXISTS idx_snapshots_crawl_mode ON dutchie_product_snapshots(crawl_mode); + +-- ============================================================ +-- CRAWL_JOBS TABLE +-- Tracks crawl execution status +-- ============================================================ +CREATE TABLE IF NOT EXISTS crawl_jobs ( + id SERIAL PRIMARY KEY, + job_type VARCHAR(50) NOT NULL, + dispensary_id INTEGER REFERENCES dispensaries(id) ON DELETE SET NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + error_message TEXT, + products_found INTEGER, + snapshots_created INTEGER, + metadata JSONB, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_crawl_jobs_type ON crawl_jobs(job_type); +CREATE INDEX IF NOT EXISTS idx_crawl_jobs_status ON crawl_jobs(status); +CREATE INDEX IF NOT EXISTS idx_crawl_jobs_dispensary ON crawl_jobs(dispensary_id); +CREATE INDEX IF NOT EXISTS idx_crawl_jobs_created ON crawl_jobs(created_at); + +-- ============================================================ +-- JOB_SCHEDULES TABLE +-- Stores schedule configuration for recurring jobs with jitter support +-- Each job has independent timing that "wanders" over time +-- ============================================================ +CREATE TABLE IF NOT EXISTS job_schedules ( + id SERIAL PRIMARY KEY, + job_name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + enabled BOOLEAN DEFAULT true, + + -- Timing configuration (jitter makes times "wander") + base_interval_minutes INTEGER NOT NULL DEFAULT 240, -- e.g., 4 hours + jitter_minutes INTEGER NOT NULL DEFAULT 30, -- e.g., ±30 min + + -- Last run tracking + last_run_at TIMESTAMPTZ, + last_status VARCHAR(20), -- 'success', 'error', 'partial', 'running' + last_error_message TEXT, + last_duration_ms INTEGER, + + -- Next run (calculated with jitter after each run) + next_run_at TIMESTAMPTZ, + + -- Additional config + job_config JSONB, -- e.g., { pricingType: 'rec', useBothModes: true } + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_job_schedules_enabled ON job_schedules(enabled); +CREATE INDEX IF NOT EXISTS idx_job_schedules_next_run ON job_schedules(next_run_at); + +-- ============================================================ +-- JOB_RUN_LOGS TABLE +-- Stores history of job runs for monitoring +-- ============================================================ +CREATE TABLE IF NOT EXISTS job_run_logs ( + id SERIAL PRIMARY KEY, + schedule_id INTEGER NOT NULL REFERENCES job_schedules(id) ON DELETE CASCADE, + job_name VARCHAR(100) NOT NULL, + status VARCHAR(20) NOT NULL, -- 'pending', 'running', 'success', 'error', 'partial' + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + duration_ms INTEGER, + error_message TEXT, + + -- Results summary + items_processed INTEGER, + items_succeeded INTEGER, + items_failed INTEGER, + + metadata JSONB, -- Additional run details + + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_job_run_logs_schedule ON job_run_logs(schedule_id); +CREATE INDEX IF NOT EXISTS idx_job_run_logs_job_name ON job_run_logs(job_name); +CREATE INDEX IF NOT EXISTS idx_job_run_logs_status ON job_run_logs(status); +CREATE INDEX IF NOT EXISTS idx_job_run_logs_created ON job_run_logs(created_at); + +-- ============================================================ +-- VIEWS FOR EASY QUERYING +-- ============================================================ + +-- Categories derived from products +CREATE OR REPLACE VIEW v_categories AS +SELECT + type, + subcategory, + COUNT(DISTINCT id) as product_count, + COUNT(DISTINCT dispensary_id) as dispensary_count, + AVG(thc) as avg_thc, + MIN(thc) as min_thc, + MAX(thc) as max_thc +FROM dutchie_products +WHERE type IS NOT NULL +GROUP BY type, subcategory +ORDER BY type, subcategory; + +-- Brands derived from products +CREATE OR REPLACE VIEW v_brands AS +SELECT + brand_name, + brand_id, + MAX(brand_logo_url) as brand_logo_url, + COUNT(DISTINCT id) as product_count, + COUNT(DISTINCT dispensary_id) as dispensary_count, + ARRAY_AGG(DISTINCT type) FILTER (WHERE type IS NOT NULL) as product_types +FROM dutchie_products +WHERE brand_name IS NOT NULL +GROUP BY brand_name, brand_id +ORDER BY product_count DESC; + +-- Latest snapshot per product (most recent crawl data) +CREATE OR REPLACE VIEW v_latest_snapshots AS +SELECT DISTINCT ON (dutchie_product_id) + s.* +FROM dutchie_product_snapshots s +ORDER BY dutchie_product_id, crawled_at DESC; + +-- Dashboard stats +CREATE OR REPLACE VIEW v_dashboard_stats AS +SELECT + (SELECT COUNT(*) FROM dispensaries WHERE state = 'AZ') as dispensary_count, + (SELECT COUNT(*) FROM dutchie_products) as product_count, + (SELECT COUNT(*) FROM dutchie_product_snapshots WHERE crawled_at > NOW() - INTERVAL '24 hours') as snapshots_24h, + (SELECT MAX(crawled_at) FROM dutchie_product_snapshots) as last_crawl_time, + (SELECT COUNT(*) FROM crawl_jobs WHERE status = 'failed' AND created_at > NOW() - INTERVAL '24 hours') as failed_jobs_24h, + (SELECT COUNT(DISTINCT brand_name) FROM dutchie_products WHERE brand_name IS NOT NULL) as brand_count, + (SELECT COUNT(DISTINCT (type, subcategory)) FROM dutchie_products WHERE type IS NOT NULL) as category_count; +`; +/** + * Run the schema migration + */ +async function createSchema() { + console.log('[DutchieAZ Schema] Creating database schema...'); + const client = await (0, connection_1.getClient)(); + try { + await client.query('BEGIN'); + // Split into individual statements and execute + const statements = SCHEMA_SQL + .split(';') + .map(s => s.trim()) + .filter(s => s.length > 0 && !s.startsWith('--')); + for (const statement of statements) { + if (statement.trim()) { + await client.query(statement + ';'); + } + } + await client.query('COMMIT'); + console.log('[DutchieAZ Schema] Schema created successfully'); + } + catch (error) { + await client.query('ROLLBACK'); + console.error('[DutchieAZ Schema] Failed to create schema:', error); + throw error; + } + finally { + client.release(); + } +} +/** + * Drop all tables (for development/testing) + */ +async function dropSchema() { + console.log('[DutchieAZ Schema] Dropping all tables...'); + await (0, connection_1.query)(` + DROP VIEW IF EXISTS v_dashboard_stats CASCADE; + DROP VIEW IF EXISTS v_latest_snapshots CASCADE; + DROP VIEW IF EXISTS v_brands CASCADE; + DROP VIEW IF EXISTS v_categories CASCADE; + DROP TABLE IF EXISTS crawl_schedule CASCADE; + DROP TABLE IF EXISTS crawl_jobs CASCADE; + DROP TABLE IF EXISTS dutchie_product_snapshots CASCADE; + DROP TABLE IF EXISTS dutchie_products CASCADE; + DROP TABLE IF EXISTS dispensaries CASCADE; + `); + console.log('[DutchieAZ Schema] All tables dropped'); +} +/** + * Check if schema exists + */ +async function schemaExists() { + try { + const result = await (0, connection_1.query)(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'dispensaries' + ) as exists + `); + return result.rows[0]?.exists === true; + } + catch (error) { + return false; + } +} +/** + * Initialize schema if it doesn't exist + */ +async function ensureSchema() { + const exists = await schemaExists(); + if (!exists) { + await createSchema(); + } + else { + console.log('[DutchieAZ Schema] Schema already exists'); + } +} diff --git a/backend/dist/dutchie-az/index.js b/backend/dist/dutchie-az/index.js new file mode 100644 index 00000000..b0887874 --- /dev/null +++ b/backend/dist/dutchie-az/index.js @@ -0,0 +1,95 @@ +"use strict"; +/** + * Dutchie AZ Data Pipeline + * + * Isolated data pipeline for crawling and storing Dutchie Arizona dispensary data. + * This module is completely separate from the main application database. + * + * Features: + * - Two-mode crawling (Mode A: UI parity, Mode B: MAX COVERAGE) + * - Derived stockStatus field (in_stock, out_of_stock, unknown) + * - Full raw payload storage for 100% data preservation + * - AZDHS dispensary list as canonical source + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.dutchieAZRouter = exports.getImportStats = exports.importFromJSON = exports.importAZDHSDispensaries = exports.getRunLogs = exports.initializeDefaultSchedules = exports.triggerScheduleNow = exports.deleteSchedule = exports.updateSchedule = exports.createSchedule = exports.getScheduleById = exports.getAllSchedules = exports.crawlSingleDispensary = exports.getSchedulerStatus = exports.triggerImmediateCrawl = exports.stopScheduler = exports.startScheduler = exports.crawlAllArizonaDispensaries = exports.crawlDispensaryProducts = exports.normalizeSnapshot = exports.normalizeProduct = exports.getDispensariesWithPlatformIds = exports.getDispensaryById = exports.getAllDispensaries = exports.resolvePlatformDispensaryIds = exports.discoverAndSaveDispensaries = exports.importFromExistingDispensaries = exports.discoverDispensaries = exports.discoverArizonaDispensaries = exports.fetchAllProductsBothModes = exports.fetchAllProducts = exports.resolveDispensaryId = exports.ARIZONA_CENTERPOINTS = exports.GRAPHQL_HASHES = exports.ensureSchema = exports.schemaExists = exports.dropSchema = exports.createSchema = exports.healthCheck = exports.closePool = exports.getClient = exports.query = exports.getDutchieAZPool = void 0; +// Types +__exportStar(require("./types"), exports); +// Database +var connection_1 = require("./db/connection"); +Object.defineProperty(exports, "getDutchieAZPool", { enumerable: true, get: function () { return connection_1.getDutchieAZPool; } }); +Object.defineProperty(exports, "query", { enumerable: true, get: function () { return connection_1.query; } }); +Object.defineProperty(exports, "getClient", { enumerable: true, get: function () { return connection_1.getClient; } }); +Object.defineProperty(exports, "closePool", { enumerable: true, get: function () { return connection_1.closePool; } }); +Object.defineProperty(exports, "healthCheck", { enumerable: true, get: function () { return connection_1.healthCheck; } }); +var schema_1 = require("./db/schema"); +Object.defineProperty(exports, "createSchema", { enumerable: true, get: function () { return schema_1.createSchema; } }); +Object.defineProperty(exports, "dropSchema", { enumerable: true, get: function () { return schema_1.dropSchema; } }); +Object.defineProperty(exports, "schemaExists", { enumerable: true, get: function () { return schema_1.schemaExists; } }); +Object.defineProperty(exports, "ensureSchema", { enumerable: true, get: function () { return schema_1.ensureSchema; } }); +// Services - GraphQL Client +var graphql_client_1 = require("./services/graphql-client"); +Object.defineProperty(exports, "GRAPHQL_HASHES", { enumerable: true, get: function () { return graphql_client_1.GRAPHQL_HASHES; } }); +Object.defineProperty(exports, "ARIZONA_CENTERPOINTS", { enumerable: true, get: function () { return graphql_client_1.ARIZONA_CENTERPOINTS; } }); +Object.defineProperty(exports, "resolveDispensaryId", { enumerable: true, get: function () { return graphql_client_1.resolveDispensaryId; } }); +Object.defineProperty(exports, "fetchAllProducts", { enumerable: true, get: function () { return graphql_client_1.fetchAllProducts; } }); +Object.defineProperty(exports, "fetchAllProductsBothModes", { enumerable: true, get: function () { return graphql_client_1.fetchAllProductsBothModes; } }); +Object.defineProperty(exports, "discoverArizonaDispensaries", { enumerable: true, get: function () { return graphql_client_1.discoverArizonaDispensaries; } }); +// Alias for backward compatibility +Object.defineProperty(exports, "discoverDispensaries", { enumerable: true, get: function () { return graphql_client_1.discoverArizonaDispensaries; } }); +// Services - Discovery +var discovery_1 = require("./services/discovery"); +Object.defineProperty(exports, "importFromExistingDispensaries", { enumerable: true, get: function () { return discovery_1.importFromExistingDispensaries; } }); +Object.defineProperty(exports, "discoverAndSaveDispensaries", { enumerable: true, get: function () { return discovery_1.discoverDispensaries; } }); +Object.defineProperty(exports, "resolvePlatformDispensaryIds", { enumerable: true, get: function () { return discovery_1.resolvePlatformDispensaryIds; } }); +Object.defineProperty(exports, "getAllDispensaries", { enumerable: true, get: function () { return discovery_1.getAllDispensaries; } }); +Object.defineProperty(exports, "getDispensaryById", { enumerable: true, get: function () { return discovery_1.getDispensaryById; } }); +Object.defineProperty(exports, "getDispensariesWithPlatformIds", { enumerable: true, get: function () { return discovery_1.getDispensariesWithPlatformIds; } }); +// Services - Product Crawler +var product_crawler_1 = require("./services/product-crawler"); +Object.defineProperty(exports, "normalizeProduct", { enumerable: true, get: function () { return product_crawler_1.normalizeProduct; } }); +Object.defineProperty(exports, "normalizeSnapshot", { enumerable: true, get: function () { return product_crawler_1.normalizeSnapshot; } }); +Object.defineProperty(exports, "crawlDispensaryProducts", { enumerable: true, get: function () { return product_crawler_1.crawlDispensaryProducts; } }); +Object.defineProperty(exports, "crawlAllArizonaDispensaries", { enumerable: true, get: function () { return product_crawler_1.crawlAllArizonaDispensaries; } }); +// Services - Scheduler +var scheduler_1 = require("./services/scheduler"); +Object.defineProperty(exports, "startScheduler", { enumerable: true, get: function () { return scheduler_1.startScheduler; } }); +Object.defineProperty(exports, "stopScheduler", { enumerable: true, get: function () { return scheduler_1.stopScheduler; } }); +Object.defineProperty(exports, "triggerImmediateCrawl", { enumerable: true, get: function () { return scheduler_1.triggerImmediateCrawl; } }); +Object.defineProperty(exports, "getSchedulerStatus", { enumerable: true, get: function () { return scheduler_1.getSchedulerStatus; } }); +Object.defineProperty(exports, "crawlSingleDispensary", { enumerable: true, get: function () { return scheduler_1.crawlSingleDispensary; } }); +// Schedule config CRUD +Object.defineProperty(exports, "getAllSchedules", { enumerable: true, get: function () { return scheduler_1.getAllSchedules; } }); +Object.defineProperty(exports, "getScheduleById", { enumerable: true, get: function () { return scheduler_1.getScheduleById; } }); +Object.defineProperty(exports, "createSchedule", { enumerable: true, get: function () { return scheduler_1.createSchedule; } }); +Object.defineProperty(exports, "updateSchedule", { enumerable: true, get: function () { return scheduler_1.updateSchedule; } }); +Object.defineProperty(exports, "deleteSchedule", { enumerable: true, get: function () { return scheduler_1.deleteSchedule; } }); +Object.defineProperty(exports, "triggerScheduleNow", { enumerable: true, get: function () { return scheduler_1.triggerScheduleNow; } }); +Object.defineProperty(exports, "initializeDefaultSchedules", { enumerable: true, get: function () { return scheduler_1.initializeDefaultSchedules; } }); +// Run logs +Object.defineProperty(exports, "getRunLogs", { enumerable: true, get: function () { return scheduler_1.getRunLogs; } }); +// Services - AZDHS Import +var azdhs_import_1 = require("./services/azdhs-import"); +Object.defineProperty(exports, "importAZDHSDispensaries", { enumerable: true, get: function () { return azdhs_import_1.importAZDHSDispensaries; } }); +Object.defineProperty(exports, "importFromJSON", { enumerable: true, get: function () { return azdhs_import_1.importFromJSON; } }); +Object.defineProperty(exports, "getImportStats", { enumerable: true, get: function () { return azdhs_import_1.getImportStats; } }); +// Routes +var routes_1 = require("./routes"); +Object.defineProperty(exports, "dutchieAZRouter", { enumerable: true, get: function () { return __importDefault(routes_1).default; } }); diff --git a/backend/dist/dutchie-az/routes/index.js b/backend/dist/dutchie-az/routes/index.js new file mode 100644 index 00000000..7f770a57 --- /dev/null +++ b/backend/dist/dutchie-az/routes/index.js @@ -0,0 +1,1610 @@ +"use strict"; +/** + * Dutchie AZ API Routes + * + * Express routes for the Dutchie AZ data pipeline. + * Provides API endpoints for stores, products, categories, and dashboard. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const connection_1 = require("../db/connection"); +const schema_1 = require("../db/schema"); +const azdhs_import_1 = require("../services/azdhs-import"); +const discovery_1 = require("../services/discovery"); +const product_crawler_1 = require("../services/product-crawler"); +// Explicit column list for dispensaries table (avoids SELECT * issues with schema differences) +const DISPENSARY_COLUMNS = ` + id, name, slug, city, state, zip, address, latitude, longitude, + menu_type, menu_url, platform_dispensary_id, website, + provider_detection_data, created_at, updated_at +`; +const scheduler_1 = require("../services/scheduler"); +const router = (0, express_1.Router)(); +// ============================================================ +// DASHBOARD +// ============================================================ +/** + * GET /api/dutchie-az/dashboard + * Dashboard stats overview + */ +router.get('/dashboard', async (_req, res) => { + try { + const { rows } = await (0, connection_1.query)(`SELECT * FROM v_dashboard_stats`); + const stats = rows[0] || {}; + res.json({ + dispensaryCount: parseInt(stats.dispensary_count || '0', 10), + productCount: parseInt(stats.product_count || '0', 10), + snapshotCount24h: parseInt(stats.snapshots_24h || '0', 10), + lastCrawlTime: stats.last_crawl_time, + failedJobCount: parseInt(stats.failed_jobs_24h || '0', 10), + brandCount: parseInt(stats.brand_count || '0', 10), + categoryCount: parseInt(stats.category_count || '0', 10), + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// DISPENSARIES (STORES) +// ============================================================ +/** + * GET /api/dutchie-az/stores + * List all stores with optional filters + */ +router.get('/stores', async (req, res) => { + try { + const { city, hasPlatformId, limit = '100', offset = '0' } = req.query; + let whereClause = 'WHERE state = \'AZ\''; + const params = []; + let paramIndex = 1; + if (city) { + whereClause += ` AND city = $${paramIndex}`; + params.push(city); + paramIndex++; + } + if (hasPlatformId === 'true') { + whereClause += ' AND platform_dispensary_id IS NOT NULL'; + } + else if (hasPlatformId === 'false') { + whereClause += ' AND platform_dispensary_id IS NULL'; + } + params.push(parseInt(limit, 10), parseInt(offset, 10)); + const { rows, rowCount } = await (0, connection_1.query)(` + SELECT ${DISPENSARY_COLUMNS} FROM dispensaries + ${whereClause} + ORDER BY name + LIMIT $${paramIndex} OFFSET $${paramIndex + 1} + `, params); + // Get total count + const { rows: countRows } = await (0, connection_1.query)(`SELECT COUNT(*) as total FROM dispensaries ${whereClause}`, params.slice(0, -2)); + res.json({ + stores: rows, + total: parseInt(countRows[0]?.total || '0', 10), + limit: parseInt(limit, 10), + offset: parseInt(offset, 10), + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/stores/slug/:slug + * Resolve a store by slug (case-insensitive) or platform_dispensary_id + */ +router.get('/stores/slug/:slug', async (req, res) => { + try { + const { slug } = req.params; + const normalized = slug.toLowerCase(); + const { rows } = await (0, connection_1.query)(` + SELECT ${DISPENSARY_COLUMNS} + FROM dispensaries + WHERE lower(slug) = $1 + OR lower(platform_dispensary_id) = $1 + LIMIT 1 + `, [normalized]); + if (!rows || rows.length === 0) { + return res.status(404).json({ error: 'Store not found' }); + } + res.json(rows[0]); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/stores/:id + * Get a single store by ID + */ +router.get('/stores/:id', async (req, res) => { + try { + const { id } = req.params; + const store = await (0, discovery_1.getDispensaryById)(parseInt(id, 10)); + if (!store) { + return res.status(404).json({ error: 'Store not found' }); + } + res.json(store); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/stores/:id/summary + * Get store summary with product count, categories, and brands + * This is the main endpoint for the DispensaryDetail panel + */ +router.get('/stores/:id/summary', async (req, res) => { + try { + const { id } = req.params; + // Get dispensary info + const { rows: dispensaryRows } = await (0, connection_1.query)(`SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE id = $1`, [parseInt(id, 10)]); + if (dispensaryRows.length === 0) { + return res.status(404).json({ error: 'Store not found' }); + } + const dispensary = dispensaryRows[0]; + // Get product counts by stock status + const { rows: countRows } = await (0, connection_1.query)(` + SELECT + COUNT(*) as total_products, + COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock_count, + COUNT(*) FILTER (WHERE stock_status = 'out_of_stock') as out_of_stock_count, + COUNT(*) FILTER (WHERE stock_status = 'unknown') as unknown_count, + COUNT(*) FILTER (WHERE stock_status = 'missing_from_feed') as missing_count + FROM dutchie_products + WHERE dispensary_id = $1 + `, [id]); + // Get categories with counts for this store + const { rows: categories } = await (0, connection_1.query)(` + SELECT + type, + subcategory, + COUNT(*) as product_count + FROM dutchie_products + WHERE dispensary_id = $1 AND type IS NOT NULL + GROUP BY type, subcategory + ORDER BY type, subcategory + `, [id]); + // Get brands with counts for this store + const { rows: brands } = await (0, connection_1.query)(` + SELECT + brand_name, + COUNT(*) as product_count + FROM dutchie_products + WHERE dispensary_id = $1 AND brand_name IS NOT NULL + GROUP BY brand_name + ORDER BY product_count DESC + `, [id]); + // Get last crawl info + const { rows: lastCrawl } = await (0, connection_1.query)(` + SELECT + id, + status, + started_at, + completed_at, + products_found, + products_new, + products_updated, + error_message + FROM dispensary_crawl_jobs + WHERE dispensary_id = $1 + ORDER BY created_at DESC + LIMIT 1 + `, [id]); + const counts = countRows[0] || {}; + res.json({ + dispensary, + totalProducts: parseInt(counts.total_products || '0', 10), + inStockCount: parseInt(counts.in_stock_count || '0', 10), + outOfStockCount: parseInt(counts.out_of_stock_count || '0', 10), + unknownStockCount: parseInt(counts.unknown_count || '0', 10), + missingFromFeedCount: parseInt(counts.missing_count || '0', 10), + categories, + brands, + brandCount: brands.length, + categoryCount: categories.length, + lastCrawl: lastCrawl[0] || null, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/stores/:id/products + * Get paginated products for a store with latest snapshot data + */ +router.get('/stores/:id/products', async (req, res) => { + try { + const { id } = req.params; + const { stockStatus, type, subcategory, brandName, search, limit = '50', offset = '0', } = req.query; + let whereClause = 'WHERE p.dispensary_id = $1'; + const params = [parseInt(id, 10)]; + let paramIndex = 2; + if (stockStatus) { + whereClause += ` AND p.stock_status = $${paramIndex}`; + params.push(stockStatus); + paramIndex++; + } + if (type) { + whereClause += ` AND p.type = $${paramIndex}`; + params.push(type); + paramIndex++; + } + if (subcategory) { + whereClause += ` AND p.subcategory = $${paramIndex}`; + params.push(subcategory); + paramIndex++; + } + if (brandName) { + whereClause += ` AND p.brand_name ILIKE $${paramIndex}`; + params.push(`%${brandName}%`); + paramIndex++; + } + if (search) { + whereClause += ` AND (p.name ILIKE $${paramIndex} OR p.brand_name ILIKE $${paramIndex})`; + params.push(`%${search}%`); + paramIndex++; + } + params.push(parseInt(limit, 10), parseInt(offset, 10)); + // Get products with their latest snapshot data + const { rows: products } = await (0, connection_1.query)(` + SELECT + p.id, + p.external_product_id, + p.name, + p.brand_name, + p.type, + p.subcategory, + p.strain_type, + p.stock_status, + p.created_at, + p.updated_at, + p.primary_image_url, + p.thc_content, + p.cbd_content, + -- Latest snapshot data (prices in cents) + s.rec_min_price_cents, + s.rec_max_price_cents, + s.med_min_price_cents, + s.med_max_price_cents, + s.rec_min_special_price_cents, + s.med_min_special_price_cents, + s.total_quantity_available, + s.options, + s.stock_status as snapshot_stock_status, + s.crawled_at as snapshot_at + FROM dutchie_products p + LEFT JOIN LATERAL ( + SELECT * FROM dutchie_product_snapshots + WHERE dutchie_product_id = p.id + ORDER BY crawled_at DESC + LIMIT 1 + ) s ON true + ${whereClause} + ORDER BY p.updated_at DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1} + `, params); + // Get total count + const { rows: countRows } = await (0, connection_1.query)(`SELECT COUNT(*) as total FROM dutchie_products p ${whereClause}`, params.slice(0, -2)); + // Transform products for frontend compatibility + const transformedProducts = products.map((p) => ({ + id: p.id, + external_id: p.external_product_id, + name: p.name, + brand: p.brand_name, + type: p.type, + subcategory: p.subcategory, + strain_type: p.strain_type, + stock_status: p.snapshot_stock_status || p.stock_status, + in_stock: (p.snapshot_stock_status || p.stock_status) === 'in_stock', + // Prices from latest snapshot (convert cents to dollars) + regular_price: p.rec_min_price_cents ? p.rec_min_price_cents / 100 : null, + regular_price_max: p.rec_max_price_cents ? p.rec_max_price_cents / 100 : null, + sale_price: p.rec_min_special_price_cents ? p.rec_min_special_price_cents / 100 : null, + med_price: p.med_min_price_cents ? p.med_min_price_cents / 100 : null, + med_price_max: p.med_max_price_cents ? p.med_max_price_cents / 100 : null, + med_sale_price: p.med_min_special_price_cents ? p.med_min_special_price_cents / 100 : null, + // Potency from products table + thc_percentage: p.thc_content, + cbd_percentage: p.cbd_content, + // Images from products table + image_url: p.primary_image_url, + // Other + options: p.options, + total_quantity: p.total_quantity_available, + // Timestamps + created_at: p.created_at, + updated_at: p.updated_at, + snapshot_at: p.snapshot_at, + })); + res.json({ + products: transformedProducts, + total: parseInt(countRows[0]?.total || '0', 10), + limit: parseInt(limit, 10), + offset: parseInt(offset, 10), + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/stores/:id/brands + * Get brands for a specific store + */ +router.get('/stores/:id/brands', async (req, res) => { + try { + const { id } = req.params; + const { rows: brands } = await (0, connection_1.query)(` + SELECT + brand_name as brand, + COUNT(*) as product_count + FROM dutchie_products + WHERE dispensary_id = $1 AND brand_name IS NOT NULL + GROUP BY brand_name + ORDER BY product_count DESC + `, [parseInt(id, 10)]); + res.json({ brands }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/stores/:id/categories + * Get categories for a specific store + */ +router.get('/stores/:id/categories', async (req, res) => { + try { + const { id } = req.params; + const { rows: categories } = await (0, connection_1.query)(` + SELECT + type, + subcategory, + COUNT(*) as product_count + FROM dutchie_products + WHERE dispensary_id = $1 AND type IS NOT NULL + GROUP BY type, subcategory + ORDER BY type, subcategory + `, [parseInt(id, 10)]); + res.json({ categories }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// PRODUCTS +// ============================================================ +/** + * GET /api/dutchie-az/products + * List products with filtering on our own DB + */ +router.get('/products', async (req, res) => { + try { + const { storeId, stockStatus, type, subcategory, brandName, search, limit = '50', offset = '0', } = req.query; + let whereClause = 'WHERE 1=1'; + const params = []; + let paramIndex = 1; + if (storeId) { + whereClause += ` AND dispensary_id = $${paramIndex}`; + params.push(parseInt(storeId, 10)); + paramIndex++; + } + if (stockStatus) { + whereClause += ` AND stock_status = $${paramIndex}`; + params.push(stockStatus); + paramIndex++; + } + if (type) { + whereClause += ` AND type = $${paramIndex}`; + params.push(type); + paramIndex++; + } + if (subcategory) { + whereClause += ` AND subcategory = $${paramIndex}`; + params.push(subcategory); + paramIndex++; + } + if (brandName) { + whereClause += ` AND brand_name ILIKE $${paramIndex}`; + params.push(`%${brandName}%`); + paramIndex++; + } + if (search) { + whereClause += ` AND (name ILIKE $${paramIndex} OR brand_name ILIKE $${paramIndex})`; + params.push(`%${search}%`); + paramIndex++; + } + params.push(parseInt(limit, 10), parseInt(offset, 10)); + const { rows } = await (0, connection_1.query)(` + SELECT + p.*, + d.name as store_name, + d.city as store_city + FROM dutchie_products p + JOIN dispensaries d ON p.dispensary_id = d.id + ${whereClause} + ORDER BY p.updated_at DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1} + `, params); + // Get total count + const { rows: countRows } = await (0, connection_1.query)(`SELECT COUNT(*) as total FROM dutchie_products ${whereClause}`, params.slice(0, -2)); + res.json({ + products: rows, + total: parseInt(countRows[0]?.total || '0', 10), + limit: parseInt(limit, 10), + offset: parseInt(offset, 10), + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/products/:id + * Get a single product with its latest snapshot + */ +router.get('/products/:id', async (req, res) => { + try { + const { id } = req.params; + const { rows: productRows } = await (0, connection_1.query)(` + SELECT + p.*, + d.name as store_name, + d.city as store_city, + d.slug as store_slug + FROM dutchie_products p + JOIN dispensaries d ON p.dispensary_id = d.id + WHERE p.id = $1 + `, [id]); + if (productRows.length === 0) { + return res.status(404).json({ error: 'Product not found' }); + } + // Get latest snapshot + const { rows: snapshotRows } = await (0, connection_1.query)(` + SELECT * FROM dutchie_product_snapshots + WHERE dutchie_product_id = $1 + ORDER BY crawled_at DESC + LIMIT 1 + `, [id]); + res.json({ + product: productRows[0], + latestSnapshot: snapshotRows[0] || null, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/products/:id/snapshots + * Get snapshot history for a product + */ +router.get('/products/:id/snapshots', async (req, res) => { + try { + const { id } = req.params; + const { limit = '50', offset = '0' } = req.query; + const { rows } = await (0, connection_1.query)(` + SELECT * FROM dutchie_product_snapshots + WHERE dutchie_product_id = $1 + ORDER BY crawled_at DESC + LIMIT $2 OFFSET $3 + `, [id, parseInt(limit, 10), parseInt(offset, 10)]); + res.json({ snapshots: rows }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// CATEGORIES +// ============================================================ +/** + * GET /api/dutchie-az/categories + * Get all categories with counts + */ +router.get('/categories', async (_req, res) => { + try { + const { rows } = await (0, connection_1.query)(`SELECT * FROM v_categories`); + res.json({ categories: rows }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// BRANDS +// ============================================================ +/** + * GET /api/dutchie-az/brands + * Get all brands with counts + */ +router.get('/brands', async (req, res) => { + try { + const { limit = '100', offset = '0' } = req.query; + const { rows } = await (0, connection_1.query)(` + SELECT * FROM v_brands + LIMIT $1 OFFSET $2 + `, [parseInt(limit, 10), parseInt(offset, 10)]); + res.json({ brands: rows }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// ADMIN ACTIONS +// ============================================================ +/** + * POST /api/dutchie-az/admin/init-schema + * Initialize the database schema + */ +router.post('/admin/init-schema', async (_req, res) => { + try { + await (0, schema_1.ensureSchema)(); + res.json({ success: true, message: 'Schema initialized' }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/import-azdhs + * Import dispensaries from AZDHS (main database) + */ +router.post('/admin/import-azdhs', async (_req, res) => { + try { + const result = await (0, azdhs_import_1.importAZDHSDispensaries)(); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/resolve-platform-ids + * Resolve Dutchie platform IDs for all dispensaries + */ +router.post('/admin/resolve-platform-ids', async (_req, res) => { + try { + const result = await (0, discovery_1.resolvePlatformDispensaryIds)(); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/crawl-store/:id + * Crawl a single store + */ +router.post('/admin/crawl-store/:id', async (req, res) => { + try { + const { id } = req.params; + const { pricingType = 'rec', useBothModes = true } = req.body; + const dispensary = await (0, discovery_1.getDispensaryById)(parseInt(id, 10)); + if (!dispensary) { + return res.status(404).json({ error: 'Store not found' }); + } + const result = await (0, product_crawler_1.crawlDispensaryProducts)(dispensary, pricingType, { useBothModes }); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/stats + * Get import and crawl statistics + */ +router.get('/admin/stats', async (_req, res) => { + try { + const importStats = await (0, azdhs_import_1.getImportStats)(); + // Get stock status distribution + const { rows: stockStats } = await (0, connection_1.query)(` + SELECT + stock_status, + COUNT(*) as count + FROM dutchie_products + GROUP BY stock_status + `); + // Get recent crawl jobs + const { rows: recentJobs } = await (0, connection_1.query)(` + SELECT * FROM dispensary_crawl_jobs + ORDER BY created_at DESC + LIMIT 10 + `); + res.json({ + import: importStats, + stockDistribution: stockStats, + recentJobs, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// SCHEDULER ADMIN +// ============================================================ +/** + * GET /api/dutchie-az/admin/scheduler/status + * Get scheduler status + */ +router.get('/admin/scheduler/status', async (_req, res) => { + try { + const status = (0, scheduler_1.getSchedulerStatus)(); + res.json(status); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/scheduler/start + * Start the scheduler + */ +router.post('/admin/scheduler/start', async (_req, res) => { + try { + (0, scheduler_1.startScheduler)(); + res.json({ success: true, message: 'Scheduler started' }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/scheduler/stop + * Stop the scheduler + */ +router.post('/admin/scheduler/stop', async (_req, res) => { + try { + (0, scheduler_1.stopScheduler)(); + res.json({ success: true, message: 'Scheduler stopped' }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/scheduler/trigger + * Trigger an immediate crawl cycle + */ +router.post('/admin/scheduler/trigger', async (_req, res) => { + try { + const result = await (0, scheduler_1.triggerImmediateCrawl)(); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/crawl/:id + * Crawl a single dispensary with job tracking + */ +router.post('/admin/crawl/:id', async (req, res) => { + try { + const { id } = req.params; + const { pricingType = 'rec', useBothModes = true } = req.body; + // Fetch the dispensary first + const dispensary = await (0, discovery_1.getDispensaryById)(parseInt(id, 10)); + if (!dispensary) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const result = await (0, scheduler_1.crawlSingleDispensary)(dispensary, pricingType, { useBothModes }); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/jobs + * Get crawl job history + */ +router.get('/admin/jobs', async (req, res) => { + try { + const { status, dispensaryId, limit = '50', offset = '0' } = req.query; + let whereClause = 'WHERE 1=1'; + const params = []; + let paramIndex = 1; + if (status) { + whereClause += ` AND status = $${paramIndex}`; + params.push(status); + paramIndex++; + } + if (dispensaryId) { + whereClause += ` AND dispensary_id = $${paramIndex}`; + params.push(parseInt(dispensaryId, 10)); + paramIndex++; + } + params.push(parseInt(limit, 10), parseInt(offset, 10)); + const { rows } = await (0, connection_1.query)(` + SELECT + cj.*, + d.name as dispensary_name, + d.slug as dispensary_slug + FROM dispensary_crawl_jobs cj + LEFT JOIN dispensaries d ON cj.dispensary_id = d.id + ${whereClause} + ORDER BY cj.created_at DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1} + `, params); + const { rows: countRows } = await (0, connection_1.query)(`SELECT COUNT(*) as total FROM dispensary_crawl_jobs ${whereClause}`, params.slice(0, -2)); + res.json({ + jobs: rows, + total: parseInt(countRows[0]?.total || '0', 10), + limit: parseInt(limit, 10), + offset: parseInt(offset, 10), + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// SCHEDULES (CONFIG CRUD) +// ============================================================ +/** + * GET /api/dutchie-az/admin/schedules + * Get all schedule configurations + */ +router.get('/admin/schedules', async (_req, res) => { + try { + const schedules = await (0, scheduler_1.getAllSchedules)(); + res.json({ schedules }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/schedules/:id + * Get a single schedule by ID + */ +router.get('/admin/schedules/:id', async (req, res) => { + try { + const { id } = req.params; + const schedule = await (0, scheduler_1.getScheduleById)(parseInt(id, 10)); + if (!schedule) { + return res.status(404).json({ error: 'Schedule not found' }); + } + res.json(schedule); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/schedules + * Create a new schedule + */ +router.post('/admin/schedules', async (req, res) => { + try { + const { jobName, description, enabled = true, baseIntervalMinutes, jitterMinutes, jobConfig, startImmediately = false, } = req.body; + if (!jobName || typeof baseIntervalMinutes !== 'number' || typeof jitterMinutes !== 'number') { + return res.status(400).json({ + error: 'jobName, baseIntervalMinutes, and jitterMinutes are required', + }); + } + const schedule = await (0, scheduler_1.createSchedule)({ + jobName, + description, + enabled, + baseIntervalMinutes, + jitterMinutes, + jobConfig, + startImmediately, + }); + res.status(201).json(schedule); + } + catch (error) { + // Handle unique constraint violation + if (error.code === '23505') { + return res.status(409).json({ error: `Schedule "${req.body.jobName}" already exists` }); + } + res.status(500).json({ error: error.message }); + } +}); +/** + * PUT /api/dutchie-az/admin/schedules/:id + * Update a schedule + */ +router.put('/admin/schedules/:id', async (req, res) => { + try { + const { id } = req.params; + const { description, enabled, baseIntervalMinutes, jitterMinutes, jobConfig } = req.body; + const schedule = await (0, scheduler_1.updateSchedule)(parseInt(id, 10), { + description, + enabled, + baseIntervalMinutes, + jitterMinutes, + jobConfig, + }); + if (!schedule) { + return res.status(404).json({ error: 'Schedule not found' }); + } + res.json(schedule); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * DELETE /api/dutchie-az/admin/schedules/:id + * Delete a schedule + */ +router.delete('/admin/schedules/:id', async (req, res) => { + try { + const { id } = req.params; + const deleted = await (0, scheduler_1.deleteSchedule)(parseInt(id, 10)); + if (!deleted) { + return res.status(404).json({ error: 'Schedule not found' }); + } + res.json({ success: true, message: 'Schedule deleted' }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/schedules/:id/trigger + * Trigger immediate execution of a schedule + */ +router.post('/admin/schedules/:id/trigger', async (req, res) => { + try { + const { id } = req.params; + const result = await (0, scheduler_1.triggerScheduleNow)(parseInt(id, 10)); + if (!result.success) { + return res.status(400).json({ error: result.message }); + } + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/schedules/init + * Initialize default schedules if they don't exist + */ +router.post('/admin/schedules/init', async (_req, res) => { + try { + await (0, scheduler_1.initializeDefaultSchedules)(); + const schedules = await (0, scheduler_1.getAllSchedules)(); + res.json({ success: true, schedules }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/schedules/:id/logs + * Get run logs for a specific schedule + */ +router.get('/admin/schedules/:id/logs', async (req, res) => { + try { + const { id } = req.params; + const { limit = '50', offset = '0' } = req.query; + const result = await (0, scheduler_1.getRunLogs)({ + scheduleId: parseInt(id, 10), + limit: parseInt(limit, 10), + offset: parseInt(offset, 10), + }); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/run-logs + * Get all run logs with filtering + */ +router.get('/admin/run-logs', async (req, res) => { + try { + const { scheduleId, jobName, limit = '50', offset = '0' } = req.query; + const result = await (0, scheduler_1.getRunLogs)({ + scheduleId: scheduleId ? parseInt(scheduleId, 10) : undefined, + jobName: jobName, + limit: parseInt(limit, 10), + offset: parseInt(offset, 10), + }); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// DEBUG ROUTES +// ============================================================ +/** + * GET /api/dutchie-az/debug/summary + * Get overall system summary for debugging + */ +router.get('/debug/summary', async (_req, res) => { + try { + // Get table counts + const { rows: tableCounts } = await (0, connection_1.query)(` + SELECT + (SELECT COUNT(*) FROM dispensaries) as dispensary_count, + (SELECT COUNT(*) FROM dispensaries WHERE platform_dispensary_id IS NOT NULL) as dispensaries_with_platform_id, + (SELECT COUNT(*) FROM dutchie_products) as product_count, + (SELECT COUNT(*) FROM dutchie_product_snapshots) as snapshot_count, + (SELECT COUNT(*) FROM dispensary_crawl_jobs) as job_count, + (SELECT COUNT(*) FROM dispensary_crawl_jobs WHERE status = 'completed') as completed_jobs, + (SELECT COUNT(*) FROM dispensary_crawl_jobs WHERE status = 'failed') as failed_jobs + `); + // Get stock status distribution + const { rows: stockDistribution } = await (0, connection_1.query)(` + SELECT + stock_status, + COUNT(*) as count + FROM dutchie_products + GROUP BY stock_status + ORDER BY count DESC + `); + // Get products by dispensary + const { rows: productsByDispensary } = await (0, connection_1.query)(` + SELECT + d.id, + d.name, + d.slug, + d.platform_dispensary_id, + COUNT(p.id) as product_count, + MAX(p.updated_at) as last_product_update + FROM dispensaries d + LEFT JOIN dutchie_products p ON d.id = p.dispensary_id + WHERE d.state = 'AZ' + GROUP BY d.id, d.name, d.slug, d.platform_dispensary_id + ORDER BY product_count DESC + LIMIT 20 + `); + // Get recent snapshots + const { rows: recentSnapshots } = await (0, connection_1.query)(` + SELECT + s.id, + s.dutchie_product_id, + p.name as product_name, + d.name as dispensary_name, + s.crawled_at + FROM dutchie_product_snapshots s + JOIN dutchie_products p ON s.dutchie_product_id = p.id + JOIN dispensaries d ON p.dispensary_id = d.id + ORDER BY s.crawled_at DESC + LIMIT 10 + `); + res.json({ + tableCounts: tableCounts[0], + stockDistribution, + productsByDispensary, + recentSnapshots, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/debug/store/:id + * Get detailed debug info for a specific store + */ +router.get('/debug/store/:id', async (req, res) => { + try { + const { id } = req.params; + // Get dispensary info + const { rows: dispensaryRows } = await (0, connection_1.query)(`SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE id = $1`, [parseInt(id, 10)]); + if (dispensaryRows.length === 0) { + return res.status(404).json({ error: 'Store not found' }); + } + const dispensary = dispensaryRows[0]; + // Get product stats + const { rows: productStats } = await (0, connection_1.query)(` + SELECT + COUNT(*) as total_products, + COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock, + COUNT(*) FILTER (WHERE stock_status = 'out_of_stock') as out_of_stock, + COUNT(*) FILTER (WHERE stock_status = 'unknown') as unknown, + COUNT(*) FILTER (WHERE stock_status = 'missing_from_feed') as missing_from_feed, + MIN(first_seen_at) as earliest_product, + MAX(last_seen_at) as latest_product, + MAX(updated_at) as last_update + FROM dutchie_products + WHERE dispensary_id = $1 + `, [id]); + // Get snapshot stats + const { rows: snapshotStats } = await (0, connection_1.query)(` + SELECT + COUNT(*) as total_snapshots, + MIN(crawled_at) as earliest_snapshot, + MAX(crawled_at) as latest_snapshot, + COUNT(DISTINCT dutchie_product_id) as products_with_snapshots + FROM dutchie_product_snapshots s + JOIN dutchie_products p ON s.dutchie_product_id = p.id + WHERE p.dispensary_id = $1 + `, [id]); + // Get crawl job history + const { rows: recentJobs } = await (0, connection_1.query)(` + SELECT + id, + status, + started_at, + completed_at, + products_found, + products_new, + products_updated, + error_message, + created_at + FROM dispensary_crawl_jobs + WHERE dispensary_id = $1 + ORDER BY created_at DESC + LIMIT 10 + `, [id]); + // Get sample products (5 in-stock, 5 out-of-stock) + const { rows: sampleInStock } = await (0, connection_1.query)(` + SELECT + p.id, + p.name, + p.brand_name, + p.type, + p.stock_status, + p.updated_at + FROM dutchie_products p + WHERE p.dispensary_id = $1 AND p.stock_status = 'in_stock' + ORDER BY p.updated_at DESC + LIMIT 5 + `, [id]); + const { rows: sampleOutOfStock } = await (0, connection_1.query)(` + SELECT + p.id, + p.name, + p.brand_name, + p.type, + p.stock_status, + p.updated_at + FROM dutchie_products p + WHERE p.dispensary_id = $1 AND p.stock_status = 'out_of_stock' + ORDER BY p.updated_at DESC + LIMIT 5 + `, [id]); + // Get categories breakdown + const { rows: categories } = await (0, connection_1.query)(` + SELECT + type, + subcategory, + COUNT(*) as count + FROM dutchie_products + WHERE dispensary_id = $1 + GROUP BY type, subcategory + ORDER BY count DESC + `, [id]); + res.json({ + dispensary, + productStats: productStats[0], + snapshotStats: snapshotStats[0], + recentJobs, + sampleProducts: { + inStock: sampleInStock, + outOfStock: sampleOutOfStock, + }, + categories, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// LIVE CRAWLER STATUS ROUTES +// ============================================================ +const job_queue_1 = require("../services/job-queue"); +/** + * GET /api/dutchie-az/monitor/active-jobs + * Get all currently running jobs with real-time status including worker info + */ +router.get('/monitor/active-jobs', async (_req, res) => { + try { + // Get running jobs from job_run_logs (scheduled jobs like "enqueue all") + const { rows: runningScheduledJobs } = await (0, connection_1.query)(` + SELECT + jrl.id, + jrl.schedule_id, + jrl.job_name, + jrl.status, + jrl.started_at, + jrl.items_processed, + jrl.items_succeeded, + jrl.items_failed, + jrl.metadata, + jrl.worker_id, + jrl.worker_hostname, + js.description as job_description, + EXTRACT(EPOCH FROM (NOW() - jrl.started_at)) as duration_seconds + FROM job_run_logs jrl + LEFT JOIN job_schedules js ON jrl.schedule_id = js.id + WHERE jrl.status = 'running' + ORDER BY jrl.started_at DESC + `); + // Get running crawl jobs (individual store crawls with worker info) + const { rows: runningCrawlJobs } = await (0, connection_1.query)(` + SELECT + cj.id, + cj.job_type, + cj.dispensary_id, + d.name as dispensary_name, + d.city, + d.platform_dispensary_id, + cj.status, + cj.started_at, + cj.claimed_by as worker_id, + cj.worker_hostname, + cj.claimed_at, + cj.products_found, + cj.products_upserted, + cj.snapshots_created, + cj.current_page, + cj.total_pages, + cj.last_heartbeat_at, + cj.retry_count, + cj.metadata, + EXTRACT(EPOCH FROM (NOW() - cj.started_at)) as duration_seconds + FROM dispensary_crawl_jobs cj + LEFT JOIN dispensaries d ON cj.dispensary_id = d.id + WHERE cj.status = 'running' + ORDER BY cj.started_at DESC + `); + // Get queue stats + const queueStats = await (0, job_queue_1.getQueueStats)(); + // Get active workers + const activeWorkers = await (0, job_queue_1.getActiveWorkers)(); + // Also get in-memory scrapers if any (from the legacy system) + let inMemoryScrapers = []; + try { + const { activeScrapers } = await Promise.resolve().then(() => __importStar(require('../../routes/scraper-monitor'))); + inMemoryScrapers = Array.from(activeScrapers.values()).map(scraper => ({ + ...scraper, + source: 'in_memory', + duration_seconds: (Date.now() - scraper.startTime.getTime()) / 1000, + })); + } + catch { + // Legacy scraper monitor not available + } + res.json({ + scheduledJobs: runningScheduledJobs, + crawlJobs: runningCrawlJobs, + inMemoryScrapers, + activeWorkers, + queueStats, + totalActive: runningScheduledJobs.length + runningCrawlJobs.length + inMemoryScrapers.length, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/monitor/recent-jobs + * Get recent completed jobs + */ +router.get('/monitor/recent-jobs', async (req, res) => { + try { + const { limit = '50' } = req.query; + const limitNum = Math.min(parseInt(limit, 10), 200); + // Recent job run logs + const { rows: recentJobLogs } = await (0, connection_1.query)(` + SELECT + jrl.id, + jrl.schedule_id, + jrl.job_name, + jrl.status, + jrl.started_at, + jrl.completed_at, + jrl.duration_ms, + jrl.error_message, + jrl.items_processed, + jrl.items_succeeded, + jrl.items_failed, + jrl.metadata, + js.description as job_description + FROM job_run_logs jrl + LEFT JOIN job_schedules js ON jrl.schedule_id = js.id + ORDER BY jrl.created_at DESC + LIMIT $1 + `, [limitNum]); + // Recent crawl jobs + const { rows: recentCrawlJobs } = await (0, connection_1.query)(` + SELECT + cj.id, + cj.job_type, + cj.dispensary_id, + d.name as dispensary_name, + d.city, + cj.status, + cj.started_at, + cj.completed_at, + cj.error_message, + cj.products_found, + cj.snapshots_created, + cj.metadata, + EXTRACT(EPOCH FROM (COALESCE(cj.completed_at, NOW()) - cj.started_at)) * 1000 as duration_ms + FROM dispensary_crawl_jobs cj + LEFT JOIN dispensaries d ON cj.dispensary_id = d.id + ORDER BY cj.created_at DESC + LIMIT $1 + `, [limitNum]); + res.json({ + jobLogs: recentJobLogs, + crawlJobs: recentCrawlJobs, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/monitor/errors + * Get recent job errors + */ +router.get('/monitor/errors', async (req, res) => { + try { + const { limit = '20', hours = '24' } = req.query; + const limitNum = Math.min(parseInt(limit, 10), 100); + const hoursNum = Math.min(parseInt(hours, 10), 168); + // Errors from job_run_logs + const { rows: jobErrors } = await (0, connection_1.query)(` + SELECT + 'job_run_log' as source, + jrl.id, + jrl.job_name, + jrl.status, + jrl.started_at, + jrl.completed_at, + jrl.error_message, + jrl.items_processed, + jrl.items_failed, + jrl.metadata + FROM job_run_logs jrl + WHERE jrl.status IN ('error', 'partial') + AND jrl.created_at > NOW() - INTERVAL '${hoursNum} hours' + ORDER BY jrl.created_at DESC + LIMIT $1 + `, [limitNum]); + // Errors from dispensary_crawl_jobs + const { rows: crawlErrors } = await (0, connection_1.query)(` + SELECT + 'crawl_job' as source, + cj.id, + cj.job_type as job_name, + d.name as dispensary_name, + cj.status, + cj.started_at, + cj.completed_at, + cj.error_message, + cj.products_found as items_processed, + cj.metadata + FROM dispensary_crawl_jobs cj + LEFT JOIN dispensaries d ON cj.dispensary_id = d.id + WHERE cj.status = 'failed' + AND cj.created_at > NOW() - INTERVAL '${hoursNum} hours' + ORDER BY cj.created_at DESC + LIMIT $1 + `, [limitNum]); + res.json({ + errors: [...jobErrors, ...crawlErrors].sort((a, b) => new Date(b.started_at || b.created_at).getTime() - + new Date(a.started_at || a.created_at).getTime()).slice(0, limitNum), + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/monitor/summary + * Get overall monitoring summary + */ +router.get('/monitor/summary', async (_req, res) => { + try { + const { rows: stats } = await (0, connection_1.query)(` + SELECT + (SELECT COUNT(*) FROM job_run_logs WHERE status = 'running') as running_scheduled_jobs, + (SELECT COUNT(*) FROM dispensary_crawl_jobs WHERE status = 'running') as running_dispensary_crawl_jobs, + (SELECT COUNT(*) FROM job_run_logs WHERE status = 'success' AND created_at > NOW() - INTERVAL '24 hours') as successful_jobs_24h, + (SELECT COUNT(*) FROM job_run_logs WHERE status IN ('error', 'partial') AND created_at > NOW() - INTERVAL '24 hours') as failed_jobs_24h, + (SELECT COUNT(*) FROM dispensary_crawl_jobs WHERE status = 'completed' AND created_at > NOW() - INTERVAL '24 hours') as successful_crawls_24h, + (SELECT COUNT(*) FROM dispensary_crawl_jobs WHERE status = 'failed' AND created_at > NOW() - INTERVAL '24 hours') as failed_crawls_24h, + (SELECT SUM(products_found) FROM dispensary_crawl_jobs WHERE status = 'completed' AND created_at > NOW() - INTERVAL '24 hours') as products_found_24h, + (SELECT SUM(snapshots_created) FROM dispensary_crawl_jobs WHERE status = 'completed' AND created_at > NOW() - INTERVAL '24 hours') as snapshots_created_24h, + (SELECT MAX(started_at) FROM job_run_logs) as last_job_started, + (SELECT MAX(completed_at) FROM job_run_logs WHERE status = 'success') as last_job_completed + `); + // Get next scheduled runs + const { rows: nextRuns } = await (0, connection_1.query)(` + SELECT + id, + job_name, + description, + enabled, + next_run_at, + last_status, + last_run_at + FROM job_schedules + WHERE enabled = true AND next_run_at IS NOT NULL + ORDER BY next_run_at ASC + LIMIT 5 + `); + res.json({ + ...(stats[0] || {}), + nextRuns, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// MENU DETECTION ROUTES +// ============================================================ +const menu_detection_1 = require("../services/menu-detection"); +/** + * GET /api/dutchie-az/admin/detection/stats + * Get menu detection statistics + */ +router.get('/admin/detection/stats', async (_req, res) => { + try { + const stats = await (0, menu_detection_1.getDetectionStats)(); + res.json(stats); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/detection/pending + * Get dispensaries that need menu detection + */ +router.get('/admin/detection/pending', async (req, res) => { + try { + const { state = 'AZ', limit = '100' } = req.query; + const dispensaries = await (0, menu_detection_1.getDispensariesNeedingDetection)({ + state: state, + limit: parseInt(limit, 10), + }); + res.json({ dispensaries, total: dispensaries.length }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/detection/detect/:id + * Detect menu provider and resolve platform ID for a single dispensary + */ +router.post('/admin/detection/detect/:id', async (req, res) => { + try { + const { id } = req.params; + const result = await (0, menu_detection_1.detectAndResolveDispensary)(parseInt(id, 10)); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/detection/detect-all + * Run bulk menu detection on all dispensaries needing it + */ +router.post('/admin/detection/detect-all', async (req, res) => { + try { + const { state = 'AZ', onlyUnknown = true, onlyMissingPlatformId = false, limit } = req.body; + const result = await (0, menu_detection_1.runBulkDetection)({ + state, + onlyUnknown, + onlyMissingPlatformId, + limit, + }); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/detection/trigger + * Trigger the menu detection scheduled job immediately + */ +router.post('/admin/detection/trigger', async (_req, res) => { + try { + // Find the menu detection schedule and trigger it + const schedules = await (0, scheduler_1.getAllSchedules)(); + const menuDetection = schedules.find(s => s.jobName === 'dutchie_az_menu_detection'); + if (!menuDetection) { + return res.status(404).json({ error: 'Menu detection schedule not found. Run /admin/schedules/init first.' }); + } + const result = await (0, scheduler_1.triggerScheduleNow)(menuDetection.id); + res.json(result); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ============================================================ +// FAILED DISPENSARIES ROUTES +// ============================================================ +/** + * GET /api/dutchie-az/admin/dispensaries/failed + * Get all dispensaries flagged as failed (for admin review) + */ +router.get('/admin/dispensaries/failed', async (_req, res) => { + try { + const { rows } = await (0, connection_1.query)(` + SELECT + id, + name, + city, + state, + menu_url, + menu_type, + platform_dispensary_id, + consecutive_failures, + last_failure_at, + last_failure_reason, + failed_at, + failure_notes, + last_crawl_at, + updated_at + FROM dispensaries + WHERE failed_at IS NOT NULL + ORDER BY failed_at DESC + `); + res.json({ + failed: rows, + total: rows.length, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/dispensaries/at-risk + * Get dispensaries with high failure counts (but not yet flagged as failed) + */ +router.get('/admin/dispensaries/at-risk', async (_req, res) => { + try { + const { rows } = await (0, connection_1.query)(` + SELECT + id, + name, + city, + state, + menu_url, + menu_type, + consecutive_failures, + last_failure_at, + last_failure_reason, + last_crawl_at + FROM dispensaries + WHERE consecutive_failures >= 1 + AND failed_at IS NULL + ORDER BY consecutive_failures DESC, last_failure_at DESC + `); + res.json({ + atRisk: rows, + total: rows.length, + }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/dispensaries/:id/unfail + * Restore a failed dispensary - clears failed status and resets for re-detection + */ +router.post('/admin/dispensaries/:id/unfail', async (req, res) => { + try { + const { id } = req.params; + await (0, connection_1.query)(` + UPDATE dispensaries + SET failed_at = NULL, + consecutive_failures = 0, + last_failure_at = NULL, + last_failure_reason = NULL, + failure_notes = NULL, + menu_type = NULL, + platform_dispensary_id = NULL, + updated_at = NOW() + WHERE id = $1 + `, [id]); + res.json({ success: true, message: `Dispensary ${id} restored for re-detection` }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/dutchie-az/admin/dispensaries/:id/reset-failures + * Reset failure counter for a dispensary (without unflagging) + */ +router.post('/admin/dispensaries/:id/reset-failures', async (req, res) => { + try { + const { id } = req.params; + await (0, connection_1.query)(` + UPDATE dispensaries + SET consecutive_failures = 0, + last_failure_at = NULL, + last_failure_reason = NULL, + updated_at = NOW() + WHERE id = $1 + `, [id]); + res.json({ success: true, message: `Failure counter reset for dispensary ${id}` }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/dutchie-az/admin/dispensaries/health-summary + * Get a summary of dispensary health status + */ +router.get('/admin/dispensaries/health-summary', async (_req, res) => { + try { + const { rows } = await (0, connection_1.query)(` + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE state = 'AZ') as arizona_total, + COUNT(*) FILTER (WHERE failed_at IS NOT NULL) as failed, + COUNT(*) FILTER (WHERE consecutive_failures >= 1 AND failed_at IS NULL) as at_risk, + COUNT(*) FILTER (WHERE menu_type = 'dutchie' AND platform_dispensary_id IS NOT NULL AND failed_at IS NULL) as ready_to_crawl, + COUNT(*) FILTER (WHERE menu_type = 'dutchie' AND failed_at IS NULL) as dutchie_detected, + COUNT(*) FILTER (WHERE (menu_type IS NULL OR menu_type = 'unknown') AND failed_at IS NULL) as needs_detection, + COUNT(*) FILTER (WHERE menu_type NOT IN ('dutchie', 'unknown') AND menu_type IS NOT NULL AND failed_at IS NULL) as non_dutchie + FROM dispensaries + WHERE state = 'AZ' + `); + res.json(rows[0] || {}); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); +exports.default = router; diff --git a/backend/dist/dutchie-az/services/azdhs-import.js b/backend/dist/dutchie-az/services/azdhs-import.js new file mode 100644 index 00000000..bad6cdcf --- /dev/null +++ b/backend/dist/dutchie-az/services/azdhs-import.js @@ -0,0 +1,229 @@ +"use strict"; +/** + * AZDHS Import Service + * + * Imports Arizona dispensaries from the main database's dispensaries table + * (which was populated from AZDHS data) into the isolated Dutchie AZ database. + * + * This establishes the canonical list of AZ dispensaries to match against Dutchie. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.importAZDHSDispensaries = importAZDHSDispensaries; +exports.importFromJSON = importFromJSON; +exports.getImportStats = getImportStats; +const pg_1 = require("pg"); +const connection_1 = require("../db/connection"); +// Main database connection (source of AZDHS data) +const MAIN_DATABASE_URL = process.env.DATABASE_URL || + 'postgresql://dutchie:dutchie_local_pass@localhost:54320/dutchie_menus'; +/** + * Create a temporary connection to the main database + */ +function getMainDBPool() { + return new pg_1.Pool({ + connectionString: MAIN_DATABASE_URL, + max: 5, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + }); +} +/** + * Fetch all AZ dispensaries from the main database + */ +async function fetchAZDHSDispensaries() { + const pool = getMainDBPool(); + try { + const result = await pool.query(` + SELECT + id, azdhs_id, name, company_name, address, city, state, zip, + latitude, longitude, dba_name, phone, email, website, + google_rating, google_review_count, slug, + menu_provider, product_provider, + created_at, updated_at + FROM dispensaries + WHERE state = 'AZ' + ORDER BY id + `); + return result.rows; + } + finally { + await pool.end(); + } +} +/** + * Import a single dispensary into the Dutchie AZ database + */ +async function importDispensary(disp) { + const result = await (0, connection_1.query)(` + INSERT INTO dispensaries ( + platform, name, slug, city, state, postal_code, address, + latitude, longitude, is_delivery, is_pickup, raw_metadata, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, $11, $12, NOW() + ) + ON CONFLICT (platform, slug, city, state) DO UPDATE SET + name = EXCLUDED.name, + postal_code = EXCLUDED.postal_code, + address = EXCLUDED.address, + latitude = EXCLUDED.latitude, + longitude = EXCLUDED.longitude, + raw_metadata = EXCLUDED.raw_metadata, + updated_at = NOW() + RETURNING id + `, [ + 'dutchie', // Will be updated when Dutchie match is found + disp.dba_name || disp.name, + disp.slug, + disp.city, + disp.state, + disp.zip, + disp.address, + disp.latitude, + disp.longitude, + false, // is_delivery - unknown + true, // is_pickup - assume true + JSON.stringify({ + azdhs_id: disp.azdhs_id, + main_db_id: disp.id, + company_name: disp.company_name, + phone: disp.phone, + email: disp.email, + website: disp.website, + google_rating: disp.google_rating, + google_review_count: disp.google_review_count, + menu_provider: disp.menu_provider, + product_provider: disp.product_provider, + }), + ]); + return result.rows[0].id; +} +/** + * Import all AZDHS dispensaries into the Dutchie AZ database + */ +async function importAZDHSDispensaries() { + console.log('[AZDHS Import] Starting import from main database...'); + const result = { + total: 0, + imported: 0, + skipped: 0, + errors: [], + }; + try { + const dispensaries = await fetchAZDHSDispensaries(); + result.total = dispensaries.length; + console.log(`[AZDHS Import] Found ${dispensaries.length} AZ dispensaries in main DB`); + for (const disp of dispensaries) { + try { + const id = await importDispensary(disp); + result.imported++; + console.log(`[AZDHS Import] Imported: ${disp.name} (${disp.city}) -> id=${id}`); + } + catch (error) { + if (error.message.includes('duplicate')) { + result.skipped++; + } + else { + result.errors.push(`${disp.name}: ${error.message}`); + } + } + } + } + catch (error) { + result.errors.push(`Failed to fetch from main DB: ${error.message}`); + } + console.log(`[AZDHS Import] Complete: ${result.imported} imported, ${result.skipped} skipped, ${result.errors.length} errors`); + return result; +} +/** + * Import dispensaries from JSON file (backup export) + */ +async function importFromJSON(jsonPath) { + console.log(`[AZDHS Import] Importing from JSON: ${jsonPath}`); + const result = { + total: 0, + imported: 0, + skipped: 0, + errors: [], + }; + try { + const fs = await Promise.resolve().then(() => __importStar(require('fs/promises'))); + const data = await fs.readFile(jsonPath, 'utf-8'); + const dispensaries = JSON.parse(data); + result.total = dispensaries.length; + console.log(`[AZDHS Import] Found ${dispensaries.length} dispensaries in JSON file`); + for (const disp of dispensaries) { + try { + const id = await importDispensary(disp); + result.imported++; + } + catch (error) { + if (error.message.includes('duplicate')) { + result.skipped++; + } + else { + result.errors.push(`${disp.name}: ${error.message}`); + } + } + } + } + catch (error) { + result.errors.push(`Failed to read JSON file: ${error.message}`); + } + console.log(`[AZDHS Import] Complete: ${result.imported} imported, ${result.skipped} skipped`); + return result; +} +/** + * Get import statistics + */ +async function getImportStats() { + const { rows } = await (0, connection_1.query)(` + SELECT + COUNT(*) as total, + COUNT(platform_dispensary_id) as with_platform_id, + COUNT(*) - COUNT(platform_dispensary_id) as without_platform_id, + MAX(updated_at) as last_updated + FROM dispensaries + WHERE state = 'AZ' + `); + const stats = rows[0]; + return { + totalDispensaries: parseInt(stats.total, 10), + withPlatformIds: parseInt(stats.with_platform_id, 10), + withoutPlatformIds: parseInt(stats.without_platform_id, 10), + lastImportedAt: stats.last_updated, + }; +} diff --git a/backend/dist/dutchie-az/services/directory-matcher.js b/backend/dist/dutchie-az/services/directory-matcher.js new file mode 100644 index 00000000..1ce11368 --- /dev/null +++ b/backend/dist/dutchie-az/services/directory-matcher.js @@ -0,0 +1,380 @@ +"use strict"; +/** + * Directory-Based Store Matcher + * + * Scrapes provider directory pages (Curaleaf, Sol, etc.) to get store lists, + * then matches them to existing dispensaries by fuzzy name/city/address matching. + * + * This allows us to: + * 1. Find specific store URLs for directory-style websites + * 2. Match stores confidently by name+city + * 3. Mark non-Dutchie providers as not_crawlable until we build crawlers + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.scrapeSolDirectory = scrapeSolDirectory; +exports.scrapeCuraleafDirectory = scrapeCuraleafDirectory; +exports.matchDirectoryToDispensaries = matchDirectoryToDispensaries; +exports.previewDirectoryMatches = previewDirectoryMatches; +exports.applyHighConfidenceMatches = applyHighConfidenceMatches; +const connection_1 = require("../db/connection"); +// ============================================================ +// NORMALIZATION FUNCTIONS +// ============================================================ +/** + * Normalize a string for comparison: + * - Lowercase + * - Remove common suffixes (dispensary, cannabis, etc.) + * - Remove punctuation + * - Collapse whitespace + */ +function normalizeForComparison(str) { + if (!str) + return ''; + return str + .toLowerCase() + .replace(/\s+(dispensary|cannabis|marijuana|medical|recreational|shop|store|flower|wellness)(\s|$)/gi, ' ') + .replace(/[^\w\s]/g, ' ') // Remove punctuation + .replace(/\s+/g, ' ') // Collapse whitespace + .trim(); +} +/** + * Normalize city name for comparison + */ +function normalizeCity(city) { + if (!city) + return ''; + return city + .toLowerCase() + .replace(/[^\w\s]/g, '') + .trim(); +} +/** + * Calculate similarity between two strings (0-1) + * Uses Levenshtein distance normalized by max length + */ +function stringSimilarity(a, b) { + if (!a || !b) + return 0; + if (a === b) + return 1; + const longer = a.length > b.length ? a : b; + const shorter = a.length > b.length ? b : a; + if (longer.length === 0) + return 1; + const distance = levenshteinDistance(longer, shorter); + return (longer.length - distance) / longer.length; +} +/** + * Levenshtein distance between two strings + */ +function levenshteinDistance(a, b) { + const matrix = []; + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } + else { + matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1 // deletion + ); + } + } + } + return matrix[b.length][a.length]; +} +/** + * Check if string contains another (with normalization) + */ +function containsNormalized(haystack, needle) { + return normalizeForComparison(haystack).includes(normalizeForComparison(needle)); +} +// ============================================================ +// PROVIDER DIRECTORY SCRAPERS +// ============================================================ +/** + * Sol Flower (livewithsol.com) - Static HTML, easy to scrape + */ +async function scrapeSolDirectory() { + console.log('[DirectoryMatcher] Scraping Sol Flower directory...'); + try { + const response = await fetch('https://www.livewithsol.com/locations/', { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: 'text/html', + }, + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const html = await response.text(); + // Extract store entries from HTML + // Sol's structure: Each location has name, address in specific divs + const stores = []; + // Pattern to find location cards + // Format: NAME with address nearby + const locationRegex = /]+href="(\/locations\/[^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?(\d+[^<]+(?:Ave|St|Blvd|Dr|Rd|Way)[^<]*)/gi; + let match; + while ((match = locationRegex.exec(html)) !== null) { + const [, path, name, address] = match; + // Extract city from common Arizona cities + let city = 'Unknown'; + const cityPatterns = [ + { pattern: /phoenix/i, city: 'Phoenix' }, + { pattern: /scottsdale/i, city: 'Scottsdale' }, + { pattern: /tempe/i, city: 'Tempe' }, + { pattern: /tucson/i, city: 'Tucson' }, + { pattern: /mesa/i, city: 'Mesa' }, + { pattern: /sun city/i, city: 'Sun City' }, + { pattern: /glendale/i, city: 'Glendale' }, + ]; + for (const { pattern, city: cityName } of cityPatterns) { + if (pattern.test(name) || pattern.test(address)) { + city = cityName; + break; + } + } + stores.push({ + name: name.trim(), + city, + state: 'AZ', + address: address.trim(), + storeUrl: `https://www.livewithsol.com${path}`, + }); + } + // If regex didn't work, use known hardcoded values (fallback) + if (stores.length === 0) { + console.log('[DirectoryMatcher] Using hardcoded Sol locations'); + return [ + { name: 'Sol Flower 32nd & Shea', city: 'Phoenix', state: 'AZ', address: '3217 E Shea Blvd Suite 1 A', storeUrl: 'https://www.livewithsol.com/locations/deer-valley/' }, + { name: 'Sol Flower Scottsdale Airpark', city: 'Scottsdale', state: 'AZ', address: '14980 N 78th Way Ste 204', storeUrl: 'https://www.livewithsol.com/locations/scottsdale-airpark/' }, + { name: 'Sol Flower Sun City', city: 'Sun City', state: 'AZ', address: '13650 N 99th Ave', storeUrl: 'https://www.livewithsol.com/locations/sun-city/' }, + { name: 'Sol Flower Tempe McClintock', city: 'Tempe', state: 'AZ', address: '1322 N McClintock Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-mcclintock/' }, + { name: 'Sol Flower Tempe University', city: 'Tempe', state: 'AZ', address: '2424 W University Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-university/' }, + { name: 'Sol Flower Foothills Tucson', city: 'Tucson', state: 'AZ', address: '6026 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/foothills-tucson/' }, + { name: 'Sol Flower South Tucson', city: 'Tucson', state: 'AZ', address: '3000 W Valencia Rd Ste 210', storeUrl: 'https://www.livewithsol.com/locations/south-tucson/' }, + { name: 'Sol Flower North Tucson', city: 'Tucson', state: 'AZ', address: '4837 N 1st Ave', storeUrl: 'https://www.livewithsol.com/locations/north-tucson/' }, + { name: 'Sol Flower Casas Adobes', city: 'Tucson', state: 'AZ', address: '6437 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/casas-adobes/' }, + ]; + } + console.log(`[DirectoryMatcher] Found ${stores.length} Sol Flower locations`); + return stores; + } + catch (error) { + console.error('[DirectoryMatcher] Error scraping Sol directory:', error.message); + // Return hardcoded fallback + return [ + { name: 'Sol Flower 32nd & Shea', city: 'Phoenix', state: 'AZ', address: '3217 E Shea Blvd Suite 1 A', storeUrl: 'https://www.livewithsol.com/locations/deer-valley/' }, + { name: 'Sol Flower Scottsdale Airpark', city: 'Scottsdale', state: 'AZ', address: '14980 N 78th Way Ste 204', storeUrl: 'https://www.livewithsol.com/locations/scottsdale-airpark/' }, + { name: 'Sol Flower Sun City', city: 'Sun City', state: 'AZ', address: '13650 N 99th Ave', storeUrl: 'https://www.livewithsol.com/locations/sun-city/' }, + { name: 'Sol Flower Tempe McClintock', city: 'Tempe', state: 'AZ', address: '1322 N McClintock Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-mcclintock/' }, + { name: 'Sol Flower Tempe University', city: 'Tempe', state: 'AZ', address: '2424 W University Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-university/' }, + { name: 'Sol Flower Foothills Tucson', city: 'Tucson', state: 'AZ', address: '6026 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/foothills-tucson/' }, + { name: 'Sol Flower South Tucson', city: 'Tucson', state: 'AZ', address: '3000 W Valencia Rd Ste 210', storeUrl: 'https://www.livewithsol.com/locations/south-tucson/' }, + { name: 'Sol Flower North Tucson', city: 'Tucson', state: 'AZ', address: '4837 N 1st Ave', storeUrl: 'https://www.livewithsol.com/locations/north-tucson/' }, + { name: 'Sol Flower Casas Adobes', city: 'Tucson', state: 'AZ', address: '6437 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/casas-adobes/' }, + ]; + } +} +/** + * Curaleaf - Has age-gate, so we need hardcoded AZ locations + * In production, this would use Playwright to bypass age-gate + */ +async function scrapeCuraleafDirectory() { + console.log('[DirectoryMatcher] Using hardcoded Curaleaf AZ locations (age-gate blocks simple fetch)...'); + // Hardcoded Arizona Curaleaf locations from public knowledge + // These would be scraped via Playwright in production + return [ + { name: 'Curaleaf Phoenix Camelback', city: 'Phoenix', state: 'AZ', address: '4811 E Camelback Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-phoenix-camelback' }, + { name: 'Curaleaf Phoenix Midtown', city: 'Phoenix', state: 'AZ', address: '1928 E Highland Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-phoenix-midtown' }, + { name: 'Curaleaf Glendale East', city: 'Glendale', state: 'AZ', address: '5150 W Glendale Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-glendale-east' }, + { name: 'Curaleaf Glendale West', city: 'Glendale', state: 'AZ', address: '6501 W Glendale Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-glendale-west' }, + { name: 'Curaleaf Gilbert', city: 'Gilbert', state: 'AZ', address: '1736 E Williams Field Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-gilbert' }, + { name: 'Curaleaf Mesa', city: 'Mesa', state: 'AZ', address: '1540 S Power Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-mesa' }, + { name: 'Curaleaf Tempe', city: 'Tempe', state: 'AZ', address: '1815 E Broadway Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-tempe' }, + { name: 'Curaleaf Scottsdale', city: 'Scottsdale', state: 'AZ', address: '8904 E Indian Bend Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-scottsdale' }, + { name: 'Curaleaf Tucson Prince', city: 'Tucson', state: 'AZ', address: '3955 W Prince Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-tucson-prince' }, + { name: 'Curaleaf Tucson Midvale', city: 'Tucson', state: 'AZ', address: '2936 N Midvale Park Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-tucson-midvale' }, + { name: 'Curaleaf Sedona', city: 'Sedona', state: 'AZ', address: '525 AZ-179', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-sedona' }, + { name: 'Curaleaf Youngtown', city: 'Youngtown', state: 'AZ', address: '11125 W Grand Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-youngtown' }, + ]; +} +/** + * Match a directory store to an existing dispensary + */ +function matchStoreToDispensary(store, dispensaries) { + const normalizedStoreName = normalizeForComparison(store.name); + const normalizedStoreCity = normalizeCity(store.city); + let bestMatch = null; + let bestScore = 0; + let matchReason = ''; + for (const disp of dispensaries) { + const normalizedDispName = normalizeForComparison(disp.name); + const normalizedDispCity = normalizeCity(disp.city || ''); + let score = 0; + const reasons = []; + // 1. Name similarity (max 50 points) + const nameSimilarity = stringSimilarity(normalizedStoreName, normalizedDispName); + score += nameSimilarity * 50; + if (nameSimilarity > 0.8) + reasons.push(`name_match(${(nameSimilarity * 100).toFixed(0)}%)`); + // 2. City match (25 points for exact, 15 for partial) + if (normalizedStoreCity && normalizedDispCity) { + if (normalizedStoreCity === normalizedDispCity) { + score += 25; + reasons.push('city_exact'); + } + else if (normalizedStoreCity.includes(normalizedDispCity) || + normalizedDispCity.includes(normalizedStoreCity)) { + score += 15; + reasons.push('city_partial'); + } + } + // 3. Address contains street name (15 points) + if (store.address && disp.address) { + const storeStreet = store.address.toLowerCase().split(/\s+/).slice(1, 4).join(' '); + const dispStreet = disp.address.toLowerCase().split(/\s+/).slice(1, 4).join(' '); + if (storeStreet && dispStreet && stringSimilarity(storeStreet, dispStreet) > 0.7) { + score += 15; + reasons.push('address_match'); + } + } + // 4. Brand name in dispensary name (10 points) + const brandName = store.name.split(' ')[0].toLowerCase(); // e.g., "Curaleaf", "Sol" + if (disp.name.toLowerCase().includes(brandName)) { + score += 10; + reasons.push('brand_match'); + } + if (score > bestScore) { + bestScore = score; + bestMatch = disp; + matchReason = reasons.join(', '); + } + } + // Determine confidence level + let confidence; + if (bestScore >= 70) { + confidence = 'high'; + } + else if (bestScore >= 50) { + confidence = 'medium'; + } + else if (bestScore >= 30) { + confidence = 'low'; + } + else { + confidence = 'none'; + } + return { + directoryStore: store, + dispensaryId: bestMatch?.id || null, + dispensaryName: bestMatch?.name || null, + confidence, + matchReason: matchReason || 'no_match', + }; +} +// ============================================================ +// MAIN FUNCTIONS +// ============================================================ +/** + * Run directory matching for a provider and update database + * Only applies high-confidence matches automatically + */ +async function matchDirectoryToDispensaries(provider, dryRun = true) { + console.log(`[DirectoryMatcher] Running ${provider} directory matching (dryRun=${dryRun})...`); + // Get directory stores + let directoryStores; + if (provider === 'curaleaf') { + directoryStores = await scrapeCuraleafDirectory(); + } + else if (provider === 'sol') { + directoryStores = await scrapeSolDirectory(); + } + else { + throw new Error(`Unknown provider: ${provider}`); + } + // Get all AZ dispensaries from database + const { rows: dispensaries } = await (0, connection_1.query)(`SELECT id, name, city, state, address, menu_type, menu_url, website + FROM dispensaries + WHERE state = 'AZ'`); + console.log(`[DirectoryMatcher] Matching ${directoryStores.length} directory stores against ${dispensaries.length} dispensaries`); + // Match each directory store + const results = []; + for (const store of directoryStores) { + const match = matchStoreToDispensary(store, dispensaries); + results.push(match); + // Only apply high-confidence matches if not dry run + if (!dryRun && match.confidence === 'high' && match.dispensaryId) { + await applyDirectoryMatch(match.dispensaryId, provider, store); + } + } + // Count results + const report = { + provider, + totalDirectoryStores: directoryStores.length, + highConfidenceMatches: results.filter((r) => r.confidence === 'high').length, + mediumConfidenceMatches: results.filter((r) => r.confidence === 'medium').length, + lowConfidenceMatches: results.filter((r) => r.confidence === 'low').length, + unmatched: results.filter((r) => r.confidence === 'none').length, + results, + }; + console.log(`[DirectoryMatcher] ${provider} matching complete:`); + console.log(` - High confidence: ${report.highConfidenceMatches}`); + console.log(` - Medium confidence: ${report.mediumConfidenceMatches}`); + console.log(` - Low confidence: ${report.lowConfidenceMatches}`); + console.log(` - Unmatched: ${report.unmatched}`); + return report; +} +/** + * Apply a directory match to a dispensary + */ +async function applyDirectoryMatch(dispensaryId, provider, store) { + console.log(`[DirectoryMatcher] Applying match: dispensary ${dispensaryId} -> ${store.storeUrl}`); + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = $1, + menu_url = $2, + platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', $1::text, + 'detection_method', 'directory_match'::text, + 'detected_at', NOW(), + 'directory_store_name', $3::text, + 'directory_store_url', $2::text, + 'directory_store_city', $4::text, + 'directory_store_address', $5::text, + 'not_crawlable', true, + 'not_crawlable_reason', $6::text + ), + updated_at = NOW() + WHERE id = $7 + `, [ + provider, + store.storeUrl, + store.name, + store.city, + store.address, + `${provider} proprietary menu - no crawler available`, + dispensaryId, + ]); +} +/** + * Preview matches without applying them + */ +async function previewDirectoryMatches(provider) { + return matchDirectoryToDispensaries(provider, true); +} +/** + * Apply high-confidence matches + */ +async function applyHighConfidenceMatches(provider) { + return matchDirectoryToDispensaries(provider, false); +} diff --git a/backend/dist/dutchie-az/services/discovery.js b/backend/dist/dutchie-az/services/discovery.js new file mode 100644 index 00000000..02b7efe7 --- /dev/null +++ b/backend/dist/dutchie-az/services/discovery.js @@ -0,0 +1,487 @@ +"use strict"; +/** + * Dutchie AZ Discovery Service + * + * Discovers and manages dispensaries from Dutchie for Arizona. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.importFromExistingDispensaries = importFromExistingDispensaries; +exports.discoverDispensaries = discoverDispensaries; +exports.extractCNameFromMenuUrl = extractCNameFromMenuUrl; +exports.resolvePlatformDispensaryIds = resolvePlatformDispensaryIds; +exports.getAllDispensaries = getAllDispensaries; +exports.mapDbRowToDispensary = mapDbRowToDispensary; +exports.getDispensaryById = getDispensaryById; +exports.getDispensariesWithPlatformIds = getDispensariesWithPlatformIds; +exports.reResolveDispensaryPlatformId = reResolveDispensaryPlatformId; +exports.updateMenuUrlAndResolve = updateMenuUrlAndResolve; +exports.markDispensaryNotCrawlable = markDispensaryNotCrawlable; +exports.getDispensaryCName = getDispensaryCName; +const connection_1 = require("../db/connection"); +const graphql_client_1 = require("./graphql-client"); +/** + * Upsert a dispensary record + */ +async function upsertDispensary(dispensary) { + const result = await (0, connection_1.query)(` + INSERT INTO dispensaries ( + platform, name, slug, city, state, postal_code, address, + latitude, longitude, platform_dispensary_id, + is_delivery, is_pickup, raw_metadata, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, + $11, $12, $13, NOW() + ) + ON CONFLICT (platform, slug, city, state) DO UPDATE SET + name = EXCLUDED.name, + postal_code = EXCLUDED.postal_code, + address = EXCLUDED.address, + latitude = EXCLUDED.latitude, + longitude = EXCLUDED.longitude, + platform_dispensary_id = COALESCE(EXCLUDED.platform_dispensary_id, dispensaries.platform_dispensary_id), + is_delivery = EXCLUDED.is_delivery, + is_pickup = EXCLUDED.is_pickup, + raw_metadata = EXCLUDED.raw_metadata, + updated_at = NOW() + RETURNING id + `, [ + dispensary.platform || 'dutchie', + dispensary.name, + dispensary.slug, + dispensary.city, + dispensary.state || 'AZ', + dispensary.postalCode, + dispensary.address, + dispensary.latitude, + dispensary.longitude, + dispensary.platformDispensaryId, + dispensary.isDelivery || false, + dispensary.isPickup || true, + dispensary.rawMetadata ? JSON.stringify(dispensary.rawMetadata) : null, + ]); + return result.rows[0].id; +} +/** + * Normalize a raw discovery result to Dispensary + */ +function normalizeDispensary(raw) { + return { + platform: 'dutchie', + name: raw.name || raw.Name || '', + slug: raw.slug || raw.cName || raw.id || '', + city: raw.city || raw.address?.city || '', + state: 'AZ', + postalCode: raw.postalCode || raw.address?.postalCode || raw.address?.zip, + address: raw.streetAddress || raw.address?.streetAddress, + latitude: raw.latitude || raw.location?.lat, + longitude: raw.longitude || raw.location?.lng, + platformDispensaryId: raw.dispensaryId || raw.id || null, + isDelivery: raw.isDelivery || raw.delivery || false, + isPickup: raw.isPickup || raw.pickup || true, + rawMetadata: raw, + }; +} +/** + * Import dispensaries from the existing dispensaries table (from AZDHS data) + * This creates records in the dutchie_az database for AZ dispensaries + */ +async function importFromExistingDispensaries() { + console.log('[Discovery] Importing from existing dispensaries table...'); + // This is a workaround - we'll use the dispensaries we already know about + // and try to resolve their Dutchie IDs + const knownDispensaries = [ + { name: 'Deeply Rooted', slug: 'AZ-Deeply-Rooted', city: 'Phoenix', state: 'AZ' }, + { name: 'Curaleaf Gilbert', slug: 'curaleaf-gilbert', city: 'Gilbert', state: 'AZ' }, + { name: 'Zen Leaf Prescott', slug: 'AZ-zen-leaf-prescott', city: 'Prescott', state: 'AZ' }, + // Add more known Dutchie stores here + ]; + let imported = 0; + for (const disp of knownDispensaries) { + try { + const id = await upsertDispensary({ + platform: 'dutchie', + name: disp.name, + slug: disp.slug, + city: disp.city, + state: disp.state, + }); + imported++; + console.log(`[Discovery] Imported: ${disp.name} (id=${id})`); + } + catch (error) { + console.error(`[Discovery] Failed to import ${disp.name}:`, error.message); + } + } + return { imported }; +} +/** + * Discover all Arizona Dutchie dispensaries via GraphQL + */ +async function discoverDispensaries() { + console.log('[Discovery] Starting Arizona dispensary discovery...'); + const errors = []; + let discovered = 0; + try { + const rawDispensaries = await (0, graphql_client_1.discoverArizonaDispensaries)(); + console.log(`[Discovery] Found ${rawDispensaries.length} dispensaries from GraphQL`); + for (const raw of rawDispensaries) { + try { + const normalized = normalizeDispensary(raw); + if (normalized.name && normalized.slug && normalized.city) { + await upsertDispensary(normalized); + discovered++; + } + } + catch (error) { + errors.push(`${raw.name || raw.slug}: ${error.message}`); + } + } + } + catch (error) { + errors.push(`Discovery failed: ${error.message}`); + } + console.log(`[Discovery] Completed: ${discovered} dispensaries, ${errors.length} errors`); + return { discovered, errors }; +} +/** + * Extract cName (slug) from a Dutchie menu_url + * Supports formats: + * - https://dutchie.com/embedded-menu/ + * - https://dutchie.com/dispensary/ + */ +function extractCNameFromMenuUrl(menuUrl) { + if (!menuUrl) + return null; + try { + const url = new URL(menuUrl); + const pathname = url.pathname; + // Match /embedded-menu/ or /dispensary/ + const embeddedMatch = pathname.match(/^\/embedded-menu\/([^/?]+)/); + if (embeddedMatch) + return embeddedMatch[1]; + const dispensaryMatch = pathname.match(/^\/dispensary\/([^/?]+)/); + if (dispensaryMatch) + return dispensaryMatch[1]; + return null; + } + catch { + return null; + } +} +/** + * Resolve platform dispensary IDs for all dispensaries that don't have one + * CRITICAL: Uses cName extracted from menu_url, NOT the slug column! + * + * Uses the new resolveDispensaryIdWithDetails which: + * 1. Extracts dispensaryId from window.reactEnv in the embedded menu page (preferred) + * 2. Falls back to GraphQL if reactEnv extraction fails + * 3. Returns HTTP status so we can mark 403/404 stores as not_crawlable + */ +async function resolvePlatformDispensaryIds() { + console.log('[Discovery] Resolving platform dispensary IDs...'); + const { rows: dispensaries } = await (0, connection_1.query)(` + SELECT id, name, slug, menu_url, menu_type, platform_dispensary_id, crawl_status + FROM dispensaries + WHERE menu_type = 'dutchie' + AND platform_dispensary_id IS NULL + AND menu_url IS NOT NULL + AND (crawl_status IS NULL OR crawl_status != 'not_crawlable') + ORDER BY id + `); + let resolved = 0; + let failed = 0; + let skipped = 0; + let notCrawlable = 0; + for (const dispensary of dispensaries) { + try { + // Extract cName from menu_url - this is the CORRECT way to get the Dutchie slug + const cName = extractCNameFromMenuUrl(dispensary.menu_url); + if (!cName) { + console.log(`[Discovery] Skipping ${dispensary.name}: Could not extract cName from menu_url: ${dispensary.menu_url}`); + skipped++; + continue; + } + console.log(`[Discovery] Resolving ID for: ${dispensary.name} (cName=${cName}, menu_url=${dispensary.menu_url})`); + // Use the new detailed resolver that extracts from reactEnv first + const result = await (0, graphql_client_1.resolveDispensaryIdWithDetails)(cName); + if (result.dispensaryId) { + // SUCCESS: Store resolved + await (0, connection_1.query)(` + UPDATE dispensaries + SET platform_dispensary_id = $1, + platform_dispensary_id_resolved_at = NOW(), + crawl_status = 'ready', + crawl_status_reason = $2, + crawl_status_updated_at = NOW(), + last_tested_menu_url = $3, + last_http_status = $4, + updated_at = NOW() + WHERE id = $5 + `, [ + result.dispensaryId, + `Resolved from ${result.source || 'page'}`, + dispensary.menu_url, + result.httpStatus, + dispensary.id, + ]); + resolved++; + console.log(`[Discovery] Resolved: ${cName} -> ${result.dispensaryId} (source: ${result.source})`); + } + else if (result.httpStatus === 403 || result.httpStatus === 404) { + // NOT CRAWLABLE: Store removed or not accessible + await (0, connection_1.query)(` + UPDATE dispensaries + SET platform_dispensary_id = NULL, + crawl_status = 'not_crawlable', + crawl_status_reason = $1, + crawl_status_updated_at = NOW(), + last_tested_menu_url = $2, + last_http_status = $3, + updated_at = NOW() + WHERE id = $4 + `, [ + result.error || `HTTP ${result.httpStatus}: Removed from Dutchie`, + dispensary.menu_url, + result.httpStatus, + dispensary.id, + ]); + notCrawlable++; + console.log(`[Discovery] Marked not crawlable: ${cName} (HTTP ${result.httpStatus})`); + } + else { + // FAILED: Could not resolve but page loaded + await (0, connection_1.query)(` + UPDATE dispensaries + SET crawl_status = 'not_ready', + crawl_status_reason = $1, + crawl_status_updated_at = NOW(), + last_tested_menu_url = $2, + last_http_status = $3, + updated_at = NOW() + WHERE id = $4 + `, [ + result.error || 'Could not extract dispensaryId from page', + dispensary.menu_url, + result.httpStatus, + dispensary.id, + ]); + failed++; + console.log(`[Discovery] Could not resolve: ${cName} - ${result.error}`); + } + // Delay between requests + await new Promise((r) => setTimeout(r, 2000)); + } + catch (error) { + failed++; + console.error(`[Discovery] Error resolving ${dispensary.name}:`, error.message); + } + } + console.log(`[Discovery] Completed: ${resolved} resolved, ${failed} failed, ${skipped} skipped, ${notCrawlable} not crawlable`); + return { resolved, failed, skipped, notCrawlable }; +} +/** + * Get all dispensaries + */ +// Explicit column list for dispensaries table (avoids SELECT * issues with schema differences) +const DISPENSARY_COLUMNS = ` + id, name, slug, city, state, zip, address, latitude, longitude, + menu_type, menu_url, platform_dispensary_id, website, + provider_detection_data, created_at, updated_at +`; +async function getAllDispensaries() { + const { rows } = await (0, connection_1.query)(`SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE menu_type = 'dutchie' ORDER BY name`); + return rows.map(mapDbRowToDispensary); +} +/** + * Map snake_case DB row to camelCase Dispensary object + * CRITICAL: DB returns snake_case (platform_dispensary_id) but TypeScript expects camelCase (platformDispensaryId) + * This function is exported for use in other modules that query dispensaries directly. + * + * NOTE: The consolidated dispensaries table column mappings: + * - zip → postalCode + * - menu_type → menuType (keep platform as 'dutchie') + * - last_crawl_at → lastCrawledAt + * - platform_dispensary_id → platformDispensaryId + */ +function mapDbRowToDispensary(row) { + // Extract website from raw_metadata if available (field may not exist in all environments) + let rawMetadata = undefined; + if (row.raw_metadata !== undefined) { + rawMetadata = typeof row.raw_metadata === 'string' + ? JSON.parse(row.raw_metadata) + : row.raw_metadata; + } + const website = row.website || rawMetadata?.website || undefined; + return { + id: row.id, + platform: row.platform || 'dutchie', // keep platform as-is, default to 'dutchie' + name: row.name, + slug: row.slug, + city: row.city, + state: row.state, + postalCode: row.postalCode || row.zip || row.postal_code, + latitude: row.latitude ? parseFloat(row.latitude) : undefined, + longitude: row.longitude ? parseFloat(row.longitude) : undefined, + address: row.address, + platformDispensaryId: row.platformDispensaryId || row.platform_dispensary_id, // CRITICAL mapping! + isDelivery: row.is_delivery, + isPickup: row.is_pickup, + rawMetadata: rawMetadata, + lastCrawledAt: row.lastCrawledAt || row.last_crawl_at, // use last_crawl_at + productCount: row.product_count, + createdAt: row.created_at, + updatedAt: row.updated_at, + menuType: row.menuType || row.menu_type, + menuUrl: row.menuUrl || row.menu_url, + scrapeEnabled: row.scrapeEnabled ?? row.scrape_enabled, + providerDetectionData: row.provider_detection_data, + platformDispensaryIdResolvedAt: row.platform_dispensary_id_resolved_at, + website, + }; +} +/** + * Get dispensary by ID + * NOTE: Uses SQL aliases to map snake_case → camelCase directly + */ +async function getDispensaryById(id) { + const { rows } = await (0, connection_1.query)(` + SELECT + id, + name, + slug, + city, + state, + zip AS "postalCode", + address, + latitude, + longitude, + menu_type AS "menuType", + menu_url AS "menuUrl", + platform_dispensary_id AS "platformDispensaryId", + website, + provider_detection_data AS "providerDetectionData", + created_at, + updated_at + FROM dispensaries + WHERE id = $1 + `, [id]); + if (!rows[0]) + return null; + return mapDbRowToDispensary(rows[0]); +} +/** + * Get dispensaries with platform IDs (ready for crawling) + */ +async function getDispensariesWithPlatformIds() { + const { rows } = await (0, connection_1.query)(` + SELECT ${DISPENSARY_COLUMNS} FROM dispensaries + WHERE menu_type = 'dutchie' AND platform_dispensary_id IS NOT NULL + ORDER BY name + `); + return rows.map(mapDbRowToDispensary); +} +/** + * Re-resolve a single dispensary's platform ID + * Clears the existing ID and re-resolves from the menu_url cName + */ +async function reResolveDispensaryPlatformId(dispensaryId) { + console.log(`[Discovery] Re-resolving platform ID for dispensary ${dispensaryId}...`); + const dispensary = await getDispensaryById(dispensaryId); + if (!dispensary) { + return { success: false, platformId: null, cName: null, error: 'Dispensary not found' }; + } + const cName = extractCNameFromMenuUrl(dispensary.menuUrl); + if (!cName) { + console.log(`[Discovery] Could not extract cName from menu_url: ${dispensary.menuUrl}`); + return { + success: false, + platformId: null, + cName: null, + error: `Could not extract cName from menu_url: ${dispensary.menuUrl}`, + }; + } + console.log(`[Discovery] Extracted cName: ${cName} from menu_url: ${dispensary.menuUrl}`); + try { + const platformId = await (0, graphql_client_1.resolveDispensaryId)(cName); + if (platformId) { + await (0, connection_1.query)(` + UPDATE dispensaries + SET platform_dispensary_id = $1, + platform_dispensary_id_resolved_at = NOW(), + updated_at = NOW() + WHERE id = $2 + `, [platformId, dispensaryId]); + console.log(`[Discovery] Resolved: ${cName} -> ${platformId}`); + return { success: true, platformId, cName }; + } + else { + // Clear the invalid platform ID and mark as not crawlable + await (0, connection_1.query)(` + UPDATE dispensaries + SET platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + '{"resolution_error": "cName no longer exists on Dutchie", "not_crawlable": true}'::jsonb, + updated_at = NOW() + WHERE id = $1 + `, [dispensaryId]); + console.log(`[Discovery] Could not resolve: ${cName} - marked as not crawlable`); + return { + success: false, + platformId: null, + cName, + error: `cName "${cName}" no longer exists on Dutchie`, + }; + } + } + catch (error) { + console.error(`[Discovery] Error resolving ${cName}:`, error.message); + return { success: false, platformId: null, cName, error: error.message }; + } +} +/** + * Update menu_url for a dispensary and re-resolve platform ID + */ +async function updateMenuUrlAndResolve(dispensaryId, newMenuUrl) { + console.log(`[Discovery] Updating menu_url for dispensary ${dispensaryId} to: ${newMenuUrl}`); + const cName = extractCNameFromMenuUrl(newMenuUrl); + if (!cName) { + return { + success: false, + platformId: null, + cName: null, + error: `Could not extract cName from new menu_url: ${newMenuUrl}`, + }; + } + // Update the menu_url first + await (0, connection_1.query)(` + UPDATE dispensaries + SET menu_url = $1, + menu_type = 'dutchie', + platform_dispensary_id = NULL, + updated_at = NOW() + WHERE id = $2 + `, [newMenuUrl, dispensaryId]); + // Now resolve the platform ID with the new cName + return await reResolveDispensaryPlatformId(dispensaryId); +} +/** + * Mark a dispensary as not crawlable (when resolution fails permanently) + */ +async function markDispensaryNotCrawlable(dispensaryId, reason) { + await (0, connection_1.query)(` + UPDATE dispensaries + SET platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object('not_crawlable', true, 'not_crawlable_reason', $1::text, 'not_crawlable_at', NOW()::text), + updated_at = NOW() + WHERE id = $2 + `, [reason, dispensaryId]); + console.log(`[Discovery] Marked dispensary ${dispensaryId} as not crawlable: ${reason}`); +} +/** + * Get the cName for a dispensary (extracted from menu_url) + */ +function getDispensaryCName(dispensary) { + return extractCNameFromMenuUrl(dispensary.menuUrl); +} diff --git a/backend/dist/dutchie-az/services/graphql-client.js b/backend/dist/dutchie-az/services/graphql-client.js new file mode 100644 index 00000000..b19f7146 --- /dev/null +++ b/backend/dist/dutchie-az/services/graphql-client.js @@ -0,0 +1,538 @@ +"use strict"; +/** + * Dutchie GraphQL Client + * + * Uses Puppeteer to establish a session (get CF cookies), then makes + * SERVER-SIDE fetch calls to api-gw.dutchie.com with those cookies. + * + * DUTCHIE FETCH RULES: + * 1. Server-side only - use axios (never browser fetch with CORS) + * 2. Use dispensaryFilter.cNameOrID, NOT dispensaryId directly + * 3. Headers must mimic Chrome: User-Agent, Origin, Referer + * 4. If 403, extract CF cookies from Puppeteer session and include them + * 5. Log status codes, error bodies, and product counts + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ARIZONA_CENTERPOINTS = exports.GRAPHQL_HASHES = void 0; +exports.resolveDispensaryId = resolveDispensaryId; +exports.resolveDispensaryIdWithDetails = resolveDispensaryIdWithDetails; +exports.discoverArizonaDispensaries = discoverArizonaDispensaries; +exports.fetchAllProducts = fetchAllProducts; +exports.fetchAllProductsBothModes = fetchAllProductsBothModes; +const axios_1 = __importDefault(require("axios")); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +const dutchie_1 = require("../config/dutchie"); +Object.defineProperty(exports, "GRAPHQL_HASHES", { enumerable: true, get: function () { return dutchie_1.GRAPHQL_HASHES; } }); +Object.defineProperty(exports, "ARIZONA_CENTERPOINTS", { enumerable: true, get: function () { return dutchie_1.ARIZONA_CENTERPOINTS; } }); +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +/** + * Create a session by navigating to the embedded menu page + * and extracting CF clearance cookies for server-side requests. + * Also extracts dispensaryId from window.reactEnv if available. + */ +async function createSession(cName) { + const browser = await puppeteer_extra_1.default.launch({ + headless: 'new', + args: dutchie_1.dutchieConfig.browserArgs, + }); + const page = await browser.newPage(); + const userAgent = dutchie_1.dutchieConfig.userAgent; + await page.setUserAgent(userAgent); + await page.setViewport({ width: 1920, height: 1080 }); + await page.evaluateOnNewDocument(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => false }); + window.chrome = { runtime: {} }; + }); + // Navigate to the embedded menu page for this dispensary + const embeddedMenuUrl = `https://dutchie.com/embedded-menu/${cName}`; + console.log(`[GraphQL Client] Loading ${embeddedMenuUrl} to get CF cookies...`); + let httpStatus; + let dispensaryId; + try { + const response = await page.goto(embeddedMenuUrl, { + waitUntil: 'networkidle2', + timeout: dutchie_1.dutchieConfig.navigationTimeout, + }); + httpStatus = response?.status(); + await new Promise((r) => setTimeout(r, dutchie_1.dutchieConfig.pageLoadDelay)); + // Try to extract dispensaryId from window.reactEnv + try { + dispensaryId = await page.evaluate(() => { + return window.reactEnv?.dispensaryId || null; + }); + if (dispensaryId) { + console.log(`[GraphQL Client] Extracted dispensaryId from reactEnv: ${dispensaryId}`); + } + } + catch (evalError) { + console.log(`[GraphQL Client] Could not extract dispensaryId from reactEnv: ${evalError.message}`); + } + } + catch (error) { + console.warn(`[GraphQL Client] Navigation warning: ${error.message}`); + // Continue anyway - we may have gotten cookies + } + // Extract cookies + const cookies = await page.cookies(); + const cookieString = cookies.map((c) => `${c.name}=${c.value}`).join('; '); + console.log(`[GraphQL Client] Got ${cookies.length} cookies, HTTP status: ${httpStatus}`); + if (cookies.length > 0) { + console.log(`[GraphQL Client] Cookie names: ${cookies.map(c => c.name).join(', ')}`); + } + return { cookies: cookieString, userAgent, browser, page, dispensaryId, httpStatus }; +} +/** + * Close session (browser) + */ +async function closeSession(session) { + await session.browser.close(); +} +// ============================================================ +// SERVER-SIDE GRAPHQL FETCH USING AXIOS +// ============================================================ +/** + * Build headers that mimic a real browser request + */ +function buildHeaders(session, cName) { + const embeddedMenuUrl = `https://dutchie.com/embedded-menu/${cName}`; + return { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'en-US,en;q=0.9', + 'accept-encoding': 'gzip, deflate, br', + 'content-type': 'application/json', + 'origin': 'https://dutchie.com', + 'referer': embeddedMenuUrl, + 'user-agent': session.userAgent, + 'apollographql-client-name': 'Marketplace (production)', + 'sec-ch-ua': '"Chromium";v="120", "Google Chrome";v="120", "Not-A.Brand";v="99"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + ...(session.cookies ? { 'cookie': session.cookies } : {}), + }; +} +/** + * Execute GraphQL query server-side using axios + * Uses cookies from the browser session to bypass CF + */ +async function executeGraphQL(session, operationName, variables, hash, cName) { + const endpoint = dutchie_1.dutchieConfig.graphqlEndpoint; + const headers = buildHeaders(session, cName); + // Build request body for POST + const body = { + operationName, + variables, + extensions: { + persistedQuery: { version: 1, sha256Hash: hash }, + }, + }; + console.log(`[GraphQL Client] POST: ${operationName} -> ${endpoint}`); + console.log(`[GraphQL Client] Variables: ${JSON.stringify(variables).slice(0, 300)}...`); + try { + const response = await axios_1.default.post(endpoint, body, { + headers, + timeout: 30000, + validateStatus: () => true, // Don't throw on non-2xx + }); + // Log response details + console.log(`[GraphQL Client] Response status: ${response.status}`); + if (response.status !== 200) { + const bodyPreview = typeof response.data === 'string' + ? response.data.slice(0, 500) + : JSON.stringify(response.data).slice(0, 500); + console.error(`[GraphQL Client] HTTP ${response.status}: ${bodyPreview}`); + throw new Error(`HTTP ${response.status}`); + } + // Check for GraphQL errors + if (response.data?.errors && response.data.errors.length > 0) { + console.error(`[GraphQL Client] GraphQL errors: ${JSON.stringify(response.data.errors[0])}`); + } + return response.data; + } + catch (error) { + if (axios_1.default.isAxiosError(error)) { + const axiosError = error; + console.error(`[GraphQL Client] Axios error: ${axiosError.message}`); + if (axiosError.response) { + console.error(`[GraphQL Client] Response status: ${axiosError.response.status}`); + console.error(`[GraphQL Client] Response data: ${JSON.stringify(axiosError.response.data).slice(0, 500)}`); + } + if (axiosError.code) { + console.error(`[GraphQL Client] Error code: ${axiosError.code}`); + } + } + else { + console.error(`[GraphQL Client] Error: ${error.message}`); + } + throw error; + } +} +/** + * Resolve a dispensary slug to its internal platform ID. + * + * STRATEGY: + * 1. Navigate to embedded menu page and extract window.reactEnv.dispensaryId (preferred) + * 2. Fall back to GraphQL GetAddressBasedDispensaryData query if reactEnv fails + * + * Returns the dispensaryId (platform_dispensary_id) or null if not found. + * Throws if page returns 403/404 so caller can mark as not_crawlable. + */ +async function resolveDispensaryId(slug) { + const result = await resolveDispensaryIdWithDetails(slug); + return result.dispensaryId; +} +/** + * Resolve a dispensary slug with full details (HTTP status, source, error). + * Use this when you need to know WHY resolution failed. + */ +async function resolveDispensaryIdWithDetails(slug) { + console.log(`[GraphQL Client] Resolving dispensary ID for slug: ${slug}`); + const session = await createSession(slug); + try { + // Check HTTP status first - if 403/404, the store is not crawlable + if (session.httpStatus && (session.httpStatus === 403 || session.httpStatus === 404)) { + console.log(`[GraphQL Client] Page returned HTTP ${session.httpStatus} for ${slug} - not crawlable`); + return { + dispensaryId: null, + httpStatus: session.httpStatus, + error: `HTTP ${session.httpStatus}: Store removed or not accessible`, + source: 'reactEnv', + }; + } + // PREFERRED: Use dispensaryId from window.reactEnv (extracted during createSession) + if (session.dispensaryId) { + console.log(`[GraphQL Client] Resolved ${slug} -> ${session.dispensaryId} (from reactEnv)`); + return { + dispensaryId: session.dispensaryId, + httpStatus: session.httpStatus, + source: 'reactEnv', + }; + } + // FALLBACK: Try GraphQL query + console.log(`[GraphQL Client] reactEnv.dispensaryId not found for ${slug}, trying GraphQL...`); + const variables = { + dispensaryFilter: { + cNameOrID: slug, + }, + }; + const result = await executeGraphQL(session, 'GetAddressBasedDispensaryData', variables, dutchie_1.GRAPHQL_HASHES.GetAddressBasedDispensaryData, slug); + const dispensaryId = result?.data?.dispensaryBySlug?.id || + result?.data?.dispensary?.id || + result?.data?.getAddressBasedDispensaryData?.dispensary?.id; + if (dispensaryId) { + console.log(`[GraphQL Client] Resolved ${slug} -> ${dispensaryId} (from GraphQL)`); + return { + dispensaryId, + httpStatus: session.httpStatus, + source: 'graphql', + }; + } + console.log(`[GraphQL Client] Could not resolve ${slug}, GraphQL response:`, JSON.stringify(result).slice(0, 300)); + return { + dispensaryId: null, + httpStatus: session.httpStatus, + error: 'Could not extract dispensaryId from reactEnv or GraphQL', + }; + } + finally { + await closeSession(session); + } +} +/** + * Discover Arizona dispensaries via geo-based query + */ +async function discoverArizonaDispensaries() { + console.log('[GraphQL Client] Discovering Arizona dispensaries...'); + // Use Phoenix as the default center + const session = await createSession('AZ-Deeply-Rooted'); + const allDispensaries = []; + const seenIds = new Set(); + try { + for (const centerpoint of dutchie_1.ARIZONA_CENTERPOINTS) { + console.log(`[GraphQL Client] Scanning ${centerpoint.name}...`); + const variables = { + dispensariesFilter: { + latitude: centerpoint.lat, + longitude: centerpoint.lng, + distance: 100, + state: 'AZ', + }, + }; + try { + const result = await executeGraphQL(session, 'ConsumerDispensaries', variables, dutchie_1.GRAPHQL_HASHES.ConsumerDispensaries, 'AZ-Deeply-Rooted'); + const dispensaries = result?.data?.consumerDispensaries || []; + for (const d of dispensaries) { + const id = d.id || d.dispensaryId; + if (id && !seenIds.has(id)) { + seenIds.add(id); + allDispensaries.push(d); + } + } + console.log(`[GraphQL Client] Found ${dispensaries.length} in ${centerpoint.name} (${allDispensaries.length} total unique)`); + } + catch (error) { + console.warn(`[GraphQL Client] Error scanning ${centerpoint.name}: ${error.message}`); + } + // Delay between requests + await new Promise((r) => setTimeout(r, 1000)); + } + } + finally { + await closeSession(session); + } + console.log(`[GraphQL Client] Discovery complete: ${allDispensaries.length} dispensaries`); + return allDispensaries; +} +// ============================================================ +// PRODUCT FILTERING VARIABLES +// ============================================================ +/** + * Build filter variables for FilteredProducts query + * + * CRITICAL: Uses dispensaryId directly (the MongoDB ObjectId, e.g. "6405ef617056e8014d79101b") + * NOT dispensaryFilter.cNameOrID! + * + * The actual browser request structure is: + * { + * "productsFilter": { + * "dispensaryId": "6405ef617056e8014d79101b", + * "pricingType": "rec", + * "Status": "Active", // Mode A only + * "strainTypes": [], + * "subcategories": [], + * "types": [], + * "useCache": true, + * ... + * }, + * "page": 0, + * "perPage": 100 + * } + * + * Mode A = UI parity (Status: "Active") + * Mode B = MAX COVERAGE (no Status filter) + */ +function buildFilterVariables(platformDispensaryId, pricingType, crawlMode, page, perPage) { + const isModeA = crawlMode === 'mode_a'; + // Per CLAUDE.md Rule #11: Use simple productsFilter with dispensaryId directly + // Do NOT use dispensaryFilter.cNameOrID - that's outdated + const productsFilter = { + dispensaryId: platformDispensaryId, + pricingType: pricingType, + }; + // Mode A: Only active products (UI parity) - Status: "Active" + // Mode B: MAX COVERAGE (OOS/inactive) - omit Status or set to null + if (isModeA) { + productsFilter.Status = 'Active'; + } + // Mode B: No Status filter = returns all products including OOS/inactive + return { + productsFilter, + page, + perPage, + }; +} +// ============================================================ +// PRODUCT FETCHING WITH PAGINATION +// ============================================================ +/** + * Fetch products for a single mode with pagination + */ +async function fetchProductsForMode(session, platformDispensaryId, cName, pricingType, crawlMode) { + const perPage = dutchie_1.dutchieConfig.perPage; + const maxPages = dutchie_1.dutchieConfig.maxPages; + const maxRetries = dutchie_1.dutchieConfig.maxRetries; + const pageDelayMs = dutchie_1.dutchieConfig.pageDelayMs; + const allProducts = []; + let pageNum = 0; + let totalCount = 0; + let consecutiveEmptyPages = 0; + console.log(`[GraphQL Client] Fetching products for ${cName} (platformId: ${platformDispensaryId}, ${pricingType}, ${crawlMode})...`); + while (pageNum < maxPages) { + const variables = buildFilterVariables(platformDispensaryId, pricingType, crawlMode, pageNum, perPage); + let result = null; + let lastError = null; + // Retry logic + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + result = await executeGraphQL(session, 'FilteredProducts', variables, dutchie_1.GRAPHQL_HASHES.FilteredProducts, cName); + lastError = null; + break; + } + catch (error) { + lastError = error; + console.warn(`[GraphQL Client] Page ${pageNum} attempt ${attempt + 1} failed: ${error.message}`); + if (attempt < maxRetries) { + await new Promise((r) => setTimeout(r, 1000 * (attempt + 1))); + } + } + } + if (lastError) { + console.error(`[GraphQL Client] Page ${pageNum} failed after ${maxRetries + 1} attempts`); + break; + } + if (result?.errors) { + console.error('[GraphQL Client] GraphQL errors:', JSON.stringify(result.errors)); + break; + } + // Log response shape on first page + if (pageNum === 0) { + console.log(`[GraphQL Client] Response keys: ${Object.keys(result || {}).join(', ')}`); + if (result?.data) { + console.log(`[GraphQL Client] data keys: ${Object.keys(result.data || {}).join(', ')}`); + } + if (!result?.data?.filteredProducts) { + console.log(`[GraphQL Client] WARNING: No filteredProducts in response!`); + console.log(`[GraphQL Client] Full response: ${JSON.stringify(result).slice(0, 1000)}`); + } + } + const products = result?.data?.filteredProducts?.products || []; + const queryInfo = result?.data?.filteredProducts?.queryInfo; + if (queryInfo?.totalCount) { + totalCount = queryInfo.totalCount; + } + console.log(`[GraphQL Client] Page ${pageNum}: ${products.length} products (total so far: ${allProducts.length + products.length}/${totalCount})`); + if (products.length === 0) { + consecutiveEmptyPages++; + if (consecutiveEmptyPages >= 2) { + console.log('[GraphQL Client] Multiple empty pages, stopping pagination'); + break; + } + } + else { + consecutiveEmptyPages = 0; + allProducts.push(...products); + } + // Stop if incomplete page (last page) + if (products.length < perPage) { + console.log(`[GraphQL Client] Incomplete page (${products.length} < ${perPage}), stopping`); + break; + } + pageNum++; + await new Promise((r) => setTimeout(r, pageDelayMs)); + } + console.log(`[GraphQL Client] Fetched ${allProducts.length} total products (${crawlMode})`); + return { products: allProducts, totalCount: totalCount || allProducts.length, crawlMode }; +} +// ============================================================ +// LEGACY SINGLE-MODE INTERFACE +// ============================================================ +/** + * Fetch all products for a dispensary (single mode) + */ +async function fetchAllProducts(platformDispensaryId, pricingType = 'rec', options = {}) { + const { crawlMode = 'mode_a' } = options; + // cName is now REQUIRED - no default fallback to avoid using wrong store's session + const cName = options.cName; + if (!cName) { + throw new Error('[GraphQL Client] cName is required for fetchAllProducts - cannot use another store\'s session'); + } + const session = await createSession(cName); + try { + return await fetchProductsForMode(session, platformDispensaryId, cName, pricingType, crawlMode); + } + finally { + await closeSession(session); + } +} +// ============================================================ +// MODE A+B MERGING +// ============================================================ +/** + * Merge POSMetaData.children arrays from Mode A and Mode B products + */ +function mergeProductOptions(modeAProduct, modeBProduct) { + const modeAChildren = modeAProduct.POSMetaData?.children || []; + const modeBChildren = modeBProduct.POSMetaData?.children || []; + const getOptionKey = (child) => { + return child.canonicalID || child.canonicalSKU || child.canonicalPackageId || child.option || ''; + }; + const mergedMap = new Map(); + for (const child of modeAChildren) { + const key = getOptionKey(child); + if (key) + mergedMap.set(key, child); + } + for (const child of modeBChildren) { + const key = getOptionKey(child); + if (key && !mergedMap.has(key)) { + mergedMap.set(key, child); + } + } + return Array.from(mergedMap.values()); +} +/** + * Merge a Mode A product with a Mode B product + */ +function mergeProducts(modeAProduct, modeBProduct) { + if (!modeBProduct) { + return modeAProduct; + } + const mergedChildren = mergeProductOptions(modeAProduct, modeBProduct); + return { + ...modeAProduct, + POSMetaData: { + ...modeAProduct.POSMetaData, + children: mergedChildren, + }, + }; +} +// ============================================================ +// MAIN EXPORT: TWO-MODE CRAWL +// ============================================================ +/** + * Fetch products using BOTH crawl modes with SINGLE session + * Runs Mode A then Mode B, merges results + */ +async function fetchAllProductsBothModes(platformDispensaryId, pricingType = 'rec', options = {}) { + // cName is now REQUIRED - no default fallback to avoid using wrong store's session + const cName = options.cName; + if (!cName) { + throw new Error('[GraphQL Client] cName is required for fetchAllProductsBothModes - cannot use another store\'s session'); + } + console.log(`[GraphQL Client] Running two-mode crawl for ${cName} (${pricingType})...`); + console.log(`[GraphQL Client] Platform ID: ${platformDispensaryId}, cName: ${cName}`); + const session = await createSession(cName); + try { + // Mode A (UI parity) + const modeAResult = await fetchProductsForMode(session, platformDispensaryId, cName, pricingType, 'mode_a'); + // Delay between modes + await new Promise((r) => setTimeout(r, dutchie_1.dutchieConfig.modeDelayMs)); + // Mode B (MAX COVERAGE) + const modeBResult = await fetchProductsForMode(session, platformDispensaryId, cName, pricingType, 'mode_b'); + // Merge results + const modeBMap = new Map(); + for (const product of modeBResult.products) { + modeBMap.set(product._id, product); + } + const productMap = new Map(); + // Add Mode A products, merging with Mode B if exists + for (const product of modeAResult.products) { + const modeBProduct = modeBMap.get(product._id); + const mergedProduct = mergeProducts(product, modeBProduct); + productMap.set(product._id, mergedProduct); + } + // Add Mode B products not in Mode A + for (const product of modeBResult.products) { + if (!productMap.has(product._id)) { + productMap.set(product._id, product); + } + } + const mergedProducts = Array.from(productMap.values()); + console.log(`[GraphQL Client] Merged: ${mergedProducts.length} unique products`); + console.log(`[GraphQL Client] Mode A: ${modeAResult.products.length}, Mode B: ${modeBResult.products.length}`); + return { + modeA: { products: modeAResult.products, totalCount: modeAResult.totalCount }, + modeB: { products: modeBResult.products, totalCount: modeBResult.totalCount }, + merged: { products: mergedProducts, totalCount: mergedProducts.length }, + }; + } + finally { + await closeSession(session); + } +} diff --git a/backend/dist/dutchie-az/services/job-queue.js b/backend/dist/dutchie-az/services/job-queue.js new file mode 100644 index 00000000..dca167a7 --- /dev/null +++ b/backend/dist/dutchie-az/services/job-queue.js @@ -0,0 +1,414 @@ +"use strict"; +/** + * Job Queue Service + * + * DB-backed job queue with claiming/locking for distributed workers. + * Ensures only one worker processes a given store at a time. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getWorkerId = getWorkerId; +exports.getWorkerHostname = getWorkerHostname; +exports.enqueueJob = enqueueJob; +exports.bulkEnqueueJobs = bulkEnqueueJobs; +exports.claimNextJob = claimNextJob; +exports.updateJobProgress = updateJobProgress; +exports.heartbeat = heartbeat; +exports.completeJob = completeJob; +exports.failJob = failJob; +exports.getQueueStats = getQueueStats; +exports.getActiveWorkers = getActiveWorkers; +exports.getRunningJobs = getRunningJobs; +exports.recoverStaleJobs = recoverStaleJobs; +exports.cleanupOldJobs = cleanupOldJobs; +const connection_1 = require("../db/connection"); +const uuid_1 = require("uuid"); +const os = __importStar(require("os")); +// ============================================================ +// WORKER IDENTITY +// ============================================================ +let _workerId = null; +/** + * Get or create a unique worker ID for this process + * In Kubernetes, uses POD_NAME for clarity; otherwise generates a unique ID + */ +function getWorkerId() { + if (!_workerId) { + // Prefer POD_NAME in K8s (set via fieldRef) + const podName = process.env.POD_NAME; + if (podName) { + _workerId = podName; + } + else { + const hostname = os.hostname(); + const pid = process.pid; + const uuid = (0, uuid_1.v4)().slice(0, 8); + _workerId = `${hostname}-${pid}-${uuid}`; + } + } + return _workerId; +} +/** + * Get hostname for worker tracking + * In Kubernetes, uses POD_NAME; otherwise uses os.hostname() + */ +function getWorkerHostname() { + return process.env.POD_NAME || os.hostname(); +} +// ============================================================ +// JOB ENQUEUEING +// ============================================================ +/** + * Enqueue a new job for processing + * Returns null if a pending/running job already exists for this dispensary + */ +async function enqueueJob(options) { + const { jobType, dispensaryId, priority = 0, metadata, maxRetries = 3, } = options; + // Check if there's already a pending/running job for this dispensary + if (dispensaryId) { + const { rows: existing } = await (0, connection_1.query)(`SELECT id FROM dispensary_crawl_jobs + WHERE dispensary_id = $1 AND status IN ('pending', 'running') + LIMIT 1`, [dispensaryId]); + if (existing.length > 0) { + console.log(`[JobQueue] Skipping enqueue - job already exists for dispensary ${dispensaryId}`); + return null; + } + } + const { rows } = await (0, connection_1.query)(`INSERT INTO dispensary_crawl_jobs (job_type, dispensary_id, status, priority, max_retries, metadata, created_at) + VALUES ($1, $2, 'pending', $3, $4, $5, NOW()) + RETURNING id`, [jobType, dispensaryId || null, priority, maxRetries, metadata ? JSON.stringify(metadata) : null]); + const jobId = rows[0].id; + console.log(`[JobQueue] Enqueued job ${jobId} (type=${jobType}, dispensary=${dispensaryId})`); + return jobId; +} +/** + * Bulk enqueue jobs for multiple dispensaries + * Skips dispensaries that already have pending/running jobs + */ +async function bulkEnqueueJobs(jobType, dispensaryIds, options = {}) { + const { priority = 0, metadata } = options; + // Get dispensaries that already have pending/running jobs + const { rows: existing } = await (0, connection_1.query)(`SELECT DISTINCT dispensary_id FROM dispensary_crawl_jobs + WHERE dispensary_id = ANY($1) AND status IN ('pending', 'running')`, [dispensaryIds]); + const existingSet = new Set(existing.map((r) => r.dispensary_id)); + // Filter out dispensaries with existing jobs + const toEnqueue = dispensaryIds.filter(id => !existingSet.has(id)); + if (toEnqueue.length === 0) { + return { enqueued: 0, skipped: dispensaryIds.length }; + } + // Bulk insert - each row needs 4 params: job_type, dispensary_id, priority, metadata + const metadataJson = metadata ? JSON.stringify(metadata) : null; + const values = toEnqueue.map((_, i) => { + const offset = i * 4; + return `($${offset + 1}, $${offset + 2}, 'pending', $${offset + 3}, 3, $${offset + 4}, NOW())`; + }).join(', '); + const params = []; + toEnqueue.forEach(dispensaryId => { + params.push(jobType, dispensaryId, priority, metadataJson); + }); + await (0, connection_1.query)(`INSERT INTO dispensary_crawl_jobs (job_type, dispensary_id, status, priority, max_retries, metadata, created_at) + VALUES ${values}`, params); + console.log(`[JobQueue] Bulk enqueued ${toEnqueue.length} jobs, skipped ${existingSet.size}`); + return { enqueued: toEnqueue.length, skipped: existingSet.size }; +} +// ============================================================ +// JOB CLAIMING (with locking) +// ============================================================ +/** + * Claim the next available job from the queue + * Uses SELECT FOR UPDATE SKIP LOCKED to prevent double-claims + */ +async function claimNextJob(options) { + const { workerId, jobTypes, lockDurationMinutes = 30 } = options; + const hostname = getWorkerHostname(); + const client = await (0, connection_1.getClient)(); + try { + await client.query('BEGIN'); + // Build job type filter + let typeFilter = ''; + const params = [workerId, hostname, lockDurationMinutes]; + let paramIndex = 4; + if (jobTypes && jobTypes.length > 0) { + typeFilter = `AND job_type = ANY($${paramIndex})`; + params.push(jobTypes); + paramIndex++; + } + // Claim the next pending job using FOR UPDATE SKIP LOCKED + // This atomically selects and locks a row, skipping any already locked by other workers + const { rows } = await client.query(`UPDATE dispensary_crawl_jobs + SET + status = 'running', + claimed_by = $1, + claimed_at = NOW(), + worker_id = $1, + worker_hostname = $2, + started_at = NOW(), + locked_until = NOW() + ($3 || ' minutes')::INTERVAL, + last_heartbeat_at = NOW(), + updated_at = NOW() + WHERE id = ( + SELECT id FROM dispensary_crawl_jobs + WHERE status = 'pending' + ${typeFilter} + ORDER BY priority DESC, created_at ASC + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) + RETURNING *`, params); + await client.query('COMMIT'); + if (rows.length === 0) { + return null; + } + const job = mapDbRowToJob(rows[0]); + console.log(`[JobQueue] Worker ${workerId} claimed job ${job.id} (type=${job.jobType}, dispensary=${job.dispensaryId})`); + return job; + } + catch (error) { + await client.query('ROLLBACK'); + throw error; + } + finally { + client.release(); + } +} +// ============================================================ +// JOB PROGRESS & COMPLETION +// ============================================================ +/** + * Update job progress (for live monitoring) + */ +async function updateJobProgress(jobId, progress) { + const updates = ['last_heartbeat_at = NOW()', 'updated_at = NOW()']; + const params = []; + let paramIndex = 1; + if (progress.productsFound !== undefined) { + updates.push(`products_found = $${paramIndex++}`); + params.push(progress.productsFound); + } + if (progress.productsUpserted !== undefined) { + updates.push(`products_upserted = $${paramIndex++}`); + params.push(progress.productsUpserted); + } + if (progress.snapshotsCreated !== undefined) { + updates.push(`snapshots_created = $${paramIndex++}`); + params.push(progress.snapshotsCreated); + } + if (progress.currentPage !== undefined) { + updates.push(`current_page = $${paramIndex++}`); + params.push(progress.currentPage); + } + if (progress.totalPages !== undefined) { + updates.push(`total_pages = $${paramIndex++}`); + params.push(progress.totalPages); + } + params.push(jobId); + await (0, connection_1.query)(`UPDATE dispensary_crawl_jobs SET ${updates.join(', ')} WHERE id = $${paramIndex}`, params); +} +/** + * Send heartbeat to keep job alive (prevents timeout) + */ +async function heartbeat(jobId) { + await (0, connection_1.query)(`UPDATE dispensary_crawl_jobs + SET last_heartbeat_at = NOW(), locked_until = NOW() + INTERVAL '30 minutes' + WHERE id = $1 AND status = 'running'`, [jobId]); +} +/** + * Mark job as completed + */ +async function completeJob(jobId, result) { + await (0, connection_1.query)(`UPDATE dispensary_crawl_jobs + SET + status = 'completed', + completed_at = NOW(), + products_found = COALESCE($2, products_found), + products_upserted = COALESCE($3, products_upserted), + snapshots_created = COALESCE($4, snapshots_created), + updated_at = NOW() + WHERE id = $1`, [jobId, result.productsFound, result.productsUpserted, result.snapshotsCreated]); + console.log(`[JobQueue] Job ${jobId} completed`); +} +/** + * Mark job as failed + */ +async function failJob(jobId, errorMessage) { + // Check if we should retry + const { rows } = await (0, connection_1.query)(`SELECT retry_count, max_retries FROM dispensary_crawl_jobs WHERE id = $1`, [jobId]); + if (rows.length === 0) + return false; + const { retry_count, max_retries } = rows[0]; + if (retry_count < max_retries) { + // Re-queue for retry + await (0, connection_1.query)(`UPDATE dispensary_crawl_jobs + SET + status = 'pending', + retry_count = retry_count + 1, + claimed_by = NULL, + claimed_at = NULL, + worker_id = NULL, + worker_hostname = NULL, + started_at = NULL, + locked_until = NULL, + last_heartbeat_at = NULL, + error_message = $2, + updated_at = NOW() + WHERE id = $1`, [jobId, errorMessage]); + console.log(`[JobQueue] Job ${jobId} failed, re-queued for retry (${retry_count + 1}/${max_retries})`); + return true; // Will retry + } + else { + // Mark as failed permanently + await (0, connection_1.query)(`UPDATE dispensary_crawl_jobs + SET + status = 'failed', + completed_at = NOW(), + error_message = $2, + updated_at = NOW() + WHERE id = $1`, [jobId, errorMessage]); + console.log(`[JobQueue] Job ${jobId} failed permanently after ${retry_count} retries`); + return false; // No more retries + } +} +// ============================================================ +// QUEUE MONITORING +// ============================================================ +/** + * Get queue statistics + */ +async function getQueueStats() { + const { rows } = await (0, connection_1.query)(`SELECT * FROM v_queue_stats`); + const stats = rows[0] || {}; + return { + pending: parseInt(stats.pending_jobs || '0', 10), + running: parseInt(stats.running_jobs || '0', 10), + completed1h: parseInt(stats.completed_1h || '0', 10), + failed1h: parseInt(stats.failed_1h || '0', 10), + activeWorkers: parseInt(stats.active_workers || '0', 10), + avgDurationSeconds: stats.avg_duration_seconds ? parseFloat(stats.avg_duration_seconds) : null, + }; +} +/** + * Get active workers + */ +async function getActiveWorkers() { + const { rows } = await (0, connection_1.query)(`SELECT * FROM v_active_workers`); + return rows.map((row) => ({ + workerId: row.worker_id, + hostname: row.worker_hostname, + currentJobs: parseInt(row.current_jobs || '0', 10), + totalProductsFound: parseInt(row.total_products_found || '0', 10), + totalProductsUpserted: parseInt(row.total_products_upserted || '0', 10), + totalSnapshots: parseInt(row.total_snapshots || '0', 10), + firstClaimedAt: new Date(row.first_claimed_at), + lastHeartbeat: row.last_heartbeat ? new Date(row.last_heartbeat) : null, + })); +} +/** + * Get running jobs with worker info + */ +async function getRunningJobs() { + const { rows } = await (0, connection_1.query)(`SELECT cj.*, d.name as dispensary_name, d.city + FROM dispensary_crawl_jobs cj + LEFT JOIN dispensaries d ON cj.dispensary_id = d.id + WHERE cj.status = 'running' + ORDER BY cj.started_at DESC`); + return rows.map(mapDbRowToJob); +} +/** + * Recover stale jobs (workers that died without completing) + */ +async function recoverStaleJobs(staleMinutes = 15) { + const { rowCount } = await (0, connection_1.query)(`UPDATE dispensary_crawl_jobs + SET + status = 'pending', + claimed_by = NULL, + claimed_at = NULL, + worker_id = NULL, + worker_hostname = NULL, + started_at = NULL, + locked_until = NULL, + error_message = 'Recovered from stale worker', + retry_count = retry_count + 1, + updated_at = NOW() + WHERE status = 'running' + AND last_heartbeat_at < NOW() - ($1 || ' minutes')::INTERVAL + AND retry_count < max_retries`, [staleMinutes]); + if (rowCount && rowCount > 0) { + console.log(`[JobQueue] Recovered ${rowCount} stale jobs`); + } + return rowCount || 0; +} +/** + * Clean up old completed/failed jobs + */ +async function cleanupOldJobs(olderThanDays = 7) { + const { rowCount } = await (0, connection_1.query)(`DELETE FROM dispensary_crawl_jobs + WHERE status IN ('completed', 'failed') + AND completed_at < NOW() - ($1 || ' days')::INTERVAL`, [olderThanDays]); + if (rowCount && rowCount > 0) { + console.log(`[JobQueue] Cleaned up ${rowCount} old jobs`); + } + return rowCount || 0; +} +// ============================================================ +// HELPERS +// ============================================================ +function mapDbRowToJob(row) { + return { + id: row.id, + jobType: row.job_type, + dispensaryId: row.dispensary_id, + status: row.status, + priority: row.priority || 0, + retryCount: row.retry_count || 0, + maxRetries: row.max_retries || 3, + claimedBy: row.claimed_by, + claimedAt: row.claimed_at ? new Date(row.claimed_at) : null, + workerHostname: row.worker_hostname, + startedAt: row.started_at ? new Date(row.started_at) : null, + completedAt: row.completed_at ? new Date(row.completed_at) : null, + errorMessage: row.error_message, + productsFound: row.products_found || 0, + productsUpserted: row.products_upserted || 0, + snapshotsCreated: row.snapshots_created || 0, + currentPage: row.current_page || 0, + totalPages: row.total_pages, + lastHeartbeatAt: row.last_heartbeat_at ? new Date(row.last_heartbeat_at) : null, + metadata: row.metadata, + createdAt: new Date(row.created_at), + // Add extra fields from join if present + ...(row.dispensary_name && { dispensaryName: row.dispensary_name }), + ...(row.city && { city: row.city }), + }; +} diff --git a/backend/dist/dutchie-az/services/menu-detection.js b/backend/dist/dutchie-az/services/menu-detection.js new file mode 100644 index 00000000..8d4e4005 --- /dev/null +++ b/backend/dist/dutchie-az/services/menu-detection.js @@ -0,0 +1,837 @@ +"use strict"; +/** + * Menu Detection Service + * + * Detects menu provider (dutchie, treez, jane, etc.) from dispensary menu_url + * and resolves platform_dispensary_id for dutchie stores. + * + * This service: + * 1. Iterates dispensaries with unknown/missing menu_type or platform_dispensary_id + * 2. Detects provider from menu_url patterns + * 3. For dutchie: extracts cName and resolves platform_dispensary_id via GraphQL + * 4. Logs results to job_run_logs + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.crawlWebsiteForMenuLinks = crawlWebsiteForMenuLinks; +exports.detectProviderFromUrl = detectProviderFromUrl; +exports.detectAndResolveDispensary = detectAndResolveDispensary; +exports.runBulkDetection = runBulkDetection; +exports.executeMenuDetectionJob = executeMenuDetectionJob; +exports.getDetectionStats = getDetectionStats; +exports.getDispensariesNeedingDetection = getDispensariesNeedingDetection; +const connection_1 = require("../db/connection"); +const discovery_1 = require("./discovery"); +const graphql_client_1 = require("./graphql-client"); +// Explicit column list for dispensaries table (avoids SELECT * issues with schema differences) +const DISPENSARY_COLUMNS = ` + id, name, slug, city, state, zip, address, latitude, longitude, + menu_type, menu_url, platform_dispensary_id, website, + provider_detection_data, created_at, updated_at +`; +// ============================================================ +// PROVIDER DETECTION PATTERNS +// ============================================================ +const PROVIDER_URL_PATTERNS = [ + // IMPORTANT: Curaleaf and Sol must come BEFORE dutchie to take precedence + // These stores have their own proprietary menu systems (not crawlable via Dutchie) + { + provider: 'curaleaf', + patterns: [ + /curaleaf\.com\/stores\//i, // e.g., https://curaleaf.com/stores/curaleaf-az-glendale-east + /curaleaf\.com\/dispensary\//i, // e.g., https://curaleaf.com/dispensary/arizona + ], + }, + { + provider: 'sol', + patterns: [ + /livewithsol\.com/i, // e.g., https://www.livewithsol.com/locations/sun-city/ + /solflower\.com/i, // alternate domain if any + ], + }, + { + provider: 'dutchie', + patterns: [ + /dutchie\.com/i, + /\/embedded-menu\//i, + /\/dispensary\/[A-Z]{2}-/i, // e.g., /dispensary/AZ-store-name + /dutchie-plus/i, + ], + }, + { + provider: 'treez', + patterns: [ + /treez\.io/i, + /shop\.treez/i, + /treez-ecommerce/i, + ], + }, + { + provider: 'jane', + patterns: [ + /jane\.co/i, + /iheartjane\.com/i, + /embed\.iheartjane/i, + ], + }, + { + provider: 'weedmaps', + patterns: [ + /weedmaps\.com/i, + /menu\.weedmaps/i, + ], + }, + { + provider: 'leafly', + patterns: [ + /leafly\.com/i, + /order\.leafly/i, + ], + }, + { + provider: 'meadow', + patterns: [ + /getmeadow\.com/i, + /meadow\.co/i, + ], + }, + { + provider: 'blaze', + patterns: [ + /blaze\.me/i, + /blazepos\.com/i, + ], + }, + { + provider: 'flowhub', + patterns: [ + /flowhub\.com/i, + /flowhub\.co/i, + ], + }, + { + provider: 'dispense', + patterns: [ + /dispense\.io/i, + /dispenseapp\.com/i, + ], + }, +]; +/** + * Link patterns that suggest a menu or ordering page + */ +const MENU_LINK_PATTERNS = [ + /\/menu/i, + /\/order/i, + /\/shop/i, + /\/products/i, + /\/dispensary/i, + /\/store/i, + /curaleaf\.com/i, + /dutchie\.com/i, + /treez\.io/i, + /jane\.co/i, + /iheartjane\.com/i, + /weedmaps\.com/i, + /leafly\.com/i, + /getmeadow\.com/i, + /blaze\.me/i, + /flowhub\.com/i, + /dispense\.io/i, +]; +/** + * Check if a URL is a Curaleaf store URL + */ +function isCuraleafUrl(url) { + if (!url) + return false; + return /curaleaf\.com\/(stores|dispensary)\//i.test(url); +} +/** + * Extract the Curaleaf store URL from a website URL + * Handles both /stores/ and /dispensary/ formats + */ +function extractCuraleafStoreUrl(url) { + if (!url) + return null; + // If it's already a Curaleaf stores/dispensary URL, use it + if (isCuraleafUrl(url)) { + return url; + } + return null; +} +/** + * Fetch a page and extract all links + */ +async function fetchPageLinks(url, timeout = 10000) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + const response = await fetch(url, { + signal: controller.signal, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }, + redirect: 'follow', + }); + clearTimeout(timeoutId); + if (!response.ok) { + return { links: [], error: `HTTP ${response.status}` }; + } + const html = await response.text(); + // Extract all href attributes from anchor tags + const linkRegex = /href=["']([^"']+)["']/gi; + const links = []; + let match; + while ((match = linkRegex.exec(html)) !== null) { + const href = match[1]; + // Convert relative URLs to absolute + try { + const absoluteUrl = new URL(href, url).href; + links.push(absoluteUrl); + } + catch { + // Skip invalid URLs + } + } + // Also look for iframe src attributes (common for embedded menus) + const iframeRegex = /src=["']([^"']+)["']/gi; + while ((match = iframeRegex.exec(html)) !== null) { + const src = match[1]; + try { + const absoluteUrl = new URL(src, url).href; + // Only add if it matches a provider pattern + for (const { patterns } of PROVIDER_URL_PATTERNS) { + if (patterns.some(p => p.test(absoluteUrl))) { + links.push(absoluteUrl); + break; + } + } + } + catch { + // Skip invalid URLs + } + } + return { links: [...new Set(links)] }; // Deduplicate + } + catch (error) { + if (error.name === 'AbortError') { + return { links: [], error: 'Timeout' }; + } + return { links: [], error: error.message }; + } +} +/** + * Crawl a dispensary's website to find menu provider links + * + * Strategy: + * 1. Fetch the homepage and extract all links + * 2. Look for links that match known provider patterns (dutchie, treez, etc.) + * 3. If no direct match, look for menu/order/shop links and follow them (1-2 hops) + * 4. Check followed pages for provider patterns + */ +async function crawlWebsiteForMenuLinks(websiteUrl) { + console.log(`[WebsiteCrawl] Crawling ${websiteUrl} for menu links...`); + const result = { + menuUrl: null, + provider: 'unknown', + foundLinks: [], + crawledPages: [], + }; + // Normalize URL + let baseUrl; + try { + baseUrl = new URL(websiteUrl); + if (!baseUrl.protocol.startsWith('http')) { + baseUrl = new URL(`https://${websiteUrl}`); + } + } + catch { + result.error = 'Invalid website URL'; + return result; + } + // Step 1: Fetch the homepage + const homepage = baseUrl.href; + result.crawledPages.push(homepage); + const { links: homepageLinks, error: homepageError } = await fetchPageLinks(homepage); + if (homepageError) { + result.error = `Failed to fetch homepage: ${homepageError}`; + return result; + } + result.foundLinks = homepageLinks; + // Step 2: Check for direct provider matches in homepage links + for (const link of homepageLinks) { + for (const { provider, patterns } of PROVIDER_URL_PATTERNS) { + if (patterns.some(p => p.test(link))) { + console.log(`[WebsiteCrawl] Found ${provider} link on homepage: ${link}`); + result.menuUrl = link; + result.provider = provider; + return result; + } + } + } + // Step 3: Find menu/order/shop links to follow + const menuLinks = homepageLinks.filter(link => { + // Must be same domain or a known provider domain + try { + const linkUrl = new URL(link); + const isSameDomain = linkUrl.hostname === baseUrl.hostname || + linkUrl.hostname.endsWith(`.${baseUrl.hostname}`); + const isProviderDomain = PROVIDER_URL_PATTERNS.some(({ patterns }) => patterns.some(p => p.test(link))); + const isMenuPath = MENU_LINK_PATTERNS.some(p => p.test(link)); + return (isSameDomain && isMenuPath) || isProviderDomain; + } + catch { + return false; + } + }); + console.log(`[WebsiteCrawl] Found ${menuLinks.length} potential menu links to follow`); + // Step 4: Follow menu links (limit to 3 to avoid excessive crawling) + for (const menuLink of menuLinks.slice(0, 3)) { + // Skip if we've already crawled this page + if (result.crawledPages.includes(menuLink)) + continue; + // Check if this link itself is a provider URL + for (const { provider, patterns } of PROVIDER_URL_PATTERNS) { + if (patterns.some(p => p.test(menuLink))) { + console.log(`[WebsiteCrawl] Menu link is a ${provider} URL: ${menuLink}`); + result.menuUrl = menuLink; + result.provider = provider; + return result; + } + } + result.crawledPages.push(menuLink); + // Rate limit + await new Promise(r => setTimeout(r, 500)); + const { links: pageLinks, error: pageError } = await fetchPageLinks(menuLink); + if (pageError) { + console.log(`[WebsiteCrawl] Failed to fetch ${menuLink}: ${pageError}`); + continue; + } + result.foundLinks.push(...pageLinks); + // Check for provider matches on this page + for (const link of pageLinks) { + for (const { provider, patterns } of PROVIDER_URL_PATTERNS) { + if (patterns.some(p => p.test(link))) { + console.log(`[WebsiteCrawl] Found ${provider} link on ${menuLink}: ${link}`); + result.menuUrl = link; + result.provider = provider; + return result; + } + } + } + } + console.log(`[WebsiteCrawl] No menu provider found on ${websiteUrl}`); + return result; +} +// ============================================================ +// CORE DETECTION FUNCTIONS +// ============================================================ +/** + * Detect menu provider from a URL + */ +function detectProviderFromUrl(menuUrl) { + if (!menuUrl) + return 'unknown'; + for (const { provider, patterns } of PROVIDER_URL_PATTERNS) { + for (const pattern of patterns) { + if (pattern.test(menuUrl)) { + return provider; + } + } + } + // Check if it's a custom website (has a domain but doesn't match known providers) + try { + const url = new URL(menuUrl); + if (url.hostname && !url.hostname.includes('localhost')) { + return 'custom'; + } + } + catch { + // Invalid URL + } + return 'unknown'; +} +/** + * Detect provider and resolve platform ID for a single dispensary + */ +async function detectAndResolveDispensary(dispensaryId) { + console.log(`[MenuDetection] Processing dispensary ${dispensaryId}...`); + // Get dispensary record + const { rows } = await (0, connection_1.query)(`SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE id = $1`, [dispensaryId]); + if (rows.length === 0) { + return { + dispensaryId, + dispensaryName: 'Unknown', + previousMenuType: null, + detectedProvider: 'unknown', + cName: null, + platformDispensaryId: null, + success: false, + error: 'Dispensary not found', + }; + } + const dispensary = (0, discovery_1.mapDbRowToDispensary)(rows[0]); + let menuUrl = dispensary.menuUrl; + const previousMenuType = dispensary.menuType || null; + const website = dispensary.website; + // ============================================================ + // CURALEAF CHECK: If website is Curaleaf, override any stale Dutchie menu_url + // This prevents 60s Dutchie timeouts for stores that have migrated to Curaleaf's platform + // ============================================================ + if (isCuraleafUrl(website)) { + console.log(`[MenuDetection] ${dispensary.name}: Website is Curaleaf - marking as curaleaf provider`); + // Use the Curaleaf website URL as the menu_url (clearing stale Dutchie URL if any) + // At this point we know website is defined since isCuraleafUrl returned true + const curaleafUrl = extractCuraleafStoreUrl(website) || website; + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = 'curaleaf', + menu_url = $1, + platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', 'curaleaf'::text, + 'detection_method', 'website_pattern'::text, + 'detected_at', NOW(), + 'curaleaf_store_url', $1::text, + 'stale_dutchie_url', $2::text, + 'not_crawlable', true, + 'not_crawlable_reason', 'Curaleaf proprietary menu - no Dutchie integration'::text + ), + updated_at = NOW() + WHERE id = $3 + `, [curaleafUrl, menuUrl || null, dispensaryId]); + return { + dispensaryId, + dispensaryName: dispensary.name, + previousMenuType, + detectedProvider: 'curaleaf', + cName: null, + platformDispensaryId: null, + success: true, + error: undefined, + }; + } + // If menu_url is null or empty, try to discover it by crawling the dispensary website + if (!menuUrl || menuUrl.trim() === '') { + console.log(`[MenuDetection] ${dispensary.name}: No menu_url - attempting website crawl`); + // Check if website is available + if (!website || website.trim() === '') { + console.log(`[MenuDetection] ${dispensary.name}: No website available - marking as not crawlable`); + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = 'unknown', + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', 'unknown'::text, + 'detection_method', 'no_data'::text, + 'detected_at', NOW(), + 'resolution_error', 'No menu_url and no website available'::text, + 'not_crawlable', true, + 'website_crawl_attempted', false + ), + updated_at = NOW() + WHERE id = $1 + `, [dispensaryId]); + return { + dispensaryId, + dispensaryName: dispensary.name, + previousMenuType, + detectedProvider: 'unknown', + cName: null, + platformDispensaryId: null, + success: true, + error: 'No menu_url and no website available - marked as not crawlable', + }; + } + // Crawl the website to find menu provider links + console.log(`[MenuDetection] ${dispensary.name}: Crawling website ${website} for menu links...`); + const crawlResult = await crawlWebsiteForMenuLinks(website); + if (crawlResult.menuUrl && crawlResult.provider !== 'unknown') { + // SUCCESS: Found a menu URL from website crawl! + console.log(`[MenuDetection] ${dispensary.name}: Found ${crawlResult.provider} menu at ${crawlResult.menuUrl}`); + menuUrl = crawlResult.menuUrl; + // Update the dispensary with the discovered menu_url + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_url = $1, + menu_type = $2, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', $2::text, + 'detection_method', 'website_crawl'::text, + 'detected_at', NOW(), + 'website_crawled', $3::text, + 'website_crawl_pages', $4::jsonb, + 'not_crawlable', false + ), + updated_at = NOW() + WHERE id = $5 + `, [ + crawlResult.menuUrl, + crawlResult.provider, + website, + JSON.stringify(crawlResult.crawledPages), + dispensaryId + ]); + // Continue with full detection flow using the discovered menu_url + } + else { + // Website crawl failed to find a menu provider + const errorReason = crawlResult.error || 'No menu provider links found on website'; + console.log(`[MenuDetection] ${dispensary.name}: Website crawl failed - ${errorReason}`); + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = 'unknown', + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', 'unknown'::text, + 'detection_method', 'website_crawl'::text, + 'detected_at', NOW(), + 'website_crawled', $1::text, + 'website_crawl_pages', $2::jsonb, + 'resolution_error', $3::text, + 'not_crawlable', true + ), + updated_at = NOW() + WHERE id = $4 + `, [ + website, + JSON.stringify(crawlResult.crawledPages), + errorReason, + dispensaryId + ]); + return { + dispensaryId, + dispensaryName: dispensary.name, + previousMenuType, + detectedProvider: 'unknown', + cName: null, + platformDispensaryId: null, + success: true, + error: `Website crawl failed: ${errorReason}`, + }; + } + } + // Detect provider from URL + const detectedProvider = detectProviderFromUrl(menuUrl); + console.log(`[MenuDetection] ${dispensary.name}: Detected provider = ${detectedProvider} from URL: ${menuUrl}`); + // Initialize result + const result = { + dispensaryId, + dispensaryName: dispensary.name, + previousMenuType, + detectedProvider, + cName: null, + platformDispensaryId: null, + success: false, + }; + // If not dutchie, just update menu_type and return + if (detectedProvider !== 'dutchie') { + // Special handling for proprietary providers - mark as not_crawlable until we have crawlers + const PROPRIETARY_PROVIDERS = ['curaleaf', 'sol']; + const isProprietaryProvider = PROPRIETARY_PROVIDERS.includes(detectedProvider); + const notCrawlableReason = isProprietaryProvider + ? `${detectedProvider} proprietary menu - no crawler available` + : null; + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = $1, + platform_dispensary_id = CASE WHEN $3 THEN NULL ELSE platform_dispensary_id END, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', $1::text, + 'detection_method', 'url_pattern'::text, + 'detected_at', NOW(), + 'not_crawlable', $3, + 'not_crawlable_reason', $4::text + ), + updated_at = NOW() + WHERE id = $2 + `, [detectedProvider, dispensaryId, isProprietaryProvider, notCrawlableReason]); + result.success = true; + console.log(`[MenuDetection] ${dispensary.name}: Updated menu_type to ${detectedProvider}${isProprietaryProvider ? ' (not crawlable)' : ''}`); + return result; + } + // For dutchie: extract cName and resolve platform ID + const cName = (0, discovery_1.extractCNameFromMenuUrl)(menuUrl); + result.cName = cName; + if (!cName) { + result.error = `Could not extract cName from menu_url: ${menuUrl}`; + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = 'dutchie', + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', 'dutchie'::text, + 'detection_method', 'url_pattern'::text, + 'detected_at', NOW(), + 'resolution_error', $1::text, + 'not_crawlable', true + ), + updated_at = NOW() + WHERE id = $2 + `, [result.error, dispensaryId]); + console.log(`[MenuDetection] ${dispensary.name}: ${result.error}`); + return result; + } + // Resolve platform_dispensary_id from cName + console.log(`[MenuDetection] ${dispensary.name}: Resolving platform ID for cName = ${cName}`); + try { + const platformId = await (0, graphql_client_1.resolveDispensaryId)(cName); + if (platformId) { + result.platformDispensaryId = platformId; + result.success = true; + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = 'dutchie', + platform_dispensary_id = $1, + platform_dispensary_id_resolved_at = NOW(), + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', 'dutchie'::text, + 'detection_method', 'url_pattern'::text, + 'detected_at', NOW(), + 'cname_extracted', $2::text, + 'platform_id_resolved', true, + 'resolution_error', NULL::text, + 'not_crawlable', false + ), + updated_at = NOW() + WHERE id = $3 + `, [platformId, cName, dispensaryId]); + console.log(`[MenuDetection] ${dispensary.name}: Resolved platform ID = ${platformId}`); + } + else { + result.error = `cName "${cName}" could not be resolved - may not exist on Dutchie`; + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = 'dutchie', + platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', 'dutchie'::text, + 'detection_method', 'url_pattern'::text, + 'detected_at', NOW(), + 'cname_extracted', $1::text, + 'platform_id_resolved', false, + 'resolution_error', $2::text, + 'not_crawlable', true + ), + updated_at = NOW() + WHERE id = $3 + `, [cName, result.error, dispensaryId]); + console.log(`[MenuDetection] ${dispensary.name}: ${result.error}`); + } + } + catch (error) { + result.error = `Resolution failed: ${error.message}`; + await (0, connection_1.query)(` + UPDATE dispensaries SET + menu_type = 'dutchie', + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', 'dutchie'::text, + 'detection_method', 'url_pattern'::text, + 'detected_at', NOW(), + 'cname_extracted', $1::text, + 'platform_id_resolved', false, + 'resolution_error', $2::text, + 'not_crawlable', true + ), + updated_at = NOW() + WHERE id = $3 + `, [cName, result.error, dispensaryId]); + console.error(`[MenuDetection] ${dispensary.name}: ${result.error}`); + } + return result; +} +/** + * Run bulk detection on all dispensaries with unknown/missing menu_type or platform_dispensary_id + * Also includes dispensaries with no menu_url but with a website (for website crawl discovery) + */ +async function runBulkDetection(options = {}) { + const { state, onlyUnknown = true, onlyMissingPlatformId = false, includeWebsiteCrawl = true, limit } = options; + console.log('[MenuDetection] Starting bulk detection...'); + // Build query to find dispensaries needing detection + // Now includes: dispensaries with menu_url OR (no menu_url but has website and not already marked not_crawlable) + let whereClause = `WHERE ( + menu_url IS NOT NULL + ${includeWebsiteCrawl ? `OR ( + menu_url IS NULL + AND website IS NOT NULL + AND website != '' + AND (provider_detection_data IS NULL OR NOT (provider_detection_data->>'not_crawlable')::boolean) + )` : ''} + )`; + const params = []; + let paramIndex = 1; + if (state) { + whereClause += ` AND state = $${paramIndex++}`; + params.push(state); + } + if (onlyUnknown) { + whereClause += ` AND (menu_type IS NULL OR menu_type = '' OR menu_type = 'unknown')`; + } + if (onlyMissingPlatformId) { + whereClause += ` AND (menu_type = 'dutchie' AND platform_dispensary_id IS NULL)`; + } + let query_str = ` + SELECT ${DISPENSARY_COLUMNS} FROM dispensaries + ${whereClause} + ORDER BY name + `; + if (limit) { + query_str += ` LIMIT $${paramIndex}`; + params.push(limit); + } + const { rows: dispensaries } = await (0, connection_1.query)(query_str, params); + console.log(`[MenuDetection] Found ${dispensaries.length} dispensaries to process (includeWebsiteCrawl=${includeWebsiteCrawl})`); + const result = { + totalProcessed: 0, + totalSucceeded: 0, + totalFailed: 0, + totalSkipped: 0, + results: [], + errors: [], + }; + for (const row of dispensaries) { + result.totalProcessed++; + try { + const detectionResult = await detectAndResolveDispensary(row.id); + result.results.push(detectionResult); + if (detectionResult.success) { + result.totalSucceeded++; + } + else { + result.totalFailed++; + if (detectionResult.error) { + result.errors.push(`${detectionResult.dispensaryName}: ${detectionResult.error}`); + } + } + // Rate limit between requests + await new Promise(r => setTimeout(r, 1000)); + } + catch (error) { + result.totalFailed++; + result.errors.push(`${row.name || row.id}: ${error.message}`); + } + } + console.log(`[MenuDetection] Bulk detection complete: ${result.totalSucceeded} succeeded, ${result.totalFailed} failed`); + return result; +} +// ============================================================ +// SCHEDULED JOB EXECUTOR +// ============================================================ +/** + * Execute the menu detection job (called by scheduler) + */ +async function executeMenuDetectionJob(config = {}) { + const state = config.state || 'AZ'; + const onlyUnknown = config.onlyUnknown !== false; + const onlyMissingPlatformId = config.onlyMissingPlatformId || false; + console.log(`[MenuDetection] Executing scheduled job for state=${state}...`); + try { + const result = await runBulkDetection({ + state, + onlyUnknown, + onlyMissingPlatformId, + }); + const status = result.totalFailed === 0 ? 'success' : + result.totalSucceeded === 0 ? 'error' : 'partial'; + return { + status, + itemsProcessed: result.totalProcessed, + itemsSucceeded: result.totalSucceeded, + itemsFailed: result.totalFailed, + errorMessage: result.errors.length > 0 ? result.errors.slice(0, 5).join('; ') : undefined, + metadata: { + state, + onlyUnknown, + onlyMissingPlatformId, + providerCounts: countByProvider(result.results), + }, + }; + } + catch (error) { + return { + status: 'error', + itemsProcessed: 0, + itemsSucceeded: 0, + itemsFailed: 0, + errorMessage: error.message, + }; + } +} +/** + * Count results by detected provider + */ +function countByProvider(results) { + const counts = {}; + for (const r of results) { + counts[r.detectedProvider] = (counts[r.detectedProvider] || 0) + 1; + } + return counts; +} +// ============================================================ +// UTILITY FUNCTIONS +// ============================================================ +/** + * Get detection stats for dashboard + */ +async function getDetectionStats() { + const { rows } = await (0, connection_1.query)(` + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE menu_type IS NOT NULL AND menu_type != '' AND menu_type != 'unknown') as with_menu_type, + COUNT(*) FILTER (WHERE platform_dispensary_id IS NOT NULL) as with_platform_id, + COUNT(*) FILTER (WHERE menu_url IS NOT NULL AND (menu_type IS NULL OR menu_type = '' OR menu_type = 'unknown')) as needs_detection + FROM dispensaries + WHERE state = 'AZ' + `); + const stats = rows[0] || {}; + // Get provider breakdown + const { rows: providerRows } = await (0, connection_1.query)(` + SELECT menu_type, COUNT(*) as count + FROM dispensaries + WHERE state = 'AZ' AND menu_type IS NOT NULL AND menu_type != '' + GROUP BY menu_type + ORDER BY count DESC + `); + const byProvider = {}; + for (const row of providerRows) { + byProvider[row.menu_type] = parseInt(row.count, 10); + } + return { + totalDispensaries: parseInt(stats.total || '0', 10), + withMenuType: parseInt(stats.with_menu_type || '0', 10), + withPlatformId: parseInt(stats.with_platform_id || '0', 10), + needsDetection: parseInt(stats.needs_detection || '0', 10), + byProvider, + }; +} +/** + * Get dispensaries needing detection + * Includes dispensaries with website but no menu_url for website crawl discovery + */ +async function getDispensariesNeedingDetection(options = {}) { + const { state = 'AZ', limit = 100, includeWebsiteCrawl = true } = options; + const { rows } = await (0, connection_1.query)(` + SELECT ${DISPENSARY_COLUMNS} FROM dispensaries + WHERE state = $1 + AND ( + (menu_url IS NOT NULL AND (menu_type IS NULL OR menu_type = '' OR menu_type = 'unknown' + OR (menu_type = 'dutchie' AND platform_dispensary_id IS NULL))) + ${includeWebsiteCrawl ? `OR ( + menu_url IS NULL + AND website IS NOT NULL + AND website != '' + AND (provider_detection_data IS NULL OR NOT (provider_detection_data->>'not_crawlable')::boolean) + )` : ''} + ) + ORDER BY name + LIMIT $2 + `, [state, limit]); + return rows.map(discovery_1.mapDbRowToDispensary); +} diff --git a/backend/dist/dutchie-az/services/product-crawler.js b/backend/dist/dutchie-az/services/product-crawler.js new file mode 100644 index 00000000..b831835d --- /dev/null +++ b/backend/dist/dutchie-az/services/product-crawler.js @@ -0,0 +1,843 @@ +"use strict"; +/** + * Dutchie AZ Product Crawler Service + * + * Crawls products from Dutchie dispensaries and stores them in the dutchie_az database. + * Handles normalization from GraphQL response to database entities. + * + * IMPORTANT: Uses chunked batch processing per CLAUDE.md Rule #15 to avoid OOM. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeProduct = normalizeProduct; +exports.normalizeSnapshot = normalizeSnapshot; +exports.crawlDispensaryProducts = crawlDispensaryProducts; +exports.crawlAllArizonaDispensaries = crawlAllArizonaDispensaries; +const connection_1 = require("../db/connection"); +const graphql_client_1 = require("./graphql-client"); +const discovery_1 = require("./discovery"); +const types_1 = require("../types"); +const image_storage_1 = require("../../utils/image-storage"); +// Explicit column list for dispensaries table (avoids SELECT * issues with schema differences) +const DISPENSARY_COLUMNS = ` + id, name, slug, city, state, zip, address, latitude, longitude, + menu_type, menu_url, platform_dispensary_id, website, + provider_detection_data, created_at, updated_at +`; +// ============================================================ +// BATCH PROCESSING CONFIGURATION +// ============================================================ +/** Chunk size for batch DB writes (per CLAUDE.md Rule #15) */ +const BATCH_CHUNK_SIZE = 100; +// ============================================================ +// NORMALIZATION FUNCTIONS +// ============================================================ +/** + * Convert price to cents + */ +function toCents(price) { + if (price === undefined || price === null) + return undefined; + return Math.round(price * 100); +} +/** + * Get min value from array of numbers + */ +function getMin(arr) { + if (!arr || arr.length === 0) + return undefined; + return Math.min(...arr.filter((n) => n !== null && n !== undefined)); +} +/** + * Get max value from array of numbers + */ +function getMax(arr) { + if (!arr || arr.length === 0) + return undefined; + return Math.max(...arr.filter((n) => n !== null && n !== undefined)); +} +/** + * Normalize a value to boolean + * Handles Dutchie API returning {} or [] or other non-boolean values + * that would cause "invalid input syntax for type boolean" errors + */ +function normBool(v, defaultVal = false) { + if (v === true) + return true; + if (v === false) + return false; + // Log unexpected object/array values once for debugging + if (v !== null && v !== undefined && typeof v === 'object') { + console.warn(`[normBool] Unexpected object value, coercing to ${defaultVal}:`, JSON.stringify(v)); + } + return defaultVal; +} +/** + * Normalize a value to Date or undefined + * Handles Dutchie API returning {} or [] or other non-date values + * that would cause "invalid input syntax for type timestamp" errors + */ +function normDate(v) { + if (!v) + return undefined; + // Reject objects/arrays that aren't dates + if (typeof v === 'object' && !(v instanceof Date)) { + console.warn(`[normDate] Unexpected object value, ignoring:`, JSON.stringify(v)); + return undefined; + } + // Try parsing + const d = new Date(v); + if (isNaN(d.getTime())) { + console.warn(`[normDate] Invalid date value, ignoring:`, v); + return undefined; + } + return d; +} +/** + * Extract cName (Dutchie slug) from menuUrl or dispensary slug + * Handles URL formats: + * - https://dutchie.com/embedded-menu/AZ-Deeply-Rooted -> AZ-Deeply-Rooted + * - https://dutchie.com/dispensary/sol-flower-dispensary-mcclintock -> sol-flower-dispensary-mcclintock + * Falls back to dispensary.slug if menuUrl extraction fails + */ +function extractCName(dispensary) { + if (dispensary.menuUrl) { + try { + const url = new URL(dispensary.menuUrl); + // Extract last path segment: /embedded-menu/X or /dispensary/X + const segments = url.pathname.split('/').filter(Boolean); + if (segments.length >= 2) { + const cName = segments[segments.length - 1]; + if (cName) { + console.log(`[ProductCrawler] Extracted cName "${cName}" from menuUrl`); + return cName; + } + } + } + catch (e) { + console.warn(`[ProductCrawler] Failed to parse menuUrl: ${dispensary.menuUrl}`); + } + } + // Fallback to slug + console.log(`[ProductCrawler] Using dispensary slug "${dispensary.slug}" as cName`); + return dispensary.slug; +} +/** + * Normalize a POSMetaData.children entry to DutchieProductOptionSnapshot + */ +function normalizeOption(child) { + return { + optionId: child.canonicalID || child.canonicalPackageId || child.canonicalSKU || child.option || 'unknown', + canonicalId: child.canonicalID, + canonicalPackageId: child.canonicalPackageId, + canonicalSKU: child.canonicalSKU, + canonicalName: child.canonicalName, + canonicalCategory: child.canonicalCategory, + canonicalCategoryId: child.canonicalCategoryId, + canonicalBrandId: child.canonicalBrandId, + canonicalBrandName: child.canonicalBrandName, + canonicalStrainId: child.canonicalStrainId, + canonicalVendorId: child.canonicalVendorId, + optionLabel: child.option, + packageQuantity: child.packageQuantity, + recEquivalent: child.recEquivalent, + standardEquivalent: child.standardEquivalent, + priceCents: toCents(child.price), + recPriceCents: toCents(child.recPrice), + medPriceCents: toCents(child.medPrice), + quantity: child.quantity, + quantityAvailable: child.quantityAvailable, + kioskQuantityAvailable: child.kioskQuantityAvailable, + activeBatchTags: child.activeBatchTags, + canonicalImgUrl: child.canonicalImgUrl, + canonicalLabResultUrl: child.canonicalLabResultUrl, + canonicalEffectivePotencyMg: child.canonicalEffectivePotencyMg, + rawChildPayload: child, + }; +} +/** + * Normalize a raw Dutchie product to DutchieProduct (canonical identity) + */ +function normalizeProduct(raw, dispensaryId, platformDispensaryId) { + return { + dispensaryId, + platform: 'dutchie', + externalProductId: raw._id || raw.id || '', + platformDispensaryId, + cName: raw.cName, + name: raw.Name, + // Brand + brandName: raw.brandName || raw.brand?.name, + brandId: raw.brandId || raw.brand?.id, + brandLogoUrl: raw.brandLogo || raw.brand?.imageUrl, + // Classification + type: raw.type, + subcategory: raw.subcategory, + strainType: raw.strainType, + provider: raw.provider, + // Potency + thc: raw.THC, + thcContent: raw.THCContent?.range?.[0], + cbd: raw.CBD, + cbdContent: raw.CBDContent?.range?.[0], + cannabinoidsV2: raw.cannabinoidsV2, + effects: raw.effects, + // Status / flags + status: raw.Status, + medicalOnly: normBool(raw.medicalOnly, false), + recOnly: normBool(raw.recOnly, false), + featured: normBool(raw.featured, false), + comingSoon: normBool(raw.comingSoon, false), + certificateOfAnalysisEnabled: normBool(raw.certificateOfAnalysisEnabled, false), + isBelowThreshold: normBool(raw.isBelowThreshold, false), + isBelowKioskThreshold: normBool(raw.isBelowKioskThreshold, false), + optionsBelowThreshold: normBool(raw.optionsBelowThreshold, false), + optionsBelowKioskThreshold: normBool(raw.optionsBelowKioskThreshold, false), + // Derived stock status + stockStatus: (0, types_1.deriveStockStatus)(raw), + totalQuantityAvailable: (0, types_1.calculateTotalQuantity)(raw), + // Images + primaryImageUrl: raw.Image || raw.images?.[0]?.url, + images: raw.images, + // Misc + measurements: raw.measurements, + weight: typeof raw.weight === 'number' ? String(raw.weight) : raw.weight, + pastCNames: raw.pastCNames, + createdAtDutchie: normDate(raw.createdAt), + updatedAtDutchie: normDate(raw.updatedAt), + latestRawPayload: raw, + }; +} +/** + * Normalize a raw Dutchie product to DutchieProductSnapshot (time-series data) + */ +function normalizeSnapshot(raw, dutchieProductId, dispensaryId, platformDispensaryId, pricingType, crawlMode = 'mode_a') { + const children = raw.POSMetaData?.children || []; + const options = children.map(normalizeOption); + // Aggregate prices from various sources + const recPrices = raw.recPrices || []; + const medPrices = raw.medicalPrices || []; + const recSpecialPrices = raw.recSpecialPrices || []; + const medSpecialPrices = raw.medicalSpecialPrices || []; + const wholesalePrices = raw.wholesalePrices || []; + // Also consider child prices + const childRecPrices = children.map((c) => c.recPrice).filter((p) => p !== undefined); + const childMedPrices = children.map((c) => c.medPrice).filter((p) => p !== undefined); + const childPrices = children.map((c) => c.price).filter((p) => p !== undefined); + // Aggregate inventory - use calculateTotalQuantity for proper null handling + const totalQty = (0, types_1.calculateTotalQuantity)(raw); + const hasAnyKioskQty = children.some(c => typeof c.kioskQuantityAvailable === 'number'); + const totalKioskQty = hasAnyKioskQty + ? children.reduce((sum, c) => sum + (c.kioskQuantityAvailable || 0), 0) + : null; + // Determine if on special + const isOnSpecial = raw.special === true || + (raw.specialData?.saleSpecials && raw.specialData.saleSpecials.length > 0) || + (recSpecialPrices.length > 0 && recSpecialPrices[0] !== null) || + (medSpecialPrices.length > 0 && medSpecialPrices[0] !== null); + return { + dutchieProductId, + dispensaryId, + platformDispensaryId, + externalProductId: raw._id || raw.id || '', + pricingType, + crawlMode, + status: raw.Status, + featured: normBool(raw.featured, false), + special: normBool(isOnSpecial, false), + medicalOnly: normBool(raw.medicalOnly, false), + recOnly: normBool(raw.recOnly, false), + // Product was present in feed + isPresentInFeed: true, + // Derived stock status + stockStatus: (0, types_1.deriveStockStatus)(raw), + // Price summary + recMinPriceCents: toCents(getMin([...recPrices, ...childRecPrices, ...childPrices])), + recMaxPriceCents: toCents(getMax([...recPrices, ...childRecPrices, ...childPrices])), + recMinSpecialPriceCents: toCents(getMin(recSpecialPrices)), + medMinPriceCents: toCents(getMin([...medPrices, ...childMedPrices])), + medMaxPriceCents: toCents(getMax([...medPrices, ...childMedPrices])), + medMinSpecialPriceCents: toCents(getMin(medSpecialPrices)), + wholesaleMinPriceCents: toCents(getMin(wholesalePrices)), + // Inventory summary - null = unknown, 0 = all OOS + totalQuantityAvailable: totalQty, + totalKioskQuantityAvailable: totalKioskQty, + manualInventory: normBool(raw.manualInventory, false), + isBelowThreshold: normBool(raw.isBelowThreshold, false), + isBelowKioskThreshold: normBool(raw.isBelowKioskThreshold, false), + options, + rawPayload: raw, + crawledAt: new Date(), + }; +} +// ============================================================ +// DATABASE OPERATIONS +// ============================================================ +/** + * Upsert a DutchieProduct record + */ +async function upsertProduct(product) { + const result = await (0, connection_1.query)(` + INSERT INTO dutchie_products ( + dispensary_id, platform, external_product_id, platform_dispensary_id, + c_name, name, brand_name, brand_id, brand_logo_url, + type, subcategory, strain_type, provider, + thc, thc_content, cbd, cbd_content, cannabinoids_v2, effects, + status, medical_only, rec_only, featured, coming_soon, certificate_of_analysis_enabled, + is_below_threshold, is_below_kiosk_threshold, options_below_threshold, options_below_kiosk_threshold, + stock_status, total_quantity_available, + primary_image_url, images, measurements, weight, past_c_names, + created_at_dutchie, updated_at_dutchie, latest_raw_payload, updated_at + ) VALUES ( + $1, $2, $3, $4, + $5, $6, $7, $8, $9, + $10, $11, $12, $13, + $14, $15, $16, $17, $18, $19, + $20, $21, $22, $23, $24, $25, + $26, $27, $28, $29, + $30, $31, + $32, $33, $34, $35, $36, + $37, $38, $39, NOW() + ) + ON CONFLICT (dispensary_id, external_product_id) DO UPDATE SET + c_name = EXCLUDED.c_name, + name = EXCLUDED.name, + brand_name = EXCLUDED.brand_name, + brand_id = EXCLUDED.brand_id, + brand_logo_url = EXCLUDED.brand_logo_url, + type = EXCLUDED.type, + subcategory = EXCLUDED.subcategory, + strain_type = EXCLUDED.strain_type, + provider = EXCLUDED.provider, + thc = EXCLUDED.thc, + thc_content = EXCLUDED.thc_content, + cbd = EXCLUDED.cbd, + cbd_content = EXCLUDED.cbd_content, + cannabinoids_v2 = EXCLUDED.cannabinoids_v2, + effects = EXCLUDED.effects, + status = EXCLUDED.status, + medical_only = EXCLUDED.medical_only, + rec_only = EXCLUDED.rec_only, + featured = EXCLUDED.featured, + coming_soon = EXCLUDED.coming_soon, + certificate_of_analysis_enabled = EXCLUDED.certificate_of_analysis_enabled, + is_below_threshold = EXCLUDED.is_below_threshold, + is_below_kiosk_threshold = EXCLUDED.is_below_kiosk_threshold, + options_below_threshold = EXCLUDED.options_below_threshold, + options_below_kiosk_threshold = EXCLUDED.options_below_kiosk_threshold, + stock_status = EXCLUDED.stock_status, + total_quantity_available = EXCLUDED.total_quantity_available, + primary_image_url = EXCLUDED.primary_image_url, + images = EXCLUDED.images, + measurements = EXCLUDED.measurements, + weight = EXCLUDED.weight, + past_c_names = EXCLUDED.past_c_names, + created_at_dutchie = EXCLUDED.created_at_dutchie, + updated_at_dutchie = EXCLUDED.updated_at_dutchie, + latest_raw_payload = EXCLUDED.latest_raw_payload, + updated_at = NOW() + RETURNING id + `, [ + product.dispensaryId, + product.platform, + product.externalProductId, + product.platformDispensaryId, + product.cName, + product.name, + product.brandName, + product.brandId, + product.brandLogoUrl, + product.type, + product.subcategory, + product.strainType, + product.provider, + product.thc, + product.thcContent, + product.cbd, + product.cbdContent, + product.cannabinoidsV2 ? JSON.stringify(product.cannabinoidsV2) : null, + product.effects ? JSON.stringify(product.effects) : null, + product.status, + product.medicalOnly, + product.recOnly, + product.featured, + product.comingSoon, + product.certificateOfAnalysisEnabled, + product.isBelowThreshold, + product.isBelowKioskThreshold, + product.optionsBelowThreshold, + product.optionsBelowKioskThreshold, + product.stockStatus, + product.totalQuantityAvailable, + product.primaryImageUrl, + product.images ? JSON.stringify(product.images) : null, + product.measurements ? JSON.stringify(product.measurements) : null, + product.weight, + product.pastCNames, + product.createdAtDutchie, + product.updatedAtDutchie, + product.latestRawPayload ? JSON.stringify(product.latestRawPayload) : null, + ]); + return result.rows[0].id; +} +/** + * Download product image and update local image URLs + * Skips download if local image already exists for this product+URL combo + */ +async function downloadAndUpdateProductImage(productId, dispensaryId, externalProductId, primaryImageUrl) { + if (!primaryImageUrl) { + return { downloaded: false, error: 'No image URL' }; + } + try { + // Check if we already have this image locally + const exists = await (0, image_storage_1.imageExists)(dispensaryId, externalProductId, primaryImageUrl); + if (exists) { + return { downloaded: false }; + } + // Download and process the image + const result = await (0, image_storage_1.downloadProductImage)(primaryImageUrl, dispensaryId, externalProductId); + if (!result.success || !result.urls) { + return { downloaded: false, error: result.error }; + } + // Update the product record with local image URLs + await (0, connection_1.query)(` + UPDATE dutchie_products + SET + local_image_url = $1, + local_image_thumb_url = $2, + local_image_medium_url = $3, + original_image_url = COALESCE(original_image_url, primary_image_url), + updated_at = NOW() + WHERE id = $4 + `, [result.urls.full, result.urls.thumb, result.urls.medium, productId]); + return { downloaded: true }; + } + catch (error) { + return { downloaded: false, error: error.message }; + } +} +/** + * Insert a snapshot record + */ +async function insertSnapshot(snapshot) { + const result = await (0, connection_1.query)(` + INSERT INTO dutchie_product_snapshots ( + dutchie_product_id, dispensary_id, platform_dispensary_id, external_product_id, + pricing_type, crawl_mode, status, featured, special, medical_only, rec_only, + is_present_in_feed, stock_status, + rec_min_price_cents, rec_max_price_cents, rec_min_special_price_cents, + med_min_price_cents, med_max_price_cents, med_min_special_price_cents, + wholesale_min_price_cents, + total_quantity_available, total_kiosk_quantity_available, manual_inventory, + is_below_threshold, is_below_kiosk_threshold, + options, raw_payload, crawled_at + ) VALUES ( + $1, $2, $3, $4, + $5, $6, $7, $8, $9, $10, $11, + $12, $13, + $14, $15, $16, + $17, $18, $19, + $20, + $21, $22, $23, + $24, $25, + $26, $27, $28 + ) + RETURNING id + `, [ + snapshot.dutchieProductId, + snapshot.dispensaryId, + snapshot.platformDispensaryId, + snapshot.externalProductId, + snapshot.pricingType, + snapshot.crawlMode, + snapshot.status, + snapshot.featured, + snapshot.special, + snapshot.medicalOnly, + snapshot.recOnly, + snapshot.isPresentInFeed ?? true, + snapshot.stockStatus, + snapshot.recMinPriceCents, + snapshot.recMaxPriceCents, + snapshot.recMinSpecialPriceCents, + snapshot.medMinPriceCents, + snapshot.medMaxPriceCents, + snapshot.medMinSpecialPriceCents, + snapshot.wholesaleMinPriceCents, + snapshot.totalQuantityAvailable, + snapshot.totalKioskQuantityAvailable, + snapshot.manualInventory, + snapshot.isBelowThreshold, + snapshot.isBelowKioskThreshold, + JSON.stringify(snapshot.options || []), + JSON.stringify(snapshot.rawPayload || {}), + snapshot.crawledAt, + ]); + return result.rows[0].id; +} +// ============================================================ +// BATCH DATABASE OPERATIONS (per CLAUDE.md Rule #15) +// ============================================================ +/** + * Helper to chunk an array into smaller arrays + */ +function chunkArray(array, size) { + const chunks = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +} +/** + * Batch upsert products - processes in chunks to avoid OOM + * Returns a Map of externalProductId -> database id + */ +async function batchUpsertProducts(products) { + const productIdMap = new Map(); + const chunks = chunkArray(products, BATCH_CHUNK_SIZE); + console.log(`[ProductCrawler] Batch upserting ${products.length} products in ${chunks.length} chunks of ${BATCH_CHUNK_SIZE}...`); + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + // Process each product in the chunk + for (const product of chunk) { + try { + const id = await upsertProduct(product); + if (product.externalProductId) { + productIdMap.set(product.externalProductId, id); + } + } + catch (error) { + console.error(`[ProductCrawler] Error upserting product ${product.externalProductId}:`, error.message); + } + } + // Log progress + if ((i + 1) % 5 === 0 || i === chunks.length - 1) { + console.log(`[ProductCrawler] Upserted chunk ${i + 1}/${chunks.length} (${productIdMap.size} products so far)`); + } + } + return productIdMap; +} +/** + * Batch insert snapshots - processes in chunks to avoid OOM + */ +async function batchInsertSnapshots(snapshots) { + const chunks = chunkArray(snapshots, BATCH_CHUNK_SIZE); + let inserted = 0; + console.log(`[ProductCrawler] Batch inserting ${snapshots.length} snapshots in ${chunks.length} chunks of ${BATCH_CHUNK_SIZE}...`); + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + // Process each snapshot in the chunk + for (const snapshot of chunk) { + try { + await insertSnapshot(snapshot); + inserted++; + } + catch (error) { + console.error(`[ProductCrawler] Error inserting snapshot for ${snapshot.externalProductId}:`, error.message); + } + } + // Log progress + if ((i + 1) % 5 === 0 || i === chunks.length - 1) { + console.log(`[ProductCrawler] Inserted snapshot chunk ${i + 1}/${chunks.length} (${inserted} snapshots so far)`); + } + } + return inserted; +} +/** + * Update dispensary last_crawled_at and product_count + */ +async function updateDispensaryCrawlStats(dispensaryId, productCount) { + // Update last_crawl_at to track when we last crawled + // Skip product_count as that column may not exist + await (0, connection_1.query)(` + UPDATE dispensaries + SET last_crawl_at = NOW(), updated_at = NOW() + WHERE id = $1 + `, [dispensaryId]); +} +/** + * Mark products as missing from feed + * Creates a snapshot with isPresentInFeed=false and stockStatus='missing_from_feed' + * for products that were NOT in the UNION of Mode A and Mode B product lists + * + * IMPORTANT: Uses UNION of both modes to avoid false positives + * If the union is empty (possible outage), we skip marking to avoid data corruption + */ +async function markMissingProducts(dispensaryId, platformDispensaryId, modeAProductIds, modeBProductIds, pricingType) { + // Build UNION of Mode A + Mode B product IDs + const unionProductIds = new Set([...Array.from(modeAProductIds), ...Array.from(modeBProductIds)]); + // OUTAGE DETECTION: If union is empty, something went wrong - don't mark anything as missing + if (unionProductIds.size === 0) { + console.warn('[ProductCrawler] OUTAGE DETECTED: Both Mode A and Mode B returned 0 products. Skipping missing product marking.'); + return 0; + } + // Get all existing products for this dispensary that were not in the UNION + const { rows: missingProducts } = await (0, connection_1.query)(` + SELECT id, external_product_id, name + FROM dutchie_products + WHERE dispensary_id = $1 + AND external_product_id NOT IN (SELECT unnest($2::text[])) + `, [dispensaryId, Array.from(unionProductIds)]); + if (missingProducts.length === 0) { + return 0; + } + console.log(`[ProductCrawler] Marking ${missingProducts.length} products as missing from feed (union of ${modeAProductIds.size} Mode A + ${modeBProductIds.size} Mode B = ${unionProductIds.size} unique)...`); + const crawledAt = new Date(); + // Build all missing snapshots first (per CLAUDE.md Rule #15 - batch writes) + const missingSnapshots = missingProducts.map(product => ({ + dutchieProductId: product.id, + dispensaryId, + platformDispensaryId, + externalProductId: product.external_product_id, + pricingType, + crawlMode: 'mode_a', // Use mode_a for missing snapshots (convention) + status: undefined, + featured: false, + special: false, + medicalOnly: false, + recOnly: false, + isPresentInFeed: false, + stockStatus: 'missing_from_feed', + totalQuantityAvailable: undefined, // null = unknown, not 0 + manualInventory: false, + isBelowThreshold: false, + isBelowKioskThreshold: false, + options: [], + rawPayload: { _missingFromFeed: true, lastKnownName: product.name }, + crawledAt, + })); + // Batch insert missing snapshots + const snapshotsInserted = await batchInsertSnapshots(missingSnapshots); + // Batch update product stock status in chunks + const productIds = missingProducts.map(p => p.id); + const productChunks = chunkArray(productIds, BATCH_CHUNK_SIZE); + console.log(`[ProductCrawler] Updating ${productIds.length} product statuses in ${productChunks.length} chunks...`); + for (const chunk of productChunks) { + await (0, connection_1.query)(` + UPDATE dutchie_products + SET stock_status = 'missing_from_feed', total_quantity_available = NULL, updated_at = NOW() + WHERE id = ANY($1::int[]) + `, [chunk]); + } + console.log(`[ProductCrawler] Marked ${snapshotsInserted} products as missing from feed`); + return snapshotsInserted; +} +/** + * Process a batch of products from a single crawl mode + * IMPORTANT: Stores ALL products, never filters before DB + * Uses chunked batch processing per CLAUDE.md Rule #15 to avoid OOM + * Returns the set of external product IDs that were processed + */ +async function processProducts(products, dispensary, pricingType, crawlMode, options = {}) { + const { downloadImages = true } = options; + const productIds = new Set(); + let imagesDownloaded = 0; + let imageErrors = 0; + console.log(`[ProductCrawler] Processing ${products.length} products using chunked batch processing...`); + // Step 1: Normalize all products and collect IDs + const normalizedProducts = []; + const rawByExternalId = new Map(); + for (const raw of products) { + const externalId = raw._id || raw.id || ''; + productIds.add(externalId); + rawByExternalId.set(externalId, raw); + const normalized = normalizeProduct(raw, dispensary.id, dispensary.platformDispensaryId); + normalizedProducts.push(normalized); + } + // Step 2: Batch upsert products (chunked) + const productIdMap = await batchUpsertProducts(normalizedProducts); + const upserted = productIdMap.size; + // Step 3: Create and batch insert snapshots (chunked) + // IMPORTANT: Do this BEFORE image downloads to ensure snapshots are created even if images fail + const snapshots = []; + for (const [externalId, productId] of Array.from(productIdMap.entries())) { + const raw = rawByExternalId.get(externalId); + if (raw) { + const snapshot = normalizeSnapshot(raw, productId, dispensary.id, dispensary.platformDispensaryId, pricingType, crawlMode); + snapshots.push(snapshot); + } + } + const snapshotsInserted = await batchInsertSnapshots(snapshots); + // Step 4: Download images in chunks (if enabled) + // This is done AFTER snapshots to ensure core data is saved even if image downloads fail + if (downloadImages) { + const imageChunks = chunkArray(Array.from(productIdMap.entries()), BATCH_CHUNK_SIZE); + console.log(`[ProductCrawler] Downloading images in ${imageChunks.length} chunks...`); + for (let i = 0; i < imageChunks.length; i++) { + const chunk = imageChunks[i]; + for (const [externalId, productId] of chunk) { + const normalized = normalizedProducts.find(p => p.externalProductId === externalId); + if (normalized?.primaryImageUrl) { + try { + const imageResult = await downloadAndUpdateProductImage(productId, dispensary.id, externalId, normalized.primaryImageUrl); + if (imageResult.downloaded) { + imagesDownloaded++; + } + else if (imageResult.error && imageResult.error !== 'No image URL') { + imageErrors++; + } + } + catch (error) { + imageErrors++; + } + } + } + if ((i + 1) % 5 === 0 || i === imageChunks.length - 1) { + console.log(`[ProductCrawler] Image download chunk ${i + 1}/${imageChunks.length} (${imagesDownloaded} downloaded, ${imageErrors} errors)`); + } + } + } + // Clear references to help GC + normalizedProducts.length = 0; + rawByExternalId.clear(); + return { upserted, snapshots: snapshotsInserted, productIds, imagesDownloaded, imageErrors }; +} +async function crawlDispensaryProducts(dispensary, pricingType = 'rec', options = {}) { + const { useBothModes = true, downloadImages = true, onProgress } = options; + const startTime = Date.now(); + if (!dispensary.platformDispensaryId) { + return { + success: false, + dispensaryId: dispensary.id, + productsFound: 0, + productsFetched: 0, + productsUpserted: 0, + snapshotsCreated: 0, + errorMessage: 'Missing platformDispensaryId', + durationMs: Date.now() - startTime, + }; + } + try { + console.log(`[ProductCrawler] Crawling ${dispensary.name} (${dispensary.platformDispensaryId})...`); + let totalUpserted = 0; + let totalSnapshots = 0; + let totalImagesDownloaded = 0; + let totalImageErrors = 0; + let modeAProducts = 0; + let modeBProducts = 0; + let missingMarked = 0; + // Track product IDs separately for each mode (needed for missing product detection) + const modeAProductIds = new Set(); + const modeBProductIds = new Set(); + // Extract cName for this specific dispensary (used for Puppeteer session & headers) + const cName = extractCName(dispensary); + console.log(`[ProductCrawler] Using cName="${cName}" for dispensary ${dispensary.name}`); + if (useBothModes) { + // Run two-mode crawl for maximum coverage + const bothResults = await (0, graphql_client_1.fetchAllProductsBothModes)(dispensary.platformDispensaryId, pricingType, { cName }); + modeAProducts = bothResults.modeA.products.length; + modeBProducts = bothResults.modeB.products.length; + console.log(`[ProductCrawler] Two-mode crawl: Mode A=${modeAProducts}, Mode B=${modeBProducts}, Merged=${bothResults.merged.products.length}`); + // Collect Mode A product IDs + for (const p of bothResults.modeA.products) { + modeAProductIds.add(p._id); + } + // Collect Mode B product IDs + for (const p of bothResults.modeB.products) { + modeBProductIds.add(p._id); + } + // Process MERGED products (includes options from both modes) + if (bothResults.merged.products.length > 0) { + const mergedResult = await processProducts(bothResults.merged.products, dispensary, pricingType, 'mode_a', // Use mode_a for merged products (convention) + { downloadImages }); + totalUpserted = mergedResult.upserted; + totalSnapshots = mergedResult.snapshots; + totalImagesDownloaded = mergedResult.imagesDownloaded; + totalImageErrors = mergedResult.imageErrors; + // Report progress + if (onProgress) { + await onProgress({ + productsFound: bothResults.merged.products.length, + productsUpserted: totalUpserted, + snapshotsCreated: totalSnapshots, + currentPage: 1, + totalPages: 1, + }); + } + } + } + else { + // Single mode crawl (Mode A only) + const { products, crawlMode } = await (0, graphql_client_1.fetchAllProducts)(dispensary.platformDispensaryId, pricingType, { crawlMode: 'mode_a', cName }); + modeAProducts = products.length; + // Collect Mode A product IDs + for (const p of products) { + modeAProductIds.add(p._id); + } + const result = await processProducts(products, dispensary, pricingType, crawlMode, { downloadImages }); + totalUpserted = result.upserted; + totalSnapshots = result.snapshots; + totalImagesDownloaded = result.imagesDownloaded; + totalImageErrors = result.imageErrors; + // Report progress + if (onProgress) { + await onProgress({ + productsFound: products.length, + productsUpserted: totalUpserted, + snapshotsCreated: totalSnapshots, + currentPage: 1, + totalPages: 1, + }); + } + } + // Mark products as missing using UNION of Mode A + Mode B + // The function handles outage detection (empty union = skip marking) + missingMarked = await markMissingProducts(dispensary.id, dispensary.platformDispensaryId, modeAProductIds, modeBProductIds, pricingType); + totalSnapshots += missingMarked; + // Update dispensary stats + await updateDispensaryCrawlStats(dispensary.id, totalUpserted); + console.log(`[ProductCrawler] Completed: ${totalUpserted} products, ${totalSnapshots} snapshots, ${missingMarked} marked missing, ${totalImagesDownloaded} images downloaded`); + const totalProductsFound = modeAProducts + modeBProducts; + return { + success: true, + dispensaryId: dispensary.id, + productsFound: totalProductsFound, + productsFetched: totalProductsFound, + productsUpserted: totalUpserted, + snapshotsCreated: totalSnapshots, + modeAProducts, + modeBProducts, + missingProductsMarked: missingMarked, + imagesDownloaded: totalImagesDownloaded, + imageErrors: totalImageErrors, + durationMs: Date.now() - startTime, + }; + } + catch (error) { + console.error(`[ProductCrawler] Failed to crawl ${dispensary.name}:`, error.message); + return { + success: false, + dispensaryId: dispensary.id, + productsFound: 0, + productsFetched: 0, + productsUpserted: 0, + snapshotsCreated: 0, + errorMessage: error.message, + durationMs: Date.now() - startTime, + }; + } +} +/** + * Crawl all Arizona dispensaries + */ +async function crawlAllArizonaDispensaries(pricingType = 'rec') { + const results = []; + // Get all AZ dispensaries with platform IDs + const { rows: rawRows } = await (0, connection_1.query)(` + SELECT ${DISPENSARY_COLUMNS} FROM dispensaries + WHERE state = 'AZ' AND menu_type = 'dutchie' AND platform_dispensary_id IS NOT NULL + ORDER BY id + `); + const dispensaries = rawRows.map(discovery_1.mapDbRowToDispensary); + console.log(`[ProductCrawler] Starting crawl of ${dispensaries.length} dispensaries...`); + for (const dispensary of dispensaries) { + const result = await crawlDispensaryProducts(dispensary, pricingType); + results.push(result); + // Delay between dispensaries + await new Promise((r) => setTimeout(r, 2000)); + } + const successful = results.filter((r) => r.success).length; + const totalProducts = results.reduce((sum, r) => sum + r.productsUpserted, 0); + const totalSnapshots = results.reduce((sum, r) => sum + r.snapshotsCreated, 0); + console.log(`[ProductCrawler] Completed: ${successful}/${dispensaries.length} stores, ${totalProducts} products, ${totalSnapshots} snapshots`); + return results; +} diff --git a/backend/dist/dutchie-az/services/scheduler.js b/backend/dist/dutchie-az/services/scheduler.js new file mode 100644 index 00000000..2911df96 --- /dev/null +++ b/backend/dist/dutchie-az/services/scheduler.js @@ -0,0 +1,595 @@ +"use strict"; +/** + * Dutchie AZ Scheduler Service + * + * Handles scheduled crawling with JITTER - no fixed intervals! + * Each job re-schedules itself with a NEW random offset after each run. + * This makes timing "wander" around the clock, avoiding detectable patterns. + * + * Jitter Logic: + * nextRunAt = lastRunAt + baseIntervalMinutes + random(-jitterMinutes, +jitterMinutes) + * + * Example: 4-hour base with ±30min jitter = runs anywhere from 3h30m to 4h30m apart + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.crawlSingleDispensary = void 0; +exports.getAllSchedules = getAllSchedules; +exports.getScheduleById = getScheduleById; +exports.createSchedule = createSchedule; +exports.updateSchedule = updateSchedule; +exports.deleteSchedule = deleteSchedule; +exports.getRunLogs = getRunLogs; +exports.startScheduler = startScheduler; +exports.stopScheduler = stopScheduler; +exports.getSchedulerStatus = getSchedulerStatus; +exports.triggerScheduleNow = triggerScheduleNow; +exports.initializeDefaultSchedules = initializeDefaultSchedules; +exports.triggerImmediateCrawl = triggerImmediateCrawl; +const connection_1 = require("../db/connection"); +const menu_detection_1 = require("./menu-detection"); +const job_queue_1 = require("./job-queue"); +// Scheduler poll interval (how often we check for due jobs) +const SCHEDULER_POLL_INTERVAL_MS = 60 * 1000; // 1 minute +// Track running state +let isSchedulerRunning = false; +let schedulerInterval = null; +// ============================================================ +// JITTER CALCULATION +// ============================================================ +/** + * Generate a random jitter value in minutes + * Returns a value between -jitterMinutes and +jitterMinutes + */ +function getRandomJitterMinutes(jitterMinutes) { + // random() returns [0, 1), we want [-jitter, +jitter] + return (Math.random() * 2 - 1) * jitterMinutes; +} +/** + * Calculate next run time with jitter + * nextRunAt = baseTime + baseIntervalMinutes + random(-jitter, +jitter) + */ +function calculateNextRunAt(baseTime, baseIntervalMinutes, jitterMinutes) { + const jitter = getRandomJitterMinutes(jitterMinutes); + const totalMinutes = baseIntervalMinutes + jitter; + const totalMs = totalMinutes * 60 * 1000; + return new Date(baseTime.getTime() + totalMs); +} +// ============================================================ +// DATABASE OPERATIONS +// ============================================================ +/** + * Get all job schedules + */ +async function getAllSchedules() { + const { rows } = await (0, connection_1.query)(` + SELECT + id, job_name, description, enabled, + base_interval_minutes, jitter_minutes, + last_run_at, last_status, last_error_message, last_duration_ms, + next_run_at, job_config, created_at, updated_at + FROM job_schedules + ORDER BY job_name + `); + return rows.map(row => ({ + id: row.id, + jobName: row.job_name, + description: row.description, + enabled: row.enabled, + baseIntervalMinutes: row.base_interval_minutes, + jitterMinutes: row.jitter_minutes, + lastRunAt: row.last_run_at, + lastStatus: row.last_status, + lastErrorMessage: row.last_error_message, + lastDurationMs: row.last_duration_ms, + nextRunAt: row.next_run_at, + jobConfig: row.job_config, + createdAt: row.created_at, + updatedAt: row.updated_at, + })); +} +/** + * Get a single schedule by ID + */ +async function getScheduleById(id) { + const { rows } = await (0, connection_1.query)(`SELECT * FROM job_schedules WHERE id = $1`, [id]); + if (rows.length === 0) + return null; + const row = rows[0]; + return { + id: row.id, + jobName: row.job_name, + description: row.description, + enabled: row.enabled, + baseIntervalMinutes: row.base_interval_minutes, + jitterMinutes: row.jitter_minutes, + lastRunAt: row.last_run_at, + lastStatus: row.last_status, + lastErrorMessage: row.last_error_message, + lastDurationMs: row.last_duration_ms, + nextRunAt: row.next_run_at, + jobConfig: row.job_config, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +} +/** + * Create a new schedule + */ +async function createSchedule(schedule) { + // Calculate initial nextRunAt + const nextRunAt = schedule.startImmediately + ? new Date() // Start immediately + : calculateNextRunAt(new Date(), schedule.baseIntervalMinutes, schedule.jitterMinutes); + const { rows } = await (0, connection_1.query)(` + INSERT INTO job_schedules ( + job_name, description, enabled, + base_interval_minutes, jitter_minutes, + next_run_at, job_config + ) VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, [ + schedule.jobName, + schedule.description || null, + schedule.enabled ?? true, + schedule.baseIntervalMinutes, + schedule.jitterMinutes, + nextRunAt, + schedule.jobConfig ? JSON.stringify(schedule.jobConfig) : null, + ]); + const row = rows[0]; + console.log(`[Scheduler] Created schedule "${schedule.jobName}" - next run at ${nextRunAt.toISOString()}`); + return { + id: row.id, + jobName: row.job_name, + description: row.description, + enabled: row.enabled, + baseIntervalMinutes: row.base_interval_minutes, + jitterMinutes: row.jitter_minutes, + lastRunAt: row.last_run_at, + lastStatus: row.last_status, + lastErrorMessage: row.last_error_message, + lastDurationMs: row.last_duration_ms, + nextRunAt: row.next_run_at, + jobConfig: row.job_config, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +} +/** + * Update a schedule + */ +async function updateSchedule(id, updates) { + const setClauses = []; + const params = []; + let paramIndex = 1; + if (updates.description !== undefined) { + setClauses.push(`description = $${paramIndex++}`); + params.push(updates.description); + } + if (updates.enabled !== undefined) { + setClauses.push(`enabled = $${paramIndex++}`); + params.push(updates.enabled); + } + if (updates.baseIntervalMinutes !== undefined) { + setClauses.push(`base_interval_minutes = $${paramIndex++}`); + params.push(updates.baseIntervalMinutes); + } + if (updates.jitterMinutes !== undefined) { + setClauses.push(`jitter_minutes = $${paramIndex++}`); + params.push(updates.jitterMinutes); + } + if (updates.jobConfig !== undefined) { + setClauses.push(`job_config = $${paramIndex++}`); + params.push(JSON.stringify(updates.jobConfig)); + } + if (setClauses.length === 0) { + return getScheduleById(id); + } + setClauses.push(`updated_at = NOW()`); + params.push(id); + const { rows } = await (0, connection_1.query)(`UPDATE job_schedules SET ${setClauses.join(', ')} WHERE id = $${paramIndex} RETURNING *`, params); + if (rows.length === 0) + return null; + const row = rows[0]; + return { + id: row.id, + jobName: row.job_name, + description: row.description, + enabled: row.enabled, + baseIntervalMinutes: row.base_interval_minutes, + jitterMinutes: row.jitter_minutes, + lastRunAt: row.last_run_at, + lastStatus: row.last_status, + lastErrorMessage: row.last_error_message, + lastDurationMs: row.last_duration_ms, + nextRunAt: row.next_run_at, + jobConfig: row.job_config, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +} +/** + * Delete a schedule + */ +async function deleteSchedule(id) { + const result = await (0, connection_1.query)(`DELETE FROM job_schedules WHERE id = $1`, [id]); + return (result.rowCount || 0) > 0; +} +/** + * Mark a schedule as running + */ +async function markScheduleRunning(id) { + await (0, connection_1.query)(`UPDATE job_schedules SET last_status = 'running', updated_at = NOW() WHERE id = $1`, [id]); +} +/** + * Update schedule after job completion with NEW jittered next_run_at + */ +async function updateScheduleAfterRun(id, status, durationMs, errorMessage) { + // Get current schedule to calculate new nextRunAt + const schedule = await getScheduleById(id); + if (!schedule) + return; + const now = new Date(); + const newNextRunAt = calculateNextRunAt(now, schedule.baseIntervalMinutes, schedule.jitterMinutes); + console.log(`[Scheduler] Schedule "${schedule.jobName}" completed (${status}). Next run: ${newNextRunAt.toISOString()}`); + await (0, connection_1.query)(` + UPDATE job_schedules SET + last_run_at = $2, + last_status = $3, + last_error_message = $4, + last_duration_ms = $5, + next_run_at = $6, + updated_at = NOW() + WHERE id = $1 + `, [id, now, status, errorMessage || null, durationMs, newNextRunAt]); +} +/** + * Create a job run log entry + */ +async function createRunLog(scheduleId, jobName, status) { + const { rows } = await (0, connection_1.query)(` + INSERT INTO job_run_logs (schedule_id, job_name, status, started_at) + VALUES ($1, $2, $3, NOW()) + RETURNING id + `, [scheduleId, jobName, status]); + return rows[0].id; +} +/** + * Update a job run log entry + */ +async function updateRunLog(runLogId, status, results) { + await (0, connection_1.query)(` + UPDATE job_run_logs SET + status = $2, + completed_at = NOW(), + duration_ms = $3, + error_message = $4, + items_processed = $5, + items_succeeded = $6, + items_failed = $7, + metadata = $8 + WHERE id = $1 + `, [ + runLogId, + status, + results.durationMs, + results.errorMessage || null, + results.itemsProcessed || 0, + results.itemsSucceeded || 0, + results.itemsFailed || 0, + results.metadata ? JSON.stringify(results.metadata) : null, + ]); +} +/** + * Get job run logs + */ +async function getRunLogs(options) { + const { scheduleId, jobName, limit = 50, offset = 0 } = options; + let whereClause = 'WHERE 1=1'; + const params = []; + let paramIndex = 1; + if (scheduleId) { + whereClause += ` AND schedule_id = $${paramIndex++}`; + params.push(scheduleId); + } + if (jobName) { + whereClause += ` AND job_name = $${paramIndex++}`; + params.push(jobName); + } + params.push(limit, offset); + const { rows } = await (0, connection_1.query)(` + SELECT * FROM job_run_logs + ${whereClause} + ORDER BY created_at DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1} + `, params); + const { rows: countRows } = await (0, connection_1.query)(`SELECT COUNT(*) as total FROM job_run_logs ${whereClause}`, params.slice(0, -2)); + return { + logs: rows, + total: parseInt(countRows[0]?.total || '0', 10), + }; +} +// ============================================================ +// JOB EXECUTION +// ============================================================ +/** + * Execute a job based on its name + */ +async function executeJob(schedule) { + const config = schedule.jobConfig || {}; + switch (schedule.jobName) { + case 'dutchie_az_product_crawl': + return executeProductCrawl(config); + case 'dutchie_az_discovery': + return executeDiscovery(config); + case 'dutchie_az_menu_detection': + return (0, menu_detection_1.executeMenuDetectionJob)(config); + default: + throw new Error(`Unknown job type: ${schedule.jobName}`); + } +} +/** + * Execute the AZ Dutchie product crawl job + * + * NEW BEHAVIOR: Instead of running crawls directly, this now ENQUEUES jobs + * into the crawl_jobs queue. Workers (running as separate replicas) will + * pick up and process these jobs. + * + * This allows: + * - Multiple workers to process jobs in parallel + * - No double-crawls (DB-level locking per dispensary) + * - Better scalability (add more worker replicas) + * - Live monitoring of individual job progress + */ +async function executeProductCrawl(config) { + const pricingType = config.pricingType || 'rec'; + const useBothModes = config.useBothModes !== false; + // Get all "ready" dispensaries (menu_type='dutchie' AND platform_dispensary_id IS NOT NULL AND not failed) + // Note: Menu detection is handled separately by the dutchie_az_menu_detection schedule + const { rows: rawRows } = await (0, connection_1.query)(` + SELECT id FROM dispensaries + WHERE state = 'AZ' + AND menu_type = 'dutchie' + AND platform_dispensary_id IS NOT NULL + AND failed_at IS NULL + ORDER BY last_crawl_at ASC NULLS FIRST + `); + const dispensaryIds = rawRows.map((r) => r.id); + if (dispensaryIds.length === 0) { + return { + status: 'success', + itemsProcessed: 0, + itemsSucceeded: 0, + itemsFailed: 0, + metadata: { message: 'No ready dispensaries to crawl. Run menu detection to discover more.' }, + }; + } + console.log(`[Scheduler] Enqueueing crawl jobs for ${dispensaryIds.length} dispensaries...`); + // Bulk enqueue jobs (skips dispensaries that already have pending/running jobs) + const { enqueued, skipped } = await (0, job_queue_1.bulkEnqueueJobs)('dutchie_product_crawl', dispensaryIds, { + priority: 0, + metadata: { pricingType, useBothModes }, + }); + console.log(`[Scheduler] Enqueued ${enqueued} jobs, skipped ${skipped} (already queued)`); + // Get current queue stats + const queueStats = await (0, job_queue_1.getQueueStats)(); + return { + status: 'success', + itemsProcessed: dispensaryIds.length, + itemsSucceeded: enqueued, + itemsFailed: 0, // Enqueue itself doesn't fail + metadata: { + enqueued, + skipped, + queueStats, + pricingType, + useBothModes, + message: `Enqueued ${enqueued} jobs. Workers will process them. Check /scraper-monitor for progress.`, + }, + }; +} +/** + * Execute the AZ Dutchie discovery job (placeholder) + */ +async function executeDiscovery(_config) { + // Placeholder - implement discovery logic + return { + status: 'success', + itemsProcessed: 0, + itemsSucceeded: 0, + itemsFailed: 0, + metadata: { message: 'Discovery not yet implemented' }, + }; +} +// ============================================================ +// SCHEDULER RUNNER +// ============================================================ +/** + * Check for due jobs and run them + */ +async function checkAndRunDueJobs() { + try { + // Get enabled schedules where nextRunAt <= now + const { rows } = await (0, connection_1.query)(` + SELECT * FROM job_schedules + WHERE enabled = true + AND next_run_at IS NOT NULL + AND next_run_at <= NOW() + AND (last_status IS NULL OR last_status != 'running') + ORDER BY next_run_at ASC + `); + if (rows.length === 0) + return; + console.log(`[Scheduler] Found ${rows.length} due job(s)`); + for (const row of rows) { + const schedule = { + id: row.id, + jobName: row.job_name, + description: row.description, + enabled: row.enabled, + baseIntervalMinutes: row.base_interval_minutes, + jitterMinutes: row.jitter_minutes, + lastRunAt: row.last_run_at, + lastStatus: row.last_status, + lastErrorMessage: row.last_error_message, + lastDurationMs: row.last_duration_ms, + nextRunAt: row.next_run_at, + jobConfig: row.job_config, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; + await runScheduledJob(schedule); + } + } + catch (error) { + console.error('[Scheduler] Error checking for due jobs:', error); + } +} +/** + * Run a single scheduled job + */ +async function runScheduledJob(schedule) { + const startTime = Date.now(); + console.log(`[Scheduler] Starting job "${schedule.jobName}"...`); + // Mark as running + await markScheduleRunning(schedule.id); + // Create run log entry + const runLogId = await createRunLog(schedule.id, schedule.jobName, 'running'); + try { + // Execute the job + const result = await executeJob(schedule); + const durationMs = Date.now() - startTime; + // Determine final status (exclude 'running' and null) + const finalStatus = result.status === 'running' || result.status === null + ? 'success' + : result.status; + // Update run log + await updateRunLog(runLogId, finalStatus, { + durationMs, + errorMessage: result.errorMessage, + itemsProcessed: result.itemsProcessed, + itemsSucceeded: result.itemsSucceeded, + itemsFailed: result.itemsFailed, + metadata: result.metadata, + }); + // Update schedule with NEW jittered next_run_at + await updateScheduleAfterRun(schedule.id, result.status, durationMs, result.errorMessage); + console.log(`[Scheduler] Job "${schedule.jobName}" completed in ${Math.round(durationMs / 1000)}s (${result.status})`); + } + catch (error) { + const durationMs = Date.now() - startTime; + console.error(`[Scheduler] Job "${schedule.jobName}" failed:`, error.message); + // Update run log with error + await updateRunLog(runLogId, 'error', { + durationMs, + errorMessage: error.message, + itemsProcessed: 0, + itemsSucceeded: 0, + itemsFailed: 0, + }); + // Update schedule with NEW jittered next_run_at + await updateScheduleAfterRun(schedule.id, 'error', durationMs, error.message); + } +} +// ============================================================ +// PUBLIC API +// ============================================================ +/** + * Start the scheduler + */ +function startScheduler() { + if (isSchedulerRunning) { + console.log('[Scheduler] Scheduler is already running'); + return; + } + isSchedulerRunning = true; + console.log(`[Scheduler] Starting scheduler (polling every ${SCHEDULER_POLL_INTERVAL_MS / 1000}s)...`); + // Immediately check for due jobs + checkAndRunDueJobs(); + // Set up interval to check for due jobs + schedulerInterval = setInterval(checkAndRunDueJobs, SCHEDULER_POLL_INTERVAL_MS); +} +/** + * Stop the scheduler + */ +function stopScheduler() { + if (!isSchedulerRunning) { + console.log('[Scheduler] Scheduler is not running'); + return; + } + isSchedulerRunning = false; + if (schedulerInterval) { + clearInterval(schedulerInterval); + schedulerInterval = null; + } + console.log('[Scheduler] Scheduler stopped'); +} +/** + * Get scheduler status + */ +function getSchedulerStatus() { + return { + running: isSchedulerRunning, + pollIntervalMs: SCHEDULER_POLL_INTERVAL_MS, + }; +} +/** + * Trigger immediate execution of a schedule + */ +async function triggerScheduleNow(scheduleId) { + const schedule = await getScheduleById(scheduleId); + if (!schedule) { + return { success: false, message: 'Schedule not found' }; + } + if (schedule.lastStatus === 'running') { + return { success: false, message: 'Job is already running' }; + } + // Run the job + await runScheduledJob(schedule); + return { success: true, message: 'Job triggered successfully' }; +} +/** + * Initialize default schedules if they don't exist + */ +async function initializeDefaultSchedules() { + const schedules = await getAllSchedules(); + // Check if product crawl schedule exists + const productCrawlExists = schedules.some(s => s.jobName === 'dutchie_az_product_crawl'); + if (!productCrawlExists) { + await createSchedule({ + jobName: 'dutchie_az_product_crawl', + description: 'Crawl all AZ Dutchie dispensary products', + enabled: true, + baseIntervalMinutes: 240, // 4 hours + jitterMinutes: 30, // ±30 minutes + jobConfig: { pricingType: 'rec', useBothModes: true }, + startImmediately: false, + }); + console.log('[Scheduler] Created default product crawl schedule'); + } + // Check if menu detection schedule exists + const menuDetectionExists = schedules.some(s => s.jobName === 'dutchie_az_menu_detection'); + if (!menuDetectionExists) { + await createSchedule({ + jobName: 'dutchie_az_menu_detection', + description: 'Detect menu providers and resolve platform IDs for AZ dispensaries', + enabled: true, + baseIntervalMinutes: 1440, // 24 hours + jitterMinutes: 60, // ±1 hour + jobConfig: { state: 'AZ', onlyUnknown: true }, + startImmediately: false, + }); + console.log('[Scheduler] Created default menu detection schedule'); + } +} +// Re-export for backward compatibility +var product_crawler_1 = require("./product-crawler"); +Object.defineProperty(exports, "crawlSingleDispensary", { enumerable: true, get: function () { return product_crawler_1.crawlDispensaryProducts; } }); +async function triggerImmediateCrawl() { + const schedules = await getAllSchedules(); + const productCrawl = schedules.find(s => s.jobName === 'dutchie_az_product_crawl'); + if (productCrawl) { + return triggerScheduleNow(productCrawl.id); + } + return { success: false, message: 'Product crawl schedule not found' }; +} diff --git a/backend/dist/dutchie-az/services/worker.js b/backend/dist/dutchie-az/services/worker.js new file mode 100644 index 00000000..43f0fbf6 --- /dev/null +++ b/backend/dist/dutchie-az/services/worker.js @@ -0,0 +1,440 @@ +"use strict"; +/** + * Worker Service + * + * Polls the job queue and processes crawl jobs. + * Each worker instance runs independently, claiming jobs atomically. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.startWorker = startWorker; +exports.stopWorker = stopWorker; +exports.getWorkerStatus = getWorkerStatus; +const job_queue_1 = require("./job-queue"); +const product_crawler_1 = require("./product-crawler"); +const discovery_1 = require("./discovery"); +const connection_1 = require("../db/connection"); +// Explicit column list for dispensaries table (avoids SELECT * issues with schema differences) +// NOTE: failed_at is included for worker compatibility checks +const DISPENSARY_COLUMNS = ` + id, name, slug, city, state, zip, address, latitude, longitude, + menu_type, menu_url, platform_dispensary_id, website, + provider_detection_data, created_at, updated_at, failed_at +`; +// ============================================================ +// WORKER CONFIG +// ============================================================ +const POLL_INTERVAL_MS = 5000; // Check for jobs every 5 seconds +const HEARTBEAT_INTERVAL_MS = 60000; // Send heartbeat every 60 seconds +const STALE_CHECK_INTERVAL_MS = 300000; // Check for stale jobs every 5 minutes +const SHUTDOWN_GRACE_PERIOD_MS = 30000; // Wait 30s for job to complete on shutdown +// ============================================================ +// WORKER STATE +// ============================================================ +let isRunning = false; +let currentJob = null; +let pollTimer = null; +let heartbeatTimer = null; +let staleCheckTimer = null; +let shutdownPromise = null; +// ============================================================ +// WORKER LIFECYCLE +// ============================================================ +/** + * Start the worker + */ +async function startWorker() { + if (isRunning) { + console.log('[Worker] Already running'); + return; + } + const workerId = (0, job_queue_1.getWorkerId)(); + const hostname = (0, job_queue_1.getWorkerHostname)(); + console.log(`[Worker] Starting worker ${workerId} on ${hostname}`); + isRunning = true; + // Set up graceful shutdown + setupShutdownHandlers(); + // Start polling for jobs + pollTimer = setInterval(pollForJobs, POLL_INTERVAL_MS); + // Start stale job recovery (only one worker should do this, but it's idempotent) + staleCheckTimer = setInterval(async () => { + try { + await (0, job_queue_1.recoverStaleJobs)(15); + } + catch (error) { + console.error('[Worker] Error recovering stale jobs:', error); + } + }, STALE_CHECK_INTERVAL_MS); + // Immediately poll for a job + await pollForJobs(); + console.log(`[Worker] Worker ${workerId} started, polling every ${POLL_INTERVAL_MS}ms`); +} +/** + * Stop the worker gracefully + */ +async function stopWorker() { + if (!isRunning) + return; + console.log('[Worker] Stopping worker...'); + isRunning = false; + // Clear timers + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } + if (staleCheckTimer) { + clearInterval(staleCheckTimer); + staleCheckTimer = null; + } + // Wait for current job to complete + if (currentJob) { + console.log(`[Worker] Waiting for job ${currentJob.id} to complete...`); + const startWait = Date.now(); + while (currentJob && Date.now() - startWait < SHUTDOWN_GRACE_PERIOD_MS) { + await new Promise(r => setTimeout(r, 1000)); + } + if (currentJob) { + console.log(`[Worker] Job ${currentJob.id} did not complete in time, marking for retry`); + await (0, job_queue_1.failJob)(currentJob.id, 'Worker shutdown'); + } + } + console.log('[Worker] Worker stopped'); +} +/** + * Get worker status + */ +function getWorkerStatus() { + return { + isRunning, + workerId: (0, job_queue_1.getWorkerId)(), + hostname: (0, job_queue_1.getWorkerHostname)(), + currentJob, + }; +} +// ============================================================ +// JOB PROCESSING +// ============================================================ +/** + * Poll for and process the next available job + */ +async function pollForJobs() { + if (!isRunning || currentJob) { + return; // Already processing a job + } + try { + const workerId = (0, job_queue_1.getWorkerId)(); + // Try to claim a job + const job = await (0, job_queue_1.claimNextJob)({ + workerId, + jobTypes: ['dutchie_product_crawl', 'menu_detection', 'menu_detection_single'], + lockDurationMinutes: 30, + }); + if (!job) { + return; // No jobs available + } + currentJob = job; + console.log(`[Worker] Processing job ${job.id} (type=${job.jobType}, dispensary=${job.dispensaryId})`); + // Start heartbeat for this job + heartbeatTimer = setInterval(async () => { + if (currentJob) { + try { + await (0, job_queue_1.heartbeat)(currentJob.id); + } + catch (error) { + console.error('[Worker] Heartbeat error:', error); + } + } + }, HEARTBEAT_INTERVAL_MS); + // Process the job + await processJob(job); + } + catch (error) { + console.error('[Worker] Error polling for jobs:', error); + if (currentJob) { + try { + await (0, job_queue_1.failJob)(currentJob.id, error.message); + } + catch (failError) { + console.error('[Worker] Error failing job:', failError); + } + } + } + finally { + // Clear heartbeat timer + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } + currentJob = null; + } +} +/** + * Process a single job + */ +async function processJob(job) { + try { + switch (job.jobType) { + case 'dutchie_product_crawl': + await processProductCrawlJob(job); + break; + case 'menu_detection': + await processMenuDetectionJob(job); + break; + case 'menu_detection_single': + await processSingleDetectionJob(job); + break; + default: + throw new Error(`Unknown job type: ${job.jobType}`); + } + } + catch (error) { + console.error(`[Worker] Job ${job.id} failed:`, error); + await (0, job_queue_1.failJob)(job.id, error.message); + } +} +// Maximum consecutive failures before flagging a dispensary +const MAX_CONSECUTIVE_FAILURES = 3; +/** + * Record a successful crawl - resets failure counter + */ +async function recordCrawlSuccess(dispensaryId) { + await (0, connection_1.query)(`UPDATE dispensaries + SET consecutive_failures = 0, + last_crawl_at = NOW(), + updated_at = NOW() + WHERE id = $1`, [dispensaryId]); +} +/** + * Record a crawl failure - increments counter and may flag dispensary + * Returns true if dispensary was flagged as failed + */ +async function recordCrawlFailure(dispensaryId, errorMessage) { + // Increment failure counter + const { rows } = await (0, connection_1.query)(`UPDATE dispensaries + SET consecutive_failures = consecutive_failures + 1, + last_failure_at = NOW(), + last_failure_reason = $2, + updated_at = NOW() + WHERE id = $1 + RETURNING consecutive_failures`, [dispensaryId, errorMessage]); + const failures = rows[0]?.consecutive_failures || 0; + // If we've hit the threshold, flag the dispensary as failed + if (failures >= MAX_CONSECUTIVE_FAILURES) { + await (0, connection_1.query)(`UPDATE dispensaries + SET failed_at = NOW(), + menu_type = NULL, + platform_dispensary_id = NULL, + failure_notes = $2, + updated_at = NOW() + WHERE id = $1`, [dispensaryId, `Auto-flagged after ${failures} consecutive failures. Last error: ${errorMessage}`]); + console.log(`[Worker] Dispensary ${dispensaryId} flagged as FAILED after ${failures} consecutive failures`); + return true; + } + console.log(`[Worker] Dispensary ${dispensaryId} failure recorded (${failures}/${MAX_CONSECUTIVE_FAILURES})`); + return false; +} +/** + * Process a product crawl job for a single dispensary + */ +async function processProductCrawlJob(job) { + if (!job.dispensaryId) { + throw new Error('Product crawl job requires dispensary_id'); + } + // Get dispensary details + const { rows } = await (0, connection_1.query)(`SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE id = $1`, [job.dispensaryId]); + if (rows.length === 0) { + throw new Error(`Dispensary ${job.dispensaryId} not found`); + } + const dispensary = (0, discovery_1.mapDbRowToDispensary)(rows[0]); + // Check if dispensary is already flagged as failed + if (rows[0].failed_at) { + console.log(`[Worker] Skipping dispensary ${job.dispensaryId} - already flagged as failed`); + await (0, job_queue_1.completeJob)(job.id, { productsFound: 0, productsUpserted: 0 }); + return; + } + if (!dispensary.platformDispensaryId) { + // Record failure and potentially flag + await recordCrawlFailure(job.dispensaryId, 'Missing platform_dispensary_id'); + throw new Error(`Dispensary ${job.dispensaryId} has no platform_dispensary_id`); + } + // Get crawl options from job metadata + const pricingType = job.metadata?.pricingType || 'rec'; + const useBothModes = job.metadata?.useBothModes !== false; + try { + // Crawl the dispensary + const result = await (0, product_crawler_1.crawlDispensaryProducts)(dispensary, pricingType, { + useBothModes, + onProgress: async (progress) => { + // Update progress for live monitoring + await (0, job_queue_1.updateJobProgress)(job.id, { + productsFound: progress.productsFound, + productsUpserted: progress.productsUpserted, + snapshotsCreated: progress.snapshotsCreated, + currentPage: progress.currentPage, + totalPages: progress.totalPages, + }); + }, + }); + if (result.success) { + // Success! Reset failure counter + await recordCrawlSuccess(job.dispensaryId); + await (0, job_queue_1.completeJob)(job.id, { + productsFound: result.productsFetched, + productsUpserted: result.productsUpserted, + snapshotsCreated: result.snapshotsCreated, + }); + } + else { + // Crawl returned failure - record it + const wasFlagged = await recordCrawlFailure(job.dispensaryId, result.errorMessage || 'Crawl failed'); + if (wasFlagged) { + // Don't throw - the dispensary is now flagged, job is "complete" + await (0, job_queue_1.completeJob)(job.id, { productsFound: 0, productsUpserted: 0 }); + } + else { + throw new Error(result.errorMessage || 'Crawl failed'); + } + } + } + catch (error) { + // Record the failure + const wasFlagged = await recordCrawlFailure(job.dispensaryId, error.message); + if (wasFlagged) { + // Dispensary is now flagged - complete the job rather than fail it + await (0, job_queue_1.completeJob)(job.id, { productsFound: 0, productsUpserted: 0 }); + } + else { + throw error; + } + } +} +/** + * Process a menu detection job (bulk) + */ +async function processMenuDetectionJob(job) { + const { executeMenuDetectionJob } = await Promise.resolve().then(() => __importStar(require('./menu-detection'))); + const config = job.metadata || {}; + const result = await executeMenuDetectionJob(config); + if (result.status === 'error') { + throw new Error(result.errorMessage || 'Menu detection failed'); + } + await (0, job_queue_1.completeJob)(job.id, { + productsFound: result.itemsProcessed, + productsUpserted: result.itemsSucceeded, + }); +} +/** + * Process a single dispensary menu detection job + * This is the parallelizable version - each worker can detect one dispensary at a time + */ +async function processSingleDetectionJob(job) { + if (!job.dispensaryId) { + throw new Error('Single detection job requires dispensary_id'); + } + const { detectAndResolveDispensary } = await Promise.resolve().then(() => __importStar(require('./menu-detection'))); + // Get dispensary details + const { rows } = await (0, connection_1.query)(`SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE id = $1`, [job.dispensaryId]); + if (rows.length === 0) { + throw new Error(`Dispensary ${job.dispensaryId} not found`); + } + const dispensary = rows[0]; + // Skip if already detected or failed + if (dispensary.failed_at) { + console.log(`[Worker] Skipping dispensary ${job.dispensaryId} - already flagged as failed`); + await (0, job_queue_1.completeJob)(job.id, { productsFound: 0, productsUpserted: 0 }); + return; + } + if (dispensary.menu_type && dispensary.menu_type !== 'unknown') { + console.log(`[Worker] Skipping dispensary ${job.dispensaryId} - already detected as ${dispensary.menu_type}`); + await (0, job_queue_1.completeJob)(job.id, { productsFound: 0, productsUpserted: 1 }); + return; + } + console.log(`[Worker] Detecting menu for dispensary ${job.dispensaryId} (${dispensary.name})...`); + try { + const result = await detectAndResolveDispensary(job.dispensaryId); + if (result.success) { + console.log(`[Worker] Dispensary ${job.dispensaryId}: detected ${result.detectedProvider}, platformId=${result.platformDispensaryId || 'none'}`); + await (0, job_queue_1.completeJob)(job.id, { + productsFound: 1, + productsUpserted: result.platformDispensaryId ? 1 : 0, + }); + } + else { + // Detection failed - record failure + await recordCrawlFailure(job.dispensaryId, result.error || 'Detection failed'); + throw new Error(result.error || 'Detection failed'); + } + } + catch (error) { + // Record the failure + const wasFlagged = await recordCrawlFailure(job.dispensaryId, error.message); + if (wasFlagged) { + // Dispensary is now flagged - complete the job rather than fail it + await (0, job_queue_1.completeJob)(job.id, { productsFound: 0, productsUpserted: 0 }); + } + else { + throw error; + } + } +} +// ============================================================ +// SHUTDOWN HANDLING +// ============================================================ +function setupShutdownHandlers() { + const shutdown = async (signal) => { + if (shutdownPromise) + return shutdownPromise; + console.log(`\n[Worker] Received ${signal}, shutting down...`); + shutdownPromise = stopWorker(); + await shutdownPromise; + process.exit(0); + }; + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('SIGINT', () => shutdown('SIGINT')); +} +// ============================================================ +// STANDALONE WORKER ENTRY POINT +// ============================================================ +if (require.main === module) { + // Run as standalone worker + startWorker().catch((error) => { + console.error('[Worker] Fatal error:', error); + process.exit(1); + }); +} diff --git a/backend/dist/dutchie-az/types/index.js b/backend/dist/dutchie-az/types/index.js new file mode 100644 index 00000000..098e21a3 --- /dev/null +++ b/backend/dist/dutchie-az/types/index.js @@ -0,0 +1,96 @@ +"use strict"; +/** + * Dutchie AZ Data Types + * + * Complete TypeScript interfaces for the isolated Dutchie Arizona data pipeline. + * These types map directly to Dutchie's GraphQL FilteredProducts response. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getOptionQuantity = getOptionQuantity; +exports.deriveOptionStockStatus = deriveOptionStockStatus; +exports.deriveStockStatus = deriveStockStatus; +exports.calculateTotalQuantity = calculateTotalQuantity; +exports.calculateTotalKioskQuantity = calculateTotalKioskQuantity; +/** + * Get available quantity for a single option + * Priority: quantityAvailable > kioskQuantityAvailable > quantity + */ +function getOptionQuantity(child) { + if (typeof child.quantityAvailable === 'number') + return child.quantityAvailable; + if (typeof child.kioskQuantityAvailable === 'number') + return child.kioskQuantityAvailable; + if (typeof child.quantity === 'number') + return child.quantity; + return null; // No quantity data available +} +/** + * Derive stock status for a single option + * Returns: 'in_stock' if qty > 0, 'out_of_stock' if qty === 0, 'unknown' if no data + */ +function deriveOptionStockStatus(child) { + const qty = getOptionQuantity(child); + if (qty === null) + return 'unknown'; + return qty > 0 ? 'in_stock' : 'out_of_stock'; +} +/** + * Derive product-level stock status from POSMetaData.children + * + * Logic per spec: + * - If ANY child is "in_stock" → product is "in_stock" + * - Else if ALL children are "out_of_stock" → product is "out_of_stock" + * - Else → product is "unknown" + * + * IMPORTANT: Threshold flags (isBelowThreshold, etc.) do NOT override stock status. + * They only indicate "low stock" - if qty > 0, status stays "in_stock". + */ +function deriveStockStatus(product) { + const children = product.POSMetaData?.children; + // No children data - unknown + if (!children || children.length === 0) { + return 'unknown'; + } + // Get stock status for each option + const optionStatuses = children.map(deriveOptionStockStatus); + // If ANY option is in_stock → product is in_stock + if (optionStatuses.some(status => status === 'in_stock')) { + return 'in_stock'; + } + // If ALL options are out_of_stock → product is out_of_stock + if (optionStatuses.every(status => status === 'out_of_stock')) { + return 'out_of_stock'; + } + // Otherwise (mix of out_of_stock and unknown) → unknown + return 'unknown'; +} +/** + * Calculate total quantity available across all options + * Returns null if no children data (unknown inventory), 0 if children exist but all have 0 qty + */ +function calculateTotalQuantity(product) { + const children = product.POSMetaData?.children; + // No children = unknown inventory, return null (NOT 0) + if (!children || children.length === 0) + return null; + // Check if any child has quantity data + const hasAnyQtyData = children.some(child => getOptionQuantity(child) !== null); + if (!hasAnyQtyData) + return null; // All children lack qty data = unknown + return children.reduce((sum, child) => { + const qty = getOptionQuantity(child); + return sum + (qty ?? 0); + }, 0); +} +/** + * Calculate total kiosk quantity available across all options + */ +function calculateTotalKioskQuantity(product) { + const children = product.POSMetaData?.children; + if (!children || children.length === 0) + return null; + const hasAnyKioskQty = children.some(child => typeof child.kioskQuantityAvailable === 'number'); + if (!hasAnyKioskQty) + return null; + return children.reduce((sum, child) => sum + (child.kioskQuantityAvailable ?? 0), 0); +} diff --git a/backend/dist/index.js b/backend/dist/index.js index 977e4d30..2ac40a57 100644 --- a/backend/dist/index.js +++ b/backend/dist/index.js @@ -7,18 +7,39 @@ const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const dotenv_1 = __importDefault(require("dotenv")); const minio_1 = require("./utils/minio"); +const image_storage_1 = require("./utils/image-storage"); const logger_1 = require("./services/logger"); +const proxyTestQueue_1 = require("./services/proxyTestQueue"); dotenv_1.default.config(); const app = (0, express_1.default)(); const PORT = process.env.PORT || 3010; app.use((0, cors_1.default)()); app.use(express_1.default.json()); +// Serve static images when MinIO is not configured +const LOCAL_IMAGES_PATH = process.env.LOCAL_IMAGES_PATH || '/app/public/images'; +app.use('/images', express_1.default.static(LOCAL_IMAGES_PATH)); +// Serve static downloads (plugin files, etc.) +const LOCAL_DOWNLOADS_PATH = process.env.LOCAL_DOWNLOADS_PATH || '/app/public/downloads'; +app.use('/downloads', express_1.default.static(LOCAL_DOWNLOADS_PATH)); app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); +// Endpoint to check server's outbound IP (for proxy whitelist setup) +app.get('/outbound-ip', async (req, res) => { + try { + const axios = require('axios'); + const response = await axios.get('https://api.ipify.org?format=json', { timeout: 10000 }); + res.json({ outbound_ip: response.data.ip }); + } + catch (error) { + res.status(500).json({ error: error.message }); + } +}); const auth_1 = __importDefault(require("./routes/auth")); const dashboard_1 = __importDefault(require("./routes/dashboard")); const stores_1 = __importDefault(require("./routes/stores")); +const dispensaries_1 = __importDefault(require("./routes/dispensaries")); +const changes_1 = __importDefault(require("./routes/changes")); const categories_1 = __importDefault(require("./routes/categories")); const products_1 = __importDefault(require("./routes/products")); const campaigns_1 = __importDefault(require("./routes/campaigns")); @@ -27,9 +48,27 @@ const settings_1 = __importDefault(require("./routes/settings")); const proxies_1 = __importDefault(require("./routes/proxies")); const logs_1 = __importDefault(require("./routes/logs")); const scraper_monitor_1 = __importDefault(require("./routes/scraper-monitor")); +const api_tokens_1 = __importDefault(require("./routes/api-tokens")); +const api_permissions_1 = __importDefault(require("./routes/api-permissions")); +const parallel_scrape_1 = __importDefault(require("./routes/parallel-scrape")); +const schedule_1 = __importDefault(require("./routes/schedule")); +const crawler_sandbox_1 = __importDefault(require("./routes/crawler-sandbox")); +const version_1 = __importDefault(require("./routes/version")); +const public_api_1 = __importDefault(require("./routes/public-api")); +const dutchie_az_1 = require("./dutchie-az"); +const apiTokenTracker_1 = require("./middleware/apiTokenTracker"); +const crawl_scheduler_1 = require("./services/crawl-scheduler"); +const wordpressPermissions_1 = require("./middleware/wordpressPermissions"); +// Apply WordPress permissions validation first (sets req.apiToken) +app.use(wordpressPermissions_1.validateWordPressPermissions); +// Apply API tracking middleware globally +app.use(apiTokenTracker_1.trackApiUsage); +app.use(apiTokenTracker_1.checkRateLimit); app.use('/api/auth', auth_1.default); app.use('/api/dashboard', dashboard_1.default); app.use('/api/stores', stores_1.default); +app.use('/api/dispensaries', dispensaries_1.default); +app.use('/api/changes', changes_1.default); app.use('/api/categories', categories_1.default); app.use('/api/products', products_1.default); app.use('/api/campaigns', campaigns_1.default); @@ -38,11 +77,34 @@ app.use('/api/settings', settings_1.default); app.use('/api/proxies', proxies_1.default); app.use('/api/logs', logs_1.default); app.use('/api/scraper-monitor', scraper_monitor_1.default); +app.use('/api/api-tokens', api_tokens_1.default); +app.use('/api/api-permissions', api_permissions_1.default); +app.use('/api/parallel-scrape', parallel_scrape_1.default); +app.use('/api/schedule', schedule_1.default); +app.use('/api/crawler-sandbox', crawler_sandbox_1.default); +app.use('/api/version', version_1.default); +// Vendor-agnostic AZ data pipeline routes (new public surface) +app.use('/api/az', dutchie_az_1.dutchieAZRouter); +// Legacy alias (kept temporarily for backward compatibility) +app.use('/api/dutchie-az', dutchie_az_1.dutchieAZRouter); +// Public API v1 - External consumer endpoints (WordPress, etc.) +// Uses dutchie_az data pipeline with per-dispensary API key auth +app.use('/api/v1', public_api_1.default); async function startServer() { try { logger_1.logger.info('system', 'Starting server...'); await (0, minio_1.initializeMinio)(); - logger_1.logger.info('system', 'Minio initialized'); + await (0, image_storage_1.initializeImageStorage)(); + logger_1.logger.info('system', (0, minio_1.isMinioEnabled)() ? 'MinIO storage initialized' : 'Local filesystem storage initialized'); + // Clean up any orphaned proxy test jobs from previous server runs + await (0, proxyTestQueue_1.cleanupOrphanedJobs)(); + // Start the crawl scheduler (checks every minute for jobs to run) + (0, crawl_scheduler_1.startCrawlScheduler)(); + logger_1.logger.info('system', 'Crawl scheduler started'); + // Start the Dutchie AZ scheduler (enqueues jobs for workers) + await (0, dutchie_az_1.initializeDefaultSchedules)(); + (0, dutchie_az_1.startScheduler)(); + logger_1.logger.info('system', 'Dutchie AZ scheduler started'); app.listen(PORT, () => { logger_1.logger.info('system', `Server running on port ${PORT}`); console.log(`🚀 Server running on port ${PORT}`); diff --git a/backend/dist/middleware/apiTokenTracker.js b/backend/dist/middleware/apiTokenTracker.js new file mode 100644 index 00000000..013da933 --- /dev/null +++ b/backend/dist/middleware/apiTokenTracker.js @@ -0,0 +1,94 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.trackApiUsage = trackApiUsage; +exports.checkRateLimit = checkRateLimit; +const migrate_1 = require("../db/migrate"); +async function trackApiUsage(req, res, next) { + // Only track if authenticated via API token + if (!req.apiToken) { + return next(); + } + const startTime = Date.now(); + req.startTime = startTime; + // Get request size + const requestSize = req.headers['content-length'] + ? parseInt(req.headers['content-length']) + : 0; + // Capture original res.json to measure response + const originalJson = res.json.bind(res); + let responseSize = 0; + res.json = function (body) { + responseSize = JSON.stringify(body).length; + return originalJson(body); + }; + // Track after response is sent + res.on('finish', async () => { + const responseTime = Date.now() - startTime; + try { + await migrate_1.pool.query(` + INSERT INTO api_token_usage ( + token_id, + endpoint, + method, + status_code, + response_time_ms, + request_size, + response_size, + ip_address, + user_agent + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + `, [ + req.apiToken.id, + req.path, + req.method, + res.statusCode, + responseTime, + requestSize, + responseSize, + req.ip, + req.headers['user-agent'] || null + ]); + // Update last_used_at + await migrate_1.pool.query('UPDATE api_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = $1', [req.apiToken.id]); + } + catch (error) { + console.error('Error tracking API usage:', error); + } + }); + next(); +} +// Rate limiting check +async function checkRateLimit(req, res, next) { + if (!req.apiToken) { + return next(); + } + const { id, rate_limit } = req.apiToken; + try { + // Count requests in the last minute + const result = await migrate_1.pool.query(` + SELECT COUNT(*) as request_count + FROM api_token_usage + WHERE token_id = $1 + AND created_at > NOW() - INTERVAL '1 minute' + `, [id]); + const requestCount = parseInt(result.rows[0].request_count); + if (requestCount >= rate_limit) { + return res.status(429).json({ + error: 'Rate limit exceeded', + limit: rate_limit, + current: requestCount, + retry_after: 60 + }); + } + // Add rate limit headers + res.setHeader('X-RateLimit-Limit', rate_limit.toString()); + res.setHeader('X-RateLimit-Remaining', (rate_limit - requestCount).toString()); + res.setHeader('X-RateLimit-Reset', new Date(Date.now() + 60000).toISOString()); + next(); + } + catch (error) { + console.error('Error checking rate limit:', error); + next(); + } +} diff --git a/backend/dist/middleware/wordpressPermissions.js b/backend/dist/middleware/wordpressPermissions.js new file mode 100644 index 00000000..c4e13c55 --- /dev/null +++ b/backend/dist/middleware/wordpressPermissions.js @@ -0,0 +1,163 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateWordPressPermissions = validateWordPressPermissions; +const migrate_1 = require("../db/migrate"); +const ipaddr_js_1 = __importDefault(require("ipaddr.js")); +/** + * Validates if an IP address matches any of the allowed IP patterns + * Supports CIDR notation and wildcards + */ +function isIpAllowed(clientIp, allowedIps) { + try { + const clientAddr = ipaddr_js_1.default.process(clientIp); + for (const allowedIp of allowedIps) { + const trimmed = allowedIp.trim(); + if (!trimmed) + continue; + // Check for CIDR notation + if (trimmed.includes('/')) { + try { + const [subnet, bits] = trimmed.split('/'); + const range = ipaddr_js_1.default.parseCIDR(trimmed); + if (clientAddr.match(range)) { + return true; + } + } + catch (e) { + console.warn(`Invalid CIDR notation: ${trimmed}`); + continue; + } + } + else { + // Exact match + try { + const allowedAddr = ipaddr_js_1.default.process(trimmed); + if (clientAddr.toString() === allowedAddr.toString()) { + return true; + } + } + catch (e) { + console.warn(`Invalid IP address: ${trimmed}`); + continue; + } + } + } + return false; + } + catch (error) { + console.error('Error processing client IP:', error); + return false; + } +} +/** + * Validates if a domain matches any of the allowed domain patterns + * Supports wildcard subdomains (*.example.com) + */ +function isDomainAllowed(origin, allowedDomains) { + try { + // Extract domain from origin URL + const url = new URL(origin); + const domain = url.hostname; + for (const allowedDomain of allowedDomains) { + const trimmed = allowedDomain.trim(); + if (!trimmed) + continue; + // Wildcard subdomain support + if (trimmed.startsWith('*.')) { + const baseDomain = trimmed.substring(2); + if (domain === baseDomain || domain.endsWith('.' + baseDomain)) { + return true; + } + } + else { + // Exact match + if (domain === trimmed) { + return true; + } + } + } + return false; + } + catch (error) { + console.error('Error processing domain:', error); + return false; + } +} +/** + * WordPress API Permissions Middleware + * Validates API access based on WordPress permissions table + */ +async function validateWordPressPermissions(req, res, next) { + // Get API key from header + const apiKey = req.headers['x-api-key']; + // If no API key provided, skip WordPress validation + if (!apiKey) { + return next(); + } + try { + // Query WordPress permissions table + const result = await migrate_1.pool.query(` + SELECT id, user_name, api_key, allowed_ips, allowed_domains, is_active + FROM wp_dutchie_api_permissions + WHERE api_key = $1 AND is_active = 1 + `, [apiKey]); + if (result.rows.length === 0) { + return res.status(401).json({ + error: 'Invalid API key' + }); + } + const permission = result.rows[0]; + // Get client IP + const clientIp = req.headers['x-forwarded-for']?.split(',')[0].trim() || + req.headers['x-real-ip'] || + req.ip || + req.connection.remoteAddress || + ''; + // Validate IP if configured + if (permission.allowed_ips) { + const allowedIps = permission.allowed_ips.split('\n').filter((ip) => ip.trim()); + if (allowedIps.length > 0 && !isIpAllowed(clientIp, allowedIps)) { + return res.status(403).json({ + error: 'IP address not allowed', + client_ip: clientIp + }); + } + } + // Validate domain if configured + const origin = req.get('origin') || req.get('referer') || ''; + if (permission.allowed_domains && origin) { + const allowedDomains = permission.allowed_domains.split('\n').filter((d) => d.trim()); + if (allowedDomains.length > 0 && !isDomainAllowed(origin, allowedDomains)) { + return res.status(403).json({ + error: 'Domain not allowed', + origin: origin + }); + } + } + // Update last_used_at timestamp (async, don't wait) + migrate_1.pool.query(` + UPDATE wp_dutchie_api_permissions + SET last_used_at = CURRENT_TIMESTAMP + WHERE id = $1 + `, [permission.id]).catch((err) => { + console.error('Error updating last_used_at:', err); + }); + // Set apiToken on request for tracking middleware + // Default rate limit of 100 requests/minute for WordPress permissions + req.apiToken = { + id: permission.id, + name: permission.user_name, + rate_limit: 100 + }; + next(); + } + catch (error) { + console.error('WordPress permissions validation error:', error); + return res.status(500).json({ + error: 'Internal server error during API validation' + }); + } +} diff --git a/backend/dist/migrations-runner/009_image_sizes.js b/backend/dist/migrations-runner/009_image_sizes.js new file mode 100644 index 00000000..30858a3d --- /dev/null +++ b/backend/dist/migrations-runner/009_image_sizes.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const migrate_1 = require("../db/migrate"); +(async () => { + try { + console.log('🔄 Running image sizes migration...'); + // Add thumbnail and medium paths + await migrate_1.pool.query(` + ALTER TABLE products + ADD COLUMN IF NOT EXISTS thumbnail_path TEXT, + ADD COLUMN IF NOT EXISTS medium_path TEXT + `); + console.log('✅ Added thumbnail_path and medium_path columns'); + // Rename local_image_path to full_path + await migrate_1.pool.query(` + ALTER TABLE products + RENAME COLUMN local_image_path TO full_path + `); + console.log('✅ Renamed local_image_path to full_path'); + // Add index + await migrate_1.pool.query(` + CREATE INDEX IF NOT EXISTS idx_products_images ON products(full_path, thumbnail_path, medium_path) + `); + console.log('✅ Created image index'); + console.log('✅ Migration complete!'); + process.exit(0); + } + catch (error) { + console.error('❌ Migration failed:', error); + process.exit(1); + } +})(); diff --git a/backend/dist/routes/api-permissions.js b/backend/dist/routes/api-permissions.js new file mode 100644 index 00000000..8123a646 --- /dev/null +++ b/backend/dist/routes/api-permissions.js @@ -0,0 +1,174 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const middleware_1 = require("../auth/middleware"); +const migrate_1 = require("../db/migrate"); +const crypto_1 = __importDefault(require("crypto")); +const router = (0, express_1.Router)(); +router.use(middleware_1.authMiddleware); +// Generate secure random API key (64-character hex) +function generateApiKey() { + return crypto_1.default.randomBytes(32).toString('hex'); +} +// Get all API permissions +router.get('/', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const result = await migrate_1.pool.query(` + SELECT * + FROM wp_dutchie_api_permissions + ORDER BY created_at DESC + `); + res.json({ permissions: result.rows }); + } + catch (error) { + console.error('Error fetching API permissions:', error); + res.status(500).json({ error: 'Failed to fetch API permissions' }); + } +}); +// Get all dispensaries for dropdown (must be before /:id to avoid route conflict) +router.get('/dispensaries', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const result = await migrate_1.pool.query(` + SELECT id, name + FROM dispensaries + ORDER BY name + `); + res.json({ dispensaries: result.rows }); + } + catch (error) { + console.error('Error fetching dispensaries:', error); + res.status(500).json({ error: 'Failed to fetch dispensaries' }); + } +}); +// Get single API permission +router.get('/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query(` + SELECT * + FROM wp_dutchie_api_permissions + WHERE id = $1 + `, [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Permission not found' }); + } + res.json({ permission: result.rows[0] }); + } + catch (error) { + console.error('Error fetching API permission:', error); + res.status(500).json({ error: 'Failed to fetch API permission' }); + } +}); +// Create new API permission +router.post('/', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + // Support both store_id (existing) and dispensary_id (for compatibility) + const { user_name, allowed_ips, allowed_domains, store_id, dispensary_id } = req.body; + const storeIdToUse = store_id || dispensary_id; + if (!user_name) { + return res.status(400).json({ error: 'User name is required' }); + } + if (!storeIdToUse) { + return res.status(400).json({ error: 'Store/Dispensary is required' }); + } + // Get dispensary name for display + const dispensaryResult = await migrate_1.pool.query('SELECT name FROM dispensaries WHERE id = $1', [storeIdToUse]); + if (dispensaryResult.rows.length === 0) { + return res.status(400).json({ error: 'Invalid store/dispensary ID' }); + } + const storeName = dispensaryResult.rows[0].name; + const apiKey = generateApiKey(); + const result = await migrate_1.pool.query(` + INSERT INTO wp_dutchie_api_permissions ( + user_name, + api_key, + allowed_ips, + allowed_domains, + is_active, + store_id, + store_name + ) + VALUES ($1, $2, $3, $4, 1, $5, $6) + RETURNING * + `, [ + user_name, + apiKey, + allowed_ips || null, + allowed_domains || null, + storeIdToUse, + storeName + ]); + res.status(201).json({ + permission: result.rows[0], + message: 'API permission created successfully. Save the API key securely - it cannot be retrieved later.' + }); + } + catch (error) { + console.error('Error creating API permission:', error); + res.status(500).json({ error: 'Failed to create API permission' }); + } +}); +// Update API permission +router.put('/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { id } = req.params; + const { user_name, allowed_ips, allowed_domains, is_active } = req.body; + const result = await migrate_1.pool.query(` + UPDATE wp_dutchie_api_permissions + SET + user_name = COALESCE($1, user_name), + allowed_ips = COALESCE($2, allowed_ips), + allowed_domains = COALESCE($3, allowed_domains), + is_active = COALESCE($4, is_active) + WHERE id = $5 + RETURNING * + `, [user_name, allowed_ips, allowed_domains, is_active, id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Permission not found' }); + } + res.json({ permission: result.rows[0] }); + } + catch (error) { + console.error('Error updating API permission:', error); + res.status(500).json({ error: 'Failed to update API permission' }); + } +}); +// Toggle permission active status +router.patch('/:id/toggle', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query(` + UPDATE wp_dutchie_api_permissions + SET is_active = NOT is_active + WHERE id = $1 + RETURNING * + `, [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Permission not found' }); + } + res.json({ permission: result.rows[0] }); + } + catch (error) { + console.error('Error toggling API permission:', error); + res.status(500).json({ error: 'Failed to toggle API permission' }); + } +}); +// Delete API permission +router.delete('/:id', (0, middleware_1.requireRole)('superadmin'), async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query('DELETE FROM wp_dutchie_api_permissions WHERE id = $1 RETURNING *', [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Permission not found' }); + } + res.json({ message: 'API permission deleted successfully' }); + } + catch (error) { + console.error('Error deleting API permission:', error); + res.status(500).json({ error: 'Failed to delete API permission' }); + } +}); +exports.default = router; diff --git a/backend/dist/routes/api-tokens.js b/backend/dist/routes/api-tokens.js new file mode 100644 index 00000000..39139e9c --- /dev/null +++ b/backend/dist/routes/api-tokens.js @@ -0,0 +1,265 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const middleware_1 = require("../auth/middleware"); +const migrate_1 = require("../db/migrate"); +const crypto_1 = __importDefault(require("crypto")); +const router = (0, express_1.Router)(); +router.use(middleware_1.authMiddleware); +// Generate secure random token +function generateToken() { + return crypto_1.default.randomBytes(32).toString('hex'); +} +// Get all API tokens +router.get('/', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const result = await migrate_1.pool.query(` + SELECT + t.*, + u.email as created_by_email, + ( + SELECT COUNT(*) + FROM api_token_usage + WHERE token_id = t.id + AND created_at > NOW() - INTERVAL '24 hours' + ) as requests_24h, + ( + SELECT COUNT(*) + FROM api_token_usage + WHERE token_id = t.id + AND created_at > NOW() - INTERVAL '7 days' + ) as requests_7d, + ( + SELECT COUNT(*) + FROM api_token_usage + WHERE token_id = t.id + ) as total_requests + FROM api_tokens t + LEFT JOIN users u ON t.user_id = u.id + ORDER BY t.created_at DESC + `); + res.json({ tokens: result.rows }); + } + catch (error) { + console.error('Error fetching API tokens:', error); + res.status(500).json({ error: 'Failed to fetch API tokens' }); + } +}); +// Get single API token +router.get('/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query(` + SELECT + t.*, + u.email as created_by_email + FROM api_tokens t + LEFT JOIN users u ON t.user_id = u.id + WHERE t.id = $1 + `, [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Token not found' }); + } + res.json({ token: result.rows[0] }); + } + catch (error) { + console.error('Error fetching API token:', error); + res.status(500).json({ error: 'Failed to fetch API token' }); + } +}); +// Create new API token +router.post('/', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { name, description, rate_limit, allowed_endpoints, expires_at } = req.body; + const userId = req.user.userId; + if (!name) { + return res.status(400).json({ error: 'Name is required' }); + } + const token = generateToken(); + const result = await migrate_1.pool.query(` + INSERT INTO api_tokens ( + name, + token, + description, + user_id, + rate_limit, + allowed_endpoints, + expires_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, [ + name, + token, + description || null, + userId, + rate_limit || 100, + allowed_endpoints || null, + expires_at || null + ]); + res.status(201).json({ + token: result.rows[0], + message: 'API token created successfully. Save this token securely - it cannot be retrieved later.' + }); + } + catch (error) { + console.error('Error creating API token:', error); + res.status(500).json({ error: 'Failed to create API token' }); + } +}); +// Update API token +router.put('/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { id } = req.params; + const { name, description, active, rate_limit, allowed_endpoints, expires_at } = req.body; + const result = await migrate_1.pool.query(` + UPDATE api_tokens + SET + name = COALESCE($1, name), + description = COALESCE($2, description), + active = COALESCE($3, active), + rate_limit = COALESCE($4, rate_limit), + allowed_endpoints = COALESCE($5, allowed_endpoints), + expires_at = COALESCE($6, expires_at) + WHERE id = $7 + RETURNING * + `, [name, description, active, rate_limit, allowed_endpoints, expires_at, id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Token not found' }); + } + res.json({ token: result.rows[0] }); + } + catch (error) { + console.error('Error updating API token:', error); + res.status(500).json({ error: 'Failed to update API token' }); + } +}); +// Delete API token +router.delete('/:id', (0, middleware_1.requireRole)('superadmin'), async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query('DELETE FROM api_tokens WHERE id = $1 RETURNING *', [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Token not found' }); + } + res.json({ message: 'API token deleted successfully' }); + } + catch (error) { + console.error('Error deleting API token:', error); + res.status(500).json({ error: 'Failed to delete API token' }); + } +}); +// Get token usage statistics +router.get('/:id/usage', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { id } = req.params; + const { days = 7 } = req.query; + // Get hourly usage for the past N days + const hourlyUsage = await migrate_1.pool.query(` + SELECT + DATE_TRUNC('hour', created_at) as hour, + COUNT(*) as requests, + AVG(response_time_ms) as avg_response_time, + SUM(CASE WHEN status_code >= 200 AND status_code < 300 THEN 1 ELSE 0 END) as successful_requests, + SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) as failed_requests + FROM api_token_usage + WHERE token_id = $1 + AND created_at > NOW() - INTERVAL '${parseInt(days)} days' + GROUP BY hour + ORDER BY hour DESC + `, [id]); + // Get endpoint usage + const endpointUsage = await migrate_1.pool.query(` + SELECT + endpoint, + method, + COUNT(*) as requests, + AVG(response_time_ms) as avg_response_time + FROM api_token_usage + WHERE token_id = $1 + AND created_at > NOW() - INTERVAL '${parseInt(days)} days' + GROUP BY endpoint, method + ORDER BY requests DESC + LIMIT 20 + `, [id]); + // Get recent requests + const recentRequests = await migrate_1.pool.query(` + SELECT + endpoint, + method, + status_code, + response_time_ms, + ip_address, + created_at + FROM api_token_usage + WHERE token_id = $1 + ORDER BY created_at DESC + LIMIT 100 + `, [id]); + res.json({ + hourly_usage: hourlyUsage.rows, + endpoint_usage: endpointUsage.rows, + recent_requests: recentRequests.rows + }); + } + catch (error) { + console.error('Error fetching token usage:', error); + res.status(500).json({ error: 'Failed to fetch token usage' }); + } +}); +// Get overall API usage statistics +router.get('/stats/overview', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { days = 7 } = req.query; + const stats = await migrate_1.pool.query(` + SELECT + COUNT(DISTINCT token_id) as active_tokens, + COUNT(*) as total_requests, + AVG(response_time_ms) as avg_response_time, + SUM(CASE WHEN status_code >= 200 AND status_code < 300 THEN 1 ELSE 0 END) as successful_requests, + SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) as failed_requests + FROM api_token_usage + WHERE created_at > NOW() - INTERVAL '${parseInt(days)} days' + `); + // Top tokens by usage + const topTokens = await migrate_1.pool.query(` + SELECT + t.id, + t.name, + COUNT(u.id) as requests, + AVG(u.response_time_ms) as avg_response_time + FROM api_tokens t + LEFT JOIN api_token_usage u ON t.id = u.token_id + WHERE u.created_at > NOW() - INTERVAL '${parseInt(days)} days' + GROUP BY t.id, t.name + ORDER BY requests DESC + LIMIT 10 + `); + // Most used endpoints + const topEndpoints = await migrate_1.pool.query(` + SELECT + endpoint, + method, + COUNT(*) as requests, + AVG(response_time_ms) as avg_response_time + FROM api_token_usage + WHERE created_at > NOW() - INTERVAL '${parseInt(days)} days' + GROUP BY endpoint, method + ORDER BY requests DESC + LIMIT 10 + `); + res.json({ + overview: stats.rows[0], + top_tokens: topTokens.rows, + top_endpoints: topEndpoints.rows + }); + } + catch (error) { + console.error('Error fetching API stats:', error); + res.status(500).json({ error: 'Failed to fetch API stats' }); + } +}); +exports.default = router; diff --git a/backend/dist/routes/categories.js b/backend/dist/routes/categories.js index 6d6b7ff5..e04ca1e7 100644 --- a/backend/dist/routes/categories.js +++ b/backend/dist/routes/categories.js @@ -58,11 +58,11 @@ router.get('/tree', async (req, res) => { const categoryMap = new Map(); const tree = []; // First pass: create map - categories.forEach(cat => { + categories.forEach((cat) => { categoryMap.set(cat.id, { ...cat, children: [] }); }); // Second pass: build tree - categories.forEach(cat => { + categories.forEach((cat) => { const node = categoryMap.get(cat.id); if (cat.parent_id) { const parent = categoryMap.get(cat.parent_id); diff --git a/backend/dist/routes/changes.js b/backend/dist/routes/changes.js new file mode 100644 index 00000000..0af6afd6 --- /dev/null +++ b/backend/dist/routes/changes.js @@ -0,0 +1,152 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const middleware_1 = require("../auth/middleware"); +const migrate_1 = require("../db/migrate"); +const router = (0, express_1.Router)(); +router.use(middleware_1.authMiddleware); +// Get all changes with optional status filter +router.get('/', async (req, res) => { + try { + const { status } = req.query; + let query = ` + SELECT + dc.id, + dc.dispensary_id, + dc.field_name, + dc.old_value, + dc.new_value, + dc.source, + dc.confidence_score, + dc.change_notes, + dc.status, + dc.requires_recrawl, + dc.created_at, + dc.reviewed_at, + dc.reviewed_by, + dc.rejection_reason, + d.name as dispensary_name, + d.slug as dispensary_slug, + d.city, + d.state + FROM dispensary_changes dc + JOIN dispensaries d ON dc.dispensary_id = d.id + `; + const params = []; + if (status) { + query += ` WHERE dc.status = $1`; + params.push(status); + } + query += ` ORDER BY dc.created_at DESC`; + const result = await migrate_1.pool.query(query, params); + res.json({ changes: result.rows }); + } + catch (error) { + console.error('Error fetching changes:', error); + res.status(500).json({ error: 'Failed to fetch changes' }); + } +}); +// Get changes statistics (for alert banner) +router.get('/stats', async (req, res) => { + try { + const result = await migrate_1.pool.query(` + SELECT + COUNT(*) FILTER (WHERE status = 'pending') as pending_count, + COUNT(*) FILTER (WHERE status = 'pending' AND requires_recrawl = TRUE) as pending_recrawl_count, + COUNT(*) FILTER (WHERE status = 'approved') as approved_count, + COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count + FROM dispensary_changes + `); + res.json(result.rows[0]); + } + catch (error) { + console.error('Error fetching change stats:', error); + res.status(500).json({ error: 'Failed to fetch change stats' }); + } +}); +// Approve a change and apply it to the dispensary +router.post('/:id/approve', async (req, res) => { + const client = await migrate_1.pool.connect(); + try { + await client.query('BEGIN'); + const { id } = req.params; + const userId = req.user?.id; // From auth middleware + // Get the change record + const changeResult = await client.query(` + SELECT * FROM dispensary_changes WHERE id = $1 AND status = 'pending' + `, [id]); + if (changeResult.rows.length === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ error: 'Pending change not found' }); + } + const change = changeResult.rows[0]; + // Apply the change to the dispensary table + const updateQuery = ` + UPDATE dispensaries + SET ${change.field_name} = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 + RETURNING * + `; + const dispensaryResult = await client.query(updateQuery, [ + change.new_value, + change.dispensary_id + ]); + if (dispensaryResult.rows.length === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ error: 'Dispensary not found' }); + } + // Mark the change as approved + await client.query(` + UPDATE dispensary_changes + SET + status = 'approved', + reviewed_at = CURRENT_TIMESTAMP, + reviewed_by = $1 + WHERE id = $2 + `, [userId, id]); + await client.query('COMMIT'); + res.json({ + message: 'Change approved and applied', + dispensary: dispensaryResult.rows[0], + requires_recrawl: change.requires_recrawl + }); + } + catch (error) { + await client.query('ROLLBACK'); + console.error('Error approving change:', error); + res.status(500).json({ error: 'Failed to approve change' }); + } + finally { + client.release(); + } +}); +// Reject a change with optional reason +router.post('/:id/reject', async (req, res) => { + try { + const { id } = req.params; + const { reason } = req.body; + const userId = req.user?.id; // From auth middleware + const result = await migrate_1.pool.query(` + UPDATE dispensary_changes + SET + status = 'rejected', + reviewed_at = CURRENT_TIMESTAMP, + reviewed_by = $1, + rejection_reason = $2 + WHERE id = $3 AND status = 'pending' + RETURNING * + `, [userId, reason, id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Pending change not found' }); + } + res.json({ + message: 'Change rejected', + change: result.rows[0] + }); + } + catch (error) { + console.error('Error rejecting change:', error); + res.status(500).json({ error: 'Failed to reject change' }); + } +}); +exports.default = router; diff --git a/backend/dist/routes/crawler-sandbox.js b/backend/dist/routes/crawler-sandbox.js new file mode 100644 index 00000000..b7d2870f --- /dev/null +++ b/backend/dist/routes/crawler-sandbox.js @@ -0,0 +1,497 @@ +"use strict"; +/** + * Crawler Sandbox API Routes + * + * Endpoints for managing sandbox crawls, templates, and provider detection + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const migrate_1 = require("../db/migrate"); +const middleware_1 = require("../auth/middleware"); +const logger_1 = require("../services/logger"); +const crawler_jobs_1 = require("../services/crawler-jobs"); +const router = express_1.default.Router(); +// Apply auth middleware to all routes +router.use(middleware_1.authMiddleware); +// ======================================== +// Sandbox Entries +// ======================================== +/** + * GET /api/crawler-sandbox + * List sandbox entries with optional filters + */ +router.get('/', async (req, res) => { + try { + const { status, dispensaryId, limit = 50, offset = 0 } = req.query; + let query = ` + SELECT cs.*, d.name as dispensary_name, d.website, d.menu_provider, d.crawler_status + FROM crawler_sandboxes cs + JOIN dispensaries d ON d.id = cs.dispensary_id + WHERE 1=1 + `; + const params = []; + let paramIndex = 1; + if (status) { + query += ` AND cs.status = $${paramIndex}`; + params.push(status); + paramIndex++; + } + if (dispensaryId) { + query += ` AND cs.dispensary_id = $${paramIndex}`; + params.push(Number(dispensaryId)); + paramIndex++; + } + query += ` ORDER BY cs.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; + params.push(Number(limit), Number(offset)); + const result = await migrate_1.pool.query(query, params); + // Get total count + const countResult = await migrate_1.pool.query(`SELECT COUNT(*) FROM crawler_sandboxes cs WHERE 1=1 + ${status ? 'AND cs.status = $1' : ''} + ${dispensaryId ? `AND cs.dispensary_id = $${status ? 2 : 1}` : ''}`, status && dispensaryId ? [status, dispensaryId] : status ? [status] : dispensaryId ? [dispensaryId] : []); + res.json({ + sandboxes: result.rows, + total: parseInt(countResult.rows[0].count), + limit: Number(limit), + offset: Number(offset), + }); + } + catch (error) { + logger_1.logger.error('api', `Get sandboxes error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/crawler-sandbox/:id + * Get a single sandbox entry with full details + */ +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query(`SELECT cs.*, d.name as dispensary_name, d.website, d.menu_url, + d.menu_provider, d.menu_provider_confidence, d.crawler_mode, d.crawler_status + FROM crawler_sandboxes cs + JOIN dispensaries d ON d.id = cs.dispensary_id + WHERE cs.id = $1`, [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Sandbox entry not found' }); + } + // Get related jobs + const jobs = await migrate_1.pool.query(`SELECT * FROM sandbox_crawl_jobs + WHERE sandbox_id = $1 OR dispensary_id = $2 + ORDER BY created_at DESC + LIMIT 10`, [id, result.rows[0].dispensary_id]); + res.json({ + sandbox: result.rows[0], + jobs: jobs.rows, + }); + } + catch (error) { + logger_1.logger.error('api', `Get sandbox error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/crawler-sandbox/:id/analyze + * Trigger re-analysis of a sandbox entry + */ +router.post('/:id/analyze', (0, middleware_1.requireRole)('admin'), async (req, res) => { + try { + const { id } = req.params; + const sandbox = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [id]); + if (sandbox.rows.length === 0) { + return res.status(404).json({ error: 'Sandbox entry not found' }); + } + // Queue a new sandbox job + const job = await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, sandbox_id, job_type, status, priority) + VALUES ($1, $2, 'deep_crawl', 'pending', 20) + RETURNING id`, [sandbox.rows[0].dispensary_id, id]); + // Update sandbox status + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'pending', updated_at = NOW() WHERE id = $1`, [id]); + res.json({ + message: 'Analysis job queued', + jobId: job.rows[0].id, + }); + } + catch (error) { + logger_1.logger.error('api', `Analyze sandbox error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/crawler-sandbox/:id/move-to-production + * Move a sandbox entry to production (for Dutchie dispensaries) + */ +router.post('/:id/move-to-production', (0, middleware_1.requireRole)('admin'), async (req, res) => { + try { + const { id } = req.params; + const sandbox = await migrate_1.pool.query(`SELECT cs.*, d.menu_provider + FROM crawler_sandboxes cs + JOIN dispensaries d ON d.id = cs.dispensary_id + WHERE cs.id = $1`, [id]); + if (sandbox.rows.length === 0) { + return res.status(404).json({ error: 'Sandbox entry not found' }); + } + // Can only move to production if provider is dutchie + if (sandbox.rows[0].menu_provider !== 'dutchie') { + return res.status(400).json({ + error: 'Only Dutchie dispensaries can be moved to production currently', + }); + } + // Update dispensary to production mode + await migrate_1.pool.query(`UPDATE dispensaries + SET crawler_mode = 'production', crawler_status = 'idle', updated_at = NOW() + WHERE id = $1`, [sandbox.rows[0].dispensary_id]); + // Mark sandbox as moved + await migrate_1.pool.query(`UPDATE crawler_sandboxes + SET status = 'moved_to_production', updated_at = NOW() + WHERE id = $1`, [id]); + res.json({ message: 'Dispensary moved to production' }); + } + catch (error) { + logger_1.logger.error('api', `Move to production error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * PATCH /api/crawler-sandbox/:id + * Update sandbox entry (e.g., add human review notes) + */ +router.patch('/:id', (0, middleware_1.requireRole)('admin'), async (req, res) => { + try { + const { id } = req.params; + const { human_review_notes, status, suspected_menu_provider } = req.body; + const updates = []; + const params = []; + let paramIndex = 1; + if (human_review_notes !== undefined) { + updates.push(`human_review_notes = $${paramIndex}`); + params.push(human_review_notes); + paramIndex++; + } + if (status) { + updates.push(`status = $${paramIndex}`); + params.push(status); + paramIndex++; + } + if (suspected_menu_provider !== undefined) { + updates.push(`suspected_menu_provider = $${paramIndex}`); + params.push(suspected_menu_provider); + paramIndex++; + } + if (updates.length === 0) { + return res.status(400).json({ error: 'No updates provided' }); + } + updates.push('updated_at = NOW()'); + if (human_review_notes !== undefined) { + updates.push('reviewed_at = NOW()'); + } + params.push(id); + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET ${updates.join(', ')} WHERE id = $${paramIndex}`, params); + res.json({ message: 'Sandbox updated' }); + } + catch (error) { + logger_1.logger.error('api', `Update sandbox error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +// ======================================== +// Templates +// ======================================== +/** + * GET /api/crawler-sandbox/templates + * List all crawler templates + */ +router.get('/templates/list', async (req, res) => { + try { + const result = await migrate_1.pool.query(`SELECT * FROM crawler_templates ORDER BY provider, is_default_for_provider DESC, name`); + res.json({ templates: result.rows }); + } + catch (error) { + logger_1.logger.error('api', `Get templates error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * GET /api/crawler-sandbox/templates/:id + * Get a single template + */ +router.get('/templates/:id', async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query('SELECT * FROM crawler_templates WHERE id = $1', [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Template not found' }); + } + res.json({ template: result.rows[0] }); + } + catch (error) { + logger_1.logger.error('api', `Get template error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/crawler-sandbox/templates + * Create a new template + */ +router.post('/templates', (0, middleware_1.requireRole)('admin'), async (req, res) => { + try { + const { provider, name, selector_config, navigation_config, transform_config, validation_rules, notes, } = req.body; + if (!provider || !name) { + return res.status(400).json({ error: 'provider and name are required' }); + } + const result = await migrate_1.pool.query(`INSERT INTO crawler_templates + (provider, name, selector_config, navigation_config, transform_config, validation_rules, notes, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING *`, [ + provider, + name, + JSON.stringify(selector_config || {}), + JSON.stringify(navigation_config || {}), + JSON.stringify(transform_config || {}), + JSON.stringify(validation_rules || {}), + notes, + req.user?.email || 'system', + ]); + res.status(201).json({ template: result.rows[0] }); + } + catch (error) { + logger_1.logger.error('api', `Create template error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * PUT /api/crawler-sandbox/templates/:id + * Update a template + */ +router.put('/templates/:id', (0, middleware_1.requireRole)('admin'), async (req, res) => { + try { + const { id } = req.params; + const { is_active, is_default_for_provider, selector_config, navigation_config, transform_config, validation_rules, notes, } = req.body; + const updates = []; + const params = []; + let paramIndex = 1; + if (is_active !== undefined) { + updates.push(`is_active = $${paramIndex}`); + params.push(is_active); + paramIndex++; + } + if (is_default_for_provider !== undefined) { + updates.push(`is_default_for_provider = $${paramIndex}`); + params.push(is_default_for_provider); + paramIndex++; + } + if (selector_config !== undefined) { + updates.push(`selector_config = $${paramIndex}`); + params.push(JSON.stringify(selector_config)); + paramIndex++; + } + if (navigation_config !== undefined) { + updates.push(`navigation_config = $${paramIndex}`); + params.push(JSON.stringify(navigation_config)); + paramIndex++; + } + if (transform_config !== undefined) { + updates.push(`transform_config = $${paramIndex}`); + params.push(JSON.stringify(transform_config)); + paramIndex++; + } + if (validation_rules !== undefined) { + updates.push(`validation_rules = $${paramIndex}`); + params.push(JSON.stringify(validation_rules)); + paramIndex++; + } + if (notes !== undefined) { + updates.push(`notes = $${paramIndex}`); + params.push(notes); + paramIndex++; + } + if (updates.length === 0) { + return res.status(400).json({ error: 'No updates provided' }); + } + updates.push('updated_at = NOW()'); + params.push(id); + await migrate_1.pool.query(`UPDATE crawler_templates SET ${updates.join(', ')} WHERE id = $${paramIndex}`, params); + const result = await migrate_1.pool.query('SELECT * FROM crawler_templates WHERE id = $1', [id]); + res.json({ template: result.rows[0] }); + } + catch (error) { + logger_1.logger.error('api', `Update template error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +// ======================================== +// Jobs +// ======================================== +/** + * GET /api/crawler-sandbox/jobs + * List sandbox crawl jobs + */ +router.get('/jobs/list', async (req, res) => { + try { + const { status, dispensaryId, limit = 50 } = req.query; + let query = ` + SELECT sj.*, d.name as dispensary_name + FROM sandbox_crawl_jobs sj + JOIN dispensaries d ON d.id = sj.dispensary_id + WHERE 1=1 + `; + const params = []; + let paramIndex = 1; + if (status) { + query += ` AND sj.status = $${paramIndex}`; + params.push(status); + paramIndex++; + } + if (dispensaryId) { + query += ` AND sj.dispensary_id = $${paramIndex}`; + params.push(Number(dispensaryId)); + paramIndex++; + } + query += ` ORDER BY sj.created_at DESC LIMIT $${paramIndex}`; + params.push(Number(limit)); + const result = await migrate_1.pool.query(query, params); + res.json({ jobs: result.rows }); + } + catch (error) { + logger_1.logger.error('api', `Get jobs error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/crawler-sandbox/jobs/detect/:dispensaryId + * Trigger provider detection for a dispensary + */ +router.post('/jobs/detect/:dispensaryId', (0, middleware_1.requireRole)('admin'), async (req, res) => { + try { + const { dispensaryId } = req.params; + // Create detection job + const job = await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, job_type, status, priority) + VALUES ($1, 'detection', 'pending', 30) + RETURNING id`, [dispensaryId]); + // Update dispensary status + await migrate_1.pool.query(`UPDATE dispensaries SET crawler_status = 'queued_detection', updated_at = NOW() WHERE id = $1`, [dispensaryId]); + res.json({ + message: 'Detection job queued', + jobId: job.rows[0].id, + }); + } + catch (error) { + logger_1.logger.error('api', `Queue detection error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +/** + * POST /api/crawler-sandbox/jobs/run/:id + * Immediately run a sandbox job + */ +router.post('/jobs/run/:id', (0, middleware_1.requireRole)('admin'), async (req, res) => { + try { + const { id } = req.params; + const job = await migrate_1.pool.query('SELECT * FROM sandbox_crawl_jobs WHERE id = $1', [id]); + if (job.rows.length === 0) { + return res.status(404).json({ error: 'Job not found' }); + } + const jobData = job.rows[0]; + // Run the job immediately + let result; + if (jobData.job_type === 'detection') { + result = await (0, crawler_jobs_1.runDetectMenuProviderJob)(jobData.dispensary_id); + } + else { + result = await (0, crawler_jobs_1.runSandboxCrawlJob)(jobData.dispensary_id, jobData.sandbox_id); + } + // Update job status + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs + SET status = $1, completed_at = NOW(), result_summary = $2, error_message = $3 + WHERE id = $4`, [ + result.success ? 'completed' : 'failed', + JSON.stringify(result.data || {}), + result.success ? null : result.message, + id, + ]); + res.json(result); + } + catch (error) { + logger_1.logger.error('api', `Run job error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +// ======================================== +// Stats +// ======================================== +/** + * GET /api/crawler-sandbox/stats + * Get sandbox/crawler statistics + */ +router.get('/stats/overview', async (req, res) => { + try { + // Dispensary provider stats + const providerStats = await migrate_1.pool.query(` + SELECT + menu_provider, + COUNT(*) as count, + AVG(menu_provider_confidence)::integer as avg_confidence + FROM dispensaries + WHERE menu_provider IS NOT NULL + GROUP BY menu_provider + ORDER BY count DESC + `); + // Mode stats + const modeStats = await migrate_1.pool.query(` + SELECT + crawler_mode, + COUNT(*) as count + FROM dispensaries + GROUP BY crawler_mode + `); + // Status stats + const statusStats = await migrate_1.pool.query(` + SELECT + crawler_status, + COUNT(*) as count + FROM dispensaries + GROUP BY crawler_status + ORDER BY count DESC + `); + // Sandbox stats + const sandboxStats = await migrate_1.pool.query(` + SELECT + status, + COUNT(*) as count + FROM crawler_sandboxes + GROUP BY status + `); + // Job stats + const jobStats = await migrate_1.pool.query(` + SELECT + status, + job_type, + COUNT(*) as count + FROM sandbox_crawl_jobs + GROUP BY status, job_type + `); + // Recent activity + const recentActivity = await migrate_1.pool.query(` + SELECT 'sandbox' as type, id, dispensary_id, status, created_at + FROM crawler_sandboxes + ORDER BY created_at DESC + LIMIT 5 + `); + res.json({ + providers: providerStats.rows, + modes: modeStats.rows, + statuses: statusStats.rows, + sandbox: sandboxStats.rows, + jobs: jobStats.rows, + recentActivity: recentActivity.rows, + }); + } + catch (error) { + logger_1.logger.error('api', `Get stats error: ${error.message}`); + res.status(500).json({ error: error.message }); + } +}); +exports.default = router; diff --git a/backend/dist/routes/dashboard.js b/backend/dist/routes/dashboard.js index 3e3efa83..2fbaeab3 100644 --- a/backend/dist/routes/dashboard.js +++ b/backend/dist/routes/dashboard.js @@ -2,63 +2,70 @@ Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); const middleware_1 = require("../auth/middleware"); -const migrate_1 = require("../db/migrate"); +const connection_1 = require("../dutchie-az/db/connection"); const router = (0, express_1.Router)(); router.use(middleware_1.authMiddleware); -// Get dashboard stats +// Get dashboard stats - uses consolidated dutchie-az DB router.get('/stats', async (req, res) => { try { - // Store stats - const storesResult = await migrate_1.pool.query(` - SELECT + // Store stats from dispensaries table in consolidated DB + const dispensariesResult = await (0, connection_1.query)(` + SELECT COUNT(*) as total, - COUNT(*) FILTER (WHERE active = true) as active, - MIN(last_scraped_at) as oldest_scrape, - MAX(last_scraped_at) as latest_scrape - FROM stores + COUNT(*) FILTER (WHERE menu_type IS NOT NULL AND menu_type != 'unknown') as active, + COUNT(*) FILTER (WHERE platform_dispensary_id IS NOT NULL) as with_platform_id, + COUNT(*) FILTER (WHERE menu_url IS NOT NULL) as with_menu_url, + MIN(last_crawled_at) as oldest_crawl, + MAX(last_crawled_at) as latest_crawl + FROM dispensaries `); - // Product stats - const productsResult = await migrate_1.pool.query(` - SELECT + // Product stats from dutchie_products table + const productsResult = await (0, connection_1.query)(` + SELECT COUNT(*) as total, - COUNT(*) FILTER (WHERE in_stock = true) as in_stock, - COUNT(*) FILTER (WHERE local_image_path IS NOT NULL) as with_images - FROM products + COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock, + COUNT(*) FILTER (WHERE primary_image_url IS NOT NULL) as with_images, + COUNT(DISTINCT brand_name) FILTER (WHERE brand_name IS NOT NULL AND brand_name != '') as unique_brands, + COUNT(DISTINCT dispensary_id) as dispensaries_with_products + FROM dutchie_products `); - // Campaign stats - const campaignsResult = await migrate_1.pool.query(` - SELECT - COUNT(*) as total, - COUNT(*) FILTER (WHERE active = true) as active - FROM campaigns - `); - // Recent clicks (last 24 hours) - const clicksResult = await migrate_1.pool.query(` - SELECT COUNT(*) as clicks_24h - FROM clicks - WHERE clicked_at >= NOW() - INTERVAL '24 hours' + // Brand stats from dutchie_products + const brandResult = await (0, connection_1.query)(` + SELECT COUNT(DISTINCT brand_name) as total + FROM dutchie_products + WHERE brand_name IS NOT NULL AND brand_name != '' `); // Recent products added (last 24 hours) - const recentProductsResult = await migrate_1.pool.query(` + const recentProductsResult = await (0, connection_1.query)(` SELECT COUNT(*) as new_products_24h - FROM products - WHERE first_seen_at >= NOW() - INTERVAL '24 hours' - `); - // Proxy stats - const proxiesResult = await migrate_1.pool.query(` - SELECT - COUNT(*) as total, - COUNT(*) FILTER (WHERE active = true) as active, - COUNT(*) FILTER (WHERE is_anonymous = true) as anonymous - FROM proxies + FROM dutchie_products + WHERE created_at >= NOW() - INTERVAL '24 hours' `); + // Combine results + const storeStats = dispensariesResult.rows[0]; + const productStats = productsResult.rows[0]; res.json({ - stores: storesResult.rows[0], - products: productsResult.rows[0], - campaigns: campaignsResult.rows[0], - clicks: clicksResult.rows[0], - recent: recentProductsResult.rows[0], - proxies: proxiesResult.rows[0] + stores: { + total: parseInt(storeStats.total) || 0, + active: parseInt(storeStats.active) || 0, + with_menu_url: parseInt(storeStats.with_menu_url) || 0, + with_platform_id: parseInt(storeStats.with_platform_id) || 0, + oldest_crawl: storeStats.oldest_crawl, + latest_crawl: storeStats.latest_crawl + }, + products: { + total: parseInt(productStats.total) || 0, + in_stock: parseInt(productStats.in_stock) || 0, + with_images: parseInt(productStats.with_images) || 0, + unique_brands: parseInt(productStats.unique_brands) || 0, + dispensaries_with_products: parseInt(productStats.dispensaries_with_products) || 0 + }, + brands: { + total: parseInt(brandResult.rows[0].total) || 0 + }, + campaigns: { total: 0, active: 0 }, // Legacy - no longer used + clicks: { clicks_24h: 0 }, // Legacy - no longer used + recent: recentProductsResult.rows[0] }); } catch (error) { @@ -66,27 +73,34 @@ router.get('/stats', async (req, res) => { res.status(500).json({ error: 'Failed to fetch dashboard stats' }); } }); -// Get recent activity +// Get recent activity - from consolidated dutchie-az DB router.get('/activity', async (req, res) => { try { const { limit = 20 } = req.query; - // Recent scrapes - const scrapesResult = await migrate_1.pool.query(` - SELECT s.name, s.last_scraped_at, - COUNT(p.id) as product_count - FROM stores s - LEFT JOIN products p ON s.id = p.store_id AND p.last_seen_at = s.last_scraped_at - WHERE s.last_scraped_at IS NOT NULL - GROUP BY s.id, s.name, s.last_scraped_at - ORDER BY s.last_scraped_at DESC + // Recent crawls from dispensaries (with product counts from dutchie_products) + const scrapesResult = await (0, connection_1.query)(` + SELECT + d.name, + d.last_crawled_at as last_scraped_at, + d.product_count + FROM dispensaries d + WHERE d.last_crawled_at IS NOT NULL + ORDER BY d.last_crawled_at DESC LIMIT $1 `, [limit]); - // Recent products - const productsResult = await migrate_1.pool.query(` - SELECT p.name, p.price, s.name as store_name, p.first_seen_at - FROM products p - JOIN stores s ON p.store_id = s.id - ORDER BY p.first_seen_at DESC + // Recent products from dutchie_products + const productsResult = await (0, connection_1.query)(` + SELECT + p.name, + 0 as price, + p.brand_name as brand, + p.thc as thc_percentage, + p.cbd as cbd_percentage, + d.name as store_name, + p.created_at as first_seen_at + FROM dutchie_products p + JOIN dispensaries d ON p.dispensary_id = d.id + ORDER BY p.created_at DESC LIMIT $1 `, [limit]); res.json({ diff --git a/backend/dist/routes/dispensaries.js b/backend/dist/routes/dispensaries.js new file mode 100644 index 00000000..cbb08c75 --- /dev/null +++ b/backend/dist/routes/dispensaries.js @@ -0,0 +1,437 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const middleware_1 = require("../auth/middleware"); +const migrate_1 = require("../db/migrate"); +const router = (0, express_1.Router)(); +router.use(middleware_1.authMiddleware); +// Valid menu_type values +const VALID_MENU_TYPES = ['dutchie', 'treez', 'jane', 'weedmaps', 'leafly', 'meadow', 'blaze', 'flowhub', 'dispense', 'cova', 'other', 'unknown']; +// Get all dispensaries +router.get('/', async (req, res) => { + try { + const { menu_type } = req.query; + let query = ` + SELECT + id, + azdhs_id, + name, + company_name, + slug, + address, + city, + state, + zip, + phone, + email, + website, + dba_name, + google_rating, + google_review_count, + status_line, + azdhs_url, + latitude, + longitude, + menu_url, + menu_type, + menu_provider, + menu_provider_confidence, + scraper_template, + last_menu_scrape, + menu_scrape_status, + platform_dispensary_id, + created_at, + updated_at + FROM dispensaries + `; + const params = []; + // Filter by menu_type if provided + if (menu_type) { + query += ` WHERE menu_type = $1`; + params.push(menu_type); + } + query += ` ORDER BY name`; + const result = await migrate_1.pool.query(query, params); + res.json({ dispensaries: result.rows }); + } + catch (error) { + console.error('Error fetching dispensaries:', error); + res.status(500).json({ error: 'Failed to fetch dispensaries' }); + } +}); +// Get menu type stats +router.get('/stats/menu-types', async (req, res) => { + try { + const result = await migrate_1.pool.query(` + SELECT menu_type, COUNT(*) as count + FROM dispensaries + GROUP BY menu_type + ORDER BY count DESC + `); + res.json({ menu_types: result.rows, valid_types: VALID_MENU_TYPES }); + } + catch (error) { + console.error('Error fetching menu type stats:', error); + res.status(500).json({ error: 'Failed to fetch menu type stats' }); + } +}); +// Get single dispensary by slug +router.get('/:slug', async (req, res) => { + try { + const { slug } = req.params; + const result = await migrate_1.pool.query(` + SELECT + id, + azdhs_id, + name, + company_name, + slug, + address, + city, + state, + zip, + phone, + email, + website, + dba_name, + google_rating, + google_review_count, + status_line, + azdhs_url, + latitude, + longitude, + menu_url, + menu_type, + menu_provider, + menu_provider_confidence, + scraper_template, + scraper_config, + last_menu_scrape, + menu_scrape_status, + platform_dispensary_id, + created_at, + updated_at + FROM dispensaries + WHERE slug = $1 + `, [slug]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + res.json(result.rows[0]); + } + catch (error) { + console.error('Error fetching dispensary:', error); + res.status(500).json({ error: 'Failed to fetch dispensary' }); + } +}); +// Update dispensary +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const { dba_name, website, phone, email, google_rating, google_review_count, menu_url, menu_type, scraper_template, scraper_config, menu_scrape_status } = req.body; + // Validate menu_type if provided + if (menu_type !== undefined && menu_type !== null && menu_type !== '' && !VALID_MENU_TYPES.includes(menu_type)) { + return res.status(400).json({ + error: `Invalid menu_type. Must be one of: ${VALID_MENU_TYPES.join(', ')}` + }); + } + const result = await migrate_1.pool.query(` + UPDATE dispensaries + SET + dba_name = COALESCE($1, dba_name), + website = COALESCE($2, website), + phone = COALESCE($3, phone), + email = COALESCE($4, email), + google_rating = COALESCE($5, google_rating), + google_review_count = COALESCE($6, google_review_count), + menu_url = COALESCE($7, menu_url), + menu_type = COALESCE($8, menu_type), + scraper_template = COALESCE($9, scraper_template), + scraper_config = COALESCE($10, scraper_config), + menu_scrape_status = COALESCE($11, menu_scrape_status), + updated_at = CURRENT_TIMESTAMP + WHERE id = $12 + RETURNING * + `, [ + dba_name, + website, + phone, + email, + google_rating, + google_review_count, + menu_url, + menu_type, + scraper_template, + scraper_config, + menu_scrape_status, + id + ]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + res.json(result.rows[0]); + } + catch (error) { + console.error('Error updating dispensary:', error); + res.status(500).json({ error: 'Failed to update dispensary' }); + } +}); +// Get products for a dispensary by slug +router.get('/:slug/products', async (req, res) => { + try { + const { slug } = req.params; + const { category } = req.query; + // First get the dispensary ID from slug + const dispensaryResult = await migrate_1.pool.query(` + SELECT id FROM dispensaries WHERE slug = $1 + `, [slug]); + if (dispensaryResult.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const dispensaryId = dispensaryResult.rows[0].id; + // Build query for products + let query = ` + SELECT + p.id, + p.name, + p.brand, + p.variant, + p.slug, + p.description, + p.regular_price, + p.sale_price, + p.thc_percentage, + p.cbd_percentage, + p.strain_type, + p.terpenes, + p.effects, + p.flavors, + p.image_url, + p.dutchie_url, + p.in_stock, + p.created_at, + p.updated_at + FROM products p + WHERE p.dispensary_id = $1 + `; + const params = [dispensaryId]; + if (category) { + query += ` AND p.category = $2`; + params.push(category); + } + query += ` ORDER BY p.created_at DESC`; + const result = await migrate_1.pool.query(query, params); + res.json({ products: result.rows }); + } + catch (error) { + console.error('Error fetching dispensary products:', error); + res.status(500).json({ error: 'Failed to fetch products' }); + } +}); +// Get unique brands for a dispensary by slug +router.get('/:slug/brands', async (req, res) => { + try { + const { slug } = req.params; + const { search } = req.query; + // First get the dispensary ID from slug + const dispensaryResult = await migrate_1.pool.query(` + SELECT id FROM dispensaries WHERE slug = $1 + `, [slug]); + if (dispensaryResult.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const dispensaryId = dispensaryResult.rows[0].id; + // Build query with optional search filter + let query = ` + SELECT DISTINCT + brand, + COUNT(*) as product_count + FROM products + WHERE dispensary_id = $1 AND brand IS NOT NULL + `; + const params = [dispensaryId]; + // Add search filter if provided + if (search) { + query += ` AND brand ILIKE $2`; + params.push(`%${search}%`); + } + query += ` GROUP BY brand ORDER BY product_count DESC, brand ASC`; + const result = await migrate_1.pool.query(query, params); + res.json({ brands: result.rows }); + } + catch (error) { + console.error('Error fetching dispensary brands:', error); + res.status(500).json({ error: 'Failed to fetch brands' }); + } +}); +// Get products with discounts/specials for a dispensary by slug +router.get('/:slug/specials', async (req, res) => { + try { + const { slug } = req.params; + const { search } = req.query; + // First get the dispensary ID from slug + const dispensaryResult = await migrate_1.pool.query(` + SELECT id FROM dispensaries WHERE slug = $1 + `, [slug]); + if (dispensaryResult.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const dispensaryId = dispensaryResult.rows[0].id; + // Build query to get products with discounts + let query = ` + SELECT + p.id, + p.name, + p.brand, + p.variant, + p.slug, + p.description, + p.regular_price, + p.sale_price, + p.discount_type, + p.discount_value, + p.thc_percentage, + p.cbd_percentage, + p.strain_type, + p.terpenes, + p.effects, + p.flavors, + p.image_url, + p.dutchie_url, + p.in_stock, + p.created_at, + p.updated_at + FROM products p + WHERE p.dispensary_id = $1 + AND p.discount_type IS NOT NULL + AND p.discount_value IS NOT NULL + `; + const params = [dispensaryId]; + // Add search filter if provided + if (search) { + query += ` AND (p.name ILIKE $2 OR p.brand ILIKE $2 OR p.description ILIKE $2)`; + params.push(`%${search}%`); + } + query += ` ORDER BY p.created_at DESC`; + const result = await migrate_1.pool.query(query, params); + res.json({ specials: result.rows }); + } + catch (error) { + console.error('Error fetching dispensary specials:', error); + res.status(500).json({ error: 'Failed to fetch specials' }); + } +}); +// Trigger scraping for a dispensary +router.post('/:slug/scrape', async (req, res) => { + try { + const { slug } = req.params; + const { type } = req.body; // 'products' | 'brands' | 'specials' | 'all' + if (!['products', 'brands', 'specials', 'all'].includes(type)) { + return res.status(400).json({ error: 'Invalid type. Must be: products, brands, specials, or all' }); + } + // Get the dispensary + const dispensaryResult = await migrate_1.pool.query(` + SELECT id, name, slug, website, menu_url, scraper_template, scraper_config + FROM dispensaries + WHERE slug = $1 + `, [slug]); + if (dispensaryResult.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const dispensary = dispensaryResult.rows[0]; + if (!dispensary.menu_url && !dispensary.website) { + return res.status(400).json({ error: 'Dispensary has no menu URL or website configured' }); + } + // Update last_menu_scrape time and status + await migrate_1.pool.query(` + UPDATE dispensaries + SET + last_menu_scrape = CURRENT_TIMESTAMP, + menu_scrape_status = 'pending', + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + `, [dispensary.id]); + // Log the scrape request + console.log(`[SCRAPE REQUEST] Dispensary: ${dispensary.name} (${slug}), Type: ${type}`); + console.log(` Menu URL: ${dispensary.menu_url || dispensary.website}`); + console.log(` Template: ${dispensary.scraper_template || 'N/A'}`); + // TODO: Actually trigger the scraper here + // For now, this is a placeholder that updates the status + // You can integrate with your existing scraper infrastructure + res.json({ + success: true, + message: `Scraping queued for ${dispensary.name}`, + type, + dispensary: { + id: dispensary.id, + name: dispensary.name, + slug: dispensary.slug + } + }); + } + catch (error) { + console.error('Error triggering scrape:', error); + res.status(500).json({ error: 'Failed to trigger scraping' }); + } +}); +// Update menu_type for a dispensary (dedicated endpoint) +router.patch('/:id/menu-type', async (req, res) => { + try { + const { id } = req.params; + const { menu_type } = req.body; + // Validate menu_type + if (menu_type !== null && menu_type !== '' && !VALID_MENU_TYPES.includes(menu_type)) { + return res.status(400).json({ + error: `Invalid menu_type. Must be one of: ${VALID_MENU_TYPES.join(', ')} (or null to clear)` + }); + } + const result = await migrate_1.pool.query(` + UPDATE dispensaries + SET menu_type = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 + RETURNING id, name, slug, menu_type, menu_provider, menu_url + `, [menu_type || null, id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + res.json({ + success: true, + dispensary: result.rows[0] + }); + } + catch (error) { + console.error('Error updating menu_type:', error); + res.status(500).json({ error: 'Failed to update menu_type' }); + } +}); +// Bulk update menu_type for multiple dispensaries +router.post('/bulk/menu-type', async (req, res) => { + try { + const { dispensary_ids, menu_type } = req.body; + if (!Array.isArray(dispensary_ids) || dispensary_ids.length === 0) { + return res.status(400).json({ error: 'dispensary_ids must be a non-empty array' }); + } + // Validate menu_type + if (menu_type !== null && menu_type !== '' && !VALID_MENU_TYPES.includes(menu_type)) { + return res.status(400).json({ + error: `Invalid menu_type. Must be one of: ${VALID_MENU_TYPES.join(', ')} (or null to clear)` + }); + } + const result = await migrate_1.pool.query(` + UPDATE dispensaries + SET menu_type = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = ANY($2::int[]) + RETURNING id, name, slug, menu_type + `, [menu_type || null, dispensary_ids]); + res.json({ + success: true, + updated_count: result.rowCount, + dispensaries: result.rows + }); + } + catch (error) { + console.error('Error bulk updating menu_type:', error); + res.status(500).json({ error: 'Failed to bulk update menu_type' }); + } +}); +exports.default = router; diff --git a/backend/dist/routes/parallel-scrape.js b/backend/dist/routes/parallel-scrape.js new file mode 100644 index 00000000..5384c256 --- /dev/null +++ b/backend/dist/routes/parallel-scrape.js @@ -0,0 +1,182 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const migrate_1 = require("../db/migrate"); +const proxy_1 = require("../services/proxy"); +const middleware_1 = require("../auth/middleware"); +const router = (0, express_1.Router)(); +router.use(middleware_1.authMiddleware); +const FIREFOX_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'; +// In-memory job tracking +const activeJobs = new Map(); +// Get job status +router.get('/status/:jobId', (req, res) => { + const job = activeJobs.get(req.params.jobId); + if (!job) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(job); +}); +// List active jobs +router.get('/jobs', (req, res) => { + const jobs = Array.from(activeJobs.values()); + res.json({ jobs }); +}); +// Start parallel scrape +router.post('/start', async (req, res) => { + const { storeName = 'Deeply Rooted', workers = 15, useProxies = true } = req.body; + try { + // Find the store + const storeResult = await migrate_1.pool.query(`SELECT id, name, slug, dutchie_url FROM stores WHERE name ILIKE $1 LIMIT 1`, [`%${storeName}%`]); + if (storeResult.rows.length === 0) { + return res.status(404).json({ error: `Store not found: ${storeName}` }); + } + const store = storeResult.rows[0]; + // Get categories + const categoriesResult = await migrate_1.pool.query(`SELECT id, name, slug, dutchie_url as url FROM categories WHERE store_id = $1 AND scrape_enabled = true`, [store.id]); + if (categoriesResult.rows.length === 0) { + return res.status(404).json({ error: 'No categories found for this store' }); + } + const categories = categoriesResult.rows; + // Create job + const jobId = `scrape-${Date.now()}`; + const job = { + id: jobId, + storeName: store.name, + status: 'running', + workers, + startedAt: new Date(), + results: [] + }; + activeJobs.set(jobId, job); + // Start scraping in background + runParallelScrape(job, store, categories, workers, useProxies).catch(err => { + console.error('Parallel scrape error:', err); + job.status = 'failed'; + }); + res.json({ + message: 'Parallel scrape started', + jobId, + store: store.name, + categories: categories.length, + workers + }); + } + catch (error) { + console.error('Failed to start parallel scrape:', error); + res.status(500).json({ error: error.message }); + } +}); +async function runParallelScrape(job, store, categories, numWorkers, useProxies) { + const puppeteer = require('puppeteer-extra'); + const StealthPlugin = require('puppeteer-extra-plugin-stealth'); + puppeteer.use(StealthPlugin()); + // Expand categories for multiple passes + const expandedCategories = []; + const passes = Math.ceil(numWorkers / Math.max(categories.length, 1)); + for (let i = 0; i < passes; i++) { + expandedCategories.push(...categories); + } + const categoryIndex = { current: 0 }; + const worker = async (workerId) => { + while (categoryIndex.current < expandedCategories.length) { + const idx = categoryIndex.current++; + const category = expandedCategories[idx]; + if (!category) + break; + const result = await scrapeCategory(puppeteer, workerId, category, useProxies); + job.results.push({ + category: category.name, + success: result.success, + products: result.products, + error: result.error + }); + // Delay between requests + await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 3000)); + } + }; + // Start workers with staggered starts + const workers = []; + for (let i = 0; i < numWorkers; i++) { + workers.push(worker(i + 1)); + await new Promise(resolve => setTimeout(resolve, 500)); + } + await Promise.all(workers); + job.status = 'completed'; + job.completedAt = new Date(); + // Clean up job after 1 hour + setTimeout(() => activeJobs.delete(job.id), 60 * 60 * 1000); +} +async function scrapeCategory(puppeteer, workerId, category, useProxies) { + let browser = null; + let proxyId = null; + try { + let proxy = null; + if (useProxies) { + proxy = await (0, proxy_1.getActiveProxy)(); + } + const args = [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--disable-gpu', + '--window-size=1920,1080', + ]; + if (proxy) { + proxyId = proxy.id; + if (proxy.protocol === 'socks5' || proxy.protocol === 'socks') { + args.push(`--proxy-server=socks5://${proxy.host}:${proxy.port}`); + } + else { + args.push(`--proxy-server=${proxy.protocol}://${proxy.host}:${proxy.port}`); + } + } + browser = await puppeteer.launch({ + headless: 'new', + args, + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium', + }); + const page = await browser.newPage(); + await page.setUserAgent(FIREFOX_USER_AGENT); + await page.setViewport({ width: 1920, height: 1080 }); + if (proxy?.username && proxy?.password) { + await page.authenticate({ + username: proxy.username, + password: proxy.password, + }); + } + console.log(`[Worker ${workerId}] Scraping: ${category.name} (${category.url})`); + const response = await page.goto(category.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); + if (!response || !response.ok()) { + throw new Error(`Failed to load page: ${response?.status()}`); + } + await page.waitForSelector('[data-testid="product-list-item"], a[href*="/product/"]', { + timeout: 30000, + }).catch(() => { }); + const products = await page.evaluate(() => { + // Try data-testid first, then fall back to product links + const listItems = document.querySelectorAll('[data-testid="product-list-item"]'); + if (listItems.length > 0) + return listItems.length; + return document.querySelectorAll('a[href*="/product/"]').length; + }); + console.log(`[Worker ${workerId}] Found ${products} products in ${category.name}`); + await browser.close(); + return { success: true, products }; + } + catch (error) { + console.error(`[Worker ${workerId}] Error:`, error.message); + if (proxyId && (0, proxy_1.isBotDetectionError)(error.message)) { + (0, proxy_1.putProxyInTimeout)(proxyId, error.message); + } + if (browser) { + await browser.close().catch(() => { }); + } + return { success: false, products: 0, error: error.message }; + } +} +exports.default = router; diff --git a/backend/dist/routes/products.js b/backend/dist/routes/products.js index a2596b45..3cab78b3 100644 --- a/backend/dist/routes/products.js +++ b/backend/dist/routes/products.js @@ -6,10 +6,69 @@ const migrate_1 = require("../db/migrate"); const minio_1 = require("../utils/minio"); const router = (0, express_1.Router)(); router.use(middleware_1.authMiddleware); -// Get all products with filters +// Freshness threshold: data older than this is considered stale +const STALE_THRESHOLD_HOURS = 4; +function calculateFreshness(lastCrawlAt) { + if (!lastCrawlAt) { + return { + last_crawl_at: null, + is_stale: true, + freshness: 'Never crawled', + hours_since_crawl: null + }; + } + const now = new Date(); + const diffMs = now.getTime() - lastCrawlAt.getTime(); + const diffHours = diffMs / (1000 * 60 * 60); + const isStale = diffHours > STALE_THRESHOLD_HOURS; + let freshnessText; + if (diffHours < 1) { + const mins = Math.round(diffHours * 60); + freshnessText = `Last crawled ${mins} minute${mins !== 1 ? 's' : ''} ago`; + } + else if (diffHours < 24) { + const hrs = Math.round(diffHours); + freshnessText = `Last crawled ${hrs} hour${hrs !== 1 ? 's' : ''} ago`; + } + else { + const days = Math.round(diffHours / 24); + freshnessText = `Last crawled ${days} day${days !== 1 ? 's' : ''} ago`; + } + if (isStale) { + freshnessText += ' (STALE)'; + } + return { + last_crawl_at: lastCrawlAt.toISOString(), + is_stale: isStale, + freshness: freshnessText, + hours_since_crawl: Math.round(diffHours * 10) / 10 + }; +} +// Helper function to filter fields from object +function selectFields(obj, fields) { + if (!fields || fields.length === 0) + return obj; + const result = {}; + fields.forEach(field => { + if (obj.hasOwnProperty(field)) { + result[field] = obj[field]; + } + }); + return result; +} +// Get all products with filters, sorting, and field selection router.get('/', async (req, res) => { try { - const { store_id, category_id, in_stock, search, limit = 50, offset = 0 } = req.query; + const { store_id, category_id, in_stock, search, brand, min_price, max_price, min_thc, max_thc, strain_type, sort_by = 'last_seen_at', sort_order = 'desc', limit = 50, offset = 0, fields } = req.query; + // Validate sort field to prevent SQL injection + const allowedSortFields = [ + 'id', 'name', 'brand', 'price', 'thc_percentage', + 'cbd_percentage', 'last_seen_at', 'created_at' + ]; + const sortField = allowedSortFields.includes(sort_by) + ? sort_by + : 'last_seen_at'; + const sortDirection = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; let query = ` SELECT p.*, s.name as store_name, c.name as category_name FROM products p @@ -19,35 +78,81 @@ router.get('/', async (req, res) => { `; const params = []; let paramCount = 1; + // Store filter if (store_id) { query += ` AND p.store_id = $${paramCount}`; params.push(store_id); paramCount++; } + // Category filter if (category_id) { query += ` AND p.category_id = $${paramCount}`; params.push(category_id); paramCount++; } + // Stock filter if (in_stock !== undefined) { query += ` AND p.in_stock = $${paramCount}`; params.push(in_stock === 'true'); paramCount++; } + // Search filter if (search) { - query += ` AND (p.name ILIKE $${paramCount} OR p.brand ILIKE $${paramCount})`; + query += ` AND (p.name ILIKE $${paramCount} OR p.brand ILIKE $${paramCount} OR p.description ILIKE $${paramCount})`; params.push(`%${search}%`); paramCount++; } - query += ` ORDER BY p.last_seen_at DESC LIMIT $${paramCount} OFFSET $${paramCount + 1}`; + // Brand filter + if (brand) { + query += ` AND p.brand ILIKE $${paramCount}`; + params.push(`%${brand}%`); + paramCount++; + } + // Price range filter + if (min_price) { + query += ` AND p.price >= $${paramCount}`; + params.push(parseFloat(min_price)); + paramCount++; + } + if (max_price) { + query += ` AND p.price <= $${paramCount}`; + params.push(parseFloat(max_price)); + paramCount++; + } + // THC range filter + if (min_thc) { + query += ` AND p.thc_percentage >= $${paramCount}`; + params.push(parseFloat(min_thc)); + paramCount++; + } + if (max_thc) { + query += ` AND p.thc_percentage <= $${paramCount}`; + params.push(parseFloat(max_thc)); + paramCount++; + } + // Strain type filter + if (strain_type) { + query += ` AND p.strain_type = $${paramCount}`; + params.push(strain_type); + paramCount++; + } + // Sorting + query += ` ORDER BY p.${sortField} ${sortDirection} LIMIT $${paramCount} OFFSET $${paramCount + 1}`; params.push(limit, offset); const result = await migrate_1.pool.query(query, params); // Add image URLs - const products = result.rows.map(p => ({ + let products = result.rows.map((p) => ({ ...p, - image_url_full: p.local_image_path ? (0, minio_1.getImageUrl)(p.local_image_path) : p.image_url + image_url_full: p.local_image_path ? (0, minio_1.getImageUrl)(p.local_image_path) : p.image_url, + thumbnail_url: p.thumbnail_path ? (0, minio_1.getImageUrl)(p.thumbnail_path) : null, + medium_url: p.medium_path ? (0, minio_1.getImageUrl)(p.medium_path) : null, })); - // Get total count + // Field selection + if (fields) { + const selectedFields = fields.split(',').map(f => f.trim()); + products = products.map((p) => selectFields(p, selectedFields)); + } + // Get total count (reuse same filters) let countQuery = `SELECT COUNT(*) FROM products p WHERE 1=1`; const countParams = []; let countParamCount = 1; @@ -67,16 +172,79 @@ router.get('/', async (req, res) => { countParamCount++; } if (search) { - countQuery += ` AND (p.name ILIKE $${countParamCount} OR p.brand ILIKE $${countParamCount})`; + countQuery += ` AND (p.name ILIKE $${countParamCount} OR p.brand ILIKE $${countParamCount} OR p.description ILIKE $${countParamCount})`; countParams.push(`%${search}%`); countParamCount++; } + if (brand) { + countQuery += ` AND p.brand ILIKE $${countParamCount}`; + countParams.push(`%${brand}%`); + countParamCount++; + } + if (min_price) { + countQuery += ` AND p.price >= $${countParamCount}`; + countParams.push(parseFloat(min_price)); + countParamCount++; + } + if (max_price) { + countQuery += ` AND p.price <= $${countParamCount}`; + countParams.push(parseFloat(max_price)); + countParamCount++; + } + if (min_thc) { + countQuery += ` AND p.thc_percentage >= $${countParamCount}`; + countParams.push(parseFloat(min_thc)); + countParamCount++; + } + if (max_thc) { + countQuery += ` AND p.thc_percentage <= $${countParamCount}`; + countParams.push(parseFloat(max_thc)); + countParamCount++; + } + if (strain_type) { + countQuery += ` AND p.strain_type = $${countParamCount}`; + countParams.push(strain_type); + countParamCount++; + } const countResult = await migrate_1.pool.query(countQuery, countParams); + // Get freshness info if store_id is specified + let freshnessInfo = null; + let storeInfo = null; + if (store_id) { + const storeResult = await migrate_1.pool.query('SELECT id, name, last_scraped_at FROM stores WHERE id = $1', [store_id]); + if (storeResult.rows.length > 0) { + const store = storeResult.rows[0]; + storeInfo = { id: store.id, name: store.name }; + freshnessInfo = calculateFreshness(store.last_scraped_at); + } + } res.json({ products, total: parseInt(countResult.rows[0].count), limit: parseInt(limit), - offset: parseInt(offset) + offset: parseInt(offset), + // Add freshness metadata when store_id is provided + ...(freshnessInfo && { + store: storeInfo, + last_crawl_at: freshnessInfo.last_crawl_at, + is_stale: freshnessInfo.is_stale, + freshness: freshnessInfo.freshness, + hours_since_crawl: freshnessInfo.hours_since_crawl + }), + filters: { + store_id, + category_id, + in_stock, + search, + brand, + min_price, + max_price, + min_thc, + max_thc, + strain_type, + sort_by: sortField, + sort_order: sortDirection + } }); } catch (error) { @@ -84,10 +252,11 @@ router.get('/', async (req, res) => { res.status(500).json({ error: 'Failed to fetch products' }); } }); -// Get single product +// Get single product with optional field selection router.get('/:id', async (req, res) => { try { const { id } = req.params; + const { fields } = req.query; const result = await migrate_1.pool.query(` SELECT p.*, s.name as store_name, c.name as category_name FROM products p @@ -98,10 +267,17 @@ router.get('/:id', async (req, res) => { if (result.rows.length === 0) { return res.status(404).json({ error: 'Product not found' }); } - const product = result.rows[0]; + let product = result.rows[0]; product.image_url_full = product.local_image_path ? (0, minio_1.getImageUrl)(product.local_image_path) : product.image_url; + product.thumbnail_url = product.thumbnail_path ? (0, minio_1.getImageUrl)(product.thumbnail_path) : null; + product.medium_url = product.medium_path ? (0, minio_1.getImageUrl)(product.medium_path) : null; + // Field selection + if (fields) { + const selectedFields = fields.split(',').map(f => f.trim()); + product = selectFields(product, selectedFields); + } res.json({ product }); } catch (error) { @@ -109,4 +285,57 @@ router.get('/:id', async (req, res) => { res.status(500).json({ error: 'Failed to fetch product' }); } }); +// Get available brands (for filter dropdowns) +router.get('/meta/brands', async (req, res) => { + try { + const { store_id } = req.query; + let query = ` + SELECT DISTINCT brand + FROM products + WHERE brand IS NOT NULL AND brand != '' + `; + const params = []; + if (store_id) { + query += ' AND store_id = $1'; + params.push(store_id); + } + query += ' ORDER BY brand'; + const result = await migrate_1.pool.query(query, params); + const brands = result.rows.map((row) => row.brand); + res.json({ brands }); + } + catch (error) { + console.error('Error fetching brands:', error); + res.status(500).json({ error: 'Failed to fetch brands' }); + } +}); +// Get price range (for filter sliders) +router.get('/meta/price-range', async (req, res) => { + try { + const { store_id } = req.query; + let query = ` + SELECT + MIN(price) as min_price, + MAX(price) as max_price, + AVG(price) as avg_price + FROM products + WHERE price IS NOT NULL + `; + const params = []; + if (store_id) { + query += ' AND store_id = $1'; + params.push(store_id); + } + const result = await migrate_1.pool.query(query, params); + res.json({ + min_price: parseFloat(result.rows[0].min_price) || 0, + max_price: parseFloat(result.rows[0].max_price) || 0, + avg_price: parseFloat(result.rows[0].avg_price) || 0 + }); + } + catch (error) { + console.error('Error fetching price range:', error); + res.status(500).json({ error: 'Failed to fetch price range' }); + } +}); exports.default = router; diff --git a/backend/dist/routes/proxies.js b/backend/dist/routes/proxies.js index c6275d8e..24d2d1d2 100644 --- a/backend/dist/routes/proxies.js +++ b/backend/dist/routes/proxies.js @@ -1,17 +1,52 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); const middleware_1 = require("../auth/middleware"); const migrate_1 = require("../db/migrate"); const proxy_1 = require("../services/proxy"); +const proxyTestQueue_1 = require("../services/proxyTestQueue"); const router = (0, express_1.Router)(); router.use(middleware_1.authMiddleware); // Get all proxies router.get('/', async (req, res) => { try { const result = await migrate_1.pool.query(` - SELECT id, host, port, protocol, active, is_anonymous, - last_tested_at, test_result, response_time_ms, created_at + SELECT id, host, port, protocol, active, is_anonymous, + last_tested_at, test_result, response_time_ms, created_at, + city, state, country, country_code, location_updated_at FROM proxies ORDER BY created_at DESC `); @@ -22,6 +57,32 @@ router.get('/', async (req, res) => { res.status(500).json({ error: 'Failed to fetch proxies' }); } }); +// Get active proxy test job (must be before /:id route) +router.get('/test-job', async (req, res) => { + try { + const job = await (0, proxyTestQueue_1.getActiveProxyTestJob)(); + res.json({ job }); + } + catch (error) { + console.error('Error fetching active job:', error); + res.status(500).json({ error: 'Failed to fetch active job' }); + } +}); +// Get proxy test job status (must be before /:id route) +router.get('/test-job/:jobId', async (req, res) => { + try { + const { jobId } = req.params; + const job = await (0, proxyTestQueue_1.getProxyTestJob)(parseInt(jobId)); + if (!job) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json({ job }); + } + catch (error) { + console.error('Error fetching job status:', error); + res.status(500).json({ error: 'Failed to fetch job status' }); + } +}); // Get single proxy router.get('/:id', async (req, res) => { try { @@ -113,18 +174,30 @@ router.post('/:id/test', (0, middleware_1.requireRole)('superadmin', 'admin'), a res.status(500).json({ error: 'Failed to test proxy' }); } }); -// Test all proxies +// Start proxy test job router.post('/test-all', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { - // Run in background - (0, proxy_1.testAllProxies)().catch(err => { - console.error('Background proxy testing error:', err); - }); - res.json({ message: 'Proxy testing started in background' }); + const jobId = await (0, proxyTestQueue_1.createProxyTestJob)(); + res.json({ jobId, message: 'Proxy test job started' }); } catch (error) { - console.error('Error starting proxy tests:', error); - res.status(500).json({ error: 'Failed to start proxy tests' }); + console.error('Error starting proxy test job:', error); + res.status(500).json({ error: 'Failed to start proxy test job' }); + } +}); +// Cancel proxy test job +router.post('/test-job/:jobId/cancel', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { jobId } = req.params; + const cancelled = await (0, proxyTestQueue_1.cancelProxyTestJob)(parseInt(jobId)); + if (!cancelled) { + return res.status(404).json({ error: 'Job not found or already completed' }); + } + res.json({ message: 'Job cancelled successfully' }); + } + catch (error) { + console.error('Error cancelling job:', error); + res.status(500).json({ error: 'Failed to cancel job' }); } }); // Update proxy @@ -171,4 +244,19 @@ router.delete('/:id', (0, middleware_1.requireRole)('superadmin'), async (req, r res.status(500).json({ error: 'Failed to delete proxy' }); } }); +// Update all proxy locations +router.post('/update-locations', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { updateAllProxyLocations } = await Promise.resolve().then(() => __importStar(require('../services/geolocation'))); + // Run in background + updateAllProxyLocations().catch(err => { + console.error('❌ Location update failed:', err); + }); + res.json({ message: 'Location update job started' }); + } + catch (error) { + console.error('Error starting location update:', error); + res.status(500).json({ error: 'Failed to start location update' }); + } +}); exports.default = router; diff --git a/backend/dist/routes/public-api.js b/backend/dist/routes/public-api.js new file mode 100644 index 00000000..88b78aa6 --- /dev/null +++ b/backend/dist/routes/public-api.js @@ -0,0 +1,668 @@ +"use strict"; +/** + * Public API Routes for External Consumers (WordPress, etc.) + * + * These routes use the dutchie_az data pipeline and are protected by API key auth. + * Designed for Deeply Rooted and other WordPress sites consuming menu data. + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const migrate_1 = require("../db/migrate"); +const connection_1 = require("../dutchie-az/db/connection"); +const ipaddr_js_1 = __importDefault(require("ipaddr.js")); +const router = (0, express_1.Router)(); +// ============================================================ +// MIDDLEWARE +// ============================================================ +/** + * Validates if an IP address matches any of the allowed IP patterns + */ +function isIpAllowed(clientIp, allowedIps) { + try { + const clientAddr = ipaddr_js_1.default.process(clientIp); + for (const allowedIp of allowedIps) { + const trimmed = allowedIp.trim(); + if (!trimmed) + continue; + if (trimmed.includes('/')) { + try { + const range = ipaddr_js_1.default.parseCIDR(trimmed); + if (clientAddr.match(range)) { + return true; + } + } + catch (e) { + console.warn(`Invalid CIDR notation: ${trimmed}`); + continue; + } + } + else { + try { + const allowedAddr = ipaddr_js_1.default.process(trimmed); + if (clientAddr.toString() === allowedAddr.toString()) { + return true; + } + } + catch (e) { + console.warn(`Invalid IP address: ${trimmed}`); + continue; + } + } + } + return false; + } + catch (error) { + console.error('Error processing client IP:', error); + return false; + } +} +/** + * Validates if a domain matches any of the allowed domain patterns + */ +function isDomainAllowed(origin, allowedDomains) { + try { + const url = new URL(origin); + const domain = url.hostname; + for (const allowedDomain of allowedDomains) { + const trimmed = allowedDomain.trim(); + if (!trimmed) + continue; + if (trimmed.startsWith('*.')) { + const baseDomain = trimmed.substring(2); + if (domain === baseDomain || domain.endsWith('.' + baseDomain)) { + return true; + } + } + else { + if (domain === trimmed) { + return true; + } + } + } + return false; + } + catch (error) { + console.error('Error processing domain:', error); + return false; + } +} +/** + * Middleware to validate API key and resolve dispensary -> dutchie_az store mapping + */ +async function validatePublicApiKey(req, res, next) { + const apiKey = req.headers['x-api-key']; + if (!apiKey) { + return res.status(401).json({ + error: 'Missing API key', + message: 'Provide your API key in the X-API-Key header' + }); + } + try { + // Query WordPress permissions table with store info + const result = await migrate_1.pool.query(` + SELECT + p.id, + p.user_name, + p.api_key, + p.allowed_ips, + p.allowed_domains, + p.is_active, + p.store_id, + p.store_name + FROM wp_dutchie_api_permissions p + WHERE p.api_key = $1 AND p.is_active = 1 + `, [apiKey]); + if (result.rows.length === 0) { + return res.status(401).json({ + error: 'Invalid API key' + }); + } + const permission = result.rows[0]; + // Validate IP if configured + const clientIp = req.headers['x-forwarded-for']?.split(',')[0].trim() || + req.headers['x-real-ip'] || + req.ip || + req.connection.remoteAddress || + ''; + if (permission.allowed_ips) { + const allowedIps = permission.allowed_ips.split('\n').filter((ip) => ip.trim()); + if (allowedIps.length > 0 && !isIpAllowed(clientIp, allowedIps)) { + return res.status(403).json({ + error: 'IP address not allowed', + client_ip: clientIp + }); + } + } + // Validate domain if configured + const origin = req.get('origin') || req.get('referer') || ''; + if (permission.allowed_domains && origin) { + const allowedDomains = permission.allowed_domains.split('\n').filter((d) => d.trim()); + if (allowedDomains.length > 0 && !isDomainAllowed(origin, allowedDomains)) { + return res.status(403).json({ + error: 'Domain not allowed', + origin: origin + }); + } + } + // Resolve the dutchie_az store for this store + // Match by store name (from main DB) to dutchie_az.dispensaries.name + const storeResult = await (0, connection_1.query)(` + SELECT id FROM dispensaries + WHERE LOWER(TRIM(name)) = LOWER(TRIM($1)) + OR LOWER(TRIM(name)) LIKE LOWER(TRIM($1)) || '%' + OR LOWER(TRIM($1)) LIKE LOWER(TRIM(name)) || '%' + ORDER BY + CASE WHEN LOWER(TRIM(name)) = LOWER(TRIM($1)) THEN 0 ELSE 1 END, + id + LIMIT 1 + `, [permission.store_name]); + if (storeResult.rows.length > 0) { + permission.dutchie_az_store_id = storeResult.rows[0].id; + } + // Update last_used_at timestamp (async, don't wait) + migrate_1.pool.query(` + UPDATE wp_dutchie_api_permissions + SET last_used_at = CURRENT_TIMESTAMP + WHERE id = $1 + `, [permission.id]).catch((err) => { + console.error('Error updating last_used_at:', err); + }); + req.apiPermission = permission; + next(); + } + catch (error) { + console.error('Public API validation error:', error); + return res.status(500).json({ + error: 'Internal server error during API validation' + }); + } +} +// Apply middleware to all routes +router.use(validatePublicApiKey); +// ============================================================ +// PRODUCT ENDPOINTS +// ============================================================ +/** + * GET /api/v1/products + * Get products for the authenticated dispensary + * + * Query params: + * - category: Filter by product type (e.g., 'flower', 'edible') + * - brand: Filter by brand name + * - in_stock_only: Only return in-stock products (default: false) + * - limit: Max products to return (default: 100, max: 500) + * - offset: Pagination offset (default: 0) + */ +router.get('/products', async (req, res) => { + try { + const permission = req.apiPermission; + // Check if we have a dutchie_az store mapping + if (!permission.dutchie_az_store_id) { + return res.status(503).json({ + error: 'No menu data available', + message: `Menu data for ${permission.store_name} is not yet available. The dispensary may not be set up in the new data pipeline.`, + dispensary_name: permission.store_name + }); + } + const { category, brand, in_stock_only = 'false', limit = '100', offset = '0' } = req.query; + // Build query + let whereClause = 'WHERE p.dispensary_id = $1'; + const params = [permission.dutchie_az_store_id]; + let paramIndex = 2; + // Filter by stock status if requested + if (in_stock_only === 'true' || in_stock_only === '1') { + whereClause += ` AND p.stock_status = 'in_stock'`; + } + // Filter by category (maps to 'type' in dutchie_az) + if (category) { + whereClause += ` AND LOWER(p.type) = LOWER($${paramIndex})`; + params.push(category); + paramIndex++; + } + // Filter by brand + if (brand) { + whereClause += ` AND LOWER(p.brand_name) LIKE LOWER($${paramIndex})`; + params.push(`%${brand}%`); + paramIndex++; + } + // Enforce limits + const limitNum = Math.min(parseInt(limit, 10) || 100, 500); + const offsetNum = parseInt(offset, 10) || 0; + params.push(limitNum, offsetNum); + // Query products with latest snapshot data + const { rows: products } = await (0, connection_1.query)(` + SELECT + p.id, + p.external_product_id as dutchie_id, + p.name, + p.brand_name as brand, + p.type as category, + p.subcategory, + p.strain_type, + p.stock_status, + p.thc, + p.cbd, + p.primary_image_url as image_url, + p.images, + p.effects, + p.created_at, + p.updated_at, + -- Latest snapshot data for pricing + s.rec_min_price_cents, + s.rec_max_price_cents, + s.rec_min_special_price_cents, + s.med_min_price_cents, + s.med_max_price_cents, + s.med_min_special_price_cents, + s.total_quantity_available, + s.options, + s.special, + s.crawled_at as snapshot_at + FROM dutchie_products p + LEFT JOIN LATERAL ( + SELECT * FROM dutchie_product_snapshots + WHERE dutchie_product_id = p.id + ORDER BY crawled_at DESC + LIMIT 1 + ) s ON true + ${whereClause} + ORDER BY p.name ASC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1} + `, params); + // Get total count for pagination + const { rows: countRows } = await (0, connection_1.query)(` + SELECT COUNT(*) as total FROM dutchie_products p ${whereClause} + `, params.slice(0, -2)); + // Transform products to backward-compatible format + const transformedProducts = products.map((p) => { + // Extract first image URL from images array + let imageUrl = p.image_url; + if (!imageUrl && p.images && Array.isArray(p.images) && p.images.length > 0) { + const firstImage = p.images[0]; + imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url; + } + // Convert prices from cents to dollars + const regularPrice = p.rec_min_price_cents + ? (p.rec_min_price_cents / 100).toFixed(2) + : null; + const salePrice = p.rec_min_special_price_cents + ? (p.rec_min_special_price_cents / 100).toFixed(2) + : null; + return { + id: p.id, + dutchie_id: p.dutchie_id, + name: p.name, + brand: p.brand || null, + category: p.category || null, + subcategory: p.subcategory || null, + strain_type: p.strain_type || null, + description: null, // Not stored in dutchie_products, would need snapshot + regular_price: regularPrice, + sale_price: salePrice, + thc_percentage: p.thc ? parseFloat(p.thc) : null, + cbd_percentage: p.cbd ? parseFloat(p.cbd) : null, + image_url: imageUrl || null, + in_stock: p.stock_status === 'in_stock', + on_special: p.special || false, + effects: p.effects || [], + options: p.options || [], + quantity_available: p.total_quantity_available || 0, + created_at: p.created_at, + updated_at: p.updated_at, + snapshot_at: p.snapshot_at + }; + }); + res.json({ + success: true, + dispensary: permission.store_name, + products: transformedProducts, + pagination: { + total: parseInt(countRows[0]?.total || '0', 10), + limit: limitNum, + offset: offsetNum, + has_more: offsetNum + products.length < parseInt(countRows[0]?.total || '0', 10) + } + }); + } + catch (error) { + console.error('Public API products error:', error); + res.status(500).json({ + error: 'Failed to fetch products', + message: error.message + }); + } +}); +/** + * GET /api/v1/products/:id + * Get a single product by ID + */ +router.get('/products/:id', async (req, res) => { + try { + const permission = req.apiPermission; + const { id } = req.params; + if (!permission.dutchie_az_store_id) { + return res.status(503).json({ + error: 'No menu data available', + message: `Menu data for ${permission.store_name} is not yet available.` + }); + } + // Get product with latest snapshot + const { rows: products } = await (0, connection_1.query)(` + SELECT + p.*, + s.rec_min_price_cents, + s.rec_max_price_cents, + s.rec_min_special_price_cents, + s.med_min_price_cents, + s.med_max_price_cents, + s.total_quantity_available, + s.options, + s.special, + s.crawled_at as snapshot_at + FROM dutchie_products p + LEFT JOIN LATERAL ( + SELECT * FROM dutchie_product_snapshots + WHERE dutchie_product_id = p.id + ORDER BY crawled_at DESC + LIMIT 1 + ) s ON true + WHERE p.id = $1 AND p.dispensary_id = $2 + `, [id, permission.dutchie_az_store_id]); + if (products.length === 0) { + return res.status(404).json({ + error: 'Product not found' + }); + } + const p = products[0]; + // Extract first image URL + let imageUrl = p.primary_image_url; + if (!imageUrl && p.images && Array.isArray(p.images) && p.images.length > 0) { + const firstImage = p.images[0]; + imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url; + } + res.json({ + success: true, + product: { + id: p.id, + dutchie_id: p.external_product_id, + name: p.name, + brand: p.brand_name || null, + category: p.type || null, + subcategory: p.subcategory || null, + strain_type: p.strain_type || null, + regular_price: p.rec_min_price_cents ? (p.rec_min_price_cents / 100).toFixed(2) : null, + sale_price: p.rec_min_special_price_cents ? (p.rec_min_special_price_cents / 100).toFixed(2) : null, + thc_percentage: p.thc ? parseFloat(p.thc) : null, + cbd_percentage: p.cbd ? parseFloat(p.cbd) : null, + image_url: imageUrl || null, + images: p.images || [], + in_stock: p.stock_status === 'in_stock', + on_special: p.special || false, + effects: p.effects || [], + options: p.options || [], + quantity_available: p.total_quantity_available || 0, + created_at: p.created_at, + updated_at: p.updated_at, + snapshot_at: p.snapshot_at + } + }); + } + catch (error) { + console.error('Public API product detail error:', error); + res.status(500).json({ + error: 'Failed to fetch product', + message: error.message + }); + } +}); +/** + * GET /api/v1/categories + * Get all categories for the authenticated dispensary + */ +router.get('/categories', async (req, res) => { + try { + const permission = req.apiPermission; + if (!permission.dutchie_az_store_id) { + return res.status(503).json({ + error: 'No menu data available', + message: `Menu data for ${permission.store_name} is not yet available.` + }); + } + const { rows: categories } = await (0, connection_1.query)(` + SELECT + type as category, + subcategory, + COUNT(*) as product_count, + COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock_count + FROM dutchie_products + WHERE dispensary_id = $1 AND type IS NOT NULL + GROUP BY type, subcategory + ORDER BY type, subcategory + `, [permission.dutchie_az_store_id]); + res.json({ + success: true, + dispensary: permission.store_name, + categories + }); + } + catch (error) { + console.error('Public API categories error:', error); + res.status(500).json({ + error: 'Failed to fetch categories', + message: error.message + }); + } +}); +/** + * GET /api/v1/brands + * Get all brands for the authenticated dispensary + */ +router.get('/brands', async (req, res) => { + try { + const permission = req.apiPermission; + if (!permission.dutchie_az_store_id) { + return res.status(503).json({ + error: 'No menu data available', + message: `Menu data for ${permission.store_name} is not yet available.` + }); + } + const { rows: brands } = await (0, connection_1.query)(` + SELECT + brand_name as brand, + COUNT(*) as product_count, + COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock_count + FROM dutchie_products + WHERE dispensary_id = $1 AND brand_name IS NOT NULL + GROUP BY brand_name + ORDER BY product_count DESC + `, [permission.dutchie_az_store_id]); + res.json({ + success: true, + dispensary: permission.store_name, + brands + }); + } + catch (error) { + console.error('Public API brands error:', error); + res.status(500).json({ + error: 'Failed to fetch brands', + message: error.message + }); + } +}); +/** + * GET /api/v1/specials + * Get products on special/sale for the authenticated dispensary + */ +router.get('/specials', async (req, res) => { + try { + const permission = req.apiPermission; + if (!permission.dutchie_az_store_id) { + return res.status(503).json({ + error: 'No menu data available', + message: `Menu data for ${permission.store_name} is not yet available.` + }); + } + const { limit = '100', offset = '0' } = req.query; + const limitNum = Math.min(parseInt(limit, 10) || 100, 500); + const offsetNum = parseInt(offset, 10) || 0; + // Get products with special pricing from latest snapshot + const { rows: products } = await (0, connection_1.query)(` + SELECT + p.id, + p.external_product_id as dutchie_id, + p.name, + p.brand_name as brand, + p.type as category, + p.subcategory, + p.strain_type, + p.stock_status, + p.primary_image_url as image_url, + s.rec_min_price_cents, + s.rec_min_special_price_cents, + s.special, + s.options, + p.updated_at, + s.crawled_at as snapshot_at + FROM dutchie_products p + INNER JOIN LATERAL ( + SELECT * FROM dutchie_product_snapshots + WHERE dutchie_product_id = p.id + ORDER BY crawled_at DESC + LIMIT 1 + ) s ON true + WHERE p.dispensary_id = $1 + AND s.special = true + AND p.stock_status = 'in_stock' + ORDER BY p.name ASC + LIMIT $2 OFFSET $3 + `, [permission.dutchie_az_store_id, limitNum, offsetNum]); + // Get total count + const { rows: countRows } = await (0, connection_1.query)(` + SELECT COUNT(*) as total + FROM dutchie_products p + INNER JOIN LATERAL ( + SELECT special FROM dutchie_product_snapshots + WHERE dutchie_product_id = p.id + ORDER BY crawled_at DESC + LIMIT 1 + ) s ON true + WHERE p.dispensary_id = $1 + AND s.special = true + AND p.stock_status = 'in_stock' + `, [permission.dutchie_az_store_id]); + const transformedProducts = products.map((p) => ({ + id: p.id, + dutchie_id: p.dutchie_id, + name: p.name, + brand: p.brand || null, + category: p.category || null, + strain_type: p.strain_type || null, + regular_price: p.rec_min_price_cents ? (p.rec_min_price_cents / 100).toFixed(2) : null, + sale_price: p.rec_min_special_price_cents ? (p.rec_min_special_price_cents / 100).toFixed(2) : null, + image_url: p.image_url || null, + in_stock: p.stock_status === 'in_stock', + options: p.options || [], + updated_at: p.updated_at, + snapshot_at: p.snapshot_at + })); + res.json({ + success: true, + dispensary: permission.store_name, + specials: transformedProducts, + pagination: { + total: parseInt(countRows[0]?.total || '0', 10), + limit: limitNum, + offset: offsetNum, + has_more: offsetNum + products.length < parseInt(countRows[0]?.total || '0', 10) + } + }); + } + catch (error) { + console.error('Public API specials error:', error); + res.status(500).json({ + error: 'Failed to fetch specials', + message: error.message + }); + } +}); +/** + * GET /api/v1/menu + * Get complete menu summary for the authenticated dispensary + */ +router.get('/menu', async (req, res) => { + try { + const permission = req.apiPermission; + if (!permission.dutchie_az_store_id) { + return res.status(503).json({ + error: 'No menu data available', + message: `Menu data for ${permission.store_name} is not yet available.` + }); + } + // Get counts by category + const { rows: categoryCounts } = await (0, connection_1.query)(` + SELECT + type as category, + COUNT(*) as total, + COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock + FROM dutchie_products + WHERE dispensary_id = $1 AND type IS NOT NULL + GROUP BY type + ORDER BY total DESC + `, [permission.dutchie_az_store_id]); + // Get overall stats + const { rows: stats } = await (0, connection_1.query)(` + SELECT + COUNT(*) as total_products, + COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock_count, + COUNT(DISTINCT brand_name) as brand_count, + COUNT(DISTINCT type) as category_count, + MAX(updated_at) as last_updated + FROM dutchie_products + WHERE dispensary_id = $1 + `, [permission.dutchie_az_store_id]); + // Get specials count + const { rows: specialsCount } = await (0, connection_1.query)(` + SELECT COUNT(*) as count + FROM dutchie_products p + INNER JOIN LATERAL ( + SELECT special FROM dutchie_product_snapshots + WHERE dutchie_product_id = p.id + ORDER BY crawled_at DESC + LIMIT 1 + ) s ON true + WHERE p.dispensary_id = $1 + AND s.special = true + AND p.stock_status = 'in_stock' + `, [permission.dutchie_az_store_id]); + const summary = stats[0] || {}; + res.json({ + success: true, + dispensary: permission.store_name, + menu: { + total_products: parseInt(summary.total_products || '0', 10), + in_stock_count: parseInt(summary.in_stock_count || '0', 10), + brand_count: parseInt(summary.brand_count || '0', 10), + category_count: parseInt(summary.category_count || '0', 10), + specials_count: parseInt(specialsCount[0]?.count || '0', 10), + last_updated: summary.last_updated, + categories: categoryCounts.map((c) => ({ + name: c.category, + total: parseInt(c.total, 10), + in_stock: parseInt(c.in_stock, 10) + })) + } + }); + } + catch (error) { + console.error('Public API menu error:', error); + res.status(500).json({ + error: 'Failed to fetch menu summary', + message: error.message + }); + } +}); +exports.default = router; diff --git a/backend/dist/routes/schedule.js b/backend/dist/routes/schedule.js new file mode 100644 index 00000000..1bad705c --- /dev/null +++ b/backend/dist/routes/schedule.js @@ -0,0 +1,887 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const middleware_1 = require("../auth/middleware"); +const crawl_scheduler_1 = require("../services/crawl-scheduler"); +const store_crawl_orchestrator_1 = require("../services/store-crawl-orchestrator"); +const dispensary_orchestrator_1 = require("../services/dispensary-orchestrator"); +const migrate_1 = require("../db/migrate"); +const graphql_client_1 = require("../dutchie-az/services/graphql-client"); +const router = (0, express_1.Router)(); +router.use(middleware_1.authMiddleware); +// ============================================ +// Global Schedule Endpoints +// ============================================ +/** + * GET /api/schedule/global + * Get global schedule settings + */ +router.get('/global', async (req, res) => { + try { + const schedules = await (0, crawl_scheduler_1.getGlobalSchedule)(); + res.json({ schedules }); + } + catch (error) { + console.error('Error fetching global schedule:', error); + res.status(500).json({ error: 'Failed to fetch global schedule' }); + } +}); +/** + * PUT /api/schedule/global/:type + * Update global schedule setting + */ +router.put('/global/:type', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { type } = req.params; + const { enabled, interval_hours, run_time } = req.body; + if (type !== 'global_interval' && type !== 'daily_special') { + return res.status(400).json({ error: 'Invalid schedule type' }); + } + const schedule = await (0, crawl_scheduler_1.updateGlobalSchedule)(type, { + enabled, + interval_hours, + run_time + }); + // Restart scheduler to apply changes + await (0, crawl_scheduler_1.restartCrawlScheduler)(); + res.json({ schedule, message: 'Schedule updated and scheduler restarted' }); + } + catch (error) { + console.error('Error updating global schedule:', error); + res.status(500).json({ error: 'Failed to update global schedule' }); + } +}); +// ============================================ +// Store Schedule Endpoints +// ============================================ +/** + * GET /api/schedule/stores + * Get all store schedule statuses + */ +router.get('/stores', async (req, res) => { + try { + const stores = await (0, crawl_scheduler_1.getStoreScheduleStatuses)(); + res.json({ stores }); + } + catch (error) { + console.error('Error fetching store schedules:', error); + res.status(500).json({ error: 'Failed to fetch store schedules' }); + } +}); +/** + * GET /api/schedule/stores/:storeId + * Get schedule for a specific store + */ +router.get('/stores/:storeId', async (req, res) => { + try { + const storeId = parseInt(req.params.storeId); + if (isNaN(storeId)) { + return res.status(400).json({ error: 'Invalid store ID' }); + } + const schedule = await (0, crawl_scheduler_1.getStoreSchedule)(storeId); + res.json({ schedule }); + } + catch (error) { + console.error('Error fetching store schedule:', error); + res.status(500).json({ error: 'Failed to fetch store schedule' }); + } +}); +/** + * PUT /api/schedule/stores/:storeId + * Update schedule for a specific store + */ +router.put('/stores/:storeId', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const storeId = parseInt(req.params.storeId); + if (isNaN(storeId)) { + return res.status(400).json({ error: 'Invalid store ID' }); + } + const { enabled, interval_hours, daily_special_enabled, daily_special_time, priority } = req.body; + const schedule = await (0, crawl_scheduler_1.updateStoreSchedule)(storeId, { + enabled, + interval_hours, + daily_special_enabled, + daily_special_time, + priority + }); + res.json({ schedule }); + } + catch (error) { + console.error('Error updating store schedule:', error); + res.status(500).json({ error: 'Failed to update store schedule' }); + } +}); +// ============================================ +// Job Queue Endpoints +// ============================================ +/** + * GET /api/schedule/jobs + * Get recent jobs + */ +router.get('/jobs', async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 50; + const jobs = await (0, crawl_scheduler_1.getAllRecentJobs)(Math.min(limit, 200)); + res.json({ jobs }); + } + catch (error) { + console.error('Error fetching jobs:', error); + res.status(500).json({ error: 'Failed to fetch jobs' }); + } +}); +/** + * GET /api/schedule/jobs/store/:storeId + * Get recent jobs for a specific store + */ +router.get('/jobs/store/:storeId', async (req, res) => { + try { + const storeId = parseInt(req.params.storeId); + if (isNaN(storeId)) { + return res.status(400).json({ error: 'Invalid store ID' }); + } + const limit = parseInt(req.query.limit) || 10; + const jobs = await (0, crawl_scheduler_1.getRecentJobs)(storeId, Math.min(limit, 100)); + res.json({ jobs }); + } + catch (error) { + console.error('Error fetching store jobs:', error); + res.status(500).json({ error: 'Failed to fetch store jobs' }); + } +}); +/** + * POST /api/schedule/jobs/:jobId/cancel + * Cancel a pending job + */ +router.post('/jobs/:jobId/cancel', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const jobId = parseInt(req.params.jobId); + if (isNaN(jobId)) { + return res.status(400).json({ error: 'Invalid job ID' }); + } + const cancelled = await (0, crawl_scheduler_1.cancelJob)(jobId); + if (cancelled) { + res.json({ success: true, message: 'Job cancelled' }); + } + else { + res.status(400).json({ error: 'Job could not be cancelled (may not be pending)' }); + } + } + catch (error) { + console.error('Error cancelling job:', error); + res.status(500).json({ error: 'Failed to cancel job' }); + } +}); +// ============================================ +// Manual Trigger Endpoints +// ============================================ +/** + * POST /api/schedule/trigger/store/:storeId + * Manually trigger orchestrated crawl for a specific store + * Uses the intelligent orchestrator which: + * - Checks provider detection status + * - Runs detection if needed + * - Queues appropriate crawl type (production/sandbox) + */ +router.post('/trigger/store/:storeId', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const storeId = parseInt(req.params.storeId); + if (isNaN(storeId)) { + return res.status(400).json({ error: 'Invalid store ID' }); + } + // Use the orchestrator instead of simple triggerManualCrawl + const result = await (0, store_crawl_orchestrator_1.runStoreCrawlOrchestrator)(storeId); + res.json({ + result, + message: result.summary, + success: result.status === 'success' || result.status === 'sandbox_only', + }); + } + catch (error) { + console.error('Error triggering orchestrated crawl:', error); + res.status(500).json({ error: 'Failed to trigger crawl' }); + } +}); +/** + * POST /api/schedule/trigger/store/:storeId/legacy + * Legacy: Simple job queue trigger (no orchestration) + */ +router.post('/trigger/store/:storeId/legacy', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const storeId = parseInt(req.params.storeId); + if (isNaN(storeId)) { + return res.status(400).json({ error: 'Invalid store ID' }); + } + const job = await (0, crawl_scheduler_1.triggerManualCrawl)(storeId); + res.json({ job, message: 'Crawl job created' }); + } + catch (error) { + console.error('Error triggering manual crawl:', error); + res.status(500).json({ error: 'Failed to trigger crawl' }); + } +}); +/** + * POST /api/schedule/trigger/all + * Manually trigger crawls for all stores + */ +router.post('/trigger/all', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const jobsCreated = await (0, crawl_scheduler_1.triggerAllStoresCrawl)(); + res.json({ jobs_created: jobsCreated, message: `Created ${jobsCreated} crawl jobs` }); + } + catch (error) { + console.error('Error triggering all crawls:', error); + res.status(500).json({ error: 'Failed to trigger crawls' }); + } +}); +/** + * POST /api/schedule/restart + * Restart the scheduler + */ +router.post('/restart', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + await (0, crawl_scheduler_1.restartCrawlScheduler)(); + res.json({ message: 'Scheduler restarted', mode: (0, crawl_scheduler_1.getSchedulerMode)() }); + } + catch (error) { + console.error('Error restarting scheduler:', error); + res.status(500).json({ error: 'Failed to restart scheduler' }); + } +}); +// ============================================ +// Scheduler Mode Endpoints +// ============================================ +/** + * GET /api/schedule/mode + * Get current scheduler mode + */ +router.get('/mode', async (req, res) => { + try { + const mode = (0, crawl_scheduler_1.getSchedulerMode)(); + res.json({ mode }); + } + catch (error) { + console.error('Error getting scheduler mode:', error); + res.status(500).json({ error: 'Failed to get scheduler mode' }); + } +}); +/** + * PUT /api/schedule/mode + * Set scheduler mode (legacy or orchestrator) + */ +router.put('/mode', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { mode } = req.body; + if (mode !== 'legacy' && mode !== 'orchestrator') { + return res.status(400).json({ error: 'Invalid mode. Must be "legacy" or "orchestrator"' }); + } + (0, crawl_scheduler_1.setSchedulerMode)(mode); + // Restart scheduler with new mode + await (0, crawl_scheduler_1.restartCrawlScheduler)(); + res.json({ mode, message: `Scheduler mode set to ${mode} and restarted` }); + } + catch (error) { + console.error('Error setting scheduler mode:', error); + res.status(500).json({ error: 'Failed to set scheduler mode' }); + } +}); +/** + * GET /api/schedule/due + * Get stores that are due for orchestration + */ +router.get('/due', async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 10; + const storeIds = await (0, store_crawl_orchestrator_1.getStoresDueForOrchestration)(Math.min(limit, 50)); + res.json({ stores_due: storeIds, count: storeIds.length }); + } + catch (error) { + console.error('Error getting stores due for orchestration:', error); + res.status(500).json({ error: 'Failed to get stores due' }); + } +}); +// ============================================ +// Dispensary Schedule Endpoints (NEW - dispensary-centric) +// ============================================ +/** + * GET /api/schedule/dispensaries + * Get all dispensary schedule statuses with optional filters + * Query params: + * - state: filter by state (e.g., 'AZ') + * - search: search by name or slug + */ +router.get('/dispensaries', async (req, res) => { + try { + const { state, search } = req.query; + // Build dynamic query with optional filters + const conditions = []; + const params = []; + let paramIndex = 1; + if (state) { + conditions.push(`d.state = $${paramIndex}`); + params.push(state); + paramIndex++; + } + if (search) { + conditions.push(`(d.name ILIKE $${paramIndex} OR d.slug ILIKE $${paramIndex} OR d.dba_name ILIKE $${paramIndex})`); + params.push(`%${search}%`); + paramIndex++; + } + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + const query = ` + SELECT + d.id AS dispensary_id, + COALESCE(d.dba_name, d.name) AS dispensary_name, + d.slug AS dispensary_slug, + d.city, + d.state, + d.menu_url, + d.menu_type, + d.platform_dispensary_id, + d.scrape_enabled, + d.last_crawl_at, + d.crawl_status, + d.product_crawler_mode, + d.product_provider, + cs.interval_minutes, + cs.is_active, + cs.priority, + cs.last_run_at, + cs.next_run_at, + cs.last_status AS schedule_last_status, + cs.last_error AS schedule_last_error, + cs.consecutive_failures, + j.id AS latest_job_id, + j.status AS latest_job_status, + j.job_type AS latest_job_type, + j.started_at AS latest_job_started, + j.completed_at AS latest_job_completed, + j.products_found AS latest_products_found, + j.products_new AS latest_products_created, + j.products_updated AS latest_products_updated, + j.error_message AS latest_job_error, + CASE + WHEN d.menu_type = 'dutchie' AND d.platform_dispensary_id IS NOT NULL THEN true + ELSE false + END AS can_crawl, + CASE + WHEN d.menu_type IS NULL OR d.menu_type = 'unknown' THEN 'menu_type not detected' + WHEN d.menu_type != 'dutchie' THEN 'not dutchie platform' + WHEN d.platform_dispensary_id IS NULL THEN 'platform ID not resolved' + WHEN d.scrape_enabled = false THEN 'scraping disabled' + ELSE 'ready' + END AS schedule_status_reason + FROM public.dispensaries d + LEFT JOIN public.dispensary_crawl_schedule cs ON cs.dispensary_id = d.id + LEFT JOIN LATERAL ( + SELECT * + FROM public.dispensary_crawl_jobs dj + WHERE dj.dispensary_id = d.id + ORDER BY dj.created_at DESC + LIMIT 1 + ) j ON true + ${whereClause} + ORDER BY cs.priority DESC NULLS LAST, COALESCE(d.dba_name, d.name) + `; + const result = await migrate_1.pool.query(query, params); + res.json({ dispensaries: result.rows }); + } + catch (error) { + console.error('Error fetching dispensary schedules:', error); + res.status(500).json({ error: 'Failed to fetch dispensary schedules' }); + } +}); +/** + * GET /api/schedule/dispensaries/:id + * Get schedule for a specific dispensary + */ +router.get('/dispensaries/:id', async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + const result = await migrate_1.pool.query(` + SELECT * FROM dispensary_crawl_status + WHERE dispensary_id = $1 + `, [dispensaryId]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + res.json({ schedule: result.rows[0] }); + } + catch (error) { + console.error('Error fetching dispensary schedule:', error); + res.status(500).json({ error: 'Failed to fetch dispensary schedule' }); + } +}); +/** + * PUT /api/schedule/dispensaries/:id + * Update schedule for a specific dispensary + */ +router.put('/dispensaries/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + const { is_active, interval_minutes, priority } = req.body; + // Upsert schedule + const result = await migrate_1.pool.query(` + INSERT INTO dispensary_crawl_schedule (dispensary_id, is_active, interval_minutes, priority) + VALUES ($1, COALESCE($2, TRUE), COALESCE($3, 240), COALESCE($4, 0)) + ON CONFLICT (dispensary_id) DO UPDATE SET + is_active = COALESCE($2, dispensary_crawl_schedule.is_active), + interval_minutes = COALESCE($3, dispensary_crawl_schedule.interval_minutes), + priority = COALESCE($4, dispensary_crawl_schedule.priority), + updated_at = NOW() + RETURNING * + `, [dispensaryId, is_active, interval_minutes, priority]); + res.json({ schedule: result.rows[0] }); + } + catch (error) { + console.error('Error updating dispensary schedule:', error); + res.status(500).json({ error: 'Failed to update dispensary schedule' }); + } +}); +/** + * GET /api/schedule/dispensary-jobs + * Get recent dispensary crawl jobs + */ +router.get('/dispensary-jobs', async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 50; + const result = await migrate_1.pool.query(` + SELECT dcj.*, d.name as dispensary_name + FROM dispensary_crawl_jobs dcj + JOIN dispensaries d ON d.id = dcj.dispensary_id + ORDER BY dcj.created_at DESC + LIMIT $1 + `, [Math.min(limit, 200)]); + res.json({ jobs: result.rows }); + } + catch (error) { + console.error('Error fetching dispensary jobs:', error); + res.status(500).json({ error: 'Failed to fetch dispensary jobs' }); + } +}); +/** + * GET /api/schedule/dispensary-jobs/:dispensaryId + * Get recent jobs for a specific dispensary + */ +router.get('/dispensary-jobs/:dispensaryId', async (req, res) => { + try { + const dispensaryId = parseInt(req.params.dispensaryId); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + const limit = parseInt(req.query.limit) || 10; + const result = await migrate_1.pool.query(` + SELECT dcj.*, d.name as dispensary_name + FROM dispensary_crawl_jobs dcj + JOIN dispensaries d ON d.id = dcj.dispensary_id + WHERE dcj.dispensary_id = $1 + ORDER BY dcj.created_at DESC + LIMIT $2 + `, [dispensaryId, Math.min(limit, 100)]); + res.json({ jobs: result.rows }); + } + catch (error) { + console.error('Error fetching dispensary jobs:', error); + res.status(500).json({ error: 'Failed to fetch dispensary jobs' }); + } +}); +/** + * POST /api/schedule/trigger/dispensary/:id + * Trigger orchestrator for a specific dispensary (Run Now button) + */ +router.post('/trigger/dispensary/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + // Run the dispensary orchestrator + const result = await (0, dispensary_orchestrator_1.runDispensaryOrchestrator)(dispensaryId); + res.json({ + result, + message: result.summary, + success: result.status === 'success' || result.status === 'sandbox_only' || result.status === 'detection_only', + }); + } + catch (error) { + console.error('Error triggering dispensary orchestrator:', error); + res.status(500).json({ error: 'Failed to trigger orchestrator' }); + } +}); +/** + * POST /api/schedule/trigger/dispensaries/batch + * Trigger orchestrator for multiple dispensaries + */ +router.post('/trigger/dispensaries/batch', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { dispensary_ids, concurrency } = req.body; + if (!Array.isArray(dispensary_ids) || dispensary_ids.length === 0) { + return res.status(400).json({ error: 'dispensary_ids must be a non-empty array' }); + } + const results = await (0, dispensary_orchestrator_1.runBatchDispensaryOrchestrator)(dispensary_ids, concurrency || 3); + const summary = { + total: results.length, + success: results.filter(r => r.status === 'success').length, + sandbox_only: results.filter(r => r.status === 'sandbox_only').length, + detection_only: results.filter(r => r.status === 'detection_only').length, + error: results.filter(r => r.status === 'error').length, + }; + res.json({ results, summary }); + } + catch (error) { + console.error('Error triggering batch orchestrator:', error); + res.status(500).json({ error: 'Failed to trigger batch orchestrator' }); + } +}); +/** + * GET /api/schedule/dispensary-due + * Get dispensaries that are due for orchestration + */ +router.get('/dispensary-due', async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 10; + const dispensaryIds = await (0, dispensary_orchestrator_1.getDispensariesDueForOrchestration)(Math.min(limit, 50)); + // Get details for the due dispensaries + if (dispensaryIds.length > 0) { + const details = await migrate_1.pool.query(` + SELECT d.id, d.name, d.product_provider, d.product_crawler_mode, + dcs.next_run_at, dcs.last_status, dcs.priority + FROM dispensaries d + LEFT JOIN dispensary_crawl_schedule dcs ON dcs.dispensary_id = d.id + WHERE d.id = ANY($1) + ORDER BY COALESCE(dcs.priority, 0) DESC, dcs.last_run_at ASC NULLS FIRST + `, [dispensaryIds]); + res.json({ dispensaries_due: details.rows, count: dispensaryIds.length }); + } + else { + res.json({ dispensaries_due: [], count: 0 }); + } + } + catch (error) { + console.error('Error getting dispensaries due for orchestration:', error); + res.status(500).json({ error: 'Failed to get dispensaries due' }); + } +}); +/** + * POST /api/schedule/dispensaries/bootstrap + * Ensure all dispensaries have schedule entries + */ +router.post('/dispensaries/bootstrap', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const { interval_minutes } = req.body; + const result = await (0, dispensary_orchestrator_1.ensureAllDispensariesHaveSchedules)(interval_minutes || 240); + res.json({ + message: `Created ${result.created} new schedules, ${result.existing} already existed`, + created: result.created, + existing: result.existing, + }); + } + catch (error) { + console.error('Error bootstrapping dispensary schedules:', error); + res.status(500).json({ error: 'Failed to bootstrap schedules' }); + } +}); +// ============================================ +// Platform ID & Menu Type Detection Endpoints +// ============================================ +/** + * POST /api/schedule/dispensaries/:id/resolve-platform-id + * Resolve the Dutchie platform_dispensary_id from menu_url slug + */ +router.post('/dispensaries/:id/resolve-platform-id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + // Get dispensary info + const dispensaryResult = await migrate_1.pool.query(` + SELECT id, name, slug, menu_url, menu_type, platform_dispensary_id + FROM dispensaries WHERE id = $1 + `, [dispensaryId]); + if (dispensaryResult.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const dispensary = dispensaryResult.rows[0]; + // Check if already resolved + if (dispensary.platform_dispensary_id) { + return res.json({ + success: true, + message: 'Platform ID already resolved', + platform_dispensary_id: dispensary.platform_dispensary_id, + already_resolved: true + }); + } + // Extract slug from menu_url for Dutchie URLs + let slugToResolve = dispensary.slug; + if (dispensary.menu_url) { + // Match embedded-menu or dispensary URLs + const match = dispensary.menu_url.match(/(?:embedded-menu|dispensar(?:y|ies))\/([^\/\?#]+)/i); + if (match) { + slugToResolve = match[1]; + } + } + if (!slugToResolve) { + return res.status(400).json({ + error: 'No slug available to resolve platform ID', + menu_url: dispensary.menu_url + }); + } + console.log(`[Schedule] Resolving platform ID for ${dispensary.name} using slug: ${slugToResolve}`); + // Resolve platform ID using GraphQL client + const platformId = await (0, graphql_client_1.resolveDispensaryId)(slugToResolve); + if (!platformId) { + return res.status(404).json({ + error: 'Could not resolve platform ID', + slug_tried: slugToResolve, + message: 'The dispensary might not be on Dutchie or the slug is incorrect' + }); + } + // Update the dispensary with resolved platform ID + await migrate_1.pool.query(` + UPDATE dispensaries + SET platform_dispensary_id = $1, + menu_type = COALESCE(menu_type, 'dutchie'), + updated_at = NOW() + WHERE id = $2 + `, [platformId, dispensaryId]); + res.json({ + success: true, + platform_dispensary_id: platformId, + slug_resolved: slugToResolve, + message: `Platform ID resolved: ${platformId}` + }); + } + catch (error) { + console.error('Error resolving platform ID:', error); + res.status(500).json({ error: 'Failed to resolve platform ID', details: error.message }); + } +}); +/** + * POST /api/schedule/dispensaries/:id/detect-menu-type + * Detect menu type from menu_url + */ +router.post('/dispensaries/:id/detect-menu-type', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + // Get dispensary info + const dispensaryResult = await migrate_1.pool.query(` + SELECT id, name, menu_url, website FROM dispensaries WHERE id = $1 + `, [dispensaryId]); + if (dispensaryResult.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const dispensary = dispensaryResult.rows[0]; + const urlToCheck = dispensary.menu_url || dispensary.website; + if (!urlToCheck) { + return res.status(400).json({ error: 'No menu_url or website to detect from' }); + } + // Detect menu type from URL patterns + let detectedType = 'unknown'; + if (urlToCheck.includes('dutchie.com') || urlToCheck.includes('embedded-menu')) { + detectedType = 'dutchie'; + } + else if (urlToCheck.includes('iheartjane.com') || urlToCheck.includes('jane.co')) { + detectedType = 'jane'; + } + else if (urlToCheck.includes('weedmaps.com')) { + detectedType = 'weedmaps'; + } + else if (urlToCheck.includes('leafly.com')) { + detectedType = 'leafly'; + } + else if (urlToCheck.includes('treez.io') || urlToCheck.includes('treez.co')) { + detectedType = 'treez'; + } + else if (urlToCheck.includes('meadow.com')) { + detectedType = 'meadow'; + } + else if (urlToCheck.includes('blaze.me') || urlToCheck.includes('blazepay')) { + detectedType = 'blaze'; + } + else if (urlToCheck.includes('flowhub.com')) { + detectedType = 'flowhub'; + } + else if (urlToCheck.includes('dispense.app')) { + detectedType = 'dispense'; + } + else if (urlToCheck.includes('covasoft.com')) { + detectedType = 'cova'; + } + // Update menu_type + await migrate_1.pool.query(` + UPDATE dispensaries + SET menu_type = $1, updated_at = NOW() + WHERE id = $2 + `, [detectedType, dispensaryId]); + res.json({ + success: true, + menu_type: detectedType, + url_checked: urlToCheck, + message: `Menu type detected: ${detectedType}` + }); + } + catch (error) { + console.error('Error detecting menu type:', error); + res.status(500).json({ error: 'Failed to detect menu type' }); + } +}); +/** + * POST /api/schedule/dispensaries/:id/refresh-detection + * Combined: detect menu_type AND resolve platform_dispensary_id if dutchie + */ +router.post('/dispensaries/:id/refresh-detection', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + // Get dispensary info + const dispensaryResult = await migrate_1.pool.query(` + SELECT id, name, slug, menu_url, website FROM dispensaries WHERE id = $1 + `, [dispensaryId]); + if (dispensaryResult.rows.length === 0) { + return res.status(404).json({ error: 'Dispensary not found' }); + } + const dispensary = dispensaryResult.rows[0]; + const urlToCheck = dispensary.menu_url || dispensary.website; + if (!urlToCheck) { + return res.status(400).json({ error: 'No menu_url or website to detect from' }); + } + // Detect menu type from URL patterns + let detectedType = 'unknown'; + if (urlToCheck.includes('dutchie.com') || urlToCheck.includes('embedded-menu')) { + detectedType = 'dutchie'; + } + else if (urlToCheck.includes('iheartjane.com') || urlToCheck.includes('jane.co')) { + detectedType = 'jane'; + } + else if (urlToCheck.includes('weedmaps.com')) { + detectedType = 'weedmaps'; + } + else if (urlToCheck.includes('leafly.com')) { + detectedType = 'leafly'; + } + else if (urlToCheck.includes('treez.io') || urlToCheck.includes('treez.co')) { + detectedType = 'treez'; + } + else if (urlToCheck.includes('meadow.com')) { + detectedType = 'meadow'; + } + else if (urlToCheck.includes('blaze.me') || urlToCheck.includes('blazepay')) { + detectedType = 'blaze'; + } + else if (urlToCheck.includes('flowhub.com')) { + detectedType = 'flowhub'; + } + else if (urlToCheck.includes('dispense.app')) { + detectedType = 'dispense'; + } + else if (urlToCheck.includes('covasoft.com')) { + detectedType = 'cova'; + } + // Update menu_type first + await migrate_1.pool.query(` + UPDATE dispensaries SET menu_type = $1, updated_at = NOW() WHERE id = $2 + `, [detectedType, dispensaryId]); + let platformId = null; + // If dutchie, also try to resolve platform ID + if (detectedType === 'dutchie') { + let slugToResolve = dispensary.slug; + const match = urlToCheck.match(/(?:embedded-menu|dispensar(?:y|ies))\/([^\/\?#]+)/i); + if (match) { + slugToResolve = match[1]; + } + if (slugToResolve) { + try { + console.log(`[Schedule] Resolving platform ID for ${dispensary.name} using slug: ${slugToResolve}`); + platformId = await (0, graphql_client_1.resolveDispensaryId)(slugToResolve); + if (platformId) { + await migrate_1.pool.query(` + UPDATE dispensaries SET platform_dispensary_id = $1, updated_at = NOW() WHERE id = $2 + `, [platformId, dispensaryId]); + } + } + catch (err) { + console.warn(`[Schedule] Failed to resolve platform ID: ${err.message}`); + } + } + } + res.json({ + success: true, + menu_type: detectedType, + platform_dispensary_id: platformId, + url_checked: urlToCheck, + can_crawl: detectedType === 'dutchie' && !!platformId + }); + } + catch (error) { + console.error('Error refreshing detection:', error); + res.status(500).json({ error: 'Failed to refresh detection' }); + } +}); +/** + * PUT /api/schedule/dispensaries/:id/toggle-active + * Enable or disable schedule for a dispensary + */ +router.put('/dispensaries/:id/toggle-active', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + const { is_active } = req.body; + // Upsert schedule with new is_active value + const result = await migrate_1.pool.query(` + INSERT INTO dispensary_crawl_schedule (dispensary_id, is_active, interval_minutes, priority) + VALUES ($1, $2, 240, 0) + ON CONFLICT (dispensary_id) DO UPDATE SET + is_active = $2, + updated_at = NOW() + RETURNING * + `, [dispensaryId, is_active]); + res.json({ + success: true, + schedule: result.rows[0], + message: is_active ? 'Schedule enabled' : 'Schedule disabled' + }); + } + catch (error) { + console.error('Error toggling schedule active status:', error); + res.status(500).json({ error: 'Failed to toggle schedule' }); + } +}); +/** + * DELETE /api/schedule/dispensaries/:id/schedule + * Delete schedule for a dispensary + */ +router.delete('/dispensaries/:id/schedule', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { + try { + const dispensaryId = parseInt(req.params.id); + if (isNaN(dispensaryId)) { + return res.status(400).json({ error: 'Invalid dispensary ID' }); + } + const result = await migrate_1.pool.query(` + DELETE FROM dispensary_crawl_schedule WHERE dispensary_id = $1 RETURNING id + `, [dispensaryId]); + const deleted = (result.rowCount ?? 0) > 0; + res.json({ + success: true, + deleted, + message: deleted ? 'Schedule deleted' : 'No schedule to delete' + }); + } + catch (error) { + console.error('Error deleting schedule:', error); + res.status(500).json({ error: 'Failed to delete schedule' }); + } +}); +exports.default = router; diff --git a/backend/dist/routes/scraper-monitor.js b/backend/dist/routes/scraper-monitor.js index 5c11be90..62bd924b 100644 --- a/backend/dist/routes/scraper-monitor.js +++ b/backend/dist/routes/scraper-monitor.js @@ -1,4 +1,37 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", { value: true }); exports.activeScrapers = void 0; exports.registerScraper = registerScraper; @@ -49,32 +82,42 @@ router.get('/active/:id', async (req, res) => { // Get scraper history (last 50 completed scrapes) router.get('/history', async (req, res) => { try { - const { limit = 50, store_id } = req.query; + const { limit = 50, dispensary_id } = req.query; let query = ` SELECT - s.id as store_id, - s.name as store_name, - c.id as category_id, - c.name as category_name, - c.last_scraped_at, + d.id as dispensary_id, + COALESCE(d.dba_name, d.name) as dispensary_name, + d.city, + d.state, + dcj.id as job_id, + dcj.job_type, + dcj.status, + dcj.products_found, + dcj.products_new, + dcj.products_updated, + dcj.in_stock_count, + dcj.out_of_stock_count, + dcj.duration_ms, + dcj.completed_at as last_scraped_at, + dcj.error_message, ( SELECT COUNT(*) FROM products p - WHERE p.store_id = s.id - AND p.category_id = c.id + WHERE p.dispensary_id = d.id + AND p.last_seen_at >= NOW() - INTERVAL '7 days' ) as product_count - FROM stores s - LEFT JOIN categories c ON c.store_id = s.id - WHERE c.last_scraped_at IS NOT NULL + FROM dispensary_crawl_jobs dcj + JOIN dispensaries d ON d.id = dcj.dispensary_id + WHERE dcj.completed_at IS NOT NULL `; const params = []; let paramCount = 1; - if (store_id) { - query += ` AND s.id = $${paramCount}`; - params.push(store_id); + if (dispensary_id) { + query += ` AND d.id = $${paramCount}`; + params.push(dispensary_id); paramCount++; } - query += ` ORDER BY c.last_scraped_at DESC LIMIT $${paramCount}`; + query += ` ORDER BY dcj.completed_at DESC LIMIT $${paramCount}`; params.push(limit); const result = await migrate_1.pool.query(query, params); res.json({ history: result.rows }); @@ -127,4 +170,180 @@ function completeScraper(id, error) { }, 5 * 60 * 1000); } } +// Dispensary crawl jobs endpoints +router.get('/jobs/stats', async (req, res) => { + try { + const { dispensary_id } = req.query; + let whereClause = ''; + const params = []; + if (dispensary_id) { + whereClause = 'WHERE dispensary_id = $1'; + params.push(dispensary_id); + } + const result = await migrate_1.pool.query(` + SELECT + status, + COUNT(*) as count, + SUM(products_found) as total_products_found, + SUM(COALESCE(products_new, 0) + COALESCE(products_updated, 0)) as total_products_saved + FROM dispensary_crawl_jobs + ${whereClause} + GROUP BY status + `, params); + const stats = { + pending: 0, + in_progress: 0, + completed: 0, + failed: 0, + total_products_found: 0, + total_products_saved: 0 + }; + result.rows.forEach((row) => { + stats[row.status] = parseInt(row.count); + if (row.status === 'completed') { + stats.total_products_found += parseInt(row.total_products_found || '0'); + stats.total_products_saved += parseInt(row.total_products_saved || '0'); + } + }); + res.json(stats); + } + catch (error) { + console.error('Error fetching job stats:', error); + res.status(500).json({ error: 'Failed to fetch job stats' }); + } +}); +router.get('/jobs/active', async (req, res) => { + try { + const { dispensary_id } = req.query; + let whereClause = "WHERE dcj.status = 'in_progress'"; + const params = []; + let paramCount = 1; + if (dispensary_id) { + whereClause += ` AND dcj.dispensary_id = $${paramCount}`; + params.push(dispensary_id); + paramCount++; + } + const result = await migrate_1.pool.query(` + SELECT + dcj.id, + dcj.dispensary_id, + COALESCE(d.dba_name, d.name) as dispensary_name, + dcj.job_type, + dcj.status, + dcj.worker_id, + dcj.started_at, + dcj.products_found, + COALESCE(dcj.products_new, 0) + COALESCE(dcj.products_updated, 0) as products_saved, + EXTRACT(EPOCH FROM (NOW() - dcj.started_at)) as duration_seconds + FROM dispensary_crawl_jobs dcj + JOIN dispensaries d ON d.id = dcj.dispensary_id + ${whereClause} + ORDER BY dcj.started_at DESC + `, params); + res.json({ jobs: result.rows }); + } + catch (error) { + console.error('Error fetching active jobs:', error); + res.status(500).json({ error: 'Failed to fetch active jobs' }); + } +}); +router.get('/jobs/recent', async (req, res) => { + try { + const { limit = 50, dispensary_id, status } = req.query; + let whereClause = ''; + const params = []; + let paramCount = 1; + const conditions = []; + if (dispensary_id) { + conditions.push(`dcj.dispensary_id = $${paramCount}`); + params.push(dispensary_id); + paramCount++; + } + if (status) { + conditions.push(`dcj.status = $${paramCount}`); + params.push(status); + paramCount++; + } + if (conditions.length > 0) { + whereClause = 'WHERE ' + conditions.join(' AND '); + } + params.push(limit); + const result = await migrate_1.pool.query(` + SELECT + dcj.id, + dcj.dispensary_id, + COALESCE(d.dba_name, d.name) as dispensary_name, + dcj.job_type, + dcj.status, + dcj.worker_id, + dcj.started_at, + dcj.completed_at, + dcj.products_found, + COALESCE(dcj.products_new, 0) + COALESCE(dcj.products_updated, 0) as products_saved, + dcj.error_message, + EXTRACT(EPOCH FROM (COALESCE(dcj.completed_at, NOW()) - dcj.started_at)) as duration_seconds + FROM dispensary_crawl_jobs dcj + JOIN dispensaries d ON d.id = dcj.dispensary_id + ${whereClause} + ORDER BY dcj.created_at DESC + LIMIT $${paramCount} + `, params); + res.json({ jobs: result.rows }); + } + catch (error) { + console.error('Error fetching recent jobs:', error); + res.status(500).json({ error: 'Failed to fetch recent jobs' }); + } +}); +router.get('/jobs/workers', async (req, res) => { + try { + const { dispensary_id } = req.query; + let whereClause = "WHERE status = 'in_progress' AND worker_id IS NOT NULL"; + const params = []; + if (dispensary_id) { + whereClause += ` AND dispensary_id = $1`; + params.push(dispensary_id); + } + const result = await migrate_1.pool.query(` + SELECT + worker_id, + COUNT(*) as active_jobs, + SUM(products_found) as total_products_found, + SUM(COALESCE(products_new, 0) + COALESCE(products_updated, 0)) as total_products_saved, + MIN(started_at) as earliest_start, + MAX(started_at) as latest_start + FROM dispensary_crawl_jobs + ${whereClause} + GROUP BY worker_id + ORDER BY worker_id + `, params); + res.json({ workers: result.rows }); + } + catch (error) { + console.error('Error fetching worker stats:', error); + res.status(500).json({ error: 'Failed to fetch worker stats' }); + } +}); +router.get('/jobs/worker-logs/:workerId', async (req, res) => { + try { + const { workerId } = req.params; + const fs = await Promise.resolve().then(() => __importStar(require('fs/promises'))); + const path = await Promise.resolve().then(() => __importStar(require('path'))); + const logPath = path.join('/tmp', `worker-${workerId}.log`); + try { + const logs = await fs.readFile(logPath, 'utf-8'); + const lines = logs.split('\n'); + // Return last 100 lines + const recentLogs = lines.slice(-100).join('\n'); + res.json({ logs: recentLogs }); + } + catch (fileError) { + res.json({ logs: 'No logs available for this worker yet.' }); + } + } + catch (error) { + console.error('Failed to get worker logs:', error); + res.status(500).json({ error: 'Failed to get worker logs' }); + } +}); exports.default = router; diff --git a/backend/dist/routes/stores.js b/backend/dist/routes/stores.js index e86251bc..406ca032 100644 --- a/backend/dist/routes/stores.js +++ b/backend/dist/routes/stores.js @@ -60,31 +60,185 @@ router.get('/', async (req, res) => { res.status(500).json({ error: 'Failed to fetch stores' }); } }); -// Get single store +// Freshness threshold in hours +const STALE_THRESHOLD_HOURS = 4; +function calculateFreshness(lastScrapedAt) { + if (!lastScrapedAt) { + return { + last_scraped_at: null, + is_stale: true, + freshness: 'Never scraped', + hours_since_scrape: null + }; + } + const now = new Date(); + const diffMs = now.getTime() - lastScrapedAt.getTime(); + const diffHours = diffMs / (1000 * 60 * 60); + const isStale = diffHours > STALE_THRESHOLD_HOURS; + let freshnessText; + if (diffHours < 1) { + const mins = Math.round(diffHours * 60); + freshnessText = `${mins} minute${mins !== 1 ? 's' : ''} ago`; + } + else if (diffHours < 24) { + const hrs = Math.round(diffHours); + freshnessText = `${hrs} hour${hrs !== 1 ? 's' : ''} ago`; + } + else { + const days = Math.round(diffHours / 24); + freshnessText = `${days} day${days !== 1 ? 's' : ''} ago`; + } + return { + last_scraped_at: lastScrapedAt.toISOString(), + is_stale: isStale, + freshness: freshnessText, + hours_since_scrape: Math.round(diffHours * 10) / 10 + }; +} +function detectProvider(dutchieUrl) { + if (!dutchieUrl) + return 'unknown'; + if (dutchieUrl.includes('dutchie.com')) + return 'Dutchie'; + if (dutchieUrl.includes('iheartjane.com') || dutchieUrl.includes('jane.co')) + return 'Jane'; + if (dutchieUrl.includes('treez.io')) + return 'Treez'; + if (dutchieUrl.includes('weedmaps.com')) + return 'Weedmaps'; + if (dutchieUrl.includes('leafly.com')) + return 'Leafly'; + return 'Custom'; +} +// Get single store with full details router.get('/:id', async (req, res) => { try { const { id } = req.params; + // Get store with counts and linked dispensary const result = await migrate_1.pool.query(` - SELECT + SELECT s.*, + d.id as dispensary_id, + d.name as dispensary_name, + d.slug as dispensary_slug, + d.state as dispensary_state, + d.city as dispensary_city, + d.address as dispensary_address, + d.menu_provider as dispensary_menu_provider, COUNT(DISTINCT p.id) as product_count, - COUNT(DISTINCT c.id) as category_count + COUNT(DISTINCT c.id) as category_count, + COUNT(DISTINCT p.id) FILTER (WHERE p.in_stock = true) as in_stock_count, + COUNT(DISTINCT p.id) FILTER (WHERE p.in_stock = false) as out_of_stock_count FROM stores s + LEFT JOIN dispensaries d ON s.dispensary_id = d.id LEFT JOIN products p ON s.id = p.store_id LEFT JOIN categories c ON s.id = c.store_id WHERE s.id = $1 - GROUP BY s.id + GROUP BY s.id, d.id, d.name, d.slug, d.state, d.city, d.address, d.menu_provider `, [id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Store not found' }); } - res.json(result.rows[0]); + const store = result.rows[0]; + // Get recent crawl jobs for this store + const jobsResult = await migrate_1.pool.query(` + SELECT + id, status, job_type, trigger_type, + started_at, completed_at, + products_found, products_new, products_updated, + in_stock_count, out_of_stock_count, + error_message + FROM crawl_jobs + WHERE store_id = $1 + ORDER BY created_at DESC + LIMIT 10 + `, [id]); + // Get schedule info if exists + const scheduleResult = await migrate_1.pool.query(` + SELECT + enabled, interval_hours, next_run_at, last_run_at + FROM store_crawl_schedule + WHERE store_id = $1 + `, [id]); + // Calculate freshness + const freshness = calculateFreshness(store.last_scraped_at); + // Detect provider from URL + const provider = detectProvider(store.dutchie_url); + // Build response + const response = { + ...store, + provider, + freshness: freshness.freshness, + is_stale: freshness.is_stale, + hours_since_scrape: freshness.hours_since_scrape, + linked_dispensary: store.dispensary_id ? { + id: store.dispensary_id, + name: store.dispensary_name, + slug: store.dispensary_slug, + state: store.dispensary_state, + city: store.dispensary_city, + address: store.dispensary_address, + menu_provider: store.dispensary_menu_provider + } : null, + schedule: scheduleResult.rows[0] || null, + recent_jobs: jobsResult.rows + }; + // Remove redundant dispensary fields from root + delete response.dispensary_name; + delete response.dispensary_slug; + delete response.dispensary_state; + delete response.dispensary_city; + delete response.dispensary_address; + delete response.dispensary_menu_provider; + res.json(response); } catch (error) { console.error('Error fetching store:', error); res.status(500).json({ error: 'Failed to fetch store' }); } }); +// Get store brands +router.get('/:id/brands', async (req, res) => { + try { + const { id } = req.params; + const result = await migrate_1.pool.query(` + SELECT name + FROM brands + WHERE store_id = $1 + ORDER BY name + `, [id]); + const brands = result.rows.map((row) => row.name); + res.json({ brands }); + } + catch (error) { + console.error('Error fetching store brands:', error); + res.status(500).json({ error: 'Failed to fetch store brands' }); + } +}); +// Get store specials +router.get('/:id/specials', async (req, res) => { + try { + const { id } = req.params; + const { date } = req.query; + // Use provided date or today's date + const queryDate = date || new Date().toISOString().split('T')[0]; + const result = await migrate_1.pool.query(` + SELECT + s.*, + p.name as product_name, + p.image_url as product_image + FROM specials s + LEFT JOIN products p ON s.product_id = p.id + WHERE s.store_id = $1 AND s.valid_date = $2 + ORDER BY s.name + `, [id, queryDate]); + res.json({ specials: result.rows, date: queryDate }); + } + catch (error) { + console.error('Error fetching store specials:', error); + res.status(500).json({ error: 'Failed to fetch store specials' }); + } +}); // Create store router.post('/', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { @@ -146,17 +300,18 @@ router.delete('/:id', (0, middleware_1.requireRole)('superadmin'), async (req, r router.post('/:id/scrape', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const { id } = req.params; - const { parallel = 3 } = req.body; // Default to 3 parallel scrapers + const { parallel = 3, userAgent } = req.body; // Default to 3 parallel scrapers const storeResult = await migrate_1.pool.query('SELECT id FROM stores WHERE id = $1', [id]); if (storeResult.rows.length === 0) { return res.status(404).json({ error: 'Store not found' }); } - (0, scraper_v2_1.scrapeStore)(parseInt(id), parseInt(parallel)).catch(err => { + (0, scraper_v2_1.scrapeStore)(parseInt(id), parseInt(parallel), userAgent).catch(err => { console.error('Background scrape error:', err); }); res.json({ message: 'Scrape started', - parallel: parseInt(parallel) + parallel: parseInt(parallel), + userAgent: userAgent || 'random' }); } catch (error) { diff --git a/backend/dist/routes/version.js b/backend/dist/routes/version.js new file mode 100644 index 00000000..c3f353ea --- /dev/null +++ b/backend/dist/routes/version.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const router = (0, express_1.Router)(); +/** + * GET /api/version + * Returns build version information for display in admin UI + */ +router.get('/', async (req, res) => { + try { + const versionInfo = { + build_version: process.env.APP_BUILD_VERSION || 'dev', + git_sha: process.env.APP_GIT_SHA || 'local', + build_time: process.env.APP_BUILD_TIME || new Date().toISOString(), + image_tag: process.env.CONTAINER_IMAGE_TAG || 'local', + }; + res.json(versionInfo); + } + catch (error) { + console.error('Error fetching version info:', error); + res.status(500).json({ error: 'Failed to fetch version info' }); + } +}); +exports.default = router; diff --git a/backend/dist/scraper-v2/downloader.js b/backend/dist/scraper-v2/downloader.js index d3bc55bc..2855a60b 100644 --- a/backend/dist/scraper-v2/downloader.js +++ b/backend/dist/scraper-v2/downloader.js @@ -8,15 +8,87 @@ const puppeteer_1 = __importDefault(require("puppeteer")); const axios_1 = __importDefault(require("axios")); const types_1 = require("./types"); const logger_1 = require("../services/logger"); +// Fingerprint profiles for randomization +const SCREEN_RESOLUTIONS = [ + { width: 1920, height: 1080 }, + { width: 1366, height: 768 }, + { width: 1536, height: 864 }, + { width: 1440, height: 900 }, + { width: 1280, height: 720 }, + { width: 2560, height: 1440 }, + { width: 1680, height: 1050 }, + { width: 1600, height: 900 }, +]; +const TIMEZONES = [ + 'America/New_York', + 'America/Chicago', + 'America/Denver', + 'America/Los_Angeles', + 'America/Phoenix', +]; +const LANGUAGES = [ + ['en-US', 'en'], + ['en-US', 'en', 'es'], + ['en-US'], +]; +const PLATFORMS = [ + 'Win32', + 'MacIntel', + 'Linux x86_64', +]; +const WEBGL_VENDORS = [ + 'Google Inc. (NVIDIA)', + 'Google Inc. (Intel)', + 'Google Inc. (AMD)', + 'Intel Inc.', + 'NVIDIA Corporation', +]; +const WEBGL_RENDERERS = [ + 'ANGLE (NVIDIA GeForce GTX 1080 Direct3D11 vs_5_0 ps_5_0)', + 'ANGLE (Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0)', + 'ANGLE (AMD Radeon RX 580 Series Direct3D11 vs_5_0 ps_5_0)', + 'Intel Iris OpenGL Engine', + 'NVIDIA GeForce RTX 3070/PCIe/SSE2', + 'AMD Radeon Pro 5500M OpenGL Engine', +]; +function generateRandomFingerprint() { + return { + screen: SCREEN_RESOLUTIONS[Math.floor(Math.random() * SCREEN_RESOLUTIONS.length)], + timezone: TIMEZONES[Math.floor(Math.random() * TIMEZONES.length)], + languages: LANGUAGES[Math.floor(Math.random() * LANGUAGES.length)], + platform: PLATFORMS[Math.floor(Math.random() * PLATFORMS.length)], + hardwareConcurrency: [4, 8, 12, 16][Math.floor(Math.random() * 4)], + deviceMemory: [4, 8, 16, 32][Math.floor(Math.random() * 4)], + webglVendor: WEBGL_VENDORS[Math.floor(Math.random() * WEBGL_VENDORS.length)], + webglRenderer: WEBGL_RENDERERS[Math.floor(Math.random() * WEBGL_RENDERERS.length)], + }; +} class Downloader { browser = null; page = null; pageInUse = false; + currentFingerprint = generateRandomFingerprint(); + needsNewFingerprint = false; /** - * Initialize browser instance (lazy initialization) + * Force new fingerprint on next browser creation */ - async getBrowser() { + rotateFingerprint() { + this.needsNewFingerprint = true; + logger_1.logger.info('scraper', '🔄 Fingerprint rotation scheduled'); + } + /** + * Initialize browser instance with fingerprint + */ + async getBrowser(forceNew = false) { + // Create new browser if needed for fingerprint rotation + if (forceNew || this.needsNewFingerprint) { + await this.close(); + this.currentFingerprint = generateRandomFingerprint(); + this.needsNewFingerprint = false; + logger_1.logger.info('scraper', `🎭 New fingerprint: ${this.currentFingerprint.screen.width}x${this.currentFingerprint.screen.height}, ${this.currentFingerprint.timezone}, ${this.currentFingerprint.platform}`); + } if (!this.browser || !this.browser.isConnected()) { + const { screen } = this.currentFingerprint; const launchOptions = { headless: 'new', args: [ @@ -24,9 +96,11 @@ class Downloader { '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled', - '--window-size=1920,1080', + `--window-size=${screen.width},${screen.height}`, '--disable-web-security', - '--disable-features=IsolateOrigins,site-per-process' + '--disable-features=IsolateOrigins,site-per-process', + '--disable-infobars', + '--disable-extensions', ] }; this.browser = await puppeteer_1.default.launch(launchOptions); @@ -35,45 +109,137 @@ class Downloader { return this.browser; } /** - * Get or create a page instance + * Get or create a page instance with current fingerprint */ - async getPage() { - if (!this.page || this.page.isClosed()) { - const browser = await this.getBrowser(); + async getPage(forceNew = false) { + if (!this.page || this.page.isClosed() || forceNew) { + const browser = await this.getBrowser(forceNew); this.page = await browser.newPage(); - await this.page.setViewport({ width: 1920, height: 1080 }); - logger_1.logger.debug('scraper', 'New page created'); + const { screen } = this.currentFingerprint; + await this.page.setViewport({ + width: screen.width, + height: screen.height, + deviceScaleFactor: 1, + }); + // Apply fingerprint + await this.applyFingerprint(this.page); + logger_1.logger.debug('scraper', 'New page created with fingerprint'); } return this.page; } /** - * Apply stealth mode to page + * Apply full fingerprint to page */ - async makePageStealthy(page) { - await page.evaluateOnNewDocument(() => { - // @ts-ignore - runs in browser context + async applyFingerprint(page) { + const fp = this.currentFingerprint; + await page.evaluateOnNewDocument((fingerprint) => { + // Hide webdriver Object.defineProperty(navigator, 'webdriver', { get: () => false, }); - // @ts-ignore - runs in browser context - Object.defineProperty(navigator, 'plugins', { - get: () => [1, 2, 3, 4, 5], + // Spoof platform + Object.defineProperty(navigator, 'platform', { + get: () => fingerprint.platform, }); - // @ts-ignore - runs in browser context + // Spoof languages Object.defineProperty(navigator, 'languages', { - get: () => ['en-US', 'en'], + get: () => fingerprint.languages, }); - // @ts-ignore - runs in browser context + // Spoof hardware concurrency + Object.defineProperty(navigator, 'hardwareConcurrency', { + get: () => fingerprint.hardwareConcurrency, + }); + // Spoof device memory + Object.defineProperty(navigator, 'deviceMemory', { + get: () => fingerprint.deviceMemory, + }); + // Spoof plugins (realistic count) + Object.defineProperty(navigator, 'plugins', { + get: () => { + const plugins = []; + for (let i = 0; i < 5; i++) { + plugins.push({ + name: `Plugin ${i}`, + filename: `plugin${i}.dll`, + description: `Description ${i}`, + }); + } + plugins.length = 5; + return plugins; + }, + }); + // Chrome object window.chrome = { runtime: {}, + loadTimes: () => ({}), + csi: () => ({}), + app: {}, }; - // @ts-ignore - runs in browser context + // Permissions const originalQuery = window.navigator.permissions.query; - // @ts-ignore - runs in browser context window.navigator.permissions.query = (parameters) => parameters.name === 'notifications' ? Promise.resolve({ state: 'denied' }) : originalQuery(parameters); - }); + // WebGL fingerprint spoofing + const getParameterProxyHandler = { + apply: function (target, thisArg, argumentsList) { + const param = argumentsList[0]; + // UNMASKED_VENDOR_WEBGL + if (param === 37445) { + return fingerprint.webglVendor; + } + // UNMASKED_RENDERER_WEBGL + if (param === 37446) { + return fingerprint.webglRenderer; + } + return Reflect.apply(target, thisArg, argumentsList); + } + }; + // Override WebGL + const originalGetContext = HTMLCanvasElement.prototype.getContext; + HTMLCanvasElement.prototype.getContext = function (type, ...args) { + const context = originalGetContext.call(this, type, ...args); + if (context && (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl')) { + const glContext = context; + const originalGetParameter = glContext.getParameter.bind(glContext); + glContext.getParameter = new Proxy(originalGetParameter, getParameterProxyHandler); + } + return context; + }; + // Canvas fingerprint noise + const originalToDataURL = HTMLCanvasElement.prototype.toDataURL; + HTMLCanvasElement.prototype.toDataURL = function (type) { + const context = this.getContext('2d'); + if (context) { + const imageData = context.getImageData(0, 0, this.width, this.height); + for (let i = 0; i < imageData.data.length; i += 4) { + // Add tiny noise to RGB values + imageData.data[i] = imageData.data[i] ^ (Math.random() > 0.5 ? 1 : 0); + } + context.putImageData(imageData, 0, 0); + } + return originalToDataURL.call(this, type); + }; + // Screen dimensions + Object.defineProperty(window.screen, 'width', { get: () => fingerprint.screen.width }); + Object.defineProperty(window.screen, 'height', { get: () => fingerprint.screen.height }); + Object.defineProperty(window.screen, 'availWidth', { get: () => fingerprint.screen.width }); + Object.defineProperty(window.screen, 'availHeight', { get: () => fingerprint.screen.height - 40 }); + Object.defineProperty(window, 'innerWidth', { get: () => fingerprint.screen.width }); + Object.defineProperty(window, 'innerHeight', { get: () => fingerprint.screen.height - 140 }); + Object.defineProperty(window, 'outerWidth', { get: () => fingerprint.screen.width }); + Object.defineProperty(window, 'outerHeight', { get: () => fingerprint.screen.height }); + }, fp); + // Set timezone via CDP + const client = await page.target().createCDPSession(); + await client.send('Emulation.setTimezoneOverride', { timezoneId: fp.timezone }); + } + /** + * Apply stealth mode to page (legacy - now uses applyFingerprint) + */ + async makePageStealthy(page) { + // Now handled by applyFingerprint + await this.applyFingerprint(page); } /** * Configure proxy for browser @@ -162,17 +328,29 @@ class Downloader { if (request.metadata.userAgent) { await page.setUserAgent(request.metadata.userAgent); } - // Navigate to page + // Navigate to page - use networkidle2 for SPAs like Dutchie + // Increased timeout to 90s - Dutchie pages can take 30-40s to fully load const navigationPromise = page.goto(request.url, { - waitUntil: 'domcontentloaded', - timeout: 60000 + waitUntil: 'networkidle2', + timeout: 90000 }); const response = await navigationPromise; if (!response) { throw new Error('Navigation failed - no response'); } - // Wait for initial render - await page.waitForTimeout(3000); + // Wait for React to render product content + // Try to wait for products, but don't fail if they don't appear (empty category) + try { + await page.waitForSelector('[data-testid="product-list-item"], [data-testid="empty-state"]', { + timeout: 10000 + }); + } + catch { + // Products might not exist in this category - continue anyway + logger_1.logger.debug('scraper', 'No products found within timeout - continuing'); + } + // Additional wait for any lazy-loaded content + await page.waitForTimeout(2000); // Check for lazy-loaded content await this.autoScroll(page); // Get page content diff --git a/backend/dist/scraper-v2/engine.js b/backend/dist/scraper-v2/engine.js index 78887fb5..e7cf36bf 100644 --- a/backend/dist/scraper-v2/engine.js +++ b/backend/dist/scraper-v2/engine.js @@ -346,7 +346,7 @@ class DutchieSpider { catch (error) { logger_1.logger.error('scraper', `Category scrape failed: ${error}`); if (completeScraper) { - completeScraper(scraperId, error.toString()); + completeScraper(scraperId, String(error)); } throw error; } @@ -397,7 +397,28 @@ class DutchieSpider { // @ts-ignore - runs in browser context href = window.location.origin + href; } - items.push({ name, price, originalPrice, href }); + // Extract image URL from product card + let imageUrl = null; + const imgSelectors = [ + 'img[src*="images.dutchie.com"]', + 'img[src*="dutchie"]', + 'img[data-testid*="product"]', + 'img[class*="product"]', + 'img[class*="Product"]', + 'picture img', + 'img' + ]; + for (const sel of imgSelectors) { + const img = card.querySelector(sel); + if (img) { + const src = img.getAttribute('src') || img.getAttribute('data-src') || ''; + if (src && (src.includes('dutchie.com') || src.includes('images.'))) { + imageUrl = src; + break; + } + } + } + items.push({ name, price, originalPrice, href, imageUrl }); } catch (err) { console.error('Error parsing product card:', err); @@ -416,6 +437,7 @@ class DutchieSpider { productName: card.name, productPrice: card.price, productOriginalPrice: card.originalPrice, + productImageUrl: card.imageUrl, // Pass image from category page requiresBrowser: true }, callback: this.parseProductPage.bind(this) @@ -436,20 +458,26 @@ class DutchieSpider { const details = await page.evaluate(() => { // @ts-ignore - runs in browser context const allText = document.body.textContent || ''; - // Extract image + // Extract image - expanded selectors for better coverage let fullSizeImage = null; const mainImageSelectors = [ + 'img[src*="images.dutchie.com"]', + 'img[src*="dutchie"]', 'img[class*="ProductImage"]', 'img[class*="product-image"]', + 'img[class*="Product"]', '[class*="ImageGallery"] img', - 'main img', - 'img[src*="images.dutchie.com"]' + '[data-testid*="product"] img', + '[data-testid*="image"] img', + 'picture img', + 'main img' ]; for (const sel of mainImageSelectors) { // @ts-ignore - runs in browser context const img = document.querySelector(sel); - if (img?.src && img.src.includes('dutchie.com')) { - fullSizeImage = img.src; + const src = img?.src || img?.getAttribute('data-src') || ''; + if (src && (src.includes('dutchie.com') || src.includes('images.'))) { + fullSizeImage = src; break; } } @@ -546,6 +574,8 @@ class DutchieSpider { }; }); // Create product item + // Use image from product page, fallback to category page image + const imageUrl = details.fullSizeImage || response.request.metadata.productImageUrl || undefined; const product = { dutchieProductId: `${response.request.metadata.storeSlug}-${response.request.metadata.categorySlug}-${Date.now()}-${Math.random()}`, name: productName || 'Unknown Product', @@ -556,7 +586,7 @@ class DutchieSpider { cbdPercentage: details.cbd || undefined, strainType: details.strainType || undefined, brand: details.brand || undefined, - imageUrl: details.fullSizeImage || undefined, + imageUrl: imageUrl, dutchieUrl: response.url, metadata: { terpenes: details.terpenes, @@ -573,6 +603,17 @@ class DutchieSpider { async scrapeStore(storeId, parallel = 3) { logger_1.logger.info('scraper', `🏪 Starting store scrape: ${storeId} (${parallel} parallel scrapers)`); try { + // Check if categories exist, if not, discover them first + const categoryCountResult = await migrate_1.pool.query(` + SELECT COUNT(*) as count + FROM categories + WHERE store_id = $1 + `, [storeId]); + if (parseInt(categoryCountResult.rows[0].count) === 0) { + logger_1.logger.info('scraper', 'No categories found - running discovery first'); + const { discoverCategories } = await Promise.resolve().then(() => __importStar(require('./index'))); + await discoverCategories(storeId); + } // Get all leaf categories (no children) const categoriesResult = await migrate_1.pool.query(` SELECT c.id, c.name diff --git a/backend/dist/scraper-v2/index.js b/backend/dist/scraper-v2/index.js index 091b13b0..57669863 100644 --- a/backend/dist/scraper-v2/index.js +++ b/backend/dist/scraper-v2/index.js @@ -2,6 +2,13 @@ /** * Scraper V2 - Scrapy-inspired web scraping framework * + * IMPORTANT: For Dutchie stores, DO NOT USE scrapeStore() from this module. + * Dutchie crawling must go through the dutchie-az GraphQL pipeline: + * src/dutchie-az/services/product-crawler.ts + * + * This scraper-v2 module uses DOM-based extraction which is unreliable + * for Dutchie. The new dutchie-az pipeline uses GraphQL directly. + * * Architecture: * - Engine: Main orchestrator * - Scheduler: Priority queue with deduplication @@ -77,7 +84,7 @@ async function scrapeCategory(storeId, categoryId) { /** * Scrape an entire store */ -async function scrapeStore(storeId, parallel = 3) { +async function scrapeStore(storeId, parallel = 3, _userAgent) { const engine = new engine_2.ScraperEngine(1); const spider = new engine_2.DutchieSpider(engine); try { diff --git a/backend/dist/scraper-v2/middlewares.js b/backend/dist/scraper-v2/middlewares.js index 71e0ed94..5d10ef79 100644 --- a/backend/dist/scraper-v2/middlewares.js +++ b/backend/dist/scraper-v2/middlewares.js @@ -3,13 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.MiddlewareEngine = exports.StealthMiddleware = exports.BotDetectionMiddleware = exports.RetryMiddleware = exports.RateLimitMiddleware = exports.ProxyMiddleware = exports.UserAgentMiddleware = void 0; const types_1 = require("./types"); const logger_1 = require("../services/logger"); -const migrate_1 = require("../db/migrate"); +const proxy_1 = require("../services/proxy"); +// Diverse, realistic user agents - updated for 2024/2025 const USER_AGENTS = [ + // Chrome on Windows (most common) 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + // Chrome on Mac 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + // Chrome on Linux 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + // Firefox 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15' + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.0; rv:121.0) Gecko/20100101 Firefox/121.0', + // Safari + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15', + // Edge + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', ]; function getRandomUserAgent() { return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]; @@ -18,55 +36,100 @@ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** - * User Agent Rotation Middleware + * User Agent Rotation Middleware - rotates UA on each request for better evasion */ class UserAgentMiddleware { name = 'UserAgentMiddleware'; priority = 100; + lastUserAgent = null; async processRequest(request) { - if (!request.metadata.userAgent) { - request.metadata.userAgent = getRandomUserAgent(); + // Always rotate UA on retries or bot detection + const forceRotation = request.retryCount > 0 || request.metadata.botDetected; + if (!request.metadata.userAgent || forceRotation) { + // Get a different UA than the last one used + let newUA = getRandomUserAgent(); + let attempts = 0; + while (newUA === this.lastUserAgent && attempts < 5) { + newUA = getRandomUserAgent(); + attempts++; + } + request.metadata.userAgent = newUA; + this.lastUserAgent = newUA; + if (forceRotation) { + logger_1.logger.debug('scraper', `🔄 Rotated User-Agent: ${newUA.substring(0, 50)}...`); + } } return request; } } exports.UserAgentMiddleware = UserAgentMiddleware; +// Domains that should skip proxy (datacenter IPs are blocked) +const PROXY_SKIP_DOMAINS = [ + 'dutchie.com', +]; +function shouldSkipProxy(url) { + try { + const urlObj = new URL(url); + return PROXY_SKIP_DOMAINS.some(domain => urlObj.hostname.includes(domain)); + } + catch { + return false; + } +} /** - * Proxy Rotation Middleware + * Proxy Rotation Middleware - uses the central proxy service with timeout handling */ class ProxyMiddleware { name = 'ProxyMiddleware'; priority = 90; - async getActiveProxy() { - try { - const result = await migrate_1.pool.query(` - SELECT host, port, protocol, username, password - FROM proxies - WHERE active = true AND is_anonymous = true - ORDER BY RANDOM() - LIMIT 1 - `); - if (result.rows.length === 0) { - return null; - } - return result.rows[0]; - } - catch (error) { - logger_1.logger.error('scraper', `Failed to get proxy: ${error}`); - return null; - } - } + currentProxyId = null; async processRequest(request) { - // Only add proxy if not already set - if (!request.metadata.proxy && request.retryCount > 0) { - // Use proxy on retries - request.metadata.proxy = await this.getActiveProxy(); - if (request.metadata.proxy) { - logger_1.logger.debug('scraper', `Using proxy for retry: ${request.metadata.proxy.host}:${request.metadata.proxy.port}`); + // Skip proxy for domains that block datacenter IPs + if (shouldSkipProxy(request.url)) { + logger_1.logger.info('scraper', `⏭️ Skipping proxy for ${new URL(request.url).hostname} (datacenter IPs blocked)`); + return request; + } + // Always try to use a proxy from the central proxy service + // The service handles bot detection timeouts automatically + const forceRotation = request.retryCount > 0 || request.metadata.botDetected; + if (!request.metadata.proxy || forceRotation) { + // Get proxy from central service - it handles timeouts automatically + const proxy = await (0, proxy_1.getActiveProxy)(); + if (proxy) { + request.metadata.proxy = { + host: proxy.host, + port: proxy.port, + protocol: proxy.protocol, + username: proxy.username, + password: proxy.password, + }; + request.metadata.proxyId = proxy.id; + this.currentProxyId = proxy.id; + const reason = forceRotation ? 'rotation' : 'initial'; + logger_1.logger.info('scraper', `🔄 Using proxy (${reason}): ${proxy.protocol}://${proxy.host}:${proxy.port}`); + } + else { + logger_1.logger.warn('scraper', '⚠️ No proxy available - running without proxy'); } } return request; } + async processResponse(response) { + // If bot detection was triggered, put the proxy in timeout + if (response.request.metadata.botDetected && response.request.metadata.proxyId) { + (0, proxy_1.putProxyInTimeout)(response.request.metadata.proxyId, 'Bot detection triggered'); + logger_1.logger.info('scraper', `🚫 Proxy ${response.request.metadata.proxyId} put in timeout due to bot detection`); + } + return response; + } + async processError(error, request) { + // If bot detection error, put proxy in timeout + if ((0, proxy_1.isBotDetectionError)(error.message) && request.metadata.proxyId) { + (0, proxy_1.putProxyInTimeout)(request.metadata.proxyId, error.message); + logger_1.logger.info('scraper', `🚫 Proxy ${request.metadata.proxyId} put in timeout: ${error.message}`); + } + return error; + } } exports.ProxyMiddleware = ProxyMiddleware; /** @@ -165,13 +228,15 @@ class RetryMiddleware { } exports.RetryMiddleware = RetryMiddleware; /** - * Bot Detection Middleware + * Bot Detection Middleware - detects bot blocking and triggers fingerprint rotation */ class BotDetectionMiddleware { name = 'BotDetectionMiddleware'; priority = 60; detectedCount = 0; DETECTION_THRESHOLD = 3; + // Export for use by other middlewares + static shouldRotateFingerprint = false; async processResponse(response) { const content = typeof response.content === 'string' ? response.content @@ -183,14 +248,24 @@ class BotDetectionMiddleware { /access denied/i, /you have been blocked/i, /unusual traffic/i, - /robot/i + /robot/i, + /verify.*human/i, + /security check/i, + /please wait/i, + /checking your browser/i, + /ray id/i ]; const detected = botIndicators.some(pattern => pattern.test(content)); if (detected) { this.detectedCount++; + BotDetectionMiddleware.shouldRotateFingerprint = true; logger_1.logger.warn('scraper', `Bot detection suspected (${this.detectedCount}/${this.DETECTION_THRESHOLD}): ${response.url}`); + logger_1.logger.info('scraper', '🔄 Flagging for proxy/UA rotation on next request'); + // Mark the request for rotation on retry + response.request.metadata.botDetected = true; + response.request.metadata.needsNewBrowser = true; if (this.detectedCount >= this.DETECTION_THRESHOLD) { - const error = new Error('Bot detection threshold reached'); + const error = new Error('Bot detection threshold reached - rotating fingerprint'); error.type = types_1.ErrorType.BOT_DETECTION; error.retryable = true; error.request = response.request; @@ -200,9 +275,22 @@ class BotDetectionMiddleware { else { // Gradually decrease detection count on successful requests this.detectedCount = Math.max(0, this.detectedCount - 0.5); + BotDetectionMiddleware.shouldRotateFingerprint = false; } return response; } + async processError(error, request) { + // If bot detection error, flag for rotation and allow retry + if ('type' in error && error.type === types_1.ErrorType.BOT_DETECTION) { + request.metadata.botDetected = true; + request.metadata.needsNewBrowser = true; + logger_1.logger.info('scraper', '🔄 Bot detection error - will rotate proxy/UA on retry'); + // Add delay before retry to avoid rate limiting + await sleep(5000 + Math.random() * 5000); + return null; // Return null to trigger retry + } + return error; + } } exports.BotDetectionMiddleware = BotDetectionMiddleware; /** diff --git a/backend/dist/scraper-v2/pipelines.js b/backend/dist/scraper-v2/pipelines.js index 8119c083..ce5c74ff 100644 --- a/backend/dist/scraper-v2/pipelines.js +++ b/backend/dist/scraper-v2/pipelines.js @@ -4,6 +4,7 @@ exports.PipelineEngine = exports.StatsPipeline = exports.DatabasePipeline = expo const logger_1 = require("../services/logger"); const migrate_1 = require("../db/migrate"); const minio_1 = require("../utils/minio"); +const product_normalizer_1 = require("../utils/product-normalizer"); /** * Validation Pipeline - ensures data quality */ @@ -138,82 +139,182 @@ class ImagePipeline { } exports.ImagePipeline = ImagePipeline; /** - * Database Pipeline - saves items to database + * Generate a URL-safe slug from a product name + */ +function generateSlug(name) { + return name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .substring(0, 400); +} +/** + * Database Pipeline - saves items to database with improved matching + * + * MATCHING PRIORITY: + * 1. external_id (dutchie_product_id) - exact match + * 2. normalized name + brand + category - strong match + * 3. normalized name + category - weak match (same product, different/missing brand) + * + * ALWAYS creates a snapshot after upsert for historical tracking. */ class DatabasePipeline { name = 'DatabasePipeline'; priority = 10; // Low priority - runs last + crawlId = null; + setCrawlId(id) { + this.crawlId = id; + } async process(item, spider) { const client = await migrate_1.pool.connect(); try { // Extract store and category from metadata (set by spider) const storeId = item.storeId; const categoryId = item.categoryId; + const dispensaryId = item.dispensaryId; + const categoryName = item.categoryName; + // Generate normalized values for matching + const nameNormalized = (0, product_normalizer_1.normalizeProductName)(item.name); + const brandNormalized = (0, product_normalizer_1.normalizeBrandName)(item.brand); + const slug = generateSlug(item.name); + const externalId = item.dutchieProductId || null; if (!storeId || !categoryId) { logger_1.logger.error('pipeline', `Missing storeId or categoryId for ${item.name}`); return null; } - // Check if product exists - const existingResult = await client.query(` - SELECT id, image_url, local_image_path - FROM products - WHERE store_id = $1 AND name = $2 AND category_id = $3 - `, [storeId, item.name, categoryId]); + let productId = null; let localImagePath = null; - let productId; - if (existingResult.rows.length > 0) { + let isNewProduct = false; + // STEP 1: Try to match by external_id (most reliable) + if (externalId) { + const extMatch = await client.query(` + SELECT id, image_url, local_image_path + FROM products + WHERE store_id = $1 AND (external_id = $2 OR dutchie_product_id = $2) + `, [storeId, externalId]); + if (extMatch.rows.length > 0) { + productId = extMatch.rows[0].id; + localImagePath = extMatch.rows[0].local_image_path; + logger_1.logger.debug('pipeline', `Matched by external_id: ${item.name}`); + } + } + // STEP 2: Try to match by normalized name + brand + category + if (!productId) { + const normMatch = await client.query(` + SELECT id, image_url, local_image_path + FROM products + WHERE store_id = $1 + AND name_normalized = $2 + AND brand_normalized = $3 + AND category_id = $4 + `, [storeId, nameNormalized, brandNormalized, categoryId]); + if (normMatch.rows.length > 0) { + productId = normMatch.rows[0].id; + localImagePath = normMatch.rows[0].local_image_path; + logger_1.logger.debug('pipeline', `Matched by normalized name+brand+category: ${item.name}`); + } + } + // STEP 3: Fallback to normalized name + category only (weaker match) + if (!productId) { + const weakMatch = await client.query(` + SELECT id, image_url, local_image_path + FROM products + WHERE store_id = $1 + AND name_normalized = $2 + AND category_id = $3 + LIMIT 1 + `, [storeId, nameNormalized, categoryId]); + if (weakMatch.rows.length === 1) { + productId = weakMatch.rows[0].id; + localImagePath = weakMatch.rows[0].local_image_path; + logger_1.logger.debug('pipeline', `Matched by normalized name+category: ${item.name}`); + } + } + // STEP 4: Final fallback - exact name match (legacy compatibility) + if (!productId) { + const exactMatch = await client.query(` + SELECT id, image_url, local_image_path + FROM products + WHERE store_id = $1 AND name = $2 AND category_id = $3 + `, [storeId, item.name, categoryId]); + if (exactMatch.rows.length > 0) { + productId = exactMatch.rows[0].id; + localImagePath = exactMatch.rows[0].local_image_path; + logger_1.logger.debug('pipeline', `Matched by exact name: ${item.name}`); + } + } + // UPDATE or INSERT + if (productId) { // Update existing product - productId = existingResult.rows[0].id; - localImagePath = existingResult.rows[0].local_image_path; await client.query(` UPDATE products SET name = $1, description = $2, price = $3, strain_type = $4, thc_percentage = $5, cbd_percentage = $6, - brand = $7, weight = $8, image_url = $9, dutchie_url = $10, + brand = $7, weight = $8, image_url = COALESCE($9, image_url), dutchie_url = $10, in_stock = true, metadata = $11, last_seen_at = CURRENT_TIMESTAMP, - updated_at = CURRENT_TIMESTAMP + updated_at = CURRENT_TIMESTAMP, dispensary_id = $13, slug = COALESCE(slug, $14), + name_normalized = $15, brand_normalized = $16, + external_id = COALESCE(external_id, $17), source_platform = COALESCE(source_platform, 'dutchie') WHERE id = $12 `, [ item.name, item.description, item.price, item.strainType, item.thcPercentage, item.cbdPercentage, item.brand, item.weight, item.imageUrl, item.dutchieUrl, - JSON.stringify(item.metadata || {}), productId + JSON.stringify(item.metadata || {}), productId, dispensaryId, slug, + nameNormalized, brandNormalized, externalId ]); logger_1.logger.debug('pipeline', `Updated product: ${item.name}`); } else { // Insert new product + isNewProduct = true; const insertResult = await client.query(` INSERT INTO products ( - store_id, category_id, dutchie_product_id, name, description, + store_id, category_id, dispensary_id, dutchie_product_id, external_id, + slug, name, name_normalized, description, price, strain_type, thc_percentage, cbd_percentage, - brand, weight, image_url, dutchie_url, in_stock, metadata - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, true, $14) + brand, brand_normalized, weight, image_url, dutchie_url, in_stock, metadata, + source_platform + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, true, $19, 'dutchie') RETURNING id `, [ - storeId, categoryId, item.dutchieProductId, item.name, item.description, + storeId, categoryId, dispensaryId, externalId, externalId, + slug, item.name, nameNormalized, item.description, item.price, item.strainType, item.thcPercentage, item.cbdPercentage, - item.brand, item.weight, item.imageUrl, item.dutchieUrl, + item.brand, brandNormalized, item.weight, item.imageUrl, item.dutchieUrl, JSON.stringify(item.metadata || {}) ]); productId = insertResult.rows[0].id; - logger_1.logger.debug('pipeline', `Inserted new product: ${item.name}`); + logger_1.logger.debug('pipeline', `Inserted NEW product: ${item.name}`); } - // Download image if needed - if (item.imageUrl && !localImagePath) { + // ALWAYS create a snapshot for historical tracking + await this.createSnapshot(client, { + productId: productId, + dispensaryId, + externalId, + slug, + item, + categoryName + }); + // Download image if needed (only for new products or missing local image) + if (item.imageUrl && !localImagePath && productId) { try { - localImagePath = await (0, minio_1.uploadImageFromUrl)(item.imageUrl, productId); + const storeResult = await client.query('SELECT slug FROM stores WHERE id = $1', [storeId]); + const storeSlug = storeResult.rows[0]?.slug || undefined; + const imageSizes = await (0, minio_1.uploadImageFromUrl)(item.imageUrl, productId, storeSlug); + localImagePath = imageSizes.thumbnail; await client.query(` - UPDATE products - SET local_image_path = $1 - WHERE id = $2 - `, [localImagePath, productId]); + UPDATE products SET local_image_path = $1 WHERE id = $2 + `, [imageSizes.thumbnail, productId]); logger_1.logger.debug('pipeline', `Downloaded image for: ${item.name}`); } catch (error) { logger_1.logger.error('pipeline', `Failed to download image for ${item.name}: ${error}`); } } + // Attach metadata for stats tracking + item.isNewProduct = isNewProduct; + item.productId = productId; return item; } catch (error) { @@ -224,6 +325,64 @@ class DatabasePipeline { client.release(); } } + /** + * Create a snapshot record for historical tracking + */ + async createSnapshot(client, params) { + try { + // Only create snapshots if the table exists (graceful degradation) + const tableExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'product_snapshots' + ) + `); + if (!tableExists.rows[0].exists) { + return; // Snapshot table not yet created + } + const crawlId = this.crawlId || crypto.randomUUID(); + const { productId, dispensaryId, externalId, slug, item, categoryName } = params; + await client.query(` + INSERT INTO product_snapshots ( + crawl_id, dispensary_id, external_product_id, product_slug, + name, brand, category, price, original_price, sale_price, + discount_type, discount_value, availability_status, stock_quantity, + thc_percentage, cbd_percentage, strain_type, weight, variant, + description, image_url, effects, terpenes, captured_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, NOW() + ) + `, [ + crawlId, + dispensaryId, + externalId, + slug, + item.name, + item.brand || null, + categoryName || null, + item.price || null, + item.originalPrice || null, + item.metadata?.salePrice || null, + item.metadata?.discountType || null, + item.metadata?.discountValue || null, + 'in_stock', // availability_status - if we scraped it, it's in stock + item.metadata?.stockQuantity || null, + item.thcPercentage || null, + item.cbdPercentage || null, + item.strainType || null, + item.weight || null, + item.metadata?.variant || null, + item.description || null, + item.imageUrl || null, + item.metadata?.effects || null, + item.metadata?.terpenes || null + ]); + } + catch (error) { + // Don't fail the whole pipeline if snapshot creation fails + logger_1.logger.warn('pipeline', `Failed to create snapshot for ${params.item.name}: ${error}`); + } + } } exports.DatabasePipeline = DatabasePipeline; /** diff --git a/backend/dist/scrapers/dutchie-graphql-direct.js b/backend/dist/scrapers/dutchie-graphql-direct.js new file mode 100644 index 00000000..d8710717 --- /dev/null +++ b/backend/dist/scrapers/dutchie-graphql-direct.js @@ -0,0 +1,360 @@ +"use strict"; +// ============================================================================ +// DEPRECATED: This scraper writes to the LEGACY products table. +// DO NOT USE - All Dutchie crawling must use the new dutchie-az pipeline. +// +// New pipeline location: src/dutchie-az/services/product-crawler.ts +// - Uses fetch-based GraphQL (no Puppeteer needed) +// - Writes to isolated dutchie_az_* tables with snapshot model +// - Tracks stockStatus, isPresentInFeed, missing_from_feed +// ============================================================================ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fetchAllDutchieProducts = fetchAllDutchieProducts; +exports.upsertProductsDirect = upsertProductsDirect; +exports.scrapeAllDutchieProducts = scrapeAllDutchieProducts; +/** + * @deprecated DEPRECATED - Use src/dutchie-az/services/product-crawler.ts instead. + * This scraper writes to the legacy products table, not the new dutchie_az tables. + * + * Makes direct GraphQL requests from within the browser context to: + * 1. Bypass Cloudflare (using browser session) + * 2. Fetch ALL products including out-of-stock (Status: null) + * 3. Paginate through complete menu + */ +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +const dutchie_graphql_1 = require("./dutchie-graphql"); +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +// GraphQL persisted query hashes +const GRAPHQL_HASHES = { + FilteredProducts: 'ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0', + GetAddressBasedDispensaryData: '13461f73abf7268770dfd05fe7e10c523084b2bb916a929c08efe3d87531977b', +}; +/** + * Fetch all products via in-page GraphQL requests + * This includes both in-stock and out-of-stock items + */ +async function fetchAllDutchieProducts(menuUrl, options = {}) { + const { headless = 'new', timeout = 90000, perPage = 100, includeOutOfStock = true, } = options; + let browser; + try { + browser = await puppeteer_extra_1.default.launch({ + headless, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-blink-features=AutomationControlled', + ], + }); + const page = await browser.newPage(); + // Stealth configuration + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + await page.setViewport({ width: 1920, height: 1080 }); + await page.evaluateOnNewDocument(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => false }); + window.chrome = { runtime: {} }; + }); + // Navigate to menu page to establish session + console.log('[DutchieGraphQL] Loading menu page to establish session...'); + await page.goto(menuUrl, { + waitUntil: 'networkidle2', + timeout, + }); + // Get dispensary ID from page + const dispensaryId = await page.evaluate(() => { + const env = window.reactEnv; + return env?.dispensaryId || env?.retailerId || ''; + }); + if (!dispensaryId) { + throw new Error('Could not determine dispensaryId from page'); + } + console.log(`[DutchieGraphQL] Dispensary ID: ${dispensaryId}`); + // Fetch all products via in-page GraphQL requests + const allProducts = []; + let page_num = 0; + let hasMore = true; + while (hasMore) { + console.log(`[DutchieGraphQL] Fetching page ${page_num} (perPage=${perPage})...`); + const result = await page.evaluate(async (dispensaryId, page_num, perPage, includeOutOfStock, hash) => { + const variables = { + includeEnterpriseSpecials: false, + productsFilter: { + dispensaryId, + pricingType: 'rec', + Status: includeOutOfStock ? null : 'Active', // null = include out-of-stock + types: [], + useCache: false, // Don't cache to get fresh data + isDefaultSort: true, + sortBy: 'popularSortIdx', + sortDirection: 1, + bypassOnlineThresholds: true, + isKioskMenu: false, + removeProductsBelowOptionThresholds: false, + }, + page: page_num, + perPage, + }; + const qs = new URLSearchParams({ + operationName: 'FilteredProducts', + variables: JSON.stringify(variables), + extensions: JSON.stringify({ + persistedQuery: { version: 1, sha256Hash: hash }, + }), + }); + const response = await fetch(`https://dutchie.com/graphql?${qs.toString()}`, { + method: 'GET', + headers: { + 'content-type': 'application/json', + 'apollographql-client-name': 'Marketplace (production)', + }, + credentials: 'include', // Include cookies/session + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }, dispensaryId, page_num, perPage, includeOutOfStock, GRAPHQL_HASHES.FilteredProducts); + if (result.errors) { + console.error('[DutchieGraphQL] GraphQL errors:', result.errors); + break; + } + const products = result?.data?.filteredProducts?.products || []; + console.log(`[DutchieGraphQL] Page ${page_num}: ${products.length} products`); + if (products.length === 0) { + hasMore = false; + } + else { + allProducts.push(...products); + page_num++; + // Safety limit + if (page_num > 50) { + console.log('[DutchieGraphQL] Reached page limit, stopping'); + hasMore = false; + } + } + } + // Count active vs inactive + const activeCount = allProducts.filter((p) => p.Status === 'Active').length; + const inactiveCount = allProducts.filter((p) => p.Status !== 'Active').length; + console.log(`[DutchieGraphQL] Total: ${allProducts.length} products (${activeCount} active, ${inactiveCount} inactive)`); + return { + products: allProducts, + dispensaryId, + totalProducts: allProducts.length, + activeCount, + inactiveCount, + }; + } + finally { + if (browser) { + await browser.close(); + } + } +} +/** + * Upsert products to database + */ +async function upsertProductsDirect(pool, storeId, products) { + const client = await pool.connect(); + let inserted = 0; + let updated = 0; + try { + await client.query('BEGIN'); + for (const product of products) { + const result = await client.query(` + INSERT INTO products ( + store_id, external_id, slug, name, enterprise_product_id, + brand, brand_external_id, brand_logo_url, + subcategory, strain_type, canonical_category, + price, rec_price, med_price, rec_special_price, med_special_price, + is_on_special, special_name, discount_percent, special_data, + sku, inventory_quantity, inventory_available, is_below_threshold, status, + thc_percentage, cbd_percentage, cannabinoids, + weight_mg, net_weight_value, net_weight_unit, options, raw_options, + image_url, additional_images, + is_featured, medical_only, rec_only, + source_created_at, source_updated_at, + description, raw_data, + dutchie_url, last_seen_at, updated_at + ) + VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, + $9, $10, $11, + $12, $13, $14, $15, $16, + $17, $18, $19, $20, + $21, $22, $23, $24, $25, + $26, $27, $28, + $29, $30, $31, $32, $33, + $34, $35, + $36, $37, $38, + $39, $40, + $41, $42, + '', NOW(), NOW() + ) + ON CONFLICT (store_id, slug) DO UPDATE SET + name = EXCLUDED.name, + enterprise_product_id = EXCLUDED.enterprise_product_id, + brand = EXCLUDED.brand, + brand_external_id = EXCLUDED.brand_external_id, + brand_logo_url = EXCLUDED.brand_logo_url, + subcategory = EXCLUDED.subcategory, + strain_type = EXCLUDED.strain_type, + canonical_category = EXCLUDED.canonical_category, + price = EXCLUDED.price, + rec_price = EXCLUDED.rec_price, + med_price = EXCLUDED.med_price, + rec_special_price = EXCLUDED.rec_special_price, + med_special_price = EXCLUDED.med_special_price, + is_on_special = EXCLUDED.is_on_special, + special_name = EXCLUDED.special_name, + discount_percent = EXCLUDED.discount_percent, + special_data = EXCLUDED.special_data, + sku = EXCLUDED.sku, + inventory_quantity = EXCLUDED.inventory_quantity, + inventory_available = EXCLUDED.inventory_available, + is_below_threshold = EXCLUDED.is_below_threshold, + status = EXCLUDED.status, + thc_percentage = EXCLUDED.thc_percentage, + cbd_percentage = EXCLUDED.cbd_percentage, + cannabinoids = EXCLUDED.cannabinoids, + weight_mg = EXCLUDED.weight_mg, + net_weight_value = EXCLUDED.net_weight_value, + net_weight_unit = EXCLUDED.net_weight_unit, + options = EXCLUDED.options, + raw_options = EXCLUDED.raw_options, + image_url = EXCLUDED.image_url, + additional_images = EXCLUDED.additional_images, + is_featured = EXCLUDED.is_featured, + medical_only = EXCLUDED.medical_only, + rec_only = EXCLUDED.rec_only, + source_created_at = EXCLUDED.source_created_at, + source_updated_at = EXCLUDED.source_updated_at, + description = EXCLUDED.description, + raw_data = EXCLUDED.raw_data, + last_seen_at = NOW(), + updated_at = NOW() + RETURNING (xmax = 0) AS was_inserted + `, [ + storeId, + product.external_id, + product.slug, + product.name, + product.enterprise_product_id, + product.brand, + product.brand_external_id, + product.brand_logo_url, + product.subcategory, + product.strain_type, + product.canonical_category, + product.price, + product.rec_price, + product.med_price, + product.rec_special_price, + product.med_special_price, + product.is_on_special, + product.special_name, + product.discount_percent, + product.special_data ? JSON.stringify(product.special_data) : null, + product.sku, + product.inventory_quantity, + product.inventory_available, + product.is_below_threshold, + product.status, + product.thc_percentage, + product.cbd_percentage, + product.cannabinoids ? JSON.stringify(product.cannabinoids) : null, + product.weight_mg, + product.net_weight_value, + product.net_weight_unit, + product.options, + product.raw_options, + product.image_url, + product.additional_images, + product.is_featured, + product.medical_only, + product.rec_only, + product.source_created_at, + product.source_updated_at, + product.description, + product.raw_data ? JSON.stringify(product.raw_data) : null, + ]); + if (result.rows[0]?.was_inserted) { + inserted++; + } + else { + updated++; + } + } + await client.query('COMMIT'); + return { inserted, updated }; + } + catch (error) { + await client.query('ROLLBACK'); + throw error; + } + finally { + client.release(); + } +} +/** + * @deprecated DEPRECATED - Use src/dutchie-az/services/product-crawler.ts instead. + * This function is disabled and will throw an error if called. + * Main entry point - scrape all products including out-of-stock + */ +async function scrapeAllDutchieProducts(pool, storeId, menuUrl) { + // DEPRECATED: Throw error to prevent accidental use + throw new Error('DEPRECATED: scrapeAllDutchieProducts() is deprecated. ' + + 'Use src/dutchie-az/services/product-crawler.ts instead. ' + + 'This scraper writes to the legacy products table.'); + // Original code below is unreachable but kept for reference + try { + console.log(`[DutchieGraphQL] Scraping ALL products (including out-of-stock): ${menuUrl}`); + // Fetch all products via direct GraphQL + const { products, totalProducts, activeCount, inactiveCount } = await fetchAllDutchieProducts(menuUrl, { + includeOutOfStock: true, + perPage: 100, + }); + if (products.length === 0) { + return { + success: false, + totalProducts: 0, + activeCount: 0, + inactiveCount: 0, + inserted: 0, + updated: 0, + error: 'No products returned from GraphQL', + }; + } + // Normalize products + const normalized = products.map(dutchie_graphql_1.normalizeDutchieProduct); + // Upsert to database + const { inserted, updated } = await upsertProductsDirect(pool, storeId, normalized); + console.log(`[DutchieGraphQL] Complete: ${totalProducts} products (${activeCount} active, ${inactiveCount} inactive)`); + console.log(`[DutchieGraphQL] Database: ${inserted} inserted, ${updated} updated`); + return { + success: true, + totalProducts, + activeCount, + inactiveCount, + inserted, + updated, + }; + } + catch (error) { + console.error(`[DutchieGraphQL] Error:`, error.message); + return { + success: false, + totalProducts: 0, + activeCount: 0, + inactiveCount: 0, + inserted: 0, + updated: 0, + error: error.message, + }; + } +} diff --git a/backend/dist/scrapers/dutchie-graphql.js b/backend/dist/scrapers/dutchie-graphql.js new file mode 100644 index 00000000..d1dab343 --- /dev/null +++ b/backend/dist/scrapers/dutchie-graphql.js @@ -0,0 +1,446 @@ +"use strict"; +// ============================================================================ +// DEPRECATED: This scraper writes to the LEGACY products table. +// DO NOT USE - All Dutchie crawling must use the new dutchie-az pipeline. +// +// New pipeline location: src/dutchie-az/services/product-crawler.ts +// - Uses fetch-based GraphQL (no Puppeteer needed) +// - Writes to isolated dutchie_az_* tables with snapshot model +// - Tracks stockStatus, isPresentInFeed, missing_from_feed +// +// The normalizer functions in this file (normalizeDutchieProduct) may still +// be imported for reference, but do NOT call scrapeDutchieMenu() or upsertProducts(). +// ============================================================================ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeDutchieProduct = normalizeDutchieProduct; +exports.fetchDutchieMenuViaPuppeteer = fetchDutchieMenuViaPuppeteer; +exports.upsertProducts = upsertProducts; +exports.scrapeDutchieMenu = scrapeDutchieMenu; +/** + * @deprecated DEPRECATED - Use src/dutchie-az/services/product-crawler.ts instead. + * This scraper writes to the legacy products table, not the new dutchie_az tables. + * + * Fetches product data via Puppeteer interception of Dutchie's GraphQL API. + * This bypasses Cloudflare by using a real browser to load the menu page. + * + * GraphQL Operations: + * - FilteredProducts: Returns paginated product list with full details + * - GetAddressBasedDispensaryData: Resolves dispensary cName to dispensaryId + */ +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +// ===================================================== +// NORMALIZER: Dutchie GraphQL → DB Schema +// ===================================================== +function normalizeDutchieProduct(product) { + // Extract first special if exists + const saleSpecial = product.specialData?.saleSpecials?.[0]; + // Calculate inventory from POSMetaData children + const children = product.POSMetaData?.children || []; + const totalQuantity = children.reduce((sum, c) => sum + (c.quantity || 0), 0); + const availableQuantity = children.reduce((sum, c) => sum + (c.quantityAvailable || 0), 0); + // Parse timestamps + let sourceCreatedAt; + if (product.createdAt) { + // createdAt is a timestamp string like "1729044510543" + const ts = parseInt(product.createdAt, 10); + if (!isNaN(ts)) { + sourceCreatedAt = new Date(ts); + } + } + let sourceUpdatedAt; + if (product.updatedAt) { + sourceUpdatedAt = new Date(product.updatedAt); + } + return { + // Identity + external_id: product._id || product.id, + slug: product.cName, + name: product.Name, + enterprise_product_id: product.enterpriseProductId, + // Brand + brand: product.brandName || product.brand?.name, + brand_external_id: product.brandId || product.brand?.id, + brand_logo_url: product.brandLogo || product.brand?.imageUrl, + // Category + subcategory: product.subcategory, + strain_type: product.strainType, + canonical_category: product.POSMetaData?.canonicalCategory, + // Pricing + price: product.Prices?.[0], + rec_price: product.recPrices?.[0], + med_price: product.medicalPrices?.[0], + rec_special_price: product.recSpecialPrices?.[0], + med_special_price: product.medicalSpecialPrices?.[0], + // Specials + is_on_special: product.special === true, + special_name: saleSpecial?.specialName, + discount_percent: saleSpecial?.percentDiscount ? saleSpecial.discount : undefined, + special_data: product.specialData, + // Inventory + sku: product.POSMetaData?.canonicalSKU, + inventory_quantity: totalQuantity || undefined, + inventory_available: availableQuantity || undefined, + is_below_threshold: product.isBelowThreshold === true, + status: product.Status, + // Potency + thc_percentage: product.THCContent?.range?.[0], + cbd_percentage: product.CBDContent?.range?.[0], + cannabinoids: product.cannabinoidsV2, + // Weight/Options + weight_mg: product.weight, + net_weight_value: product.measurements?.netWeight?.values?.[0], + net_weight_unit: product.measurements?.netWeight?.unit, + options: product.Options, + raw_options: product.rawOptions, + // Images + image_url: product.Image, + additional_images: product.images?.length ? product.images : undefined, + // Flags + is_featured: product.featured === true, + medical_only: product.medicalOnly === true, + rec_only: product.recOnly === true, + // Timestamps + source_created_at: sourceCreatedAt, + source_updated_at: sourceUpdatedAt, + // Description + description: typeof product.description === 'string' ? product.description : undefined, + // Raw + raw_data: product, + }; +} +async function fetchDutchieMenuViaPuppeteer(menuUrl, options = {}) { + const { headless = 'new', timeout = 90000, maxScrolls = 30, // Increased for full menu capture + } = options; + let browser; + const capturedProducts = []; + let dispensaryId = ''; + try { + browser = await puppeteer_extra_1.default.launch({ + headless, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-blink-features=AutomationControlled', + ], + }); + const page = await browser.newPage(); + // Stealth configuration + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + await page.setViewport({ width: 1920, height: 1080 }); + await page.evaluateOnNewDocument(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => false }); + window.chrome = { runtime: {} }; + }); + // Track seen product IDs to avoid duplicates + const seenIds = new Set(); + // Intercept GraphQL responses + page.on('response', async (response) => { + const url = response.url(); + if (!url.includes('graphql')) + return; + try { + const contentType = response.headers()['content-type'] || ''; + if (!contentType.includes('application/json')) + return; + const data = await response.json(); + // Capture dispensary ID + if (data?.data?.getAddressBasedDispensaryData?.dispensaryData?.dispensaryId) { + dispensaryId = data.data.getAddressBasedDispensaryData.dispensaryData.dispensaryId; + } + // Capture products from FilteredProducts + if (data?.data?.filteredProducts?.products) { + const products = data.data.filteredProducts.products; + for (const product of products) { + if (!seenIds.has(product._id)) { + seenIds.add(product._id); + capturedProducts.push(product); + } + } + } + } + catch { + // Ignore parse errors + } + }); + // Navigate to menu + console.log('[DutchieGraphQL] Loading menu page...'); + await page.goto(menuUrl, { + waitUntil: 'networkidle2', + timeout, + }); + // Get dispensary ID from window.reactEnv if not captured + if (!dispensaryId) { + dispensaryId = await page.evaluate(() => { + const env = window.reactEnv; + return env?.dispensaryId || env?.retailerId || ''; + }); + } + // Helper function to scroll through a page until no more products load + async function scrollToLoadAll(maxScrollAttempts = maxScrolls) { + let scrollCount = 0; + let previousCount = 0; + let noNewProductsCount = 0; + while (scrollCount < maxScrollAttempts && noNewProductsCount < 3) { + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await new Promise((r) => setTimeout(r, 1500)); + const currentCount = seenIds.size; + if (currentCount === previousCount) { + noNewProductsCount++; + } + else { + noNewProductsCount = 0; + } + previousCount = currentCount; + scrollCount++; + } + } + // First, scroll through the main page (all products) + console.log('[DutchieGraphQL] Scrolling main page...'); + await scrollToLoadAll(); + console.log(`[DutchieGraphQL] After main page: ${seenIds.size} products`); + // Get category links from the navigation + const categoryLinks = await page.evaluate(() => { + const links = []; + // Look for category navigation links + const navLinks = document.querySelectorAll('a[href*="/products/"]'); + navLinks.forEach((link) => { + const href = link.href; + if (href && !links.includes(href)) { + links.push(href); + } + }); + return links; + }); + console.log(`[DutchieGraphQL] Found ${categoryLinks.length} category links`); + // Visit each category page to capture all products + for (const categoryUrl of categoryLinks) { + try { + console.log(`[DutchieGraphQL] Visiting category: ${categoryUrl.split('/').pop()}`); + await page.goto(categoryUrl, { + waitUntil: 'networkidle2', + timeout: 30000, + }); + await scrollToLoadAll(15); // Fewer scrolls per category + console.log(`[DutchieGraphQL] Total products: ${seenIds.size}`); + } + catch (e) { + console.log(`[DutchieGraphQL] Category error: ${e.message}`); + } + } + // Wait for any final responses + await new Promise((r) => setTimeout(r, 2000)); + return { + products: capturedProducts, + dispensaryId, + menuUrl, + }; + } + finally { + if (browser) { + await browser.close(); + } + } +} +// ===================================================== +// DATABASE OPERATIONS +// ===================================================== +async function upsertProducts(pool, storeId, products) { + const client = await pool.connect(); + let inserted = 0; + let updated = 0; + try { + await client.query('BEGIN'); + for (const product of products) { + // Upsert product + const result = await client.query(` + INSERT INTO products ( + store_id, external_id, slug, name, enterprise_product_id, + brand, brand_external_id, brand_logo_url, + subcategory, strain_type, canonical_category, + price, rec_price, med_price, rec_special_price, med_special_price, + is_on_special, special_name, discount_percent, special_data, + sku, inventory_quantity, inventory_available, is_below_threshold, status, + thc_percentage, cbd_percentage, cannabinoids, + weight_mg, net_weight_value, net_weight_unit, options, raw_options, + image_url, additional_images, + is_featured, medical_only, rec_only, + source_created_at, source_updated_at, + description, raw_data, + dutchie_url, last_seen_at, updated_at + ) + VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, + $9, $10, $11, + $12, $13, $14, $15, $16, + $17, $18, $19, $20, + $21, $22, $23, $24, $25, + $26, $27, $28, + $29, $30, $31, $32, $33, + $34, $35, + $36, $37, $38, + $39, $40, + $41, $42, + '', NOW(), NOW() + ) + ON CONFLICT (store_id, slug) DO UPDATE SET + name = EXCLUDED.name, + enterprise_product_id = EXCLUDED.enterprise_product_id, + brand = EXCLUDED.brand, + brand_external_id = EXCLUDED.brand_external_id, + brand_logo_url = EXCLUDED.brand_logo_url, + subcategory = EXCLUDED.subcategory, + strain_type = EXCLUDED.strain_type, + canonical_category = EXCLUDED.canonical_category, + price = EXCLUDED.price, + rec_price = EXCLUDED.rec_price, + med_price = EXCLUDED.med_price, + rec_special_price = EXCLUDED.rec_special_price, + med_special_price = EXCLUDED.med_special_price, + is_on_special = EXCLUDED.is_on_special, + special_name = EXCLUDED.special_name, + discount_percent = EXCLUDED.discount_percent, + special_data = EXCLUDED.special_data, + sku = EXCLUDED.sku, + inventory_quantity = EXCLUDED.inventory_quantity, + inventory_available = EXCLUDED.inventory_available, + is_below_threshold = EXCLUDED.is_below_threshold, + status = EXCLUDED.status, + thc_percentage = EXCLUDED.thc_percentage, + cbd_percentage = EXCLUDED.cbd_percentage, + cannabinoids = EXCLUDED.cannabinoids, + weight_mg = EXCLUDED.weight_mg, + net_weight_value = EXCLUDED.net_weight_value, + net_weight_unit = EXCLUDED.net_weight_unit, + options = EXCLUDED.options, + raw_options = EXCLUDED.raw_options, + image_url = EXCLUDED.image_url, + additional_images = EXCLUDED.additional_images, + is_featured = EXCLUDED.is_featured, + medical_only = EXCLUDED.medical_only, + rec_only = EXCLUDED.rec_only, + source_created_at = EXCLUDED.source_created_at, + source_updated_at = EXCLUDED.source_updated_at, + description = EXCLUDED.description, + raw_data = EXCLUDED.raw_data, + last_seen_at = NOW(), + updated_at = NOW() + RETURNING (xmax = 0) AS was_inserted + `, [ + storeId, + product.external_id, + product.slug, + product.name, + product.enterprise_product_id, + product.brand, + product.brand_external_id, + product.brand_logo_url, + product.subcategory, + product.strain_type, + product.canonical_category, + product.price, + product.rec_price, + product.med_price, + product.rec_special_price, + product.med_special_price, + product.is_on_special, + product.special_name, + product.discount_percent, + product.special_data ? JSON.stringify(product.special_data) : null, + product.sku, + product.inventory_quantity, + product.inventory_available, + product.is_below_threshold, + product.status, + product.thc_percentage, + product.cbd_percentage, + product.cannabinoids ? JSON.stringify(product.cannabinoids) : null, + product.weight_mg, + product.net_weight_value, + product.net_weight_unit, + product.options, + product.raw_options, + product.image_url, + product.additional_images, + product.is_featured, + product.medical_only, + product.rec_only, + product.source_created_at, + product.source_updated_at, + product.description, + product.raw_data ? JSON.stringify(product.raw_data) : null, + ]); + if (result.rows[0]?.was_inserted) { + inserted++; + } + else { + updated++; + } + } + await client.query('COMMIT'); + return { inserted, updated }; + } + catch (error) { + await client.query('ROLLBACK'); + throw error; + } + finally { + client.release(); + } +} +// ===================================================== +// MAIN ENTRY POINT +// ===================================================== +/** + * @deprecated DEPRECATED - Use src/dutchie-az/services/product-crawler.ts instead. + * This function is disabled and will throw an error if called. + */ +async function scrapeDutchieMenu(pool, storeId, menuUrl) { + // DEPRECATED: Throw error to prevent accidental use + throw new Error('DEPRECATED: scrapeDutchieMenu() is deprecated. ' + + 'Use src/dutchie-az/services/product-crawler.ts instead. ' + + 'This scraper writes to the legacy products table.'); + // Original code below is unreachable but kept for reference + try { + console.log(`[DutchieGraphQL] Scraping: ${menuUrl}`); + // Fetch products via Puppeteer + const { products, dispensaryId } = await fetchDutchieMenuViaPuppeteer(menuUrl); + console.log(`[DutchieGraphQL] Captured ${products.length} products, dispensaryId: ${dispensaryId}`); + if (products.length === 0) { + return { + success: false, + productsFound: 0, + inserted: 0, + updated: 0, + error: 'No products captured from GraphQL responses', + }; + } + // Normalize products + const normalized = products.map(normalizeDutchieProduct); + // Upsert to database + const { inserted, updated } = await upsertProducts(pool, storeId, normalized); + console.log(`[DutchieGraphQL] Upsert complete: ${inserted} inserted, ${updated} updated`); + return { + success: true, + productsFound: products.length, + inserted, + updated, + }; + } + catch (error) { + console.error(`[DutchieGraphQL] Error:`, error.message); + return { + success: false, + productsFound: 0, + inserted: 0, + updated: 0, + error: error.message, + }; + } +} diff --git a/backend/dist/scrapers/templates/dutchie.js b/backend/dist/scrapers/templates/dutchie.js new file mode 100644 index 00000000..54f1f96d --- /dev/null +++ b/backend/dist/scrapers/templates/dutchie.js @@ -0,0 +1,85 @@ +"use strict"; +// ============================================================================ +// DEPRECATED: Dutchie now crawled via GraphQL only (see dutchie-az pipeline) +// DO NOT USE - This HTML scraper is unreliable and targets the legacy products table. +// All Dutchie crawling must go through: src/dutchie-az/services/product-crawler.ts +// ============================================================================ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.dutchieTemplate = void 0; +exports.getTemplateForUrl = getTemplateForUrl; +const logger_1 = require("../../services/logger"); +/** + * @deprecated DEPRECATED - Dutchie HTML scraping is no longer supported. + * Use the dutchie-az GraphQL pipeline instead: src/dutchie-az/services/product-crawler.ts + * This template relied on unstable DOM selectors and wrote to legacy tables. + */ +exports.dutchieTemplate = { + name: 'Dutchie Marketplace', + urlPattern: /dutchie\.com\/dispensary\//, + buildCategoryUrl: (baseUrl, category) => { + // Remove trailing slash + const base = baseUrl.replace(/\/$/, ''); + // Convert category name to URL-friendly slug + const categorySlug = category.toLowerCase().replace(/\s+/g, '-'); + return `${base}/products/${categorySlug}`; + }, + extractProducts: async (page) => { + const products = []; + try { + // Wait for product cards to load + await page.waitForSelector('a[data-testid="card-link"]', { timeout: 10000 }).catch(() => { + logger_1.logger.warn('scraper', 'No product cards found with data-testid="card-link"'); + }); + // Get all product card links + const productCards = await page.locator('a[href*="/product/"][data-testid="card-link"]').all(); + logger_1.logger.info('scraper', `Found ${productCards.length} Dutchie product cards`); + for (const card of productCards) { + try { + // Extract all data at once using evaluate for speed + const cardData = await card.evaluate((el) => { + const href = el.getAttribute('href') || ''; + const img = el.querySelector('img'); + const imageUrl = img ? img.getAttribute('src') || '' : ''; + // Get all text nodes in order + const textElements = Array.from(el.querySelectorAll('*')) + .filter(el => el.textContent && el.children.length === 0) + .map(el => (el.textContent || '').trim()) + .filter(text => text.length > 0); + const name = textElements[0] || ''; + const brand = textElements[1] || ''; + // Look for price + const priceMatch = el.textContent?.match(/\$(\d+(?:\.\d{2})?)/); + const price = priceMatch ? parseFloat(priceMatch[1]) : undefined; + return { href, imageUrl, name, brand, price }; + }); + if (cardData.name && cardData.href) { + products.push({ + name: cardData.name, + brand: cardData.brand || undefined, + product_url: cardData.href.startsWith('http') ? cardData.href : `https://dutchie.com${cardData.href}`, + image_url: cardData.imageUrl || undefined, + price: cardData.price, + in_stock: true, + }); + } + } + catch (err) { + logger_1.logger.warn('scraper', `Error extracting Dutchie product card: ${err}`); + } + } + } + catch (err) { + logger_1.logger.error('scraper', `Error in Dutchie product extraction: ${err}`); + } + return products; + }, +}; +/** + * Get the appropriate scraper template based on URL + */ +function getTemplateForUrl(url) { + if (exports.dutchieTemplate.urlPattern.test(url)) { + return exports.dutchieTemplate; + } + return null; +} diff --git a/backend/dist/scripts/backfill-store-dispensary.js b/backend/dist/scripts/backfill-store-dispensary.js new file mode 100644 index 00000000..4a9ea57a --- /dev/null +++ b/backend/dist/scripts/backfill-store-dispensary.js @@ -0,0 +1,287 @@ +#!/usr/bin/env npx tsx +"use strict"; +/** + * Backfill Store-Dispensary Mapping + * + * Links existing stores (scheduler) to dispensaries (master AZDHS directory) + * by matching on name, city, and zip code. + * + * Usage: + * npx tsx src/scripts/backfill-store-dispensary.ts # Preview matches + * npx tsx src/scripts/backfill-store-dispensary.ts --apply # Apply matches + * npx tsx src/scripts/backfill-store-dispensary.ts --verbose # Show all match details + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const migrate_1 = require("../db/migrate"); +const logger_1 = require("../services/logger"); +const args = process.argv.slice(2); +const flags = { + apply: args.includes('--apply'), + verbose: args.includes('--verbose'), + help: args.includes('--help') || args.includes('-h'), +}; +/** + * Normalize a store/dispensary name for comparison + * Removes common suffixes, punctuation, and extra whitespace + */ +function normalizeName(name) { + return name + .toLowerCase() + .replace(/\s*[-–—]\s*/g, ' ') // Normalize dashes to spaces + .replace(/\s*(dispensary|cannabis|marijuana|weed|shop|store|llc|inc)\s*/gi, ' ') + .replace(/['']/g, "'") // Normalize apostrophes + .replace(/[^\w\s']/g, '') // Remove other punctuation + .replace(/\s+/g, ' ') // Collapse whitespace + .trim(); +} +/** + * Simple Levenshtein distance for fuzzy matching + */ +function levenshteinDistance(a, b) { + const matrix = []; + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } + else { + matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1 // deletion + ); + } + } + } + return matrix[b.length][a.length]; +} +/** + * Calculate similarity score (0-100) + */ +function similarityScore(a, b) { + const maxLen = Math.max(a.length, b.length); + if (maxLen === 0) + return 100; + const distance = levenshteinDistance(a, b); + return Math.round((1 - distance / maxLen) * 100); +} +/** + * Find the best dispensary match for a store + */ +function findBestMatch(store, dispensaries) { + const normalizedStoreName = normalizeName(store.name); + const storeSlug = store.slug.toLowerCase(); + let bestMatch = { + store, + dispensary: null, + matchType: 'none', + score: 0, + }; + for (const disp of dispensaries) { + const normalizedDispName = normalizeName(disp.name); + const normalizedCompanyName = disp.company_name ? normalizeName(disp.company_name) : ''; + const dispSlug = disp.slug.toLowerCase(); + // 1. Exact name match (case-insensitive) + if (store.name.toLowerCase() === disp.name.toLowerCase()) { + return { + store, + dispensary: disp, + matchType: 'exact_name', + score: 100, + }; + } + // 2. Normalized name match + if (normalizedStoreName === normalizedDispName) { + return { + store, + dispensary: disp, + matchType: 'normalized_name', + score: 95, + }; + } + // 3. Store name matches company name + if (normalizedCompanyName && normalizedStoreName === normalizedCompanyName) { + return { + store, + dispensary: disp, + matchType: 'company_name', + score: 90, + }; + } + // 4. Slug match + if (storeSlug === dispSlug) { + return { + store, + dispensary: disp, + matchType: 'slug', + score: 85, + }; + } + // 5. Fuzzy matching (only if score > 70) + const nameScore = similarityScore(normalizedStoreName, normalizedDispName); + const companyScore = normalizedCompanyName + ? similarityScore(normalizedStoreName, normalizedCompanyName) + : 0; + const fuzzyScore = Math.max(nameScore, companyScore); + if (fuzzyScore > bestMatch.score && fuzzyScore >= 70) { + bestMatch = { + store, + dispensary: disp, + matchType: 'fuzzy', + score: fuzzyScore, + }; + } + } + return bestMatch; +} +async function main() { + if (flags.help) { + console.log(` +Backfill Store-Dispensary Mapping + +Links existing stores (scheduler) to dispensaries (master AZDHS directory) +by matching on name, company name, or slug similarity. + +USAGE: + npx tsx src/scripts/backfill-store-dispensary.ts [OPTIONS] + +OPTIONS: + --apply Apply the mappings to the database (default: preview only) + --verbose Show detailed match information for all stores + --help, -h Show this help message + +EXAMPLES: + # Preview what would be matched + npx tsx src/scripts/backfill-store-dispensary.ts + + # Apply the mappings + npx tsx src/scripts/backfill-store-dispensary.ts --apply + + # Show verbose output + npx tsx src/scripts/backfill-store-dispensary.ts --verbose +`); + process.exit(0); + } + console.log('\n📦 Backfill Store-Dispensary Mapping'); + console.log('=====================================\n'); + try { + // Fetch all stores without a dispensary_id + const storesResult = await migrate_1.pool.query(` + SELECT id, name, slug, dispensary_id + FROM stores + WHERE dispensary_id IS NULL + ORDER BY name + `); + const unmappedStores = storesResult.rows; + // Fetch all already-mapped stores for context + const mappedResult = await migrate_1.pool.query(` + SELECT id, name, slug, dispensary_id + FROM stores + WHERE dispensary_id IS NOT NULL + ORDER BY name + `); + const mappedStores = mappedResult.rows; + // Fetch all dispensaries + const dispResult = await migrate_1.pool.query(` + SELECT id, name, company_name, city, address, slug + FROM dispensaries + ORDER BY name + `); + const dispensaries = dispResult.rows; + console.log(`📊 Current Status:`); + console.log(` Stores without dispensary_id: ${unmappedStores.length}`); + console.log(` Stores already mapped: ${mappedStores.length}`); + console.log(` Total dispensaries: ${dispensaries.length}\n`); + if (unmappedStores.length === 0) { + console.log('✅ All stores are already mapped to dispensaries!\n'); + await migrate_1.pool.end(); + process.exit(0); + } + // Find matches for each unmapped store + const matches = []; + const noMatches = []; + for (const store of unmappedStores) { + const match = findBestMatch(store, dispensaries); + if (match.dispensary) { + matches.push(match); + } + else { + noMatches.push(store); + } + } + // Sort matches by score (highest first) + matches.sort((a, b) => b.score - a.score); + // Display results + console.log(`\n🔗 Matches Found: ${matches.length}`); + console.log('----------------------------------\n'); + if (matches.length > 0) { + // Group by match type + const byType = {}; + for (const m of matches) { + if (!byType[m.matchType]) + byType[m.matchType] = []; + byType[m.matchType].push(m); + } + const typeLabels = { + exact_name: '✅ Exact Name Match', + normalized_name: '✅ Normalized Name Match', + company_name: '🏢 Company Name Match', + slug: '🔗 Slug Match', + fuzzy: '🔍 Fuzzy Match', + }; + for (const [type, results] of Object.entries(byType)) { + console.log(`${typeLabels[type]} (${results.length}):`); + for (const r of results) { + const dispInfo = r.dispensary; + console.log(` • "${r.store.name}" → "${dispInfo.name}" (${dispInfo.city}) [${r.score}%]`); + } + console.log(''); + } + } + if (noMatches.length > 0) { + console.log(`\n❌ No Match Found: ${noMatches.length}`); + console.log('----------------------------------\n'); + for (const store of noMatches) { + console.log(` • "${store.name}" (slug: ${store.slug})`); + } + console.log(''); + } + // Apply if requested + if (flags.apply && matches.length > 0) { + console.log('\n🔧 Applying mappings...\n'); + let updated = 0; + for (const match of matches) { + if (!match.dispensary) + continue; + await migrate_1.pool.query('UPDATE stores SET dispensary_id = $1 WHERE id = $2', [match.dispensary.id, match.store.id]); + updated++; + if (flags.verbose) { + console.log(` ✓ Linked store ${match.store.id} to dispensary ${match.dispensary.id}`); + } + } + console.log(`\n✅ Updated ${updated} stores with dispensary mappings\n`); + logger_1.logger.info('system', `Backfill complete: linked ${updated} stores to dispensaries`); + } + else if (matches.length > 0 && !flags.apply) { + console.log('\n💡 Run with --apply to update the database\n'); + } + // Summary + console.log('📈 Summary:'); + console.log(` Would match: ${matches.length} stores`); + console.log(` No match: ${noMatches.length} stores`); + console.log(` Match rate: ${Math.round((matches.length / unmappedStores.length) * 100)}%\n`); + } + catch (error) { + console.error('Error:', error); + process.exit(1); + } + finally { + await migrate_1.pool.end(); + } +} +main().catch(console.error); diff --git a/backend/dist/scripts/bootstrap-discovery.js b/backend/dist/scripts/bootstrap-discovery.js new file mode 100644 index 00000000..eac151f4 --- /dev/null +++ b/backend/dist/scripts/bootstrap-discovery.js @@ -0,0 +1,332 @@ +#!/usr/bin/env npx tsx +"use strict"; +/** + * Bootstrap Discovery Script + * + * One-time (but reusable) bootstrap command that: + * 1. Ensures every Dispensary has a dispensary_crawl_schedule entry (4h default) + * 2. Optionally runs RunDispensaryOrchestrator for each dispensary + * + * Usage: + * npx tsx src/scripts/bootstrap-discovery.ts # Create schedules only + * npx tsx src/scripts/bootstrap-discovery.ts --run # Create schedules + run orchestrator + * npx tsx src/scripts/bootstrap-discovery.ts --run --limit=10 # Run for first 10 dispensaries + * npx tsx src/scripts/bootstrap-discovery.ts --dry-run # Preview what would happen + * npx tsx src/scripts/bootstrap-discovery.ts --status # Show current status only + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const migrate_1 = require("../db/migrate"); +const dispensary_orchestrator_1 = require("../services/dispensary-orchestrator"); +// Parse command line args +const args = process.argv.slice(2); +const flags = { + run: args.includes('--run'), + dryRun: args.includes('--dry-run'), + status: args.includes('--status'), + help: args.includes('--help') || args.includes('-h'), + limit: parseInt(args.find(a => a.startsWith('--limit='))?.split('=')[1] || '0'), + concurrency: parseInt(args.find(a => a.startsWith('--concurrency='))?.split('=')[1] || '3'), + interval: parseInt(args.find(a => a.startsWith('--interval='))?.split('=')[1] || '240'), + detectionOnly: args.includes('--detection-only'), + productionOnly: args.includes('--production-only'), + sandboxOnly: args.includes('--sandbox-only'), +}; +async function showHelp() { + console.log(` +Bootstrap Discovery - Initialize Dispensary Crawl System + +USAGE: + npx tsx src/scripts/bootstrap-discovery.ts [OPTIONS] + +OPTIONS: + --run After creating schedules, run the orchestrator for each dispensary + --dry-run Show what would happen without making changes + --status Show current status and exit + --limit=N Limit how many dispensaries to process (0 = all, default: 0) + --concurrency=N How many dispensaries to process in parallel (default: 3) + --interval=M Default interval in minutes for new schedules (default: 240 = 4 hours) + --detection-only Only run detection, don't crawl + --production-only Only run dispensaries in production mode + --sandbox-only Only run dispensaries in sandbox mode + --help, -h Show this help message + +EXAMPLES: + # Create schedule entries for all dispensaries (no crawling) + npx tsx src/scripts/bootstrap-discovery.ts + + # Create schedules and run orchestrator for all dispensaries + npx tsx src/scripts/bootstrap-discovery.ts --run + + # Run orchestrator for first 10 dispensaries + npx tsx src/scripts/bootstrap-discovery.ts --run --limit=10 + + # Run with higher concurrency + npx tsx src/scripts/bootstrap-discovery.ts --run --concurrency=5 + + # Show current status + npx tsx src/scripts/bootstrap-discovery.ts --status + +WHAT IT DOES: + 1. Creates dispensary_crawl_schedule entries for all dispensaries that don't have one + 2. If --run: For each dispensary, runs the orchestrator which: + a. Checks if provider detection is needed (null/unknown/stale/low confidence) + b. Runs detection if needed + c. If Dutchie + production mode: runs production crawl + d. Otherwise: runs sandbox crawl + 3. Updates schedule status and job records +`); +} +async function showStatus() { + console.log('\n📊 Current Dispensary Crawl Status\n'); + console.log('═'.repeat(70)); + // Get dispensary counts by provider + const providerStats = await migrate_1.pool.query(` + SELECT + COALESCE(product_provider, 'undetected') as provider, + COUNT(*) as count, + COUNT(*) FILTER (WHERE product_crawler_mode = 'production') as production, + COUNT(*) FILTER (WHERE product_crawler_mode = 'sandbox') as sandbox, + COUNT(*) FILTER (WHERE product_crawler_mode IS NULL) as no_mode + FROM dispensaries + GROUP BY COALESCE(product_provider, 'undetected') + ORDER BY count DESC + `); + console.log('\nProvider Distribution:'); + console.log('-'.repeat(60)); + console.log('Provider'.padEnd(20) + + 'Total'.padStart(8) + + 'Production'.padStart(12) + + 'Sandbox'.padStart(10) + + 'No Mode'.padStart(10)); + console.log('-'.repeat(60)); + for (const row of providerStats.rows) { + console.log(row.provider.padEnd(20) + + row.count.toString().padStart(8) + + row.production.toString().padStart(12) + + row.sandbox.toString().padStart(10) + + row.no_mode.toString().padStart(10)); + } + // Get schedule stats + const scheduleStats = await migrate_1.pool.query(` + SELECT + COUNT(DISTINCT d.id) as total_dispensaries, + COUNT(DISTINCT dcs.id) as with_schedule, + COUNT(DISTINCT d.id) - COUNT(DISTINCT dcs.id) as without_schedule, + COUNT(*) FILTER (WHERE dcs.is_active = TRUE) as active_schedules, + COUNT(*) FILTER (WHERE dcs.last_status = 'success') as last_success, + COUNT(*) FILTER (WHERE dcs.last_status = 'error') as last_error, + COUNT(*) FILTER (WHERE dcs.last_status = 'sandbox_only') as last_sandbox, + COUNT(*) FILTER (WHERE dcs.last_status = 'detection_only') as last_detection, + COUNT(*) FILTER (WHERE dcs.next_run_at <= NOW()) as due_now, + AVG(dcs.interval_minutes)::INTEGER as avg_interval + FROM dispensaries d + LEFT JOIN dispensary_crawl_schedule dcs ON dcs.dispensary_id = d.id + `); + const s = scheduleStats.rows[0]; + console.log('\n\nSchedule Status:'); + console.log('-'.repeat(60)); + console.log(` Total Dispensaries: ${s.total_dispensaries}`); + console.log(` With Schedule: ${s.with_schedule}`); + console.log(` Without Schedule: ${s.without_schedule}`); + console.log(` Active Schedules: ${s.active_schedules || 0}`); + console.log(` Average Interval: ${s.avg_interval || 240} minutes`); + console.log('\n Last Run Status:'); + console.log(` - Success: ${s.last_success || 0}`); + console.log(` - Error: ${s.last_error || 0}`); + console.log(` - Sandbox Only: ${s.last_sandbox || 0}`); + console.log(` - Detection Only: ${s.last_detection || 0}`); + console.log(` - Due Now: ${s.due_now || 0}`); + // Get recent job stats + const jobStats = await migrate_1.pool.query(` + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE status = 'completed') as completed, + COUNT(*) FILTER (WHERE status = 'failed') as failed, + COUNT(*) FILTER (WHERE status = 'running') as running, + COUNT(*) FILTER (WHERE status = 'pending') as pending, + COUNT(*) FILTER (WHERE detection_ran = TRUE) as with_detection, + COUNT(*) FILTER (WHERE crawl_ran = TRUE) as with_crawl, + COUNT(*) FILTER (WHERE crawl_type = 'production') as production_crawls, + COUNT(*) FILTER (WHERE crawl_type = 'sandbox') as sandbox_crawls, + SUM(products_found) as total_products_found + FROM dispensary_crawl_jobs + WHERE created_at > NOW() - INTERVAL '24 hours' + `); + const j = jobStats.rows[0]; + console.log('\n\nJobs (Last 24 Hours):'); + console.log('-'.repeat(60)); + console.log(` Total Jobs: ${j.total || 0}`); + console.log(` Completed: ${j.completed || 0}`); + console.log(` Failed: ${j.failed || 0}`); + console.log(` Running: ${j.running || 0}`); + console.log(` Pending: ${j.pending || 0}`); + console.log(` With Detection: ${j.with_detection || 0}`); + console.log(` With Crawl: ${j.with_crawl || 0}`); + console.log(` - Production: ${j.production_crawls || 0}`); + console.log(` - Sandbox: ${j.sandbox_crawls || 0}`); + console.log(` Products Found: ${j.total_products_found || 0}`); + console.log('\n' + '═'.repeat(70) + '\n'); +} +async function createSchedules() { + console.log('\n📅 Creating Dispensary Schedules...\n'); + if (flags.dryRun) { + // Count how many would be created + const result = await migrate_1.pool.query(` + SELECT COUNT(*) as count + FROM dispensaries d + WHERE NOT EXISTS ( + SELECT 1 FROM dispensary_crawl_schedule dcs WHERE dcs.dispensary_id = d.id + ) + `); + const wouldCreate = parseInt(result.rows[0].count); + console.log(` Would create ${wouldCreate} new schedule entries (${flags.interval} minute interval)`); + return { created: wouldCreate, existing: 0 }; + } + const result = await (0, dispensary_orchestrator_1.ensureAllDispensariesHaveSchedules)(flags.interval); + console.log(` ✓ Created ${result.created} new schedule entries`); + console.log(` ✓ ${result.existing} dispensaries already had schedules`); + return result; +} +async function getDispensariesToProcess() { + // Build query based on filters + let whereClause = 'TRUE'; + if (flags.productionOnly) { + whereClause += ` AND d.product_crawler_mode = 'production'`; + } + else if (flags.sandboxOnly) { + whereClause += ` AND d.product_crawler_mode = 'sandbox'`; + } + if (flags.detectionOnly) { + whereClause += ` AND (d.product_provider IS NULL OR d.product_provider = 'unknown' OR d.product_confidence < 50)`; + } + const limitClause = flags.limit > 0 ? `LIMIT ${flags.limit}` : ''; + const query = ` + SELECT d.id, d.name, d.product_provider, d.product_crawler_mode + FROM dispensaries d + LEFT JOIN dispensary_crawl_schedule dcs ON dcs.dispensary_id = d.id + WHERE ${whereClause} + ORDER BY + COALESCE(dcs.priority, 0) DESC, + dcs.last_run_at ASC NULLS FIRST, + d.id ASC + ${limitClause} + `; + const result = await migrate_1.pool.query(query); + return result.rows.map(row => row.id); +} +async function runOrchestrator() { + console.log('\n🚀 Running Dispensary Orchestrator...\n'); + const dispensaryIds = await getDispensariesToProcess(); + if (dispensaryIds.length === 0) { + console.log(' No dispensaries to process.'); + return; + } + console.log(` Found ${dispensaryIds.length} dispensaries to process`); + console.log(` Concurrency: ${flags.concurrency}`); + if (flags.dryRun) { + console.log('\n Would process these dispensaries:'); + const details = await migrate_1.pool.query(`SELECT id, name, product_provider, product_crawler_mode + FROM dispensaries WHERE id = ANY($1) ORDER BY id`, [dispensaryIds]); + for (const row of details.rows.slice(0, 20)) { + console.log(` - [${row.id}] ${row.name} (${row.product_provider || 'undetected'}, ${row.product_crawler_mode || 'no mode'})`); + } + if (details.rows.length > 20) { + console.log(` ... and ${details.rows.length - 20} more`); + } + return; + } + console.log('\n Starting batch processing...\n'); + const results = await (0, dispensary_orchestrator_1.runBatchDispensaryOrchestrator)(dispensaryIds, flags.concurrency); + // Summarize results + const summary = { + total: results.length, + success: results.filter(r => r.status === 'success').length, + sandboxOnly: results.filter(r => r.status === 'sandbox_only').length, + detectionOnly: results.filter(r => r.status === 'detection_only').length, + error: results.filter(r => r.status === 'error').length, + detectionsRan: results.filter(r => r.detectionRan).length, + crawlsRan: results.filter(r => r.crawlRan).length, + productionCrawls: results.filter(r => r.crawlType === 'production').length, + sandboxCrawls: results.filter(r => r.crawlType === 'sandbox').length, + totalProducts: results.reduce((sum, r) => sum + (r.productsFound || 0), 0), + totalDuration: results.reduce((sum, r) => sum + r.durationMs, 0), + }; + console.log('\n' + '═'.repeat(70)); + console.log(' Orchestrator Results'); + console.log('═'.repeat(70)); + console.log(` + Total Processed: ${summary.total} + + Status: + - Success: ${summary.success} + - Sandbox Only: ${summary.sandboxOnly} + - Detection Only: ${summary.detectionOnly} + - Error: ${summary.error} + + Operations: + - Detections Ran: ${summary.detectionsRan} + - Crawls Ran: ${summary.crawlsRan} + - Production: ${summary.productionCrawls} + - Sandbox: ${summary.sandboxCrawls} + + Results: + - Products Found: ${summary.totalProducts} + - Total Duration: ${(summary.totalDuration / 1000).toFixed(1)}s + - Avg per Dispensary: ${(summary.totalDuration / summary.total / 1000).toFixed(1)}s +`); + console.log('═'.repeat(70) + '\n'); + // Show errors if any + const errors = results.filter(r => r.status === 'error'); + if (errors.length > 0) { + console.log('\n⚠️ Errors encountered:'); + for (const err of errors.slice(0, 10)) { + console.log(` - [${err.dispensaryId}] ${err.dispensaryName}: ${err.error}`); + } + if (errors.length > 10) { + console.log(` ... and ${errors.length - 10} more errors`); + } + } +} +async function main() { + if (flags.help) { + await showHelp(); + process.exit(0); + } + console.log('\n' + '═'.repeat(70)); + console.log(' Dispensary Crawl Bootstrap Discovery'); + console.log('═'.repeat(70)); + if (flags.dryRun) { + console.log('\n🔍 DRY RUN MODE - No changes will be made'); + } + try { + // Always show status first + await showStatus(); + if (flags.status) { + // Status-only mode, we're done + await migrate_1.pool.end(); + process.exit(0); + } + // Step 1: Create schedule entries + await createSchedules(); + // Step 2: Optionally run orchestrator + if (flags.run) { + await runOrchestrator(); + } + else { + console.log('\n💡 Tip: Use --run to also run the orchestrator for each dispensary'); + } + // Show final status + if (!flags.dryRun) { + await showStatus(); + } + } + catch (error) { + console.error('\n❌ Fatal error:', error.message); + console.error(error.stack); + process.exit(1); + } + finally { + await migrate_1.pool.end(); + } +} +main(); diff --git a/backend/dist/scripts/capture-dutchie-schema.js b/backend/dist/scripts/capture-dutchie-schema.js new file mode 100644 index 00000000..a0960547 --- /dev/null +++ b/backend/dist/scripts/capture-dutchie-schema.js @@ -0,0 +1,236 @@ +"use strict"; +/** + * Capture Dutchie GraphQL response structure via Puppeteer interception + * This script navigates to a Dutchie menu page and captures the GraphQL responses + * to understand the exact product data structure + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +const fs = __importStar(require("fs")); +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +async function captureSchema(menuUrl) { + let browser; + const capturedResponses = []; + try { + console.log('='.repeat(80)); + console.log('DUTCHIE GRAPHQL SCHEMA CAPTURE'); + console.log('='.repeat(80)); + console.log(`\nTarget URL: ${menuUrl}\n`); + browser = await puppeteer_extra_1.default.launch({ + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-blink-features=AutomationControlled', + ] + }); + const page = await browser.newPage(); + // Use a realistic user agent + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + // Set viewport to desktop size + await page.setViewport({ width: 1920, height: 1080 }); + // Hide webdriver flag + await page.evaluateOnNewDocument(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => false }); + window.chrome = { runtime: {} }; + }); + // Intercept all GraphQL responses + page.on('response', async (response) => { + const url = response.url(); + // Only capture GraphQL responses + if (!url.includes('graphql')) + return; + try { + const contentType = response.headers()['content-type'] || ''; + if (!contentType.includes('application/json')) + return; + const data = await response.json(); + // Extract operation name from URL if possible + const urlParams = new URLSearchParams(url.split('?')[1] || ''); + const operationName = urlParams.get('operationName') || 'Unknown'; + capturedResponses.push({ + operationName, + url: url.substring(0, 200), + data, + timestamp: new Date() + }); + console.log(`📡 Captured: ${operationName}`); + // Check for product data + if (data?.data?.filteredProducts?.products) { + const products = data.data.filteredProducts.products; + console.log(` Found ${products.length} products`); + } + } + catch (e) { + // Ignore parse errors + } + }); + console.log('Navigating to page...'); + await page.goto(menuUrl, { + waitUntil: 'networkidle2', + timeout: 90000 + }); + // Check if it's a Dutchie menu + const isDutchie = await page.evaluate(() => { + return typeof window.reactEnv !== 'undefined'; + }); + if (isDutchie) { + console.log('✅ Dutchie menu detected\n'); + // Get environment info + const reactEnv = await page.evaluate(() => window.reactEnv); + console.log('Dutchie Environment:'); + console.log(` dispensaryId: ${reactEnv?.dispensaryId}`); + console.log(` retailerId: ${reactEnv?.retailerId}`); + console.log(` chainId: ${reactEnv?.chainId}`); + } + // Scroll to trigger lazy loading + console.log('\nScrolling to load more products...'); + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await new Promise(r => setTimeout(r, 3000)); + // Click on a category to trigger more loads + const categoryLinks = await page.$$('a[href*="/products/"]'); + if (categoryLinks.length > 0) { + console.log(`Found ${categoryLinks.length} category links, clicking first one...`); + try { + await categoryLinks[0].click(); + await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30000 }); + } + catch (e) { + console.log('Category navigation failed, continuing...'); + } + } + // Wait a bit more for any final responses + await new Promise(r => setTimeout(r, 2000)); + console.log(`\n${'='.repeat(80)}`); + console.log(`CAPTURED ${capturedResponses.length} GRAPHQL RESPONSES`); + console.log('='.repeat(80)); + // Find product data + let productSchema = null; + let sampleProduct = null; + for (const resp of capturedResponses) { + console.log(`\n${resp.operationName}:`); + console.log(` URL: ${resp.url.substring(0, 100)}...`); + if (resp.data?.data?.filteredProducts?.products) { + const products = resp.data.data.filteredProducts.products; + console.log(` ✅ Contains ${products.length} products`); + if (products.length > 0 && !sampleProduct) { + sampleProduct = products[0]; + productSchema = extractSchema(products[0]); + } + } + // Show top-level data keys + if (resp.data?.data) { + console.log(` Data keys: ${Object.keys(resp.data.data).join(', ')}`); + } + } + // Output the product schema + if (productSchema) { + console.log('\n' + '='.repeat(80)); + console.log('PRODUCT SCHEMA (from first product):'); + console.log('='.repeat(80)); + console.log(JSON.stringify(productSchema, null, 2)); + console.log('\n' + '='.repeat(80)); + console.log('SAMPLE PRODUCT:'); + console.log('='.repeat(80)); + console.log(JSON.stringify(sampleProduct, null, 2)); + // Save to file + const outputData = { + capturedAt: new Date().toISOString(), + menuUrl, + schema: productSchema, + sampleProduct, + allResponses: capturedResponses.map(r => ({ + operationName: r.operationName, + dataKeys: r.data?.data ? Object.keys(r.data.data) : [], + productCount: r.data?.data?.filteredProducts?.products?.length || 0 + })) + }; + const outputPath = '/tmp/dutchie-schema-capture.json'; + fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2)); + console.log(`\nSaved capture to: ${outputPath}`); + } + else { + console.log('\n❌ No product data captured'); + // Debug: show all responses + console.log('\nAll captured responses:'); + for (const resp of capturedResponses) { + console.log(`\n${resp.operationName}:`); + console.log(JSON.stringify(resp.data, null, 2).substring(0, 500)); + } + } + } + catch (error) { + console.error('Error:', error.message); + } + finally { + if (browser) { + await browser.close(); + } + } +} +/** + * Extract schema from an object (field names + types) + */ +function extractSchema(obj, prefix = '') { + if (obj === null) + return { type: 'null' }; + if (obj === undefined) + return { type: 'undefined' }; + if (Array.isArray(obj)) { + if (obj.length === 0) + return { type: 'array', items: 'unknown' }; + return { + type: 'array', + items: extractSchema(obj[0], prefix + '[]') + }; + } + if (typeof obj === 'object') { + const schema = { type: 'object', properties: {} }; + for (const [key, value] of Object.entries(obj)) { + schema.properties[key] = extractSchema(value, prefix ? `${prefix}.${key}` : key); + } + return schema; + } + return { type: typeof obj, example: String(obj).substring(0, 100) }; +} +// Run +const url = process.argv[2] || 'https://dutchie.com/embedded-menu/AZ-Deeply-Rooted'; +captureSchema(url).catch(console.error); diff --git a/backend/dist/scripts/crawl-all-dutchie.js b/backend/dist/scripts/crawl-all-dutchie.js new file mode 100644 index 00000000..96378479 --- /dev/null +++ b/backend/dist/scripts/crawl-all-dutchie.js @@ -0,0 +1,56 @@ +"use strict"; +/** + * Seed crawl: trigger dutchie crawls for all dispensaries with menu_type='dutchie' + * and a resolved platform_dispensary_id. This uses the AZ orchestrator endpoint logic. + * + * Usage (local): + * node dist/scripts/crawl-all-dutchie.js + * + * Requires: + * - DATABASE_URL/CRAWLSY_DATABASE_URL pointing to the consolidated DB + * - Dispensaries table populated with menu_type and platform_dispensary_id + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const connection_1 = require("../dutchie-az/db/connection"); +const dispensary_orchestrator_1 = require("../services/dispensary-orchestrator"); +async function main() { + const { rows } = await (0, connection_1.query)(` + SELECT id, name, slug, platform_dispensary_id + FROM dispensaries + WHERE menu_type = 'dutchie' + AND platform_dispensary_id IS NOT NULL + ORDER BY id + `); + if (!rows.length) { + console.log('No dutchie dispensaries with resolved platform_dispensary_id found.'); + process.exit(0); + } + console.log(`Found ${rows.length} dutchie dispensaries with resolved IDs. Triggering crawls...`); + let success = 0; + let failed = 0; + for (const row of rows) { + try { + console.log(`Crawling ${row.id} (${row.name})...`); + const result = await (0, dispensary_orchestrator_1.runDispensaryOrchestrator)(row.id); + const ok = result.status === 'success' || + result.status === 'sandbox_only' || + result.status === 'detection_only'; + if (ok) { + success++; + } + else { + failed++; + console.warn(`Crawl returned status ${result.status} for ${row.id} (${row.name})`); + } + } + catch (err) { + failed++; + console.error(`Failed crawl for ${row.id} (${row.name}): ${err.message}`); + } + } + console.log(`Completed. Success: ${success}, Failed: ${failed}`); +} +main().catch((err) => { + console.error('Fatal:', err); + process.exit(1); +}); diff --git a/backend/dist/scripts/crawl-five-sequential.js b/backend/dist/scripts/crawl-five-sequential.js new file mode 100644 index 00000000..f611f5be --- /dev/null +++ b/backend/dist/scripts/crawl-five-sequential.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const dispensary_orchestrator_1 = require("../services/dispensary-orchestrator"); +// Run 5 crawlers sequentially to avoid OOM +const dispensaryIds = [112, 81, 115, 140, 177]; +async function run() { + console.log('Starting 5 crawlers SEQUENTIALLY...'); + for (const id of dispensaryIds) { + console.log(`\n=== Starting crawler for dispensary ${id} ===`); + try { + const result = await (0, dispensary_orchestrator_1.runDispensaryOrchestrator)(id); + console.log(` Status: ${result.status}`); + console.log(` Summary: ${result.summary}`); + if (result.productsFound) { + console.log(` Products: ${result.productsFound} found, ${result.productsNew} new, ${result.productsUpdated} updated`); + } + } + catch (e) { + console.log(` ERROR: ${e.message}`); + } + } + console.log('\n=== All 5 crawlers complete ==='); +} +run().catch(e => console.log('Fatal:', e.message)); diff --git a/backend/dist/scripts/parallel-scrape.js b/backend/dist/scripts/parallel-scrape.js new file mode 100644 index 00000000..a13dff89 --- /dev/null +++ b/backend/dist/scripts/parallel-scrape.js @@ -0,0 +1,181 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const migrate_1 = require("../db/migrate"); +const proxy_1 = require("../services/proxy"); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +const FIREFOX_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'; +const NUM_WORKERS = parseInt(process.argv[2] || '15'); +const DISPENSARY_NAME = process.argv[3] || 'Deeply Rooted'; +const USE_PROXIES = process.argv[4] !== 'no-proxy'; +async function getStore(name) { + const result = await migrate_1.pool.query(`SELECT id, name, slug, dutchie_url FROM stores WHERE name ILIKE $1 LIMIT 1`, [`%${name}%`]); + return result.rows[0] || null; +} +async function getCategories(storeId) { + const result = await migrate_1.pool.query(`SELECT id, name, slug, dutchie_url as url FROM categories WHERE store_id = $1 AND scrape_enabled = true`, [storeId]); + return result.rows; +} +async function scrapeWithProxy(workerId, store, category) { + let browser = null; + let proxyId = null; + try { + // Get a proxy (if enabled) + let proxy = null; + if (USE_PROXIES) { + proxy = await (0, proxy_1.getActiveProxy)(); + if (proxy) { + proxyId = proxy.id; + console.log(`[Worker ${workerId}] Using proxy: ${proxy.protocol}://${proxy.host}:${proxy.port}`); + } + else { + console.log(`[Worker ${workerId}] No proxy available, using direct connection`); + } + } + else { + console.log(`[Worker ${workerId}] Direct connection (proxies disabled)`); + } + // Build browser args + const args = [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--disable-gpu', + '--window-size=1920,1080', + ]; + if (proxy) { + if (proxy.protocol === 'socks5' || proxy.protocol === 'socks') { + args.push(`--proxy-server=socks5://${proxy.host}:${proxy.port}`); + } + else { + args.push(`--proxy-server=${proxy.protocol}://${proxy.host}:${proxy.port}`); + } + } + browser = await puppeteer_extra_1.default.launch({ + headless: true, + args, + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, + }); + const page = await browser.newPage(); + await page.setUserAgent(FIREFOX_USER_AGENT); + await page.setViewport({ width: 1920, height: 1080 }); + // Handle proxy auth if needed + if (proxy?.username && proxy?.password) { + await page.authenticate({ + username: proxy.username, + password: proxy.password, + }); + } + console.log(`[Worker ${workerId}] Scraping category: ${category.name} (${category.url})`); + // Navigate to the category page + const response = await page.goto(category.url, { + waitUntil: 'networkidle2', + timeout: 60000, + }); + if (!response || !response.ok()) { + throw new Error(`Failed to load page: ${response?.status()}`); + } + // Wait for products to load + await page.waitForSelector('[data-testid="product-list-item"], a[href*="/product/"]', { + timeout: 30000, + }).catch(() => { + console.log(`[Worker ${workerId}] No products found on page`); + }); + // Extract products + const products = await page.evaluate(() => { + // Try data-testid first, then fall back to product links + const listItems = document.querySelectorAll('[data-testid="product-list-item"]'); + if (listItems.length > 0) + return listItems.length; + return document.querySelectorAll('a[href*="/product/"]').length; + }); + console.log(`[Worker ${workerId}] Found ${products} products in ${category.name}`); + await browser.close(); + return { success: true, products }; + } + catch (error) { + console.error(`[Worker ${workerId}] Error:`, error.message); + // Check for bot detection + if (proxyId && (0, proxy_1.isBotDetectionError)(error.message)) { + (0, proxy_1.putProxyInTimeout)(proxyId, error.message); + } + if (browser) { + await browser.close().catch(() => { }); + } + return { success: false, products: 0, error: error.message }; + } +} +async function worker(workerId, store, categories, categoryIndex) { + while (categoryIndex.current < categories.length) { + const idx = categoryIndex.current++; + const category = categories[idx]; + if (!category) + break; + console.log(`[Worker ${workerId}] Starting category ${idx + 1}/${categories.length}: ${category.name}`); + const result = await scrapeWithProxy(workerId, store, category); + if (result.success) { + console.log(`[Worker ${workerId}] Completed ${category.name}: ${result.products} products`); + } + else { + console.log(`[Worker ${workerId}] Failed ${category.name}: ${result.error}`); + } + // Small delay between requests + await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 3000)); + } + console.log(`[Worker ${workerId}] Finished all assigned work`); +} +async function main() { + console.log(`\n${'='.repeat(60)}`); + console.log(`Parallel Scraper - ${NUM_WORKERS} workers`); + console.log(`Target: ${DISPENSARY_NAME}`); + console.log(`User Agent: Firefox`); + console.log(`Proxies: ${USE_PROXIES ? 'Enabled' : 'Disabled'}`); + console.log(`${'='.repeat(60)}\n`); + // Find the store + const store = await getStore(DISPENSARY_NAME); + if (!store) { + console.error(`Store not found: ${DISPENSARY_NAME}`); + process.exit(1); + } + console.log(`Found store: ${store.name} (ID: ${store.id})`); + // Get categories + const categories = await getCategories(store.id); + if (categories.length === 0) { + console.error('No categories found for this store'); + process.exit(1); + } + console.log(`Found ${categories.length} categories to scrape`); + console.log(`Categories: ${categories.map(c => c.name).join(', ')}\n`); + // Check proxies + const proxyResult = await migrate_1.pool.query('SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE active = true) as active FROM proxies'); + console.log(`Proxies: ${proxyResult.rows[0].active} active / ${proxyResult.rows[0].total} total\n`); + // Shared index for work distribution + const categoryIndex = { current: 0 }; + // For a store with few categories, we'll run multiple passes + // Expand the work by duplicating categories for parallel workers + const expandedCategories = []; + const passes = Math.ceil(NUM_WORKERS / Math.max(categories.length, 1)); + for (let i = 0; i < passes; i++) { + expandedCategories.push(...categories); + } + console.log(`Running ${NUM_WORKERS} workers across ${expandedCategories.length} category scrapes\n`); + // Start workers + const workers = []; + for (let i = 0; i < NUM_WORKERS; i++) { + workers.push(worker(i + 1, store, expandedCategories, categoryIndex)); + // Stagger worker starts + await new Promise(resolve => setTimeout(resolve, 500)); + } + // Wait for all workers + await Promise.all(workers); + console.log(`\n${'='.repeat(60)}`); + console.log('All workers completed!'); + console.log(`${'='.repeat(60)}\n`); + await migrate_1.pool.end(); +} +main().catch(console.error); diff --git a/backend/dist/scripts/queue-dispensaries.js b/backend/dist/scripts/queue-dispensaries.js new file mode 100644 index 00000000..4dc7f5b8 --- /dev/null +++ b/backend/dist/scripts/queue-dispensaries.js @@ -0,0 +1,344 @@ +#!/usr/bin/env npx tsx +"use strict"; +/** + * Queue Dispensaries Script + * + * Orchestrates the multi-provider crawler system: + * 1. Queue dispensaries that need provider detection + * 2. Queue Dutchie dispensaries for production crawl + * 3. Queue sandbox dispensaries for learning crawls + * + * Usage: + * npx tsx src/scripts/queue-dispensaries.ts [--detection] [--production] [--sandbox] [--all] + * npx tsx src/scripts/queue-dispensaries.ts --dry-run + * npx tsx src/scripts/queue-dispensaries.ts --process # Process queued jobs + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const migrate_1 = require("../db/migrate"); +const crawler_jobs_1 = require("../services/crawler-jobs"); +// Parse command line args +const args = process.argv.slice(2); +const flags = { + detection: args.includes('--detection') || args.includes('--all'), + production: args.includes('--production') || args.includes('--all'), + sandbox: args.includes('--sandbox') || args.includes('--all'), + dryRun: args.includes('--dry-run'), + process: args.includes('--process'), + help: args.includes('--help') || args.includes('-h'), + limit: parseInt(args.find(a => a.startsWith('--limit='))?.split('=')[1] || '10'), +}; +// If no specific flags, default to all +if (!flags.detection && !flags.production && !flags.sandbox && !flags.process) { + flags.detection = true; + flags.production = true; + flags.sandbox = true; +} +async function showHelp() { + console.log(` +Queue Dispensaries - Multi-Provider Crawler Orchestration + +USAGE: + npx tsx src/scripts/queue-dispensaries.ts [OPTIONS] + +OPTIONS: + --detection Queue dispensaries that need provider detection + --production Queue Dutchie production crawls + --sandbox Queue sandbox/learning crawls + --all Queue all job types (default if no specific flag) + --process Process queued jobs instead of just queuing + --dry-run Show what would be queued without making changes + --limit=N Maximum dispensaries to queue per type (default: 10) + --help, -h Show this help message + +EXAMPLES: + # Queue all dispensaries for appropriate jobs + npx tsx src/scripts/queue-dispensaries.ts + + # Only queue detection jobs + npx tsx src/scripts/queue-dispensaries.ts --detection --limit=20 + + # Dry run to see what would be queued + npx tsx src/scripts/queue-dispensaries.ts --dry-run + + # Process sandbox jobs + npx tsx src/scripts/queue-dispensaries.ts --process +`); +} +async function queueDetectionJobs() { + console.log('\n📡 Queueing Detection Jobs...'); + // Find dispensaries that need provider detection: + // - menu_provider is null OR + // - menu_provider_confidence < 70 AND + // - crawler_status is idle (not already queued/running) + // - has a website URL + const query = ` + SELECT id, name, website, menu_url, menu_provider, menu_provider_confidence + FROM dispensaries + WHERE (website IS NOT NULL OR menu_url IS NOT NULL) + AND crawler_status = 'idle' + AND (menu_provider IS NULL OR menu_provider_confidence < 70) + ORDER BY + CASE WHEN menu_provider IS NULL THEN 0 ELSE 1 END, + menu_provider_confidence ASC + LIMIT $1 + `; + const result = await migrate_1.pool.query(query, [flags.limit]); + if (flags.dryRun) { + console.log(` Would queue ${result.rows.length} dispensaries for detection:`); + for (const row of result.rows) { + console.log(` - [${row.id}] ${row.name} (current: ${row.menu_provider || 'unknown'}, confidence: ${row.menu_provider_confidence}%)`); + } + return result.rows.length; + } + let queued = 0; + for (const dispensary of result.rows) { + try { + // Update status to queued + await migrate_1.pool.query(`UPDATE dispensaries SET crawler_status = 'queued_detection', updated_at = NOW() WHERE id = $1`, [dispensary.id]); + // Create sandbox job for detection + await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, job_type, status, priority) + VALUES ($1, 'detection', 'pending', 10)`, [dispensary.id]); + console.log(` ✓ Queued detection: [${dispensary.id}] ${dispensary.name}`); + queued++; + } + catch (error) { + console.error(` ✗ Failed to queue [${dispensary.id}]: ${error.message}`); + } + } + return queued; +} +async function queueProductionCrawls() { + console.log('\n🏭 Queueing Production Dutchie Crawls...'); + // Find Dutchie dispensaries ready for production crawl: + // - menu_provider = 'dutchie' + // - crawler_mode = 'production' + // - crawler_status is idle + // - last_menu_scrape is old or null + const query = ` + SELECT d.id, d.name, d.last_menu_scrape, d.menu_url + FROM dispensaries d + WHERE d.menu_provider = 'dutchie' + AND d.crawler_mode = 'production' + AND d.crawler_status = 'idle' + AND (d.last_menu_scrape IS NULL OR d.last_menu_scrape < NOW() - INTERVAL '4 hours') + ORDER BY + CASE WHEN d.last_menu_scrape IS NULL THEN 0 ELSE 1 END, + d.last_menu_scrape ASC + LIMIT $1 + `; + const result = await migrate_1.pool.query(query, [flags.limit]); + if (flags.dryRun) { + console.log(` Would queue ${result.rows.length} Dutchie dispensaries for production crawl:`); + for (const row of result.rows) { + const lastScrape = row.last_menu_scrape ? new Date(row.last_menu_scrape).toISOString() : 'never'; + console.log(` - [${row.id}] ${row.name} (last scrape: ${lastScrape})`); + } + return result.rows.length; + } + let queued = 0; + for (const dispensary of result.rows) { + try { + // Update status to queued + await migrate_1.pool.query(`UPDATE dispensaries SET crawler_status = 'queued_crawl', updated_at = NOW() WHERE id = $1`, [dispensary.id]); + // Create crawl job in the main crawl_jobs table (production queue) + await migrate_1.pool.query(`INSERT INTO crawl_jobs (store_id, job_type, trigger_type, status, priority, metadata) + SELECT s.id, 'full_crawl', 'scheduled', 'pending', 50, + jsonb_build_object('dispensary_id', $1, 'source', 'queue-dispensaries') + FROM stores s + JOIN dispensaries d ON (d.menu_url = s.dutchie_url OR d.name ILIKE '%' || s.name || '%') + WHERE d.id = $1 + LIMIT 1`, [dispensary.id]); + console.log(` ✓ Queued production crawl: [${dispensary.id}] ${dispensary.name}`); + queued++; + } + catch (error) { + console.error(` ✗ Failed to queue [${dispensary.id}]: ${error.message}`); + } + } + return queued; +} +async function queueSandboxCrawls() { + console.log('\n🧪 Queueing Sandbox Crawls...'); + // Find sandbox dispensaries needing crawls: + // - crawler_mode = 'sandbox' + // - crawler_status in (idle, error_needs_review) + // - No recent sandbox job + const query = ` + SELECT d.id, d.name, d.menu_provider, d.crawler_status, d.website + FROM dispensaries d + WHERE d.crawler_mode = 'sandbox' + AND d.crawler_status IN ('idle', 'error_needs_review') + AND (d.website IS NOT NULL OR d.menu_url IS NOT NULL) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_crawl_jobs sj + WHERE sj.dispensary_id = d.id + AND sj.status IN ('pending', 'running') + ) + ORDER BY d.updated_at ASC + LIMIT $1 + `; + const result = await migrate_1.pool.query(query, [flags.limit]); + if (flags.dryRun) { + console.log(` Would queue ${result.rows.length} dispensaries for sandbox crawl:`); + for (const row of result.rows) { + console.log(` - [${row.id}] ${row.name} (provider: ${row.menu_provider || 'unknown'}, status: ${row.crawler_status})`); + } + return result.rows.length; + } + let queued = 0; + for (const dispensary of result.rows) { + try { + // Update status + await migrate_1.pool.query(`UPDATE dispensaries SET crawler_status = 'queued_crawl', updated_at = NOW() WHERE id = $1`, [dispensary.id]); + // Create sandbox job + await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, job_type, status, priority) + VALUES ($1, 'deep_crawl', 'pending', 5)`, [dispensary.id]); + console.log(` ✓ Queued sandbox crawl: [${dispensary.id}] ${dispensary.name}`); + queued++; + } + catch (error) { + console.error(` ✗ Failed to queue [${dispensary.id}]: ${error.message}`); + } + } + return queued; +} +async function processJobs() { + console.log('\n⚙️ Processing Queued Jobs...\n'); + // Process sandbox jobs (detection + sandbox crawls) + const sandboxJobs = await migrate_1.pool.query(`SELECT * FROM sandbox_crawl_jobs + WHERE status = 'pending' + ORDER BY priority DESC, scheduled_at ASC + LIMIT $1`, [flags.limit]); + console.log(`Found ${sandboxJobs.rows.length} pending sandbox jobs\n`); + for (const job of sandboxJobs.rows) { + console.log(`Processing job ${job.id} (${job.job_type}) for dispensary ${job.dispensary_id}...`); + try { + // Mark as running + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs SET status = 'running', started_at = NOW() WHERE id = $1`, [job.id]); + let result; + if (job.job_type === 'detection') { + result = await (0, crawler_jobs_1.runDetectMenuProviderJob)(job.dispensary_id); + } + else { + result = await (0, crawler_jobs_1.runSandboxCrawlJob)(job.dispensary_id, job.sandbox_id); + } + // Update job status + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs + SET status = $1, completed_at = NOW(), result_summary = $2, error_message = $3 + WHERE id = $4`, [ + result.success ? 'completed' : 'failed', + JSON.stringify(result.data || {}), + result.success ? null : result.message, + job.id, + ]); + console.log(` ${result.success ? '✓' : '✗'} ${result.message}\n`); + } + catch (error) { + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs SET status = 'failed', error_message = $1 WHERE id = $2`, [error.message, job.id]); + console.log(` ✗ Error: ${error.message}\n`); + } + } +} +async function showStats() { + console.log('\n📊 Current Stats:'); + // Dispensary stats + const stats = await migrate_1.pool.query(` + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE menu_provider IS NULL) as no_provider, + COUNT(*) FILTER (WHERE menu_provider = 'dutchie') as dutchie, + COUNT(*) FILTER (WHERE menu_provider NOT IN ('dutchie', 'unknown') AND menu_provider IS NOT NULL) as other_providers, + COUNT(*) FILTER (WHERE menu_provider = 'unknown') as unknown, + COUNT(*) FILTER (WHERE crawler_mode = 'production') as production_mode, + COUNT(*) FILTER (WHERE crawler_mode = 'sandbox') as sandbox_mode, + COUNT(*) FILTER (WHERE crawler_status = 'idle') as idle, + COUNT(*) FILTER (WHERE crawler_status LIKE 'queued%') as queued, + COUNT(*) FILTER (WHERE crawler_status = 'running') as running, + COUNT(*) FILTER (WHERE crawler_status = 'ok') as ok, + COUNT(*) FILTER (WHERE crawler_status = 'error_needs_review') as needs_review + FROM dispensaries + `); + const s = stats.rows[0]; + console.log(` + Dispensaries: ${s.total} + - No provider detected: ${s.no_provider} + - Dutchie: ${s.dutchie} + - Other providers: ${s.other_providers} + - Unknown: ${s.unknown} + + Crawler Mode: + - Production: ${s.production_mode} + - Sandbox: ${s.sandbox_mode} + + Status: + - Idle: ${s.idle} + - Queued: ${s.queued} + - Running: ${s.running} + - OK: ${s.ok} + - Needs Review: ${s.needs_review} +`); + // Job stats + const jobStats = await migrate_1.pool.query(` + SELECT + COUNT(*) FILTER (WHERE status = 'pending') as pending, + COUNT(*) FILTER (WHERE status = 'running') as running, + COUNT(*) FILTER (WHERE status = 'completed') as completed, + COUNT(*) FILTER (WHERE status = 'failed') as failed + FROM sandbox_crawl_jobs + `); + const j = jobStats.rows[0]; + console.log(` Sandbox Jobs: + - Pending: ${j.pending} + - Running: ${j.running} + - Completed: ${j.completed} + - Failed: ${j.failed} +`); +} +async function main() { + if (flags.help) { + await showHelp(); + process.exit(0); + } + console.log('═══════════════════════════════════════════════════════'); + console.log(' Multi-Provider Crawler Queue Manager'); + console.log('═══════════════════════════════════════════════════════'); + if (flags.dryRun) { + console.log('\n🔍 DRY RUN MODE - No changes will be made\n'); + } + try { + // Show current stats first + await showStats(); + if (flags.process) { + // Process mode - run jobs instead of queuing + await processJobs(); + } + else { + // Queuing mode + let totalQueued = 0; + if (flags.detection) { + totalQueued += await queueDetectionJobs(); + } + if (flags.production) { + totalQueued += await queueProductionCrawls(); + } + if (flags.sandbox) { + totalQueued += await queueSandboxCrawls(); + } + console.log('\n═══════════════════════════════════════════════════════'); + console.log(` Total dispensaries queued: ${totalQueued}`); + console.log('═══════════════════════════════════════════════════════\n'); + } + // Show updated stats + if (!flags.dryRun) { + await showStats(); + } + } + catch (error) { + console.error('Fatal error:', error); + process.exit(1); + } + finally { + await migrate_1.pool.end(); + } +} +main(); diff --git a/backend/dist/scripts/queue-intelligence.js b/backend/dist/scripts/queue-intelligence.js new file mode 100644 index 00000000..7a07f115 --- /dev/null +++ b/backend/dist/scripts/queue-intelligence.js @@ -0,0 +1,473 @@ +#!/usr/bin/env npx tsx +"use strict"; +/** + * Queue Intelligence Script + * + * Orchestrates the multi-category intelligence crawler system: + * 1. Queue dispensaries that need provider detection (all 4 categories) + * 2. Queue per-category production crawls (Dutchie products only for now) + * 3. Queue per-category sandbox crawls (all providers) + * + * Each category (product, specials, brand, metadata) is handled independently. + * A failure in one category does NOT affect other categories. + * + * Usage: + * npx tsx src/scripts/queue-intelligence.ts [--detection] [--production] [--sandbox] [--all] + * npx tsx src/scripts/queue-intelligence.ts --category=product --sandbox + * npx tsx src/scripts/queue-intelligence.ts --process --category=product + * npx tsx src/scripts/queue-intelligence.ts --dry-run + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const migrate_1 = require("../db/migrate"); +const intelligence_detector_1 = require("../services/intelligence-detector"); +const category_crawler_jobs_1 = require("../services/category-crawler-jobs"); +// Parse command line args +const args = process.argv.slice(2); +const flags = { + detection: args.includes('--detection') || args.includes('--all'), + production: args.includes('--production') || args.includes('--all'), + sandbox: args.includes('--sandbox') || args.includes('--all'), + dryRun: args.includes('--dry-run'), + process: args.includes('--process'), + help: args.includes('--help') || args.includes('-h'), + limit: parseInt(args.find(a => a.startsWith('--limit='))?.split('=')[1] || '10'), + category: args.find(a => a.startsWith('--category='))?.split('=')[1], + dispensary: parseInt(args.find(a => a.startsWith('--dispensary='))?.split('=')[1] || '0'), +}; +// If no specific flags, default to all +if (!flags.detection && !flags.production && !flags.sandbox && !flags.process) { + flags.detection = true; + flags.production = true; + flags.sandbox = true; +} +const CATEGORIES = ['product', 'specials', 'brand', 'metadata']; +async function showHelp() { + console.log(` +Queue Intelligence - Multi-Category Crawler Orchestration + +USAGE: + npx tsx src/scripts/queue-intelligence.ts [OPTIONS] + +OPTIONS: + --detection Queue dispensaries that need multi-category detection + --production Queue per-category production crawls + --sandbox Queue per-category sandbox crawls + --all Queue all job types (default if no specific flag) + --process Process queued jobs instead of just queuing + --category=CATEGORY Filter to specific category (product|specials|brand|metadata) + --dispensary=ID Process only a specific dispensary + --dry-run Show what would be queued without making changes + --limit=N Maximum dispensaries to queue per type (default: 10) + --help, -h Show this help message + +CATEGORIES: + product - Product/menu data (Dutchie=production, others=sandbox) + specials - Deals and specials (all sandbox for now) + brand - Brand intelligence (all sandbox for now) + metadata - Categories/taxonomy (all sandbox for now) + +EXAMPLES: + # Queue all dispensaries for appropriate jobs + npx tsx src/scripts/queue-intelligence.ts + + # Only queue product detection jobs + npx tsx src/scripts/queue-intelligence.ts --detection --category=product + + # Process sandbox jobs for specials category + npx tsx src/scripts/queue-intelligence.ts --process --category=specials --limit=5 + + # Run full detection for a specific dispensary + npx tsx src/scripts/queue-intelligence.ts --process --detection --dispensary=123 + + # Dry run to see what would be queued + npx tsx src/scripts/queue-intelligence.ts --dry-run +`); +} +async function queueMultiCategoryDetection() { + console.log('\n📡 Queueing Multi-Category Detection Jobs...'); + // Find dispensaries that need provider detection for any category: + // - Any *_provider is null OR + // - Any *_confidence < 70 + // - has a website URL + const query = ` + SELECT id, name, website, menu_url, + product_provider, product_confidence, product_crawler_mode, + specials_provider, specials_confidence, specials_crawler_mode, + brand_provider, brand_confidence, brand_crawler_mode, + metadata_provider, metadata_confidence, metadata_crawler_mode + FROM dispensaries + WHERE (website IS NOT NULL OR menu_url IS NOT NULL) + AND ( + product_provider IS NULL OR product_confidence < 70 OR + specials_provider IS NULL OR specials_confidence < 70 OR + brand_provider IS NULL OR brand_confidence < 70 OR + metadata_provider IS NULL OR metadata_confidence < 70 + ) + ORDER BY + CASE WHEN product_provider IS NULL THEN 0 ELSE 1 END, + product_confidence ASC + LIMIT $1 + `; + const result = await migrate_1.pool.query(query, [flags.limit]); + if (flags.dryRun) { + console.log(` Would queue ${result.rows.length} dispensaries for multi-category detection:`); + for (const row of result.rows) { + const needsDetection = []; + if (!row.product_provider || row.product_confidence < 70) + needsDetection.push('product'); + if (!row.specials_provider || row.specials_confidence < 70) + needsDetection.push('specials'); + if (!row.brand_provider || row.brand_confidence < 70) + needsDetection.push('brand'); + if (!row.metadata_provider || row.metadata_confidence < 70) + needsDetection.push('metadata'); + console.log(` - [${row.id}] ${row.name} (needs: ${needsDetection.join(', ')})`); + } + return result.rows.length; + } + let queued = 0; + for (const dispensary of result.rows) { + try { + // Create detection jobs for each category that needs it + for (const category of CATEGORIES) { + const provider = dispensary[`${category}_provider`]; + const confidence = dispensary[`${category}_confidence`]; + if (!provider || confidence < 70) { + await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, category, job_type, status, priority) + VALUES ($1, $2, 'detection', 'pending', 10) + ON CONFLICT DO NOTHING`, [dispensary.id, category]); + } + } + console.log(` ✓ Queued detection: [${dispensary.id}] ${dispensary.name}`); + queued++; + } + catch (error) { + console.error(` ✗ Failed to queue [${dispensary.id}]: ${error.message}`); + } + } + return queued; +} +async function queueCategoryProductionCrawls(category) { + const categories = category ? [category] : CATEGORIES; + let totalQueued = 0; + for (const cat of categories) { + console.log(`\n🏭 Queueing Production ${cat.toUpperCase()} Crawls...`); + // For now, only products have production-ready crawlers (Dutchie only) + if (cat !== 'product') { + console.log(` ⏭️ No production crawler for ${cat} yet - skipping`); + continue; + } + // Find dispensaries ready for production crawl + const query = ` + SELECT id, name, ${cat}_provider as provider, last_${cat}_scan_at as last_scan + FROM dispensaries + WHERE ${cat}_provider = 'dutchie' + AND ${cat}_crawler_mode = 'production' + AND ${cat}_confidence >= 70 + AND (last_${cat}_scan_at IS NULL OR last_${cat}_scan_at < NOW() - INTERVAL '4 hours') + ORDER BY + CASE WHEN last_${cat}_scan_at IS NULL THEN 0 ELSE 1 END, + last_${cat}_scan_at ASC + LIMIT $1 + `; + const result = await migrate_1.pool.query(query, [flags.limit]); + if (flags.dryRun) { + console.log(` Would queue ${result.rows.length} dispensaries for ${cat} production crawl:`); + for (const row of result.rows) { + const lastScan = row.last_scan ? new Date(row.last_scan).toISOString() : 'never'; + console.log(` - [${row.id}] ${row.name} (provider: ${row.provider}, last: ${lastScan})`); + } + totalQueued += result.rows.length; + continue; + } + for (const dispensary of result.rows) { + try { + // For products, use the existing crawl_jobs table for production + await migrate_1.pool.query(`INSERT INTO crawl_jobs (store_id, job_type, trigger_type, status, priority, metadata) + SELECT s.id, 'full_crawl', 'scheduled', 'pending', 50, + jsonb_build_object('dispensary_id', $1, 'category', $2, 'source', 'queue-intelligence') + FROM stores s + JOIN dispensaries d ON (d.menu_url = s.dutchie_url OR d.name ILIKE '%' || s.name || '%') + WHERE d.id = $1 + LIMIT 1`, [dispensary.id, cat]); + console.log(` ✓ Queued ${cat} production: [${dispensary.id}] ${dispensary.name}`); + totalQueued++; + } + catch (error) { + console.error(` ✗ Failed to queue [${dispensary.id}]: ${error.message}`); + } + } + } + return totalQueued; +} +async function queueCategorySandboxCrawls(category) { + const categories = category ? [category] : CATEGORIES; + let totalQueued = 0; + for (const cat of categories) { + console.log(`\n🧪 Queueing Sandbox ${cat.toUpperCase()} Crawls...`); + // Find dispensaries in sandbox mode for this category + const query = ` + SELECT d.id, d.name, d.${cat}_provider as provider, d.${cat}_confidence as confidence, + d.website, d.menu_url + FROM dispensaries d + WHERE d.${cat}_crawler_mode = 'sandbox' + AND d.${cat}_provider IS NOT NULL + AND (d.website IS NOT NULL OR d.menu_url IS NOT NULL) + AND NOT EXISTS ( + SELECT 1 FROM sandbox_crawl_jobs sj + WHERE sj.dispensary_id = d.id + AND sj.category = $1 + AND sj.status IN ('pending', 'running') + ) + ORDER BY d.${cat}_confidence DESC, d.updated_at ASC + LIMIT $2 + `; + const result = await migrate_1.pool.query(query, [cat, flags.limit]); + if (flags.dryRun) { + console.log(` Would queue ${result.rows.length} dispensaries for ${cat} sandbox crawl:`); + for (const row of result.rows) { + console.log(` - [${row.id}] ${row.name} (provider: ${row.provider}, confidence: ${row.confidence}%)`); + } + totalQueued += result.rows.length; + continue; + } + for (const dispensary of result.rows) { + try { + // Create sandbox entry if needed + const sandboxResult = await migrate_1.pool.query(`INSERT INTO crawler_sandboxes (dispensary_id, category, suspected_menu_provider, mode, status) + VALUES ($1, $2, $3, 'template_learning', 'pending') + ON CONFLICT (dispensary_id, category) WHERE status NOT IN ('moved_to_production', 'failed') + DO UPDATE SET updated_at = NOW() + RETURNING id`, [dispensary.id, cat, dispensary.provider]); + const sandboxId = sandboxResult.rows[0]?.id; + // Create sandbox job + await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, sandbox_id, category, job_type, status, priority) + VALUES ($1, $2, $3, 'crawl', 'pending', 5)`, [dispensary.id, sandboxId, cat]); + console.log(` ✓ Queued ${cat} sandbox: [${dispensary.id}] ${dispensary.name} (${dispensary.provider})`); + totalQueued++; + } + catch (error) { + console.error(` ✗ Failed to queue [${dispensary.id}]: ${error.message}`); + } + } + } + return totalQueued; +} +async function processDetectionJobs() { + console.log('\n🔍 Processing Detection Jobs...'); + // Get pending detection jobs + const jobs = await migrate_1.pool.query(`SELECT DISTINCT dispensary_id + FROM sandbox_crawl_jobs + WHERE job_type = 'detection' AND status = 'pending' + ${flags.category ? `AND category = $2` : ''} + ${flags.dispensary ? `AND dispensary_id = $${flags.category ? '3' : '2'}` : ''} + LIMIT $1`, flags.category + ? (flags.dispensary ? [flags.limit, flags.category, flags.dispensary] : [flags.limit, flags.category]) + : (flags.dispensary ? [flags.limit, flags.dispensary] : [flags.limit])); + for (const job of jobs.rows) { + console.log(`\nProcessing detection for dispensary ${job.dispensary_id}...`); + try { + // Get dispensary info + const dispResult = await migrate_1.pool.query('SELECT id, name, website, menu_url FROM dispensaries WHERE id = $1', [job.dispensary_id]); + const dispensary = dispResult.rows[0]; + if (!dispensary) { + console.log(` ✗ Dispensary not found`); + continue; + } + const websiteUrl = dispensary.website || dispensary.menu_url; + if (!websiteUrl) { + console.log(` ✗ No website URL`); + continue; + } + // Mark jobs as running + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs SET status = 'running', started_at = NOW() + WHERE dispensary_id = $1 AND job_type = 'detection' AND status = 'pending'`, [job.dispensary_id]); + // Run multi-category detection + console.log(` Detecting providers for ${dispensary.name}...`); + const detection = await (0, intelligence_detector_1.detectMultiCategoryProviders)(websiteUrl, { timeout: 45000 }); + // Update all categories + await (0, intelligence_detector_1.updateAllCategoryProviders)(job.dispensary_id, detection); + // Mark jobs as completed + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs SET status = 'completed', completed_at = NOW(), + result_summary = $1 + WHERE dispensary_id = $2 AND job_type = 'detection' AND status = 'running'`, [JSON.stringify({ + product: { provider: detection.product.provider, confidence: detection.product.confidence }, + specials: { provider: detection.specials.provider, confidence: detection.specials.confidence }, + brand: { provider: detection.brand.provider, confidence: detection.brand.confidence }, + metadata: { provider: detection.metadata.provider, confidence: detection.metadata.confidence }, + }), job.dispensary_id]); + console.log(` ✓ Detection complete:`); + console.log(` Product: ${detection.product.provider} (${detection.product.confidence}%) -> ${detection.product.mode}`); + console.log(` Specials: ${detection.specials.provider} (${detection.specials.confidence}%) -> ${detection.specials.mode}`); + console.log(` Brand: ${detection.brand.provider} (${detection.brand.confidence}%) -> ${detection.brand.mode}`); + console.log(` Metadata: ${detection.metadata.provider} (${detection.metadata.confidence}%) -> ${detection.metadata.mode}`); + } + catch (error) { + console.log(` ✗ Error: ${error.message}`); + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs SET status = 'failed', error_message = $1 + WHERE dispensary_id = $2 AND job_type = 'detection' AND status = 'running'`, [error.message, job.dispensary_id]); + } + } +} +async function processCrawlJobs() { + const categories = flags.category ? [flags.category] : CATEGORIES; + for (const cat of categories) { + console.log(`\n⚙️ Processing ${cat.toUpperCase()} Crawl Jobs...\n`); + // Process sandbox jobs for this category + if (flags.sandbox || !flags.production) { + await (0, category_crawler_jobs_1.processCategorySandboxJobs)(cat, flags.limit); + } + // Process production jobs for this category + if (flags.production && cat === 'product') { + // Get pending production crawls + const prodJobs = await migrate_1.pool.query(`SELECT d.id + FROM dispensaries d + WHERE d.product_provider = 'dutchie' + AND d.product_crawler_mode = 'production' + AND d.product_confidence >= 70 + ${flags.dispensary ? 'AND d.id = $2' : ''} + LIMIT $1`, flags.dispensary ? [flags.limit, flags.dispensary] : [flags.limit]); + for (const job of prodJobs.rows) { + console.log(`Processing production ${cat} crawl for dispensary ${job.id}...`); + const result = await (0, category_crawler_jobs_1.runCrawlProductsJob)(job.id); + console.log(` ${result.success ? '✓' : '✗'} ${result.message}`); + } + } + } +} +async function processSpecificDispensary() { + if (!flags.dispensary) + return; + console.log(`\n🎯 Processing Dispensary ${flags.dispensary}...\n`); + const dispResult = await migrate_1.pool.query('SELECT * FROM dispensaries WHERE id = $1', [flags.dispensary]); + if (dispResult.rows.length === 0) { + console.log('Dispensary not found'); + return; + } + const dispensary = dispResult.rows[0]; + console.log(`Name: ${dispensary.name}`); + console.log(`Website: ${dispensary.website || dispensary.menu_url || 'none'}`); + console.log(''); + if (flags.detection) { + console.log('Running multi-category detection...'); + const websiteUrl = dispensary.website || dispensary.menu_url; + if (websiteUrl) { + const detection = await (0, intelligence_detector_1.detectMultiCategoryProviders)(websiteUrl); + await (0, intelligence_detector_1.updateAllCategoryProviders)(flags.dispensary, detection); + console.log('Detection results:'); + console.log(` Product: ${detection.product.provider} (${detection.product.confidence}%) -> ${detection.product.mode}`); + console.log(` Specials: ${detection.specials.provider} (${detection.specials.confidence}%) -> ${detection.specials.mode}`); + console.log(` Brand: ${detection.brand.provider} (${detection.brand.confidence}%) -> ${detection.brand.mode}`); + console.log(` Metadata: ${detection.metadata.provider} (${detection.metadata.confidence}%) -> ${detection.metadata.mode}`); + } + } + if (flags.production) { + console.log('\nRunning production crawls...'); + const results = await (0, category_crawler_jobs_1.runAllCategoryProductionCrawls)(flags.dispensary); + console.log(` ${results.summary}`); + } + if (flags.sandbox) { + console.log('\nRunning sandbox crawls...'); + const results = await (0, category_crawler_jobs_1.runAllCategorySandboxCrawls)(flags.dispensary); + console.log(` ${results.summary}`); + } +} +async function showStats() { + console.log('\n📊 Multi-Category Intelligence Stats:'); + // Per-category stats + for (const cat of CATEGORIES) { + const stats = await migrate_1.pool.query(` + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE ${cat}_provider IS NULL) as no_provider, + COUNT(*) FILTER (WHERE ${cat}_provider = 'dutchie') as dutchie, + COUNT(*) FILTER (WHERE ${cat}_provider = 'treez') as treez, + COUNT(*) FILTER (WHERE ${cat}_provider NOT IN ('dutchie', 'treez', 'unknown') AND ${cat}_provider IS NOT NULL) as other, + COUNT(*) FILTER (WHERE ${cat}_provider = 'unknown') as unknown, + COUNT(*) FILTER (WHERE ${cat}_crawler_mode = 'production') as production, + COUNT(*) FILTER (WHERE ${cat}_crawler_mode = 'sandbox') as sandbox, + AVG(${cat}_confidence) as avg_confidence + FROM dispensaries + `); + const s = stats.rows[0]; + console.log(` + ${cat.toUpperCase()}: + Providers: Dutchie=${s.dutchie}, Treez=${s.treez}, Other=${s.other}, Unknown=${s.unknown}, None=${s.no_provider} + Modes: Production=${s.production}, Sandbox=${s.sandbox} + Avg Confidence: ${Math.round(s.avg_confidence || 0)}%`); + } + // Job stats per category + console.log('\n Sandbox Jobs by Category:'); + const jobStats = await migrate_1.pool.query(` + SELECT + category, + COUNT(*) FILTER (WHERE status = 'pending') as pending, + COUNT(*) FILTER (WHERE status = 'running') as running, + COUNT(*) FILTER (WHERE status = 'completed') as completed, + COUNT(*) FILTER (WHERE status = 'failed') as failed + FROM sandbox_crawl_jobs + GROUP BY category + ORDER BY category + `); + for (const row of jobStats.rows) { + console.log(` ${row.category}: pending=${row.pending}, running=${row.running}, completed=${row.completed}, failed=${row.failed}`); + } +} +async function main() { + if (flags.help) { + await showHelp(); + process.exit(0); + } + console.log('═══════════════════════════════════════════════════════'); + console.log(' Multi-Category Intelligence Queue Manager'); + console.log('═══════════════════════════════════════════════════════'); + if (flags.dryRun) { + console.log('\n🔍 DRY RUN MODE - No changes will be made\n'); + } + if (flags.category) { + console.log(`\n📌 Filtering to category: ${flags.category}\n`); + } + try { + // Show current stats first + await showStats(); + // If specific dispensary specified, process it directly + if (flags.dispensary && flags.process) { + await processSpecificDispensary(); + } + else if (flags.process) { + // Process mode - run jobs + if (flags.detection) { + await processDetectionJobs(); + } + await processCrawlJobs(); + } + else { + // Queuing mode + let totalQueued = 0; + if (flags.detection) { + totalQueued += await queueMultiCategoryDetection(); + } + if (flags.production) { + totalQueued += await queueCategoryProductionCrawls(flags.category); + } + if (flags.sandbox) { + totalQueued += await queueCategorySandboxCrawls(flags.category); + } + console.log('\n═══════════════════════════════════════════════════════'); + console.log(` Total queued: ${totalQueued}`); + console.log('═══════════════════════════════════════════════════════\n'); + } + // Show updated stats + if (!flags.dryRun) { + await showStats(); + } + } + catch (error) { + console.error('Fatal error:', error); + process.exit(1); + } + finally { + await migrate_1.pool.end(); + } +} +main(); diff --git a/backend/dist/scripts/run-dutchie-scrape.js b/backend/dist/scripts/run-dutchie-scrape.js new file mode 100644 index 00000000..c2c8ca98 --- /dev/null +++ b/backend/dist/scripts/run-dutchie-scrape.js @@ -0,0 +1,125 @@ +"use strict"; +/** + * Run Dutchie GraphQL Scrape + * + * This script demonstrates the full pipeline: + * 1. Puppeteer navigates to Dutchie menu + * 2. GraphQL responses are intercepted + * 3. Products are normalized to our schema + * 4. Products are upserted to database + * 5. Derived views (brands, categories, specials) are automatically updated + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const pg_1 = require("pg"); +const dutchie_graphql_1 = require("../scrapers/dutchie-graphql"); +const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://dutchie:dutchie_local_pass@localhost:54320/dutchie_menus'; +async function main() { + const pool = new pg_1.Pool({ connectionString: DATABASE_URL }); + try { + console.log('='.repeat(80)); + console.log('DUTCHIE GRAPHQL SCRAPER - FULL PIPELINE TEST'); + console.log('='.repeat(80)); + console.log(`Database: ${DATABASE_URL.replace(/:[^:@]+@/, ':***@')}`); + // Configuration + const storeId = 1; // Deeply Rooted + const menuUrl = 'https://dutchie.com/embedded-menu/AZ-Deeply-Rooted'; + console.log(`\nStore ID: ${storeId}`); + console.log(`Menu URL: ${menuUrl}`); + console.log('\n' + '-'.repeat(80)); + // Run the scrape + console.log('\n🚀 Starting scrape...\n'); + const result = await (0, dutchie_graphql_1.scrapeDutchieMenu)(pool, storeId, menuUrl); + console.log('\n' + '-'.repeat(80)); + console.log('📊 SCRAPE RESULTS:'); + console.log('-'.repeat(80)); + console.log(` Success: ${result.success}`); + console.log(` Products Found: ${result.productsFound}`); + console.log(` Inserted: ${result.inserted}`); + console.log(` Updated: ${result.updated}`); + if (result.error) { + console.log(` Error: ${result.error}`); + } + // Query derived views to show the result + if (result.success) { + console.log('\n' + '-'.repeat(80)); + console.log('📈 DERIVED DATA (from products table):'); + console.log('-'.repeat(80)); + // Brands + const brandsResult = await pool.query(` + SELECT brand_name, product_count, min_price, max_price + FROM derived_brands + WHERE store_id = $1 + ORDER BY product_count DESC + LIMIT 5 + `, [storeId]); + console.log('\nTop 5 Brands:'); + brandsResult.rows.forEach(row => { + console.log(` - ${row.brand_name}: ${row.product_count} products ($${row.min_price} - $${row.max_price})`); + }); + // Specials + const specialsResult = await pool.query(` + SELECT name, brand, rec_price, rec_special_price, discount_percent + FROM current_specials + WHERE store_id = $1 + LIMIT 5 + `, [storeId]); + console.log('\nTop 5 Specials:'); + if (specialsResult.rows.length === 0) { + console.log(' (No specials found - is_on_special may not be populated yet)'); + } + else { + specialsResult.rows.forEach(row => { + console.log(` - ${row.name} (${row.brand}): $${row.rec_price} → $${row.rec_special_price} (${row.discount_percent}% off)`); + }); + } + // Categories + const categoriesResult = await pool.query(` + SELECT category_name, product_count + FROM derived_categories + WHERE store_id = $1 + ORDER BY product_count DESC + LIMIT 5 + `, [storeId]); + console.log('\nTop 5 Categories:'); + if (categoriesResult.rows.length === 0) { + console.log(' (No categories found - subcategory may not be populated yet)'); + } + else { + categoriesResult.rows.forEach(row => { + console.log(` - ${row.category_name}: ${row.product_count} products`); + }); + } + // Sample product + const sampleResult = await pool.query(` + SELECT name, brand, subcategory, rec_price, rec_special_price, is_on_special, thc_percentage, status + FROM products + WHERE store_id = $1 AND subcategory IS NOT NULL + ORDER BY updated_at DESC + LIMIT 1 + `, [storeId]); + if (sampleResult.rows.length > 0) { + const sample = sampleResult.rows[0]; + console.log('\nSample Product (with new fields):'); + console.log(` Name: ${sample.name}`); + console.log(` Brand: ${sample.brand}`); + console.log(` Category: ${sample.subcategory}`); + console.log(` Price: $${sample.rec_price}`); + console.log(` Sale Price: ${sample.rec_special_price ? `$${sample.rec_special_price}` : 'N/A'}`); + console.log(` On Special: ${sample.is_on_special}`); + console.log(` THC: ${sample.thc_percentage}%`); + console.log(` Status: ${sample.status}`); + } + } + console.log('\n' + '='.repeat(80)); + console.log('✅ SCRAPE COMPLETE'); + console.log('='.repeat(80)); + } + catch (error) { + console.error('\n❌ Error:', error.message); + throw error; + } + finally { + await pool.end(); + } +} +main().catch(console.error); diff --git a/backend/dist/scripts/scrape-all-active.js b/backend/dist/scripts/scrape-all-active.js new file mode 100644 index 00000000..fb55b0d6 --- /dev/null +++ b/backend/dist/scripts/scrape-all-active.js @@ -0,0 +1,279 @@ +"use strict"; +/** + * Scrape ALL active products via direct GraphQL pagination + * This is more reliable than category navigation + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +const pg_1 = require("pg"); +const dutchie_graphql_1 = require("../scrapers/dutchie-graphql"); +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://dutchie:dutchie_local_pass@localhost:54320/dutchie_menus'; +const GRAPHQL_HASH = 'ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0'; +async function scrapeAllProducts(menuUrl, storeId) { + const pool = new pg_1.Pool({ connectionString: DATABASE_URL }); + const browser = await puppeteer_extra_1.default.launch({ + headless: 'new', + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + try { + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36'); + console.log('Loading menu to establish session...'); + await page.goto(menuUrl, { + waitUntil: 'networkidle2', + timeout: 60000, + }); + await new Promise((r) => setTimeout(r, 3000)); + const dispensaryId = await page.evaluate(() => window.reactEnv?.dispensaryId); + console.log('Dispensary ID:', dispensaryId); + // Paginate through all products + const allProducts = []; + let pageNum = 0; + const perPage = 100; + console.log('\nFetching all products via paginated GraphQL...'); + while (true) { + const result = await page.evaluate(async (dispId, hash, page, perPage) => { + const variables = { + includeEnterpriseSpecials: false, + productsFilter: { + dispensaryId: dispId, + pricingType: 'rec', + Status: 'Active', + types: [], + useCache: false, + isDefaultSort: true, + sortBy: 'popularSortIdx', + sortDirection: 1, + bypassOnlineThresholds: true, + isKioskMenu: false, + removeProductsBelowOptionThresholds: false, + }, + page, + perPage, + }; + const qs = new URLSearchParams({ + operationName: 'FilteredProducts', + variables: JSON.stringify(variables), + extensions: JSON.stringify({ persistedQuery: { version: 1, sha256Hash: hash } }), + }); + const resp = await fetch(`https://dutchie.com/graphql?${qs.toString()}`, { + method: 'GET', + headers: { + 'content-type': 'application/json', + 'apollographql-client-name': 'Marketplace (production)', + }, + credentials: 'include', + }); + const json = await resp.json(); + return { + products: json?.data?.filteredProducts?.products || [], + totalCount: json?.data?.filteredProducts?.queryInfo?.totalCount, + }; + }, dispensaryId, GRAPHQL_HASH, pageNum, perPage); + if (result.products.length === 0) { + break; + } + allProducts.push(...result.products); + console.log(`Page ${pageNum}: ${result.products.length} products (total so far: ${allProducts.length}/${result.totalCount})`); + pageNum++; + // Safety limit + if (pageNum > 50) { + console.log('Reached page limit'); + break; + } + } + console.log(`\nTotal products fetched: ${allProducts.length}`); + // Normalize and upsert + console.log('\nNormalizing and upserting to database...'); + const normalized = allProducts.map(dutchie_graphql_1.normalizeDutchieProduct); + const client = await pool.connect(); + let inserted = 0; + let updated = 0; + try { + await client.query('BEGIN'); + for (const product of normalized) { + const result = await client.query(` + INSERT INTO products ( + store_id, external_id, slug, name, enterprise_product_id, + brand, brand_external_id, brand_logo_url, + subcategory, strain_type, canonical_category, + price, rec_price, med_price, rec_special_price, med_special_price, + is_on_special, special_name, discount_percent, special_data, + sku, inventory_quantity, inventory_available, is_below_threshold, status, + thc_percentage, cbd_percentage, cannabinoids, + weight_mg, net_weight_value, net_weight_unit, options, raw_options, + image_url, additional_images, + is_featured, medical_only, rec_only, + source_created_at, source_updated_at, + description, raw_data, + dutchie_url, last_seen_at, updated_at + ) + VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, + $9, $10, $11, + $12, $13, $14, $15, $16, + $17, $18, $19, $20, + $21, $22, $23, $24, $25, + $26, $27, $28, + $29, $30, $31, $32, $33, + $34, $35, + $36, $37, $38, + $39, $40, + $41, $42, + '', NOW(), NOW() + ) + ON CONFLICT (store_id, slug) DO UPDATE SET + name = EXCLUDED.name, + enterprise_product_id = EXCLUDED.enterprise_product_id, + brand = EXCLUDED.brand, + brand_external_id = EXCLUDED.brand_external_id, + brand_logo_url = EXCLUDED.brand_logo_url, + subcategory = EXCLUDED.subcategory, + strain_type = EXCLUDED.strain_type, + canonical_category = EXCLUDED.canonical_category, + price = EXCLUDED.price, + rec_price = EXCLUDED.rec_price, + med_price = EXCLUDED.med_price, + rec_special_price = EXCLUDED.rec_special_price, + med_special_price = EXCLUDED.med_special_price, + is_on_special = EXCLUDED.is_on_special, + special_name = EXCLUDED.special_name, + discount_percent = EXCLUDED.discount_percent, + special_data = EXCLUDED.special_data, + sku = EXCLUDED.sku, + inventory_quantity = EXCLUDED.inventory_quantity, + inventory_available = EXCLUDED.inventory_available, + is_below_threshold = EXCLUDED.is_below_threshold, + status = EXCLUDED.status, + thc_percentage = EXCLUDED.thc_percentage, + cbd_percentage = EXCLUDED.cbd_percentage, + cannabinoids = EXCLUDED.cannabinoids, + weight_mg = EXCLUDED.weight_mg, + net_weight_value = EXCLUDED.net_weight_value, + net_weight_unit = EXCLUDED.net_weight_unit, + options = EXCLUDED.options, + raw_options = EXCLUDED.raw_options, + image_url = EXCLUDED.image_url, + additional_images = EXCLUDED.additional_images, + is_featured = EXCLUDED.is_featured, + medical_only = EXCLUDED.medical_only, + rec_only = EXCLUDED.rec_only, + source_created_at = EXCLUDED.source_created_at, + source_updated_at = EXCLUDED.source_updated_at, + description = EXCLUDED.description, + raw_data = EXCLUDED.raw_data, + last_seen_at = NOW(), + updated_at = NOW() + RETURNING (xmax = 0) AS was_inserted + `, [ + storeId, + product.external_id, + product.slug, + product.name, + product.enterprise_product_id, + product.brand, + product.brand_external_id, + product.brand_logo_url, + product.subcategory, + product.strain_type, + product.canonical_category, + product.price, + product.rec_price, + product.med_price, + product.rec_special_price, + product.med_special_price, + product.is_on_special, + product.special_name, + product.discount_percent, + product.special_data ? JSON.stringify(product.special_data) : null, + product.sku, + product.inventory_quantity, + product.inventory_available, + product.is_below_threshold, + product.status, + product.thc_percentage, + product.cbd_percentage, + product.cannabinoids ? JSON.stringify(product.cannabinoids) : null, + product.weight_mg, + product.net_weight_value, + product.net_weight_unit, + product.options, + product.raw_options, + product.image_url, + product.additional_images, + product.is_featured, + product.medical_only, + product.rec_only, + product.source_created_at, + product.source_updated_at, + product.description, + product.raw_data ? JSON.stringify(product.raw_data) : null, + ]); + if (result.rows[0]?.was_inserted) { + inserted++; + } + else { + updated++; + } + } + await client.query('COMMIT'); + } + catch (error) { + await client.query('ROLLBACK'); + throw error; + } + finally { + client.release(); + } + console.log(`\nDatabase: ${inserted} inserted, ${updated} updated`); + // Show summary stats + const stats = await pool.query(` + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE is_on_special) as specials, + COUNT(DISTINCT brand) as brands, + COUNT(DISTINCT subcategory) as categories + FROM products WHERE store_id = $1 + `, [storeId]); + console.log('\nStore summary:'); + console.log(` Total products: ${stats.rows[0].total}`); + console.log(` On special: ${stats.rows[0].specials}`); + console.log(` Unique brands: ${stats.rows[0].brands}`); + console.log(` Categories: ${stats.rows[0].categories}`); + return { + success: true, + totalProducts: allProducts.length, + inserted, + updated, + }; + } + finally { + await browser.close(); + await pool.end(); + } +} +// Run +const menuUrl = process.argv[2] || 'https://dutchie.com/embedded-menu/AZ-Deeply-Rooted'; +const storeId = parseInt(process.argv[3] || '1', 10); +console.log('='.repeat(60)); +console.log('DUTCHIE GRAPHQL FULL SCRAPE'); +console.log('='.repeat(60)); +console.log(`Menu URL: ${menuUrl}`); +console.log(`Store ID: ${storeId}`); +console.log(''); +scrapeAllProducts(menuUrl, storeId) + .then((result) => { + console.log('\n' + '='.repeat(60)); + console.log('COMPLETE'); + console.log(JSON.stringify(result, null, 2)); +}) + .catch((error) => { + console.error('Error:', error.message); + process.exit(1); +}); diff --git a/backend/dist/scripts/test-dutchie-e2e.js b/backend/dist/scripts/test-dutchie-e2e.js new file mode 100644 index 00000000..63bb215a --- /dev/null +++ b/backend/dist/scripts/test-dutchie-e2e.js @@ -0,0 +1,169 @@ +"use strict"; +/** + * Test script: End-to-end Dutchie GraphQL → DB → Dashboard flow + * + * This demonstrates the complete data pipeline: + * 1. Fetch one product from Dutchie GraphQL via Puppeteer + * 2. Normalize it to our schema + * 3. Show the mapping + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const dutchie_graphql_1 = require("../scrapers/dutchie-graphql"); +const fs = __importStar(require("fs")); +// Load the captured sample product from schema capture +const capturedData = JSON.parse(fs.readFileSync('/tmp/dutchie-schema-capture.json', 'utf-8')); +const sampleProduct = capturedData.sampleProduct; +console.log('='.repeat(80)); +console.log('DUTCHIE GRAPHQL → DATABASE MAPPING DEMONSTRATION'); +console.log('='.repeat(80)); +console.log('\n📥 RAW DUTCHIE GRAPHQL PRODUCT:'); +console.log('-'.repeat(80)); +// Show key fields from raw product +const keyRawFields = { + '_id': sampleProduct._id, + 'Name': sampleProduct.Name, + 'cName': sampleProduct.cName, + 'brandName': sampleProduct.brandName, + 'brand.id': sampleProduct.brand?.id, + 'type': sampleProduct.type, + 'subcategory': sampleProduct.subcategory, + 'strainType': sampleProduct.strainType, + 'Prices': sampleProduct.Prices, + 'recPrices': sampleProduct.recPrices, + 'recSpecialPrices': sampleProduct.recSpecialPrices, + 'special': sampleProduct.special, + 'specialData.saleSpecials[0].specialName': sampleProduct.specialData?.saleSpecials?.[0]?.specialName, + 'specialData.saleSpecials[0].discount': sampleProduct.specialData?.saleSpecials?.[0]?.discount, + 'THCContent.range[0]': sampleProduct.THCContent?.range?.[0], + 'CBDContent.range[0]': sampleProduct.CBDContent?.range?.[0], + 'Status': sampleProduct.Status, + 'Image': sampleProduct.Image, + 'POSMetaData.canonicalSKU': sampleProduct.POSMetaData?.canonicalSKU, + 'POSMetaData.children[0].quantity': sampleProduct.POSMetaData?.children?.[0]?.quantity, + 'POSMetaData.children[0].quantityAvailable': sampleProduct.POSMetaData?.children?.[0]?.quantityAvailable, +}; +Object.entries(keyRawFields).forEach(([key, value]) => { + console.log(` ${key}: ${JSON.stringify(value)}`); +}); +console.log('\n📤 NORMALIZED DATABASE ROW:'); +console.log('-'.repeat(80)); +// Normalize the product +const normalized = (0, dutchie_graphql_1.normalizeDutchieProduct)(sampleProduct); +// Show the normalized result (excluding raw_data for readability) +const { raw_data, cannabinoids, special_data, ...displayFields } = normalized; +Object.entries(displayFields).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + console.log(` ${key}: ${JSON.stringify(value)}`); + } +}); +console.log('\n🔗 FIELD MAPPING:'); +console.log('-'.repeat(80)); +const fieldMappings = [ + ['_id / id', 'external_id', sampleProduct._id, normalized.external_id], + ['Name', 'name', sampleProduct.Name, normalized.name], + ['cName', 'slug', sampleProduct.cName, normalized.slug], + ['brandName', 'brand', sampleProduct.brandName, normalized.brand], + ['brand.id', 'brand_external_id', sampleProduct.brand?.id, normalized.brand_external_id], + ['subcategory', 'subcategory', sampleProduct.subcategory, normalized.subcategory], + ['strainType', 'strain_type', sampleProduct.strainType, normalized.strain_type], + ['recPrices[0]', 'rec_price', sampleProduct.recPrices?.[0], normalized.rec_price], + ['recSpecialPrices[0]', 'rec_special_price', sampleProduct.recSpecialPrices?.[0], normalized.rec_special_price], + ['special', 'is_on_special', sampleProduct.special, normalized.is_on_special], + ['specialData...specialName', 'special_name', sampleProduct.specialData?.saleSpecials?.[0]?.specialName?.substring(0, 40) + '...', normalized.special_name?.substring(0, 40) + '...'], + ['THCContent.range[0]', 'thc_percentage', sampleProduct.THCContent?.range?.[0], normalized.thc_percentage], + ['CBDContent.range[0]', 'cbd_percentage', sampleProduct.CBDContent?.range?.[0], normalized.cbd_percentage], + ['Status', 'status', sampleProduct.Status, normalized.status], + ['Image', 'image_url', sampleProduct.Image?.substring(0, 50) + '...', normalized.image_url?.substring(0, 50) + '...'], + ['POSMetaData.canonicalSKU', 'sku', sampleProduct.POSMetaData?.canonicalSKU, normalized.sku], +]; +console.log(' GraphQL Field → DB Column | Value'); +console.log(' ' + '-'.repeat(75)); +fieldMappings.forEach(([gqlField, dbCol, gqlVal, dbVal]) => { + const gqlStr = String(gqlField).padEnd(30); + const dbStr = String(dbCol).padEnd(20); + console.log(` ${gqlStr} → ${dbStr} | ${JSON.stringify(dbVal)}`); +}); +console.log('\n📊 SQL INSERT STATEMENT:'); +console.log('-'.repeat(80)); +// Generate example SQL +const sqlExample = ` +INSERT INTO products ( + store_id, external_id, slug, name, + brand, brand_external_id, + subcategory, strain_type, + rec_price, rec_special_price, + is_on_special, special_name, discount_percent, + thc_percentage, cbd_percentage, + status, image_url, sku +) VALUES ( + 1, -- store_id (Deeply Rooted) + '${normalized.external_id}', -- external_id + '${normalized.slug}', -- slug + '${normalized.name}', -- name + '${normalized.brand}', -- brand + '${normalized.brand_external_id}', -- brand_external_id + '${normalized.subcategory}', -- subcategory + '${normalized.strain_type}', -- strain_type + ${normalized.rec_price}, -- rec_price + ${normalized.rec_special_price}, -- rec_special_price + ${normalized.is_on_special}, -- is_on_special + '${normalized.special_name?.substring(0, 50)}...', -- special_name + ${normalized.discount_percent || 'NULL'}, -- discount_percent + ${normalized.thc_percentage}, -- thc_percentage + ${normalized.cbd_percentage}, -- cbd_percentage + '${normalized.status}', -- status + '${normalized.image_url}', -- image_url + '${normalized.sku}' -- sku +) +ON CONFLICT (store_id, slug) DO UPDATE SET ...; +`; +console.log(sqlExample); +console.log('\n✅ SUMMARY:'); +console.log('-'.repeat(80)); +console.log(` Product: ${normalized.name}`); +console.log(` Brand: ${normalized.brand}`); +console.log(` Category: ${normalized.subcategory}`); +console.log(` Price: $${normalized.rec_price} → $${normalized.rec_special_price} (${normalized.discount_percent}% off)`); +console.log(` THC: ${normalized.thc_percentage}%`); +console.log(` Status: ${normalized.status}`); +console.log(` On Special: ${normalized.is_on_special}`); +console.log(` SKU: ${normalized.sku}`); +console.log('\n🎯 DERIVED VIEWS (computed from products table):'); +console.log('-'.repeat(80)); +console.log(' - current_specials: Products where is_on_special = true'); +console.log(' - derived_brands: Aggregated by brand name with counts/prices'); +console.log(' - derived_categories: Aggregated by subcategory'); +console.log('\nAll views are computed from the single products table - no separate tables needed!'); diff --git a/backend/dist/scripts/test-dutchie-graphql.js b/backend/dist/scripts/test-dutchie-graphql.js new file mode 100644 index 00000000..8cf8962f --- /dev/null +++ b/backend/dist/scripts/test-dutchie-graphql.js @@ -0,0 +1,179 @@ +"use strict"; +/** + * Test script to validate Dutchie GraphQL API access and capture response structure + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// @ts-ignore - node-fetch type declaration not installed +const node_fetch_1 = __importDefault(require("node-fetch")); +const GRAPHQL_HASHES = { + ConsumerDispensaries: '0a5bfa6ca1d64ae47bcccb7c8077c87147cbc4e6982c17ceec97a2a4948b311b', + GetAddressBasedDispensaryData: '13461f73abf7268770dfd05fe7e10c523084b2bb916a929c08efe3d87531977b', + FilteredProducts: 'ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0', + MenuFiltersV2: '2f0b3233b8a2426b391649ca3f0f7a5d43b9aefd683f6286d7261a2517e3568e', + FilteredSpecials: '0dfb85a4fc138c55a076d4d11bf6d1a25f7cbd511428e1cf5a5b863b3eb23f25', +}; +async function fetchProducts(dispensaryId, page = 0, perPage = 25) { + const session = 'crawlsy-session-' + Date.now(); + const variables = { + includeEnterpriseSpecials: false, + productsFilter: { + dispensaryId, + pricingType: 'rec', + Status: null, // null to include all (in-stock and out-of-stock) + types: [], + useCache: true, + isDefaultSort: true, + sortBy: 'popularSortIdx', + sortDirection: 1, + bypassOnlineThresholds: true, + isKioskMenu: false, + removeProductsBelowOptionThresholds: false + }, + page, + perPage + }; + const qs = new URLSearchParams({ + operationName: 'FilteredProducts', + variables: JSON.stringify(variables), + extensions: JSON.stringify({ persistedQuery: { version: 1, sha256Hash: GRAPHQL_HASHES.FilteredProducts } }) + }); + const res = await (0, node_fetch_1.default)(`https://dutchie.com/api-3/graphql?${qs.toString()}`, { + headers: { + 'x-dutchie-session': session, + 'apollographql-client-name': 'Marketplace (production)', + 'content-type': 'application/json', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }); + if (!res.ok) { + const text = await res.text(); + console.error('HTTP Status:', res.status); + console.error('Response:', text.substring(0, 500)); + throw new Error(`HTTP ${res.status}: ${text.substring(0, 200)}`); + } + return res.json(); +} +async function resolveDispensaryId(cName) { + const session = 'crawlsy-session-' + Date.now(); + const variables = { input: { dispensaryId: cName } }; + const qs = new URLSearchParams({ + operationName: 'GetAddressBasedDispensaryData', + variables: JSON.stringify(variables), + extensions: JSON.stringify({ persistedQuery: { version: 1, sha256Hash: GRAPHQL_HASHES.GetAddressBasedDispensaryData } }) + }); + const res = await (0, node_fetch_1.default)(`https://dutchie.com/graphql?${qs.toString()}`, { + headers: { + 'x-dutchie-session': session, + 'apollographql-client-name': 'Marketplace (production)', + 'content-type': 'application/json', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + }); + if (!res.ok) { + console.error('Failed to resolve dispensary ID:', res.status); + return null; + } + const data = await res.json(); + return data?.data?.getAddressBasedDispensaryData?.dispensaryData?.dispensaryId || null; +} +function enumerateFields(obj, prefix = '') { + const fields = []; + for (const [key, value] of Object.entries(obj)) { + const path = prefix ? `${prefix}.${key}` : key; + if (value === null) { + fields.push(`${path}: null`); + } + else if (Array.isArray(value)) { + fields.push(`${path}: Array[${value.length}]`); + if (value.length > 0 && typeof value[0] === 'object') { + const subFields = enumerateFields(value[0], `${path}[0]`); + fields.push(...subFields); + } + } + else if (typeof value === 'object') { + fields.push(`${path}: Object`); + const subFields = enumerateFields(value, path); + fields.push(...subFields); + } + else { + const typeStr = typeof value; + const preview = String(value).substring(0, 50); + fields.push(`${path}: ${typeStr} = "${preview}"`); + } + } + return fields; +} +async function main() { + console.log('='.repeat(80)); + console.log('DUTCHIE GRAPHQL API TEST'); + console.log('='.repeat(80)); + const cName = 'AZ-Deeply-Rooted'; + // Step 1: Resolve dispensary ID + console.log(`\n1. Resolving dispensary ID for "${cName}"...`); + const dispensaryId = await resolveDispensaryId(cName); + const finalDispensaryId = dispensaryId || '6405ef617056e8014d79101b'; // Fallback to known ID + if (!dispensaryId) { + console.log(' Failed to resolve via API, using hardcoded ID: 6405ef617056e8014d79101b'); + } + console.log(` Final ID: ${finalDispensaryId}`); + // Step 2: Fetch first page of products + console.log('\n2. Fetching products (page 0, perPage 5)...'); + const result = await fetchProducts(finalDispensaryId, 0, 5); + if (result.errors) { + console.error('\nGraphQL Errors:'); + console.error(JSON.stringify(result.errors, null, 2)); + return; + } + const products = result?.data?.filteredProducts?.products || []; + console.log(` Found ${products.length} products in this page`); + if (products.length === 0) { + console.log('No products returned. Full response:'); + console.log(JSON.stringify(result, null, 2)); + return; + } + // Step 3: Enumerate all fields from first product + console.log('\n3. PRODUCT FIELD STRUCTURE (from first product):'); + console.log('-'.repeat(80)); + const product = products[0]; + const fields = enumerateFields(product); + fields.forEach(f => console.log(` ${f}`)); + // Step 4: Show full sample product JSON + console.log('\n4. FULL SAMPLE PRODUCT JSON:'); + console.log('-'.repeat(80)); + console.log(JSON.stringify(product, null, 2)); + // Step 5: Summary of key fields for schema design + console.log('\n5. KEY FIELDS FOR SCHEMA DESIGN:'); + console.log('-'.repeat(80)); + const keyFields = [ + { field: 'id', value: product.id }, + { field: 'name', value: product.name }, + { field: 'slug', value: product.slug }, + { field: 'brand', value: product.brand }, + { field: 'brandId', value: product.brandId }, + { field: 'type', value: product.type }, + { field: 'category', value: product.category }, + { field: 'subcategory', value: product.subcategory }, + { field: 'strainType', value: product.strainType }, + { field: 'THCContent', value: product.THCContent }, + { field: 'CBDContent', value: product.CBDContent }, + { field: 'description', value: product.description?.substring(0, 100) + '...' }, + { field: 'image', value: product.image }, + { field: 'options.length', value: product.options?.length }, + { field: 'pricing', value: product.pricing }, + { field: 'terpenes.length', value: product.terpenes?.length }, + { field: 'effects.length', value: product.effects?.length }, + ]; + keyFields.forEach(({ field, value }) => { + console.log(` ${field}: ${JSON.stringify(value)}`); + }); + // Step 6: Show an option (variant) if available + if (product.options && product.options.length > 0) { + console.log('\n6. SAMPLE OPTION/VARIANT:'); + console.log('-'.repeat(80)); + console.log(JSON.stringify(product.options[0], null, 2)); + } +} +main().catch(console.error); diff --git a/backend/dist/scripts/test-status-filter.js b/backend/dist/scripts/test-status-filter.js new file mode 100644 index 00000000..86a663c0 --- /dev/null +++ b/backend/dist/scripts/test-status-filter.js @@ -0,0 +1,84 @@ +"use strict"; +/** + * Test different Status filter values in Dutchie GraphQL + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +const GRAPHQL_HASH = 'ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0'; +async function main() { + const browser = await puppeteer_extra_1.default.launch({ + headless: 'new', + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36'); + console.log('Loading menu...'); + await page.goto('https://dutchie.com/embedded-menu/AZ-Deeply-Rooted', { + waitUntil: 'networkidle2', + timeout: 60000, + }); + await new Promise((r) => setTimeout(r, 3000)); + const dispensaryId = await page.evaluate(() => window.reactEnv?.dispensaryId); + console.log('Dispensary ID:', dispensaryId); + // Test different status values + const testCases = [ + { label: 'Active', status: 'Active', includeStatus: true }, + { label: 'Inactive', status: 'Inactive', includeStatus: true }, + { label: 'null', status: null, includeStatus: true }, + { label: 'omitted', status: null, includeStatus: false }, + ]; + for (const testCase of testCases) { + const result = await page.evaluate(async (dispId, hash, status, includeStatus) => { + const filter = { + dispensaryId: dispId, + pricingType: 'rec', + types: [], + useCache: false, + isDefaultSort: true, + sortBy: 'popularSortIdx', + sortDirection: 1, + bypassOnlineThresholds: true, + isKioskMenu: false, + removeProductsBelowOptionThresholds: false, + }; + if (includeStatus) { + filter.Status = status; + } + const variables = { + includeEnterpriseSpecials: false, + productsFilter: filter, + page: 0, + perPage: 100, + }; + const qs = new URLSearchParams({ + operationName: 'FilteredProducts', + variables: JSON.stringify(variables), + extensions: JSON.stringify({ persistedQuery: { version: 1, sha256Hash: hash } }), + }); + const resp = await fetch(`https://dutchie.com/graphql?${qs.toString()}`, { + method: 'GET', + headers: { + 'content-type': 'application/json', + 'apollographql-client-name': 'Marketplace (production)', + }, + credentials: 'include', + }); + const json = await resp.json(); + const products = json?.data?.filteredProducts?.products || []; + return { + count: products.length, + totalCount: json?.data?.filteredProducts?.queryInfo?.totalCount, + sampleStatus: products[0]?.Status, + statuses: [...new Set(products.map((p) => p.Status))], + }; + }, dispensaryId, GRAPHQL_HASH, testCase.status, testCase.includeStatus); + console.log(`Status ${testCase.label}: Products=${result.count}, Total=${result.totalCount}, Statuses=${JSON.stringify(result.statuses)}`); + } + await browser.close(); +} +main().catch(console.error); diff --git a/backend/dist/services/availability.js b/backend/dist/services/availability.js new file mode 100644 index 00000000..001c5917 --- /dev/null +++ b/backend/dist/services/availability.js @@ -0,0 +1,201 @@ +"use strict"; +/** + * Availability Service + * + * Normalizes product availability from various menu providers and tracks + * state transitions for inventory analytics. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeAvailability = normalizeAvailability; +exports.extractAvailabilityHints = extractAvailabilityHints; +exports.hintsToAvailability = hintsToAvailability; +exports.aggregateAvailability = aggregateAvailability; +// Threshold for considering stock as "limited" +const LIMITED_THRESHOLD = 5; +/** + * Normalize availability from a Dutchie product + * + * Dutchie products can have various availability indicators: + * - potencyAmount.quantity: explicit stock count + * - status: sometimes includes stock status + * - variants[].quantity: stock per variant + * - isInStock / inStock: boolean flags + */ +function normalizeAvailability(dutchieProduct) { + const raw = {}; + // Collect raw availability data for debugging + if (dutchieProduct.potencyAmount?.quantity !== undefined) { + raw.potencyQuantity = dutchieProduct.potencyAmount.quantity; + } + if (dutchieProduct.status !== undefined) { + raw.status = dutchieProduct.status; + } + if (dutchieProduct.isInStock !== undefined) { + raw.isInStock = dutchieProduct.isInStock; + } + if (dutchieProduct.inStock !== undefined) { + raw.inStock = dutchieProduct.inStock; + } + if (dutchieProduct.variants?.length) { + const variantQuantities = dutchieProduct.variants + .filter((v) => v.quantity !== undefined) + .map((v) => ({ option: v.option, quantity: v.quantity })); + if (variantQuantities.length) { + raw.variantQuantities = variantQuantities; + } + } + // Try to extract quantity + let quantity = null; + // Check potencyAmount.quantity first (most reliable for Dutchie) + if (typeof dutchieProduct.potencyAmount?.quantity === 'number') { + quantity = dutchieProduct.potencyAmount.quantity; + } + // Sum variant quantities if available + else if (dutchieProduct.variants?.length) { + const totalVariantQty = dutchieProduct.variants.reduce((sum, v) => { + return sum + (typeof v.quantity === 'number' ? v.quantity : 0); + }, 0); + if (totalVariantQty > 0) { + quantity = totalVariantQty; + } + } + // Determine status + let status = 'unknown'; + // Explicit boolean flags take precedence + if (dutchieProduct.isInStock === false || dutchieProduct.inStock === false) { + status = 'out_of_stock'; + } + else if (dutchieProduct.isInStock === true || dutchieProduct.inStock === true) { + status = quantity !== null && quantity <= LIMITED_THRESHOLD ? 'limited' : 'in_stock'; + } + // Check status string + else if (typeof dutchieProduct.status === 'string') { + const statusLower = dutchieProduct.status.toLowerCase(); + if (statusLower.includes('out') || statusLower.includes('unavailable')) { + status = 'out_of_stock'; + } + else if (statusLower.includes('limited') || statusLower.includes('low')) { + status = 'limited'; + } + else if (statusLower.includes('in') || statusLower.includes('available')) { + status = 'in_stock'; + } + } + // Infer from quantity + else if (quantity !== null) { + if (quantity === 0) { + status = 'out_of_stock'; + } + else if (quantity <= LIMITED_THRESHOLD) { + status = 'limited'; + } + else { + status = 'in_stock'; + } + } + return { status, quantity, raw }; +} +/** + * Extract availability hints from page content or product card HTML + * + * Used for sandbox provider scraping where we don't have structured data + */ +function extractAvailabilityHints(pageContent, productElement) { + const hints = {}; + const content = (productElement || pageContent).toLowerCase(); + // Check for out-of-stock indicators + const oosPatterns = [ + 'out of stock', + 'out-of-stock', + 'sold out', + 'soldout', + 'unavailable', + 'not available', + 'coming soon', + 'notify me' + ]; + hints.hasOutOfStockBadge = oosPatterns.some(p => content.includes(p)); + // Check for limited stock indicators + const limitedPatterns = [ + 'limited stock', + 'limited quantity', + 'low stock', + 'only \\d+ left', + 'few remaining', + 'almost gone', + 'selling fast' + ]; + hints.hasLimitedBadge = limitedPatterns.some(p => { + if (p.includes('\\d')) { + return new RegExp(p, 'i').test(content); + } + return content.includes(p); + }); + // Check for in-stock indicators + const inStockPatterns = [ + 'in stock', + 'in-stock', + 'add to cart', + 'add to bag', + 'buy now', + 'available' + ]; + hints.hasInStockBadge = inStockPatterns.some(p => content.includes(p)); + // Try to extract quantity text + const qtyMatch = content.match(/(\d+)\s*(left|remaining|in stock|available)/i); + if (qtyMatch) { + hints.quantityText = qtyMatch[0]; + } + // Look for explicit stock text + const stockTextMatch = content.match(/(out of stock|in stock|low stock|limited|sold out)[^<]*/i); + if (stockTextMatch) { + hints.stockText = stockTextMatch[0].trim(); + } + return hints; +} +/** + * Convert availability hints to normalized availability + */ +function hintsToAvailability(hints) { + let status = 'unknown'; + let quantity = null; + // Extract quantity if present + if (hints.quantityText) { + const match = hints.quantityText.match(/(\d+)/); + if (match) { + quantity = parseInt(match[1], 10); + } + } + // Determine status from hints + if (hints.hasOutOfStockBadge) { + status = 'out_of_stock'; + } + else if (hints.hasLimitedBadge) { + status = 'limited'; + } + else if (hints.hasInStockBadge) { + status = quantity !== null && quantity <= LIMITED_THRESHOLD ? 'limited' : 'in_stock'; + } + return { + status, + quantity, + raw: hints + }; +} +function aggregateAvailability(products) { + const counts = { + in_stock: 0, + out_of_stock: 0, + limited: 0, + unknown: 0, + changed: 0 + }; + for (const product of products) { + const status = product.availability_status || 'unknown'; + counts[status]++; + if (product.previous_status && product.previous_status !== status) { + counts.changed++; + } + } + return counts; +} diff --git a/backend/dist/services/category-crawler-jobs.js b/backend/dist/services/category-crawler-jobs.js new file mode 100644 index 00000000..a646a3de --- /dev/null +++ b/backend/dist/services/category-crawler-jobs.js @@ -0,0 +1,1098 @@ +"use strict"; +/** + * Category-Specific Crawler Jobs + * + * Handles crawl jobs for each intelligence category independently: + * - CrawlProductsJob - Production product crawling (Dutchie only) + * - CrawlSpecialsJob - Production specials crawling + * - CrawlBrandIntelligenceJob - Production brand intelligence crawling + * - CrawlMetadataJob - Production metadata crawling + * - SandboxProductsJob - Sandbox product crawling (all providers) + * - SandboxSpecialsJob - Sandbox specials crawling + * - SandboxBrandJob - Sandbox brand crawling + * - SandboxMetadataJob - Sandbox metadata crawling + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runCrawlProductsJob = runCrawlProductsJob; +exports.runCrawlSpecialsJob = runCrawlSpecialsJob; +exports.runCrawlBrandIntelligenceJob = runCrawlBrandIntelligenceJob; +exports.runCrawlMetadataJob = runCrawlMetadataJob; +exports.runSandboxProductsJob = runSandboxProductsJob; +exports.runSandboxSpecialsJob = runSandboxSpecialsJob; +exports.runSandboxBrandJob = runSandboxBrandJob; +exports.runSandboxMetadataJob = runSandboxMetadataJob; +exports.processCategorySandboxJobs = processCategorySandboxJobs; +exports.runAllCategoryProductionCrawls = runAllCategoryProductionCrawls; +exports.runAllCategorySandboxCrawls = runAllCategorySandboxCrawls; +const migrate_1 = require("../db/migrate"); +const crawler_logger_1 = require("./crawler-logger"); +const intelligence_detector_1 = require("./intelligence-detector"); +const scraper_v2_1 = require("../scraper-v2"); +const puppeteer_1 = __importDefault(require("puppeteer")); +const WORKER_ID = `crawler-${process.pid}-${Date.now()}`; +// ======================================== +// Helper Functions +// ======================================== +async function getDispensaryWithCategories(dispensaryId) { + const result = await migrate_1.pool.query(`SELECT id, name, website, menu_url, + product_provider, product_confidence, product_crawler_mode, last_product_scan_at, + specials_provider, specials_confidence, specials_crawler_mode, last_specials_scan_at, + brand_provider, brand_confidence, brand_crawler_mode, last_brand_scan_at, + metadata_provider, metadata_confidence, metadata_crawler_mode, last_metadata_scan_at, + crawler_status, scraper_template + FROM dispensaries WHERE id = $1`, [dispensaryId]); + return result.rows[0] || null; +} +async function updateCategoryScanTime(dispensaryId, category) { + const column = `last_${category}_scan_at`; + await migrate_1.pool.query(`UPDATE dispensaries SET ${column} = NOW(), updated_at = NOW() WHERE id = $1`, [dispensaryId]); +} +async function getStoreIdForDispensary(dispensaryId) { + // First check if dispensary has menu_url - if so, try to match with stores.dutchie_url + const result = await migrate_1.pool.query(`SELECT s.id FROM stores s + JOIN dispensaries d ON d.menu_url = s.dutchie_url OR d.name ILIKE '%' || s.name || '%' + WHERE d.id = $1 + LIMIT 1`, [dispensaryId]); + if (result.rows.length > 0) { + return result.rows[0].id; + } + // Try matching by slug + const result2 = await migrate_1.pool.query(`SELECT s.id FROM stores s + JOIN dispensaries d ON d.website ILIKE '%' || s.slug || '%' + WHERE d.id = $1 + LIMIT 1`, [dispensaryId]); + return result2.rows[0]?.id || null; +} +async function createCategorySandboxEntry(dispensaryId, category, suspectedProvider, templateName, detectionSignals) { + // Check for existing sandbox for this category + const existing = await migrate_1.pool.query(`SELECT id FROM crawler_sandboxes + WHERE dispensary_id = $1 AND category = $2 AND status NOT IN ('moved_to_production', 'failed')`, [dispensaryId, category]); + if (existing.rows.length > 0) { + await migrate_1.pool.query(`UPDATE crawler_sandboxes + SET suspected_menu_provider = $2, template_name = $3, detection_signals = COALESCE($4, detection_signals), updated_at = NOW() + WHERE id = $1`, [existing.rows[0].id, suspectedProvider, templateName, detectionSignals ? JSON.stringify(detectionSignals) : null]); + return existing.rows[0].id; + } + const result = await migrate_1.pool.query(`INSERT INTO crawler_sandboxes (dispensary_id, category, suspected_menu_provider, template_name, mode, detection_signals, status) + VALUES ($1, $2, $3, $4, 'template_learning', $5, 'pending') + RETURNING id`, [dispensaryId, category, suspectedProvider, templateName, detectionSignals ? JSON.stringify(detectionSignals) : '{}']); + return result.rows[0].id; +} +async function createCategorySandboxJob(dispensaryId, sandboxId, category, templateName, jobType = 'crawl', priority = 0) { + const result = await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, sandbox_id, category, template_name, job_type, status, priority) + VALUES ($1, $2, $3, $4, $5, 'pending', $6) + RETURNING id`, [dispensaryId, sandboxId, category, templateName, jobType, priority]); + return result.rows[0].id; +} +async function updateSandboxQuality(sandboxId, metrics) { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET + quality_score = $1, + products_extracted = $2, + fields_missing = $3, + error_count = $4, + analysis_json = COALESCE(analysis_json, '{}'::jsonb) || $5::jsonb, + analyzed_at = NOW(), + updated_at = NOW() + WHERE id = $6`, [ + metrics.quality_score, + metrics.items_extracted, + metrics.fields_missing, + metrics.error_count, + JSON.stringify({ sample_data: metrics.sample_data }), + sandboxId, + ]); +} +async function getCrawlerTemplate(provider, category, environment) { + const result = await migrate_1.pool.query(`SELECT id, name, selector_config, navigation_config + FROM crawler_templates + WHERE provider = $1 AND environment = $2 AND is_active = true + ORDER BY is_default_for_provider DESC, version DESC + LIMIT 1`, [provider, environment]); + return result.rows[0] || null; +} +// ======================================== +// Production Crawl Jobs +// ======================================== +/** + * CrawlProductsJob - Production product crawling + * Only runs for Dutchie dispensaries in production mode + */ +async function runCrawlProductsJob(dispensaryId) { + const category = 'product'; + const startTime = Date.now(); + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + // Verify production eligibility + if (dispensary.product_provider !== 'dutchie') { + return { success: false, category, message: 'Not a Dutchie dispensary for products' }; + } + if (dispensary.product_crawler_mode !== 'production') { + return { success: false, category, message: 'Products not in production mode' }; + } + const storeId = await getStoreIdForDispensary(dispensaryId); + if (!storeId) { + return { success: false, category, message: 'No linked store found for Dutchie crawl' }; + } + let browser = null; + // Log job start + crawler_logger_1.crawlerLogger.jobStarted({ + job_id: 0, // Category jobs don't have traditional job IDs + store_id: storeId, + store_name: dispensary.name, + job_type: 'CrawlProductsJob', + trigger_type: 'category_crawl', + provider: 'dutchie', + }); + try { + // Run the existing Dutchie scraper + await (0, scraper_v2_1.scrapeStore)(storeId, 3); + // Update scan time + await updateCategoryScanTime(dispensaryId, category); + const durationMs = Date.now() - startTime; + // Log job completion with summary + crawler_logger_1.crawlerLogger.jobCompleted({ + job_id: 0, + store_id: storeId, + store_name: dispensary.name, + duration_ms: durationMs, + products_found: 0, // Not tracked at this level + products_new: 0, + products_updated: 0, + provider: 'dutchie', + }); + return { + success: true, + category, + message: 'Product crawl completed successfully', + data: { storeId, provider: 'dutchie', durationMs }, + }; + } + catch (error) { + const durationMs = Date.now() - startTime; + // Log job failure + crawler_logger_1.crawlerLogger.jobFailed({ + job_id: 0, + store_id: storeId, + store_name: dispensary.name, + duration_ms: durationMs, + error_message: error.message, + provider: 'dutchie', + }); + // Check for provider change + try { + browser = await puppeteer_1.default.launch({ headless: true, args: ['--no-sandbox'] }); + const page = await browser.newPage(); + const url = dispensary.menu_url || dispensary.website; + if (url) { + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); + const changeResult = await (0, intelligence_detector_1.detectCategoryProviderChange)(page, category, 'dutchie'); + if (changeResult.changed) { + // Provider changed - move ONLY products to sandbox + await (0, intelligence_detector_1.moveCategoryToSandbox)(dispensaryId, category, `Provider changed from dutchie to ${changeResult.newProvider}`); + // Create sandbox entry for re-analysis + const sandboxId = await createCategorySandboxEntry(dispensaryId, category, changeResult.newProvider || 'unknown', null, { providerChangeDetected: true, previousProvider: 'dutchie' }); + await createCategorySandboxJob(dispensaryId, sandboxId, category, null, 'detection'); + // Log provider change + crawler_logger_1.crawlerLogger.providerChanged({ + dispensary_id: dispensaryId, + dispensary_name: dispensary.name, + old_provider: 'dutchie', + new_provider: changeResult.newProvider || 'unknown', + old_confidence: dispensary.product_confidence, + new_confidence: 0, + category: 'product', + }); + } + } + } + catch { + // Ignore detection errors + } + finally { + if (browser) + await browser.close(); + } + return { success: false, category, message: error.message }; + } +} +/** + * CrawlSpecialsJob - Production specials crawling + * Currently no production-ready providers, so always returns false + */ +async function runCrawlSpecialsJob(dispensaryId) { + const category = 'specials'; + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + // No production-ready providers for specials yet + if (dispensary.specials_crawler_mode !== 'production') { + return { success: false, category, message: 'Specials not in production mode' }; + } + // Would implement provider-specific specials crawling here + // For now, no providers are production-ready + return { + success: false, + category, + message: `No production crawler for specials provider: ${dispensary.specials_provider}`, + }; +} +/** + * CrawlBrandIntelligenceJob - Production brand intelligence crawling + * Currently no production-ready providers + */ +async function runCrawlBrandIntelligenceJob(dispensaryId) { + const category = 'brand'; + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + if (dispensary.brand_crawler_mode !== 'production') { + return { success: false, category, message: 'Brand not in production mode' }; + } + return { + success: false, + category, + message: `No production crawler for brand provider: ${dispensary.brand_provider}`, + }; +} +/** + * CrawlMetadataJob - Production metadata crawling + * Currently no production-ready providers + */ +async function runCrawlMetadataJob(dispensaryId) { + const category = 'metadata'; + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + if (dispensary.metadata_crawler_mode !== 'production') { + return { success: false, category, message: 'Metadata not in production mode' }; + } + return { + success: false, + category, + message: `No production crawler for metadata provider: ${dispensary.metadata_provider}`, + }; +} +// ======================================== +// Sandbox Crawl Jobs +// ======================================== +/** + * SandboxProductsJob - Sandbox product crawling + * Works with any provider including Treez + */ +async function runSandboxProductsJob(dispensaryId, sandboxId) { + const category = 'product'; + const startTime = Date.now(); + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + // Get or create sandbox entry + let sandbox; + if (sandboxId) { + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [sandboxId]); + sandbox = result.rows[0]; + } + else { + const result = await migrate_1.pool.query(`SELECT * FROM crawler_sandboxes + WHERE dispensary_id = $1 AND category = $2 AND status NOT IN ('moved_to_production', 'failed') + ORDER BY created_at DESC LIMIT 1`, [dispensaryId, category]); + sandbox = result.rows[0]; + if (!sandbox) { + const newSandboxId = await createCategorySandboxEntry(dispensaryId, category, dispensary.product_provider, null); + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [newSandboxId]); + sandbox = result.rows[0]; + } + } + const websiteUrl = dispensary.menu_url || dispensary.website; + if (!websiteUrl) { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'failed', failure_reason = 'No website URL' WHERE id = $1`, [sandbox.id]); + return { success: false, category, message: 'No website URL available' }; + } + let browser = null; + try { + // Update status + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'analyzing', updated_at = NOW() WHERE id = $1`, [sandbox.id]); + browser = await puppeteer_1.default.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + // Get provider-specific template if available + const provider = dispensary.product_provider || 'unknown'; + const template = await getCrawlerTemplate(provider, category, 'sandbox'); + let products = []; + let metrics = { + quality_score: 0, + items_extracted: 0, + fields_missing: 0, + error_count: 0, + }; + // Provider-specific extraction logic + if (provider === 'treez' && template) { + // Use Treez-specific extraction + const treezResult = await extractTreezProducts(page, websiteUrl); + products = treezResult.products; + metrics = treezResult.metrics; + } + else { + // Generic product extraction + const genericResult = await extractGenericProducts(page, websiteUrl); + products = genericResult.products; + metrics = genericResult.metrics; + } + // Update sandbox with results + metrics.sample_data = products.slice(0, 5); + await updateSandboxQuality(sandbox.id, metrics); + // Determine final status based on quality + const status = metrics.quality_score >= 70 ? 'ready_for_review' : + metrics.quality_score >= 40 ? 'needs_human_review' : 'pending'; + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET + status = $1, + urls_tested = $2, + updated_at = NOW() + WHERE id = $3`, [status, JSON.stringify([websiteUrl]), sandbox.id]); + // Update scan time + await updateCategoryScanTime(dispensaryId, category); + // Log sandbox completion + crawler_logger_1.crawlerLogger.sandboxEvent({ + event: 'sandbox_completed', + dispensary_id: dispensaryId, + dispensary_name: dispensary.name, + template_name: provider, + category: 'product', + quality_score: metrics.quality_score, + products_extracted: products.length, + fields_missing: metrics.fields_missing, + provider: provider, + }); + return { + success: true, + category, + message: `Sandbox crawl completed. ${products.length} products extracted, quality score ${metrics.quality_score}`, + data: { + sandboxId: sandbox.id, + productsExtracted: products.length, + qualityScore: metrics.quality_score, + status, + }, + }; + } + catch (error) { + // Log sandbox failure + crawler_logger_1.crawlerLogger.sandboxEvent({ + event: 'sandbox_failed', + dispensary_id: dispensaryId, + dispensary_name: dispensary.name, + template_name: dispensary.product_provider || 'unknown', + category: 'product', + error_message: error.message, + }); + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'failed', failure_reason = $1, error_count = error_count + 1 WHERE id = $2`, [error.message, sandbox.id]); + return { success: false, category, message: error.message }; + } + finally { + if (browser) + await browser.close(); + } +} +/** + * SandboxSpecialsJob - Sandbox specials crawling + */ +async function runSandboxSpecialsJob(dispensaryId, sandboxId) { + const category = 'specials'; + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + let sandbox; + if (sandboxId) { + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [sandboxId]); + sandbox = result.rows[0]; + } + else { + const newSandboxId = await createCategorySandboxEntry(dispensaryId, category, dispensary.specials_provider, null); + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [newSandboxId]); + sandbox = result.rows[0]; + } + const websiteUrl = dispensary.website; + if (!websiteUrl) { + return { success: false, category, message: 'No website URL available' }; + } + let browser = null; + try { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'analyzing', updated_at = NOW() WHERE id = $1`, [sandbox.id]); + browser = await puppeteer_1.default.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + const result = await extractSpecials(page, websiteUrl); + await updateSandboxQuality(sandbox.id, { + ...result.metrics, + sample_data: result.specials.slice(0, 5), + }); + const status = result.metrics.quality_score >= 70 ? 'ready_for_review' : + result.metrics.quality_score >= 40 ? 'needs_human_review' : 'pending'; + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = $1, updated_at = NOW() WHERE id = $2`, [status, sandbox.id]); + await updateCategoryScanTime(dispensaryId, category); + return { + success: true, + category, + message: `Sandbox specials crawl completed. ${result.specials.length} specials found.`, + data: { sandboxId: sandbox.id, specialsCount: result.specials.length }, + }; + } + catch (error) { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'failed', failure_reason = $1 WHERE id = $2`, [error.message, sandbox.id]); + return { success: false, category, message: error.message }; + } + finally { + if (browser) + await browser.close(); + } +} +/** + * SandboxBrandJob - Sandbox brand intelligence crawling + */ +async function runSandboxBrandJob(dispensaryId, sandboxId) { + const category = 'brand'; + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + let sandbox; + if (sandboxId) { + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [sandboxId]); + sandbox = result.rows[0]; + } + else { + const newSandboxId = await createCategorySandboxEntry(dispensaryId, category, dispensary.brand_provider, null); + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [newSandboxId]); + sandbox = result.rows[0]; + } + const websiteUrl = dispensary.website; + if (!websiteUrl) { + return { success: false, category, message: 'No website URL available' }; + } + let browser = null; + try { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'analyzing', updated_at = NOW() WHERE id = $1`, [sandbox.id]); + browser = await puppeteer_1.default.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + const result = await extractBrands(page, websiteUrl); + await updateSandboxQuality(sandbox.id, { + ...result.metrics, + sample_data: result.brands.slice(0, 10), + }); + const status = result.metrics.quality_score >= 70 ? 'ready_for_review' : 'pending'; + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = $1, updated_at = NOW() WHERE id = $2`, [status, sandbox.id]); + await updateCategoryScanTime(dispensaryId, category); + return { + success: true, + category, + message: `Sandbox brand crawl completed. ${result.brands.length} brands found.`, + data: { sandboxId: sandbox.id, brandsCount: result.brands.length }, + }; + } + catch (error) { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'failed', failure_reason = $1 WHERE id = $2`, [error.message, sandbox.id]); + return { success: false, category, message: error.message }; + } + finally { + if (browser) + await browser.close(); + } +} +/** + * SandboxMetadataJob - Sandbox metadata crawling + */ +async function runSandboxMetadataJob(dispensaryId, sandboxId) { + const category = 'metadata'; + const dispensary = await getDispensaryWithCategories(dispensaryId); + if (!dispensary) { + return { success: false, category, message: `Dispensary ${dispensaryId} not found` }; + } + let sandbox; + if (sandboxId) { + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [sandboxId]); + sandbox = result.rows[0]; + } + else { + const newSandboxId = await createCategorySandboxEntry(dispensaryId, category, dispensary.metadata_provider, null); + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [newSandboxId]); + sandbox = result.rows[0]; + } + const websiteUrl = dispensary.website; + if (!websiteUrl) { + return { success: false, category, message: 'No website URL available' }; + } + let browser = null; + try { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'analyzing', updated_at = NOW() WHERE id = $1`, [sandbox.id]); + browser = await puppeteer_1.default.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + const result = await extractMetadata(page, websiteUrl); + await updateSandboxQuality(sandbox.id, { + ...result.metrics, + sample_data: result.categories.slice(0, 20), + }); + const status = result.metrics.quality_score >= 70 ? 'ready_for_review' : 'pending'; + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = $1, updated_at = NOW() WHERE id = $2`, [status, sandbox.id]); + await updateCategoryScanTime(dispensaryId, category); + return { + success: true, + category, + message: `Sandbox metadata crawl completed. ${result.categories.length} categories found.`, + data: { sandboxId: sandbox.id, categoriesCount: result.categories.length }, + }; + } + catch (error) { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'failed', failure_reason = $1 WHERE id = $2`, [error.message, sandbox.id]); + return { success: false, category, message: error.message }; + } + finally { + if (browser) + await browser.close(); + } +} +// ======================================== +// Extraction Functions +// ======================================== +/** + * Extract products from Treez-powered sites + */ +async function extractTreezProducts(page, baseUrl) { + const products = []; + let errorCount = 0; + let fieldsMissing = 0; + try { + // Navigate to menu + const menuUrls = ['/menu', '/shop', '/products', '/order']; + let menuUrl = baseUrl; + for (const path of menuUrls) { + try { + const testUrl = new URL(path, baseUrl).toString(); + await page.goto(testUrl, { waitUntil: 'networkidle2', timeout: 20000 }); + const hasProducts = await page.evaluate(() => { + const text = document.body.innerText.toLowerCase(); + return text.includes('add to cart') || text.includes('thc') || text.includes('indica'); + }); + if (hasProducts) { + menuUrl = testUrl; + break; + } + } + catch { + // Try next URL + } + } + await page.goto(menuUrl, { waitUntil: 'networkidle2', timeout: 30000 }); + await new Promise(r => setTimeout(r, 3000)); // Wait for dynamic content + // Look for Treez API data in network requests or page content + const pageProducts = await page.evaluate(() => { + const extractedProducts = []; + // Try common Treez selectors + const selectors = [ + '.product-card', + '.menu-item', + '[data-product]', + '.product-tile', + '.menu-product', + ]; + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + if (elements.length > 3) { + elements.forEach((el) => { + const nameEl = el.querySelector('h2, h3, .product-name, .name, [class*="name"]'); + const priceEl = el.querySelector('.price, [class*="price"]'); + const thcEl = el.querySelector('[class*="thc"], [class*="potency"]'); + if (nameEl) { + extractedProducts.push({ + name: nameEl.textContent?.trim(), + price: priceEl?.textContent?.trim(), + thc: thcEl?.textContent?.trim(), + html: el.outerHTML.slice(0, 500), + }); + } + }); + break; + } + } + return extractedProducts; + }); + products.push(...pageProducts); + // Calculate quality metrics + for (const product of products) { + if (!product.name) + fieldsMissing++; + if (!product.price) + fieldsMissing++; + } + } + catch (error) { + // Error tracked via errorCount - logged at job level + errorCount++; + } + const qualityScore = products.length > 0 + ? Math.min(100, Math.max(0, 100 - (fieldsMissing * 5) - (errorCount * 10))) + : 0; + return { + products, + metrics: { + quality_score: qualityScore, + items_extracted: products.length, + fields_missing: fieldsMissing, + error_count: errorCount, + }, + }; +} +/** + * Extract products using generic selectors + */ +async function extractGenericProducts(page, baseUrl) { + const products = []; + let errorCount = 0; + let fieldsMissing = 0; + try { + // Try common menu paths + const menuPaths = ['/menu', '/shop', '/products', '/order']; + let foundMenu = false; + for (const path of menuPaths) { + try { + const fullUrl = new URL(path, baseUrl).toString(); + await page.goto(fullUrl, { waitUntil: 'networkidle2', timeout: 20000 }); + const hasProducts = await page.evaluate(() => { + const text = document.body.innerText.toLowerCase(); + return text.includes('add to cart') || text.includes('thc') || text.includes('gram'); + }); + if (hasProducts) { + foundMenu = true; + break; + } + } + catch { + continue; + } + } + if (!foundMenu) { + await page.goto(baseUrl, { waitUntil: 'networkidle2', timeout: 30000 }); + } + await new Promise(r => setTimeout(r, 2000)); + // Generic product extraction + const pageProducts = await page.evaluate(() => { + const extractedProducts = []; + const selectors = [ + '.product', + '.product-card', + '.menu-item', + '.item-card', + '[data-product]', + '.strain', + '.listing', + ]; + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + if (elements.length > 3) { + elements.forEach((el) => { + const nameEl = el.querySelector('h2, h3, h4, .name, .title, [class*="name"]'); + const priceEl = el.querySelector('.price, [class*="price"]'); + const brandEl = el.querySelector('.brand, [class*="brand"]'); + const categoryEl = el.querySelector('.category, [class*="category"], [class*="type"]'); + if (nameEl?.textContent?.trim()) { + extractedProducts.push({ + name: nameEl.textContent.trim(), + price: priceEl?.textContent?.trim(), + brand: brandEl?.textContent?.trim(), + category: categoryEl?.textContent?.trim(), + }); + } + }); + break; + } + } + return extractedProducts; + }); + products.push(...pageProducts); + // Calculate missing fields + for (const product of products) { + if (!product.name) + fieldsMissing++; + if (!product.price) + fieldsMissing++; + } + } + catch (error) { + // Error tracked via errorCount - logged at job level + errorCount++; + } + const qualityScore = products.length > 0 + ? Math.min(100, Math.max(0, 80 - (fieldsMissing * 3) - (errorCount * 10))) + : 0; + return { + products, + metrics: { + quality_score: qualityScore, + items_extracted: products.length, + fields_missing: fieldsMissing, + error_count: errorCount, + }, + }; +} +/** + * Extract specials/deals + */ +async function extractSpecials(page, baseUrl) { + const specials = []; + let errorCount = 0; + let fieldsMissing = 0; + try { + const specialsPaths = ['/specials', '/deals', '/promotions', '/offers', '/sale']; + for (const path of specialsPaths) { + try { + const fullUrl = new URL(path, baseUrl).toString(); + await page.goto(fullUrl, { waitUntil: 'networkidle2', timeout: 20000 }); + const pageSpecials = await page.evaluate(() => { + const extracted = []; + const selectors = [ + '.special', + '.deal', + '.promotion', + '.offer', + '[class*="special"]', + '[class*="deal"]', + ]; + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + elements.forEach((el) => { + const titleEl = el.querySelector('h2, h3, h4, .title, .name'); + const descEl = el.querySelector('p, .description, .details'); + const discountEl = el.querySelector('.discount, .savings, [class*="percent"]'); + if (titleEl?.textContent?.trim()) { + extracted.push({ + title: titleEl.textContent.trim(), + description: descEl?.textContent?.trim(), + discount: discountEl?.textContent?.trim(), + }); + } + }); + } + return extracted; + }); + specials.push(...pageSpecials); + if (specials.length > 0) + break; + } + catch { + continue; + } + } + for (const special of specials) { + if (!special.title) + fieldsMissing++; + if (!special.description && !special.discount) + fieldsMissing++; + } + } + catch (error) { + // Error tracked via errorCount - logged at job level + errorCount++; + } + const qualityScore = specials.length > 0 + ? Math.min(100, Math.max(0, 70 - (fieldsMissing * 5) - (errorCount * 10))) + : 0; + return { + specials, + metrics: { + quality_score: qualityScore, + items_extracted: specials.length, + fields_missing: fieldsMissing, + error_count: errorCount, + }, + }; +} +/** + * Extract brand information + */ +async function extractBrands(page, baseUrl) { + const brands = []; + let errorCount = 0; + let fieldsMissing = 0; + try { + const brandPaths = ['/brands', '/vendors', '/producers', '/menu']; + for (const path of brandPaths) { + try { + const fullUrl = new URL(path, baseUrl).toString(); + await page.goto(fullUrl, { waitUntil: 'networkidle2', timeout: 20000 }); + const pageBrands = await page.evaluate(() => { + const extracted = []; + const brandNames = new Set(); + // Look for brand elements + const selectors = [ + '.brand', + '[class*="brand"]', + '.vendor', + '.producer', + ]; + for (const selector of selectors) { + document.querySelectorAll(selector).forEach((el) => { + const name = el.textContent?.trim(); + if (name && name.length > 1 && name.length < 100 && !brandNames.has(name)) { + brandNames.add(name); + extracted.push({ name }); + } + }); + } + // Also extract from filter dropdowns + document.querySelectorAll('select option, [role="option"]').forEach((el) => { + const name = el.textContent?.trim(); + if (name && name.length > 1 && name.length < 100 && !brandNames.has(name)) { + const lowerName = name.toLowerCase(); + if (!['all', 'any', 'select', 'choose', '--'].some(skip => lowerName.includes(skip))) { + brandNames.add(name); + extracted.push({ name, source: 'filter' }); + } + } + }); + return extracted; + }); + brands.push(...pageBrands); + if (brands.length > 5) + break; + } + catch { + continue; + } + } + } + catch (error) { + // Error tracked via errorCount - logged at job level + errorCount++; + } + const qualityScore = brands.length > 0 + ? Math.min(100, Math.max(0, 60 + Math.min(30, brands.length * 2) - (errorCount * 10))) + : 0; + return { + brands, + metrics: { + quality_score: qualityScore, + items_extracted: brands.length, + fields_missing: fieldsMissing, + error_count: errorCount, + }, + }; +} +/** + * Extract metadata (categories, taxonomy) + */ +async function extractMetadata(page, baseUrl) { + const categories = []; + let errorCount = 0; + let fieldsMissing = 0; + try { + await page.goto(baseUrl, { waitUntil: 'networkidle2', timeout: 30000 }); + const menuPaths = ['/menu', '/shop', '/products']; + for (const path of menuPaths) { + try { + await page.goto(new URL(path, baseUrl).toString(), { waitUntil: 'networkidle2', timeout: 15000 }); + break; + } + catch { + continue; + } + } + const pageCategories = await page.evaluate(() => { + const extracted = []; + const categoryNames = new Set(); + // Navigation/tab categories + const navSelectors = [ + 'nav a', + '.category-nav a', + '.menu-categories a', + '[class*="category"] a', + '.tabs button', + '.tab-list button', + ]; + for (const selector of navSelectors) { + document.querySelectorAll(selector).forEach((el) => { + const name = el.textContent?.trim(); + if (name && name.length > 1 && name.length < 50 && !categoryNames.has(name)) { + const lowerName = name.toLowerCase(); + const categoryKeywords = ['flower', 'edible', 'concentrate', 'vape', 'preroll', 'tincture', 'topical', 'accessory', 'indica', 'sativa', 'hybrid']; + if (categoryKeywords.some(kw => lowerName.includes(kw)) || el.closest('[class*="category"], [class*="menu"]')) { + categoryNames.add(name); + extracted.push({ name, type: 'navigation' }); + } + } + }); + } + // Filter categories + document.querySelectorAll('select, [role="listbox"]').forEach((select) => { + const label = select.getAttribute('aria-label') || select.previousElementSibling?.textContent?.trim(); + if (label?.toLowerCase().includes('category') || label?.toLowerCase().includes('type')) { + select.querySelectorAll('option, [role="option"]').forEach((opt) => { + const name = opt.textContent?.trim(); + if (name && name.length > 1 && !categoryNames.has(name)) { + const lowerName = name.toLowerCase(); + if (!['all', 'any', 'select', 'choose'].some(skip => lowerName.includes(skip))) { + categoryNames.add(name); + extracted.push({ name, type: 'filter' }); + } + } + }); + } + }); + return extracted; + }); + categories.push(...pageCategories); + } + catch (error) { + // Error tracked via errorCount - logged at job level + errorCount++; + } + const qualityScore = categories.length > 0 + ? Math.min(100, Math.max(0, 50 + Math.min(40, categories.length * 3) - (errorCount * 10))) + : 0; + return { + categories, + metrics: { + quality_score: qualityScore, + items_extracted: categories.length, + fields_missing: fieldsMissing, + error_count: errorCount, + }, + }; +} +// ======================================== +// Queue Processing Functions +// ======================================== +/** + * Process pending category-specific sandbox jobs + */ +async function processCategorySandboxJobs(category, limit = 5) { + const jobs = await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs + SET status = 'running', worker_id = $1, started_at = NOW() + WHERE id IN ( + SELECT id FROM sandbox_crawl_jobs + WHERE status = 'pending' AND category = $2 AND scheduled_at <= NOW() + ORDER BY priority DESC, scheduled_at ASC + LIMIT $3 + FOR UPDATE SKIP LOCKED + ) + RETURNING *`, [WORKER_ID, category, limit]); + for (const job of jobs.rows) { + try { + let result; + switch (category) { + case 'product': + result = await runSandboxProductsJob(job.dispensary_id, job.sandbox_id); + break; + case 'specials': + result = await runSandboxSpecialsJob(job.dispensary_id, job.sandbox_id); + break; + case 'brand': + result = await runSandboxBrandJob(job.dispensary_id, job.sandbox_id); + break; + case 'metadata': + result = await runSandboxMetadataJob(job.dispensary_id, job.sandbox_id); + break; + default: + result = { success: false, category, message: `Unknown category: ${category}` }; + } + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs + SET status = $1, completed_at = NOW(), result_summary = $2, error_message = $3 + WHERE id = $4`, [ + result.success ? 'completed' : 'failed', + JSON.stringify(result.data || {}), + result.success ? null : result.message, + job.id, + ]); + } + catch (error) { + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs SET status = 'failed', error_message = $1 WHERE id = $2`, [error.message, job.id]); + } + } +} +/** + * Run all category production crawls for a dispensary + * Each category runs independently - failures don't affect others + */ +async function runAllCategoryProductionCrawls(dispensaryId) { + const results = []; + // Run all categories in parallel - independent failures + const [productResult, specialsResult, brandResult, metadataResult] = await Promise.allSettled([ + runCrawlProductsJob(dispensaryId), + runCrawlSpecialsJob(dispensaryId), + runCrawlBrandIntelligenceJob(dispensaryId), + runCrawlMetadataJob(dispensaryId), + ]); + if (productResult.status === 'fulfilled') + results.push(productResult.value); + else + results.push({ success: false, category: 'product', message: productResult.reason?.message || 'Unknown error' }); + if (specialsResult.status === 'fulfilled') + results.push(specialsResult.value); + else + results.push({ success: false, category: 'specials', message: specialsResult.reason?.message || 'Unknown error' }); + if (brandResult.status === 'fulfilled') + results.push(brandResult.value); + else + results.push({ success: false, category: 'brand', message: brandResult.reason?.message || 'Unknown error' }); + if (metadataResult.status === 'fulfilled') + results.push(metadataResult.value); + else + results.push({ success: false, category: 'metadata', message: metadataResult.reason?.message || 'Unknown error' }); + const successCount = results.filter(r => r.success).length; + const summary = `${successCount}/4 categories succeeded: ${results.map(r => `${r.category}:${r.success ? 'ok' : 'fail'}`).join(', ')}`; + // Individual category jobs log their own completion via crawlerLogger + return { results, summary }; +} +/** + * Run all category sandbox crawls for a dispensary + */ +async function runAllCategorySandboxCrawls(dispensaryId) { + const results = []; + const [productResult, specialsResult, brandResult, metadataResult] = await Promise.allSettled([ + runSandboxProductsJob(dispensaryId), + runSandboxSpecialsJob(dispensaryId), + runSandboxBrandJob(dispensaryId), + runSandboxMetadataJob(dispensaryId), + ]); + if (productResult.status === 'fulfilled') + results.push(productResult.value); + else + results.push({ success: false, category: 'product', message: productResult.reason?.message || 'Unknown error' }); + if (specialsResult.status === 'fulfilled') + results.push(specialsResult.value); + else + results.push({ success: false, category: 'specials', message: specialsResult.reason?.message || 'Unknown error' }); + if (brandResult.status === 'fulfilled') + results.push(brandResult.value); + else + results.push({ success: false, category: 'brand', message: brandResult.reason?.message || 'Unknown error' }); + if (metadataResult.status === 'fulfilled') + results.push(metadataResult.value); + else + results.push({ success: false, category: 'metadata', message: metadataResult.reason?.message || 'Unknown error' }); + const successCount = results.filter(r => r.success).length; + const summary = `${successCount}/4 sandbox crawls: ${results.map(r => `${r.category}:${r.success ? 'ok' : 'fail'}`).join(', ')}`; + // Individual sandbox jobs log their own completion via crawlerLogger + return { results, summary }; +} diff --git a/backend/dist/services/category-discovery.js b/backend/dist/services/category-discovery.js index 850e5317..ce53f818 100644 --- a/backend/dist/services/category-discovery.js +++ b/backend/dist/services/category-discovery.js @@ -4,9 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.discoverCategories = discoverCategories; -const puppeteer_1 = __importDefault(require("puppeteer")); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); const migrate_1 = require("../db/migrate"); const logger_1 = require("./logger"); +const age_gate_1 = require("../utils/age-gate"); +const dutchie_1 = require("../scrapers/templates/dutchie"); +// Apply stealth plugin +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); const DUTCHIE_CATEGORIES = [ { name: 'Shop', slug: 'shop' }, { name: 'Flower', slug: 'flower', parentSlug: 'shop' }, @@ -19,6 +24,18 @@ const DUTCHIE_CATEGORIES = [ { name: 'Brands', slug: 'brands' }, { name: 'Specials', slug: 'specials' } ]; +const CURALEAF_CATEGORIES = [ + { name: 'Shop', slug: 'shop' }, + { name: 'Flower', slug: 'flower', parentSlug: 'shop' }, + { name: 'Pre-Rolls', slug: 'pre-rolls', parentSlug: 'shop' }, + { name: 'Vaporizers', slug: 'vaporizers', parentSlug: 'shop' }, + { name: 'Concentrates', slug: 'concentrates', parentSlug: 'shop' }, + { name: 'Edibles', slug: 'edibles', parentSlug: 'shop' }, + { name: 'Tinctures', slug: 'tinctures', parentSlug: 'shop' }, + { name: 'Topicals', slug: 'topicals', parentSlug: 'shop' }, + { name: 'Capsules', slug: 'capsules', parentSlug: 'shop' }, + { name: 'Accessories', slug: 'accessories', parentSlug: 'shop' } +]; async function makePageStealthy(page) { await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', { get: () => false }); @@ -72,7 +89,7 @@ async function discoverCategories(storeId) { const store = storeResult.rows[0]; const baseUrl = store.dutchie_url; // Launch browser to check page source - browser = await puppeteer_1.default.launch({ + browser = await puppeteer_extra_1.default.launch({ headless: 'new', args: [ '--no-sandbox', @@ -85,9 +102,14 @@ async function discoverCategories(storeId) { await makePageStealthy(page); await page.setViewport({ width: 1920, height: 1080 }); await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + // Set age gate bypass cookies BEFORE navigation (standard for all cannabis sites) + const state = (0, age_gate_1.detectStateFromUrl)(baseUrl); + await (0, age_gate_1.setAgeGateCookies)(page, baseUrl, state); logger_1.logger.info('categories', `Loading page to detect menu type: ${baseUrl}`); await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForTimeout(3000); + // If age gate still appears, try to bypass it + await (0, age_gate_1.bypassAgeGate)(page, state); // Detect if it's a Dutchie menu by inspecting page source const isDutchie = await isDutchieMenu(page); await browser.close(); @@ -97,8 +119,9 @@ async function discoverCategories(storeId) { await createDutchieCategories(storeId, store); } else { - logger_1.logger.info('categories', `⚠️ Non-Dutchie menu detected, would need custom scraping logic`); - throw new Error('Non-Dutchie menus not yet supported. Please contact support.'); + // Fallback: Use standard cannabis categories for non-Dutchie sites + logger_1.logger.info('categories', `Non-Dutchie menu detected, using standard cannabis categories for ${store.name}`); + await createCuraleafCategories(storeId, store); } } catch (error) { @@ -116,24 +139,24 @@ async function createDutchieCategories(storeId, store) { const baseUrl = store.dutchie_url; for (const category of DUTCHIE_CATEGORIES) { let categoryUrl; + // Use Dutchie template to build correct category URLs if (category.parentSlug) { - // Subcategory: /embedded-menu/{slug}/shop/flower - categoryUrl = `${baseUrl}/${category.parentSlug}/${category.slug}`; + // Subcategory: Use template's buildCategoryUrl (e.g., /products/flower) + categoryUrl = dutchie_1.dutchieTemplate.buildCategoryUrl(baseUrl, category.name); } else { - // Top-level: /embedded-menu/{slug}/shop + // Top-level: Use base URL with slug categoryUrl = `${baseUrl}/${category.slug}`; } - const path = category.parentSlug ? `${category.parentSlug}/${category.slug}` : category.slug; if (!category.parentSlug) { // Create parent category await client.query(` - INSERT INTO categories (store_id, name, slug, dutchie_url, path, scrape_enabled, parent_id) - VALUES ($1, $2, $3, $4, $5, true, NULL) - ON CONFLICT (store_id, slug) - DO UPDATE SET name = $2, dutchie_url = $4, path = $5 + INSERT INTO categories (store_id, name, slug, dutchie_url, scrape_enabled) + VALUES ($1, $2, $3, $4, true) + ON CONFLICT (store_id, slug) + DO UPDATE SET name = $2, dutchie_url = $4 RETURNING id - `, [storeId, category.name, category.slug, categoryUrl, path]); + `, [storeId, category.name, category.slug, categoryUrl]); logger_1.logger.info('categories', `📁 ${category.name}`); } else { @@ -143,13 +166,12 @@ async function createDutchieCategories(storeId, store) { WHERE store_id = $1 AND slug = $2 `, [storeId, category.parentSlug]); if (parentResult.rows.length > 0) { - const parentId = parentResult.rows[0].id; await client.query(` - INSERT INTO categories (store_id, name, slug, dutchie_url, path, scrape_enabled, parent_id) - VALUES ($1, $2, $3, $4, $5, true, $6) + INSERT INTO categories (store_id, name, slug, dutchie_url, scrape_enabled) + VALUES ($1, $2, $3, $4, true) ON CONFLICT (store_id, slug) - DO UPDATE SET name = $2, dutchie_url = $4, path = $5, parent_id = $6 - `, [storeId, category.name, category.slug, categoryUrl, path, parentId]); + DO UPDATE SET name = $2, dutchie_url = $4 + `, [storeId, category.name, category.slug, categoryUrl]); logger_1.logger.info('categories', ` └── ${category.name}`); } } @@ -166,3 +188,59 @@ async function createDutchieCategories(storeId, store) { client.release(); } } +async function createCuraleafCategories(storeId, store) { + const client = await migrate_1.pool.connect(); + try { + await client.query('BEGIN'); + logger_1.logger.info('categories', `Creating predefined Curaleaf category structure`); + const baseUrl = store.dutchie_url; + for (const category of CURALEAF_CATEGORIES) { + let categoryUrl; + if (category.parentSlug) { + // Subcategory URL - Curaleaf uses pattern like: /stores/{store-slug}/{category} + categoryUrl = `${baseUrl}?category=${category.slug}`; + } + else { + // Top-level category + categoryUrl = baseUrl; + } + if (!category.parentSlug) { + // Create parent category + await client.query(` + INSERT INTO categories (store_id, name, slug, dutchie_url, scrape_enabled) + VALUES ($1, $2, $3, $4, true) + ON CONFLICT (store_id, slug) + DO UPDATE SET name = $2, dutchie_url = $4 + RETURNING id + `, [storeId, category.name, category.slug, categoryUrl]); + logger_1.logger.info('categories', `📁 ${category.name}`); + } + else { + // Create subcategory + const parentResult = await client.query(` + SELECT id FROM categories + WHERE store_id = $1 AND slug = $2 + `, [storeId, category.parentSlug]); + if (parentResult.rows.length > 0) { + await client.query(` + INSERT INTO categories (store_id, name, slug, dutchie_url, scrape_enabled) + VALUES ($1, $2, $3, $4, true) + ON CONFLICT (store_id, slug) + DO UPDATE SET name = $2, dutchie_url = $4 + `, [storeId, category.name, category.slug, categoryUrl]); + logger_1.logger.info('categories', ` └── ${category.name}`); + } + } + } + await client.query('COMMIT'); + logger_1.logger.info('categories', `✅ Created ${CURALEAF_CATEGORIES.length} Curaleaf categories successfully`); + } + catch (error) { + await client.query('ROLLBACK'); + logger_1.logger.error('categories', `Failed to create Curaleaf categories: ${error}`); + throw error; + } + finally { + client.release(); + } +} diff --git a/backend/dist/services/crawl-scheduler.js b/backend/dist/services/crawl-scheduler.js new file mode 100644 index 00000000..271609bc --- /dev/null +++ b/backend/dist/services/crawl-scheduler.js @@ -0,0 +1,536 @@ +"use strict"; +/** + * Crawl Scheduler Service + * + * This service manages crawl scheduling using a job queue approach. + * It does NOT modify the crawler - it only TRIGGERS the existing crawler. + * + * Features: + * - Global schedule: crawl all stores every N hours + * - Daily special run: 12:01 AM local store time + * - Per-store schedule overrides + * - Job queue for tracking pending/running crawls + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getGlobalSchedule = getGlobalSchedule; +exports.updateGlobalSchedule = updateGlobalSchedule; +exports.getStoreScheduleStatuses = getStoreScheduleStatuses; +exports.getStoreSchedule = getStoreSchedule; +exports.updateStoreSchedule = updateStoreSchedule; +exports.createCrawlJob = createCrawlJob; +exports.getPendingJobs = getPendingJobs; +exports.claimJob = claimJob; +exports.completeJob = completeJob; +exports.getRecentJobs = getRecentJobs; +exports.getAllRecentJobs = getAllRecentJobs; +exports.checkAndCreateScheduledJobs = checkAndCreateScheduledJobs; +exports.checkAndCreateDailySpecialJobs = checkAndCreateDailySpecialJobs; +exports.processJobs = processJobs; +exports.processOrchestrator = processOrchestrator; +exports.setSchedulerMode = setSchedulerMode; +exports.getSchedulerMode = getSchedulerMode; +exports.startCrawlScheduler = startCrawlScheduler; +exports.stopCrawlScheduler = stopCrawlScheduler; +exports.restartCrawlScheduler = restartCrawlScheduler; +exports.triggerManualCrawl = triggerManualCrawl; +exports.triggerAllStoresCrawl = triggerAllStoresCrawl; +exports.cancelJob = cancelJob; +const node_cron_1 = __importDefault(require("node-cron")); +const migrate_1 = require("../db/migrate"); +const scraper_v2_1 = require("../scraper-v2"); +const store_crawl_orchestrator_1 = require("./store-crawl-orchestrator"); +// Worker identification +const WORKER_ID = `worker-${process.pid}-${Date.now()}`; +let schedulerCronJob = null; +let jobProcessorRunning = false; +let orchestratorProcessorRunning = false; +// Scheduler mode: 'legacy' uses job queue, 'orchestrator' uses intelligent orchestration +let schedulerMode = 'orchestrator'; +// ============================================ +// Schedule Management +// ============================================ +/** + * Get global schedule settings + */ +async function getGlobalSchedule() { + const result = await migrate_1.pool.query(` + SELECT * FROM crawler_schedule ORDER BY id + `); + return result.rows; +} +/** + * Update global schedule setting + */ +async function updateGlobalSchedule(scheduleType, updates) { + const setClauses = []; + const values = []; + let paramIndex = 1; + if (updates.enabled !== undefined) { + setClauses.push(`enabled = $${paramIndex++}`); + values.push(updates.enabled); + } + if (updates.interval_hours !== undefined) { + setClauses.push(`interval_hours = $${paramIndex++}`); + values.push(updates.interval_hours); + } + if (updates.run_time !== undefined) { + setClauses.push(`run_time = $${paramIndex++}`); + values.push(updates.run_time); + } + values.push(scheduleType); + const result = await migrate_1.pool.query(` + UPDATE crawler_schedule + SET ${setClauses.join(', ')} + WHERE schedule_type = $${paramIndex} + RETURNING * + `, values); + return result.rows[0]; +} +/** + * Get all store schedule statuses + */ +async function getStoreScheduleStatuses() { + const result = await migrate_1.pool.query(`SELECT * FROM crawl_schedule_status ORDER BY priority DESC, store_name`); + return result.rows; +} +/** + * Get or create per-store schedule override + */ +async function getStoreSchedule(storeId) { + const result = await migrate_1.pool.query(` + SELECT * FROM store_crawl_schedule WHERE store_id = $1 + `, [storeId]); + if (result.rows.length > 0) { + return result.rows[0]; + } + // Return default (use global) + return { + store_id: storeId, + enabled: true, + interval_hours: null, + daily_special_enabled: true, + daily_special_time: null, + priority: 0 + }; +} +/** + * Update per-store schedule override + */ +async function updateStoreSchedule(storeId, updates) { + const result = await migrate_1.pool.query(` + INSERT INTO store_crawl_schedule (store_id, enabled, interval_hours, daily_special_enabled, daily_special_time, priority) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (store_id) DO UPDATE SET + enabled = COALESCE(EXCLUDED.enabled, store_crawl_schedule.enabled), + interval_hours = EXCLUDED.interval_hours, + daily_special_enabled = COALESCE(EXCLUDED.daily_special_enabled, store_crawl_schedule.daily_special_enabled), + daily_special_time = EXCLUDED.daily_special_time, + priority = COALESCE(EXCLUDED.priority, store_crawl_schedule.priority), + updated_at = NOW() + RETURNING * + `, [ + storeId, + updates.enabled ?? true, + updates.interval_hours ?? null, + updates.daily_special_enabled ?? true, + updates.daily_special_time ?? null, + updates.priority ?? 0 + ]); + return result.rows[0]; +} +// ============================================ +// Job Queue Management +// ============================================ +/** + * Create a new crawl job + */ +async function createCrawlJob(storeId, jobType = 'full_crawl', triggerType = 'scheduled', scheduledAt = new Date(), priority = 0) { + // Check if there's already a pending or running job for this store + const existing = await migrate_1.pool.query(` + SELECT id FROM crawl_jobs + WHERE store_id = $1 AND status IN ('pending', 'running') + LIMIT 1 + `, [storeId]); + if (existing.rows.length > 0) { + console.log(`Skipping job creation for store ${storeId} - already has pending/running job`); + return existing.rows[0]; + } + const result = await migrate_1.pool.query(` + INSERT INTO crawl_jobs (store_id, job_type, trigger_type, scheduled_at, priority, status) + VALUES ($1, $2, $3, $4, $5, 'pending') + RETURNING * + `, [storeId, jobType, triggerType, scheduledAt, priority]); + console.log(`Created crawl job ${result.rows[0].id} for store ${storeId} (${triggerType})`); + return result.rows[0]; +} +/** + * Get pending jobs ready to run + */ +async function getPendingJobs(limit = 5) { + const result = await migrate_1.pool.query(` + SELECT cj.*, s.name as store_name + FROM crawl_jobs cj + JOIN stores s ON s.id = cj.store_id + WHERE cj.status = 'pending' + AND cj.scheduled_at <= NOW() + ORDER BY cj.priority DESC, cj.scheduled_at ASC + LIMIT $1 + `, [limit]); + return result.rows; +} +/** + * Claim a job for processing + */ +async function claimJob(jobId) { + const result = await migrate_1.pool.query(` + UPDATE crawl_jobs + SET status = 'running', started_at = NOW(), worker_id = $2 + WHERE id = $1 AND status = 'pending' + RETURNING id + `, [jobId, WORKER_ID]); + return result.rows.length > 0; +} +/** + * Complete a job + */ +async function completeJob(jobId, success, results) { + await migrate_1.pool.query(` + UPDATE crawl_jobs + SET + status = $2, + completed_at = NOW(), + products_found = $3, + error_message = $4 + WHERE id = $1 + `, [ + jobId, + success ? 'completed' : 'failed', + results?.products_found ?? null, + results?.error_message ?? null + ]); +} +/** + * Get recent jobs for a store + */ +async function getRecentJobs(storeId, limit = 10) { + const result = await migrate_1.pool.query(` + SELECT * FROM crawl_jobs + WHERE store_id = $1 + ORDER BY created_at DESC + LIMIT $2 + `, [storeId, limit]); + return result.rows; +} +/** + * Get all recent jobs + */ +async function getAllRecentJobs(limit = 50) { + const result = await migrate_1.pool.query(` + SELECT cj.*, s.name as store_name, s.slug as store_slug + FROM crawl_jobs cj + JOIN stores s ON s.id = cj.store_id + ORDER BY cj.created_at DESC + LIMIT $1 + `, [limit]); + return result.rows; +} +// ============================================ +// Scheduler Logic +// ============================================ +/** + * Check which stores are due for a crawl and create jobs + */ +async function checkAndCreateScheduledJobs() { + console.log('Checking for stores due for crawl...'); + // Get global schedule settings + const globalSchedule = await migrate_1.pool.query(` + SELECT * FROM crawler_schedule WHERE schedule_type = 'global_interval' + `); + if (globalSchedule.rows.length === 0 || !globalSchedule.rows[0].enabled) { + console.log('Global scheduler is disabled'); + return 0; + } + const intervalHours = globalSchedule.rows[0].interval_hours || 4; + // Find stores due for crawl + const result = await migrate_1.pool.query(` + SELECT + s.id, + s.name, + s.timezone, + s.last_scraped_at, + COALESCE(scs.enabled, TRUE) as schedule_enabled, + COALESCE(scs.interval_hours, $1) as interval_hours, + COALESCE(scs.priority, 0) as priority + FROM stores s + LEFT JOIN store_crawl_schedule scs ON scs.store_id = s.id + WHERE s.active = TRUE + AND s.scrape_enabled = TRUE + AND COALESCE(scs.enabled, TRUE) = TRUE + AND ( + s.last_scraped_at IS NULL + OR s.last_scraped_at < NOW() - (COALESCE(scs.interval_hours, $1) || ' hours')::INTERVAL + ) + AND NOT EXISTS ( + SELECT 1 FROM crawl_jobs cj + WHERE cj.store_id = s.id AND cj.status IN ('pending', 'running') + ) + ORDER BY COALESCE(scs.priority, 0) DESC, s.last_scraped_at ASC NULLS FIRST + `, [intervalHours]); + let jobsCreated = 0; + for (const store of result.rows) { + try { + await createCrawlJob(store.id, 'full_crawl', 'scheduled', new Date(), store.priority); + jobsCreated++; + console.log(`Scheduled crawl job for: ${store.name}`); + } + catch (error) { + console.error(`Failed to create job for store ${store.name}:`, error); + } + } + console.log(`Created ${jobsCreated} scheduled crawl jobs`); + return jobsCreated; +} +/** + * Check for daily special runs (12:01 AM local time) + */ +async function checkAndCreateDailySpecialJobs() { + console.log('Checking for daily special runs...'); + // Get daily special schedule + const dailySchedule = await migrate_1.pool.query(` + SELECT * FROM crawler_schedule WHERE schedule_type = 'daily_special' + `); + if (dailySchedule.rows.length === 0 || !dailySchedule.rows[0].enabled) { + console.log('Daily special scheduler is disabled'); + return 0; + } + const targetTime = dailySchedule.rows[0].run_time || '00:01'; + // Find stores where it's currently the target time in their local timezone + // and they haven't had a daily special run today + const result = await migrate_1.pool.query(` + SELECT + s.id, + s.name, + s.timezone, + COALESCE(scs.daily_special_enabled, TRUE) as daily_special_enabled, + COALESCE(scs.daily_special_time, $1::TIME) as daily_special_time, + COALESCE(scs.priority, 0) as priority + FROM stores s + LEFT JOIN store_crawl_schedule scs ON scs.store_id = s.id + WHERE s.active = TRUE + AND s.scrape_enabled = TRUE + AND COALESCE(scs.daily_special_enabled, TRUE) = TRUE + -- Check if current time in store timezone matches the target time (within 2 minutes) + AND ABS( + EXTRACT(EPOCH FROM ( + (NOW() AT TIME ZONE COALESCE(s.timezone, 'America/Phoenix'))::TIME + - COALESCE(scs.daily_special_time, $1::TIME) + )) + ) < 120 -- within 2 minutes + -- Ensure we haven't already created a daily_special job today for this store + AND NOT EXISTS ( + SELECT 1 FROM crawl_jobs cj + WHERE cj.store_id = s.id + AND cj.trigger_type = 'daily_special' + AND cj.created_at > (NOW() AT TIME ZONE COALESCE(s.timezone, 'America/Phoenix'))::DATE + ) + AND NOT EXISTS ( + SELECT 1 FROM crawl_jobs cj + WHERE cj.store_id = s.id AND cj.status IN ('pending', 'running') + ) + ORDER BY COALESCE(scs.priority, 0) DESC + `, [targetTime]); + let jobsCreated = 0; + for (const store of result.rows) { + try { + await createCrawlJob(store.id, 'full_crawl', 'daily_special', new Date(), store.priority + 10); + jobsCreated++; + console.log(`Created daily special job for: ${store.name} (${store.timezone})`); + } + catch (error) { + console.error(`Failed to create daily special job for store ${store.name}:`, error); + } + } + if (jobsCreated > 0) { + console.log(`Created ${jobsCreated} daily special crawl jobs`); + } + return jobsCreated; +} +/** + * Process pending jobs + */ +async function processJobs() { + if (jobProcessorRunning) { + console.log('Job processor already running, skipping...'); + return; + } + jobProcessorRunning = true; + try { + const jobs = await getPendingJobs(1); // Process one at a time for safety + for (const job of jobs) { + console.log(`Processing job ${job.id} for store: ${job.store_name}`); + const claimed = await claimJob(job.id); + if (!claimed) { + console.log(`Job ${job.id} already claimed by another worker`); + continue; + } + try { + // Call the existing scraper - DO NOT MODIFY SCRAPER LOGIC + await (0, scraper_v2_1.scrapeStore)(job.store_id); + // Update store's last_scraped_at + await migrate_1.pool.query(` + UPDATE stores SET last_scraped_at = NOW() WHERE id = $1 + `, [job.store_id]); + await completeJob(job.id, true, {}); + console.log(`Job ${job.id} completed successfully`); + } + catch (error) { + console.error(`Job ${job.id} failed:`, error); + await completeJob(job.id, false, { error_message: error.message }); + } + } + } + finally { + jobProcessorRunning = false; + } +} +/** + * Process stores using the intelligent orchestrator + * This replaces the simple job queue approach with intelligent provider detection + */ +async function processOrchestrator() { + if (orchestratorProcessorRunning) { + console.log('Orchestrator processor already running, skipping...'); + return; + } + orchestratorProcessorRunning = true; + try { + // Get stores due for orchestration (respects schedule, intervals, etc.) + const storeIds = await (0, store_crawl_orchestrator_1.getStoresDueForOrchestration)(3); // Process up to 3 at a time + if (storeIds.length === 0) { + return; + } + console.log(`Orchestrator: Processing ${storeIds.length} stores due for crawl`); + // Process each store through the orchestrator + for (const storeId of storeIds) { + try { + console.log(`Orchestrator: Starting crawl for store ${storeId}`); + const result = await (0, store_crawl_orchestrator_1.runStoreCrawlOrchestrator)(storeId); + console.log(`Orchestrator: Store ${storeId} completed - ${result.summary}`); + } + catch (error) { + console.error(`Orchestrator: Store ${storeId} failed - ${error.message}`); + } + } + console.log(`Orchestrator: Finished processing ${storeIds.length} stores`); + } + finally { + orchestratorProcessorRunning = false; + } +} +// ============================================ +// Scheduler Control +// ============================================ +/** + * Set scheduler mode + */ +function setSchedulerMode(mode) { + schedulerMode = mode; + console.log(`Scheduler mode set to: ${mode}`); +} +/** + * Get current scheduler mode + */ +function getSchedulerMode() { + return schedulerMode; +} +/** + * Start the scheduler (runs every minute to check for due jobs) + */ +async function startCrawlScheduler() { + stopCrawlScheduler(); + console.log(`Starting crawl scheduler in ${schedulerMode} mode...`); + // Run every minute + schedulerCronJob = node_cron_1.default.schedule('* * * * *', async () => { + try { + if (schedulerMode === 'orchestrator') { + // Use intelligent orchestrator (handles detection + crawl) + await processOrchestrator(); + } + else { + // Legacy mode: job queue approach + // Check for interval-based scheduled jobs + await checkAndCreateScheduledJobs(); + // Check for daily special runs + await checkAndCreateDailySpecialJobs(); + // Process any pending jobs + await processJobs(); + } + } + catch (error) { + console.error('Scheduler tick error:', error); + } + }); + console.log(`Crawl scheduler started in ${schedulerMode} mode (checking every minute)`); +} +/** + * Stop the scheduler + */ +function stopCrawlScheduler() { + if (schedulerCronJob) { + schedulerCronJob.stop(); + schedulerCronJob = null; + console.log('Crawl scheduler stopped'); + } +} +/** + * Restart the scheduler + */ +async function restartCrawlScheduler() { + await startCrawlScheduler(); +} +// ============================================ +// Manual Triggers +// ============================================ +/** + * Manually trigger a crawl for a specific store (creates a job immediately) + */ +async function triggerManualCrawl(storeId) { + console.log(`Manual crawl triggered for store ID: ${storeId}`); + return await createCrawlJob(storeId, 'full_crawl', 'manual', new Date(), 100); // High priority +} +/** + * Manually trigger crawls for all stores + */ +async function triggerAllStoresCrawl() { + console.log('Manual crawl triggered for all stores'); + const result = await migrate_1.pool.query(` + SELECT id, name FROM stores + WHERE active = TRUE AND scrape_enabled = TRUE + AND NOT EXISTS ( + SELECT 1 FROM crawl_jobs cj + WHERE cj.store_id = stores.id AND cj.status IN ('pending', 'running') + ) + `); + let jobsCreated = 0; + for (const store of result.rows) { + await createCrawlJob(store.id, 'full_crawl', 'manual', new Date(), 50); + jobsCreated++; + } + console.log(`Created ${jobsCreated} manual crawl jobs`); + return jobsCreated; +} +/** + * Cancel a pending job + */ +async function cancelJob(jobId) { + const result = await migrate_1.pool.query(` + UPDATE crawl_jobs + SET status = 'cancelled' + WHERE id = $1 AND status = 'pending' + RETURNING id + `, [jobId]); + return result.rows.length > 0; +} diff --git a/backend/dist/services/crawler-jobs.js b/backend/dist/services/crawler-jobs.js new file mode 100644 index 00000000..6bf28e3f --- /dev/null +++ b/backend/dist/services/crawler-jobs.js @@ -0,0 +1,476 @@ +"use strict"; +/** + * Crawler Jobs Service + * + * Handles three types of jobs: + * 1. DetectMenuProviderJob - Detect menu provider for a dispensary + * 2. DutchieMenuCrawlJob - Production Dutchie crawl + * 3. SandboxCrawlJob - Learning/testing crawl for unknown providers + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runDetectMenuProviderJob = runDetectMenuProviderJob; +exports.runDutchieMenuCrawlJob = runDutchieMenuCrawlJob; +exports.runSandboxCrawlJob = runSandboxCrawlJob; +exports.processSandboxJobs = processSandboxJobs; +const migrate_1 = require("../db/migrate"); +const logger_1 = require("./logger"); +const menu_provider_detector_1 = require("./menu-provider-detector"); +const scraper_v2_1 = require("../scraper-v2"); +const puppeteer_1 = __importDefault(require("puppeteer")); +const fs_1 = require("fs"); +const path_1 = __importDefault(require("path")); +const availability_1 = require("./availability"); +const WORKER_ID = `crawler-${process.pid}-${Date.now()}`; +// ======================================== +// Helper Functions +// ======================================== +async function getDispensary(dispensaryId) { + const result = await migrate_1.pool.query(`SELECT id, name, website, menu_url, menu_provider, menu_provider_confidence, + crawler_mode, crawler_status, scraper_template + FROM dispensaries WHERE id = $1`, [dispensaryId]); + return result.rows[0] || null; +} +async function updateDispensary(dispensaryId, updates) { + const setClauses = []; + const values = []; + let paramIndex = 1; + for (const [key, value] of Object.entries(updates)) { + setClauses.push(`${key} = $${paramIndex}`); + values.push(value); + paramIndex++; + } + setClauses.push(`updated_at = NOW()`); + values.push(dispensaryId); + await migrate_1.pool.query(`UPDATE dispensaries SET ${setClauses.join(', ')} WHERE id = $${paramIndex}`, values); +} +async function createSandboxEntry(dispensaryId, suspectedProvider, mode, detectionSignals) { + // First, check if there's an existing active sandbox + const existing = await migrate_1.pool.query(`SELECT id FROM crawler_sandboxes + WHERE dispensary_id = $1 AND status NOT IN ('moved_to_production', 'failed')`, [dispensaryId]); + if (existing.rows.length > 0) { + // Update existing + await migrate_1.pool.query(`UPDATE crawler_sandboxes + SET suspected_menu_provider = $2, mode = $3, detection_signals = COALESCE($4, detection_signals), updated_at = NOW() + WHERE id = $1`, [existing.rows[0].id, suspectedProvider, mode, detectionSignals ? JSON.stringify(detectionSignals) : null]); + return existing.rows[0].id; + } + // Create new + const result = await migrate_1.pool.query(`INSERT INTO crawler_sandboxes (dispensary_id, suspected_menu_provider, mode, detection_signals, status) + VALUES ($1, $2, $3, $4, 'pending') + RETURNING id`, [dispensaryId, suspectedProvider, mode, detectionSignals ? JSON.stringify(detectionSignals) : '{}']); + return result.rows[0].id; +} +async function createSandboxJob(dispensaryId, sandboxId, jobType, priority = 0) { + const result = await migrate_1.pool.query(`INSERT INTO sandbox_crawl_jobs (dispensary_id, sandbox_id, job_type, status, priority) + VALUES ($1, $2, $3, 'pending', $4) + RETURNING id`, [dispensaryId, sandboxId, jobType, priority]); + return result.rows[0].id; +} +// Get linked store ID for a dispensary (for using existing scraper) +async function getStoreIdForDispensary(dispensaryId) { + // Check if there's a stores entry linked to this dispensary + const result = await migrate_1.pool.query(`SELECT s.id FROM stores s + JOIN dispensaries d ON d.menu_url = s.dutchie_url OR d.name ILIKE '%' || s.name || '%' + WHERE d.id = $1 + LIMIT 1`, [dispensaryId]); + if (result.rows.length > 0) { + return result.rows[0].id; + } + // Try to find by website + const result2 = await migrate_1.pool.query(`SELECT s.id FROM stores s + JOIN dispensaries d ON d.website ILIKE '%' || s.slug || '%' + WHERE d.id = $1 + LIMIT 1`, [dispensaryId]); + return result2.rows[0]?.id || null; +} +// ======================================== +// Job 1: Detect Menu Provider +// ======================================== +async function runDetectMenuProviderJob(dispensaryId) { + logger_1.logger.info('crawler-jobs', `Starting menu provider detection for dispensary ${dispensaryId}`); + const dispensary = await getDispensary(dispensaryId); + if (!dispensary) { + return { success: false, message: `Dispensary ${dispensaryId} not found` }; + } + // Check for website URL + const websiteUrl = dispensary.website || dispensary.menu_url; + if (!websiteUrl) { + await updateDispensary(dispensaryId, { + crawler_status: 'error_needs_review', + last_menu_error_at: new Date(), + last_error_message: 'No website URL available for detection', + }); + return { success: false, message: 'No website URL available' }; + } + try { + // Run detection + const detection = await (0, menu_provider_detector_1.detectMenuProvider)(websiteUrl, { + checkMenuPaths: true, + timeout: 30000, + }); + // Update dispensary with results + const updates = { + menu_provider: detection.provider, + menu_provider_confidence: detection.confidence, + provider_detection_data: JSON.stringify({ + signals: detection.signals, + urlsTested: detection.urlsTested, + menuEntryPoints: detection.menuEntryPoints, + rawSignals: detection.rawSignals, + detectedAt: new Date().toISOString(), + }), + crawler_status: 'idle', + }; + // Decide crawler mode based on provider + if (detection.provider === 'dutchie' && detection.confidence >= 70) { + // Dutchie with high confidence -> production + updates.crawler_mode = 'production'; + logger_1.logger.info('crawler-jobs', `Dispensary ${dispensaryId} detected as Dutchie (${detection.confidence}%), setting to production`); + } + else { + // Unknown or non-Dutchie -> sandbox + updates.crawler_mode = 'sandbox'; + // Create sandbox entry for further analysis + const sandboxId = await createSandboxEntry(dispensaryId, detection.provider, 'detection', { + signals: detection.signals, + rawSignals: detection.rawSignals, + }); + // Queue sandbox crawl job + await createSandboxJob(dispensaryId, sandboxId, 'detection'); + logger_1.logger.info('crawler-jobs', `Dispensary ${dispensaryId} detected as ${detection.provider} (${detection.confidence}%), setting to sandbox`); + } + // Update menu entry points if found + if (detection.menuEntryPoints.length > 0 && !dispensary.menu_url) { + updates.menu_url = detection.menuEntryPoints[0]; + } + await updateDispensary(dispensaryId, updates); + return { + success: true, + message: `Detected provider: ${detection.provider} (${detection.confidence}%)`, + data: { + provider: detection.provider, + confidence: detection.confidence, + mode: updates.crawler_mode, + menuEntryPoints: detection.menuEntryPoints, + }, + }; + } + catch (error) { + logger_1.logger.error('crawler-jobs', `Detection failed for dispensary ${dispensaryId}: ${error.message}`); + await updateDispensary(dispensaryId, { + crawler_status: 'error_needs_review', + last_menu_error_at: new Date(), + last_error_message: `Detection failed: ${error.message}`, + }); + return { success: false, message: error.message }; + } +} +// ======================================== +// Job 2: Dutchie Menu Crawl (Production) +// ======================================== +async function runDutchieMenuCrawlJob(dispensaryId) { + logger_1.logger.info('crawler-jobs', `Starting Dutchie production crawl for dispensary ${dispensaryId}`); + const dispensary = await getDispensary(dispensaryId); + if (!dispensary) { + return { success: false, message: `Dispensary ${dispensaryId} not found` }; + } + // Verify it's a Dutchie production dispensary + if (dispensary.menu_provider !== 'dutchie') { + logger_1.logger.warn('crawler-jobs', `Dispensary ${dispensaryId} is not Dutchie, skipping production crawl`); + return { success: false, message: 'Not a Dutchie dispensary' }; + } + if (dispensary.crawler_mode !== 'production') { + logger_1.logger.warn('crawler-jobs', `Dispensary ${dispensaryId} is not in production mode, skipping`); + return { success: false, message: 'Not in production mode' }; + } + // Find linked store ID + const storeId = await getStoreIdForDispensary(dispensaryId); + if (!storeId) { + // Need to create a store entry or handle differently + logger_1.logger.warn('crawler-jobs', `No linked store found for dispensary ${dispensaryId}`); + return { success: false, message: 'No linked store found - needs setup' }; + } + try { + // Update status to running + await updateDispensary(dispensaryId, { crawler_status: 'running' }); + // Run the existing Dutchie scraper + await (0, scraper_v2_1.scrapeStore)(storeId, 3); // 3 parallel workers + // Update success status + await updateDispensary(dispensaryId, { + crawler_status: 'ok', + last_menu_scrape: new Date(), + menu_scrape_status: 'active', + }); + logger_1.logger.info('crawler-jobs', `Dutchie crawl completed for dispensary ${dispensaryId}`); + return { + success: true, + message: 'Dutchie crawl completed successfully', + data: { storeId }, + }; + } + catch (error) { + logger_1.logger.error('crawler-jobs', `Dutchie crawl failed for dispensary ${dispensaryId}: ${error.message}`); + // Check if this might be a provider change + let providerChanged = false; + try { + const browser = await puppeteer_1.default.launch({ headless: true, args: ['--no-sandbox'] }); + const page = await browser.newPage(); + const url = dispensary.menu_url || dispensary.website; + if (url) { + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); + const changeResult = await (0, menu_provider_detector_1.detectProviderChange)(page, 'dutchie'); + providerChanged = changeResult.changed; + if (providerChanged) { + // Provider changed - move to sandbox + await updateDispensary(dispensaryId, { + crawler_mode: 'sandbox', + crawler_status: 'error_needs_review', + last_menu_error_at: new Date(), + last_error_message: `Provider appears to have changed from Dutchie to ${changeResult.newProvider}`, + }); + const sandboxId = await createSandboxEntry(dispensaryId, changeResult.newProvider || 'unknown', 'detection', { providerChangeDetected: true, previousProvider: 'dutchie' }); + await createSandboxJob(dispensaryId, sandboxId, 'detection'); + logger_1.logger.warn('crawler-jobs', `Provider change detected for dispensary ${dispensaryId}: Dutchie -> ${changeResult.newProvider}`); + } + } + await browser.close(); + } + catch { + // Ignore detection errors during failure handling + } + if (!providerChanged) { + await updateDispensary(dispensaryId, { + crawler_status: 'error_needs_review', + last_menu_error_at: new Date(), + last_error_message: error.message, + }); + } + return { success: false, message: error.message }; + } +} +// ======================================== +// Job 3: Sandbox Crawl (Learning Mode) +// ======================================== +async function runSandboxCrawlJob(dispensaryId, sandboxId) { + logger_1.logger.info('crawler-jobs', `Starting sandbox crawl for dispensary ${dispensaryId}`); + const dispensary = await getDispensary(dispensaryId); + if (!dispensary) { + return { success: false, message: `Dispensary ${dispensaryId} not found` }; + } + // Get or create sandbox entry + let sandbox; + if (sandboxId) { + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [sandboxId]); + sandbox = result.rows[0]; + } + else { + const result = await migrate_1.pool.query(`SELECT * FROM crawler_sandboxes + WHERE dispensary_id = $1 AND status NOT IN ('moved_to_production', 'failed') + ORDER BY created_at DESC LIMIT 1`, [dispensaryId]); + sandbox = result.rows[0]; + if (!sandbox) { + const newSandboxId = await createSandboxEntry(dispensaryId, dispensary.menu_provider, 'template_learning'); + const result = await migrate_1.pool.query('SELECT * FROM crawler_sandboxes WHERE id = $1', [newSandboxId]); + sandbox = result.rows[0]; + } + } + const websiteUrl = dispensary.menu_url || dispensary.website; + if (!websiteUrl) { + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'failed', failure_reason = 'No website URL' WHERE id = $1`, [sandbox.id]); + return { success: false, message: 'No website URL available' }; + } + let browser = null; + try { + // Update status + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'analyzing', updated_at = NOW() WHERE id = $1`, [sandbox.id]); + await updateDispensary(dispensaryId, { crawler_status: 'running' }); + // Launch browser + browser = await puppeteer_1.default.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + // URLs to crawl (limited depth for sandbox) + const urlsToVisit = [websiteUrl]; + const menuPaths = ['/menu', '/shop', '/products', '/order']; + for (const path of menuPaths) { + const baseUrl = new URL(websiteUrl).origin; + urlsToVisit.push(`${baseUrl}${path}`); + } + const urlsTested = []; + const menuEntryPoints = []; + const capturedHtml = []; + const analysisData = { + provider_signals: {}, + selector_candidates: [], + page_structures: [], + }; + // Crawl each URL + for (const url of urlsToVisit) { + try { + urlsTested.push(url); + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); + await new Promise(r => setTimeout(r, 2000)); // Wait for dynamic content + // Get page HTML + const html = await page.content(); + // Check if this looks like a menu page + const hasMenuContent = await page.evaluate(() => { + const text = document.body.innerText.toLowerCase(); + return (text.includes('add to cart') || + text.includes('thc') || + text.includes('indica') || + text.includes('sativa')); + }); + if (hasMenuContent) { + menuEntryPoints.push(url); + capturedHtml.push({ url, html }); + // Analyze page structure for selector candidates + const structure = await page.evaluate(() => { + const candidates = []; + // Look for product-like containers + const productSelectors = [ + '.product', '.product-card', '.menu-item', '.item-card', + '[data-product]', '[data-item]', '.strain', '.listing', + ]; + for (const selector of productSelectors) { + const els = document.querySelectorAll(selector); + if (els.length > 3) { // Likely a list + candidates.push({ + selector, + count: els.length, + type: 'product_container', + }); + } + } + // Look for price patterns + const pricePattern = /\$\d+(\.\d{2})?/; + const textNodes = document.body.innerText; + const priceMatches = textNodes.match(/\$\d+(\.\d{2})?/g); + return { + candidates, + priceCount: priceMatches?.length || 0, + hasAddToCart: textNodes.toLowerCase().includes('add to cart'), + }; + }); + // Extract availability hints from page content + const availabilityHints = (0, availability_1.extractAvailabilityHints)(html); + analysisData.page_structures.push({ + url, + ...structure, + availabilityHints, + }); + } + } + catch (pageError) { + if (!pageError.message.includes('404')) { + logger_1.logger.warn('crawler-jobs', `Sandbox crawl error for ${url}: ${pageError.message}`); + } + } + } + // Save HTML to storage (local for now, S3 later) + let rawHtmlLocation = null; + if (capturedHtml.length > 0) { + const htmlDir = path_1.default.join(process.cwd(), 'sandbox-data', `dispensary-${dispensaryId}`); + await fs_1.promises.mkdir(htmlDir, { recursive: true }); + for (const { url, html } of capturedHtml) { + const filename = `${Date.now()}-${url.replace(/[^a-z0-9]/gi, '_')}.html`; + await fs_1.promises.writeFile(path_1.default.join(htmlDir, filename), html); + } + rawHtmlLocation = htmlDir; + } + // Update sandbox with results + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET + status = $1, + urls_tested = $2, + menu_entry_points = $3, + raw_html_location = $4, + analysis_json = $5, + confidence_score = $6, + analyzed_at = NOW(), + updated_at = NOW() + WHERE id = $7`, [ + menuEntryPoints.length > 0 ? 'needs_human_review' : 'pending', + JSON.stringify(urlsTested), + JSON.stringify(menuEntryPoints), + rawHtmlLocation, + JSON.stringify(analysisData), + menuEntryPoints.length > 0 ? 50 : 20, + sandbox.id, + ]); + // Update dispensary status + await updateDispensary(dispensaryId, { + crawler_status: 'error_needs_review', // Sandbox results need review + }); + logger_1.logger.info('crawler-jobs', `Sandbox crawl completed for dispensary ${dispensaryId}: ${menuEntryPoints.length} menu pages found`); + return { + success: true, + message: `Sandbox crawl completed. Found ${menuEntryPoints.length} menu entry points.`, + data: { + sandboxId: sandbox.id, + urlsTested: urlsTested.length, + menuEntryPoints, + analysisData, + }, + }; + } + catch (error) { + logger_1.logger.error('crawler-jobs', `Sandbox crawl failed for dispensary ${dispensaryId}: ${error.message}`); + await migrate_1.pool.query(`UPDATE crawler_sandboxes SET status = 'failed', failure_reason = $1 WHERE id = $2`, [error.message, sandbox.id]); + await updateDispensary(dispensaryId, { + crawler_status: 'error_needs_review', + last_menu_error_at: new Date(), + last_error_message: `Sandbox crawl failed: ${error.message}`, + }); + return { success: false, message: error.message }; + } + finally { + if (browser) { + await browser.close(); + } + } +} +// ======================================== +// Queue Processing Functions +// ======================================== +/** + * Process pending sandbox jobs + */ +async function processSandboxJobs(limit = 5) { + // Claim pending jobs + const jobs = await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs + SET status = 'running', worker_id = $1, started_at = NOW() + WHERE id IN ( + SELECT id FROM sandbox_crawl_jobs + WHERE status = 'pending' AND scheduled_at <= NOW() + ORDER BY priority DESC, scheduled_at ASC + LIMIT $2 + FOR UPDATE SKIP LOCKED + ) + RETURNING *`, [WORKER_ID, limit]); + for (const job of jobs.rows) { + try { + let result; + if (job.job_type === 'detection') { + result = await runDetectMenuProviderJob(job.dispensary_id); + } + else { + result = await runSandboxCrawlJob(job.dispensary_id, job.sandbox_id); + } + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs + SET status = $1, completed_at = NOW(), result_summary = $2, error_message = $3 + WHERE id = $4`, [ + result.success ? 'completed' : 'failed', + JSON.stringify(result.data || {}), + result.success ? null : result.message, + job.id, + ]); + } + catch (error) { + await migrate_1.pool.query(`UPDATE sandbox_crawl_jobs SET status = 'failed', error_message = $1 WHERE id = $2`, [error.message, job.id]); + } + } +} diff --git a/backend/dist/services/crawler-logger.js b/backend/dist/services/crawler-logger.js new file mode 100644 index 00000000..72c0fcbe --- /dev/null +++ b/backend/dist/services/crawler-logger.js @@ -0,0 +1,202 @@ +"use strict"; +/** + * CrawlerLogger - Structured logging for crawler operations + * + * High-signal, low-noise logging with JSON output for: + * - Job lifecycle (one summary per job) + * - Provider/mode changes + * - Sandbox events + * - Queue failures + * + * NO per-product logging - that's too noisy. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.crawlerLogger = void 0; +class CrawlerLoggerService { + formatLog(payload) { + return JSON.stringify(payload); + } + log(payload) { + const formatted = this.formatLog(payload); + switch (payload.level) { + case 'error': + console.error(`[CRAWLER] ${formatted}`); + break; + case 'warn': + console.warn(`[CRAWLER] ${formatted}`); + break; + case 'debug': + console.debug(`[CRAWLER] ${formatted}`); + break; + default: + console.log(`[CRAWLER] ${formatted}`); + } + } + /** + * Log when a crawl job starts + */ + jobStarted(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'job_started', + job_id: params.job_id, + store_id: params.store_id, + store_name: params.store_name, + job_type: params.job_type, + trigger_type: params.trigger_type, + provider: params.provider, + }); + } + /** + * Log when a crawl job completes successfully + */ + jobCompleted(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'job_completed', + job_id: params.job_id, + store_id: params.store_id, + store_name: params.store_name, + duration_ms: params.duration_ms, + products_found: params.products_found, + products_new: params.products_new, + products_updated: params.products_updated, + products_marked_oos: params.products_marked_oos, + provider: params.provider, + }); + } + /** + * Log when a crawl job fails + */ + jobFailed(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'error', + event: 'job_failed', + job_id: params.job_id, + store_id: params.store_id, + store_name: params.store_name, + duration_ms: params.duration_ms, + error_message: params.error_message, + error_code: params.error_code, + provider: params.provider, + }); + } + /** + * Log when a provider is detected for a dispensary + */ + providerDetected(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'provider_detected', + dispensary_id: params.dispensary_id, + dispensary_name: params.dispensary_name, + detected_provider: params.detected_provider, + confidence: params.confidence, + detection_method: params.detection_method, + menu_url: params.menu_url, + category: params.category, + }); + } + /** + * Log when a dispensary's provider changes + */ + providerChanged(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'provider_changed', + dispensary_id: params.dispensary_id, + dispensary_name: params.dispensary_name, + old_provider: params.old_provider, + new_provider: params.new_provider, + old_confidence: params.old_confidence, + new_confidence: params.new_confidence, + category: params.category, + }); + } + /** + * Log when a dispensary's crawler mode changes (sandbox -> production, etc.) + */ + modeChanged(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'mode_changed', + dispensary_id: params.dispensary_id, + dispensary_name: params.dispensary_name, + old_mode: params.old_mode, + new_mode: params.new_mode, + reason: params.reason, + category: params.category, + provider: params.provider, + }); + } + /** + * Log sandbox crawl events + */ + sandboxEvent(params) { + const level = params.event === 'sandbox_failed' ? 'error' : 'info'; + this.log({ + timestamp: new Date().toISOString(), + level, + event: params.event, + dispensary_id: params.dispensary_id, + dispensary_name: params.dispensary_name, + template_name: params.template_name, + category: params.category, + quality_score: params.quality_score, + products_extracted: params.products_extracted, + fields_missing: params.fields_missing, + error_message: params.error_message, + provider: params.provider, + }); + } + /** + * Log queue processing failures + */ + queueFailure(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'error', + event: 'queue_failure', + queue_type: params.queue_type, + error_message: params.error_message, + affected_items: params.affected_items, + }); + } + /** + * Log detection scan summary + */ + detectionScan(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'detection_scan', + total_scanned: params.total_scanned, + detected: params.detected, + failed: params.failed, + skipped: params.skipped, + duration_ms: params.duration_ms, + }); + } + /** + * Log intelligence run summary + */ + intelligenceRun(params) { + this.log({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'intelligence_run', + run_type: params.run_type, + dispensaries_processed: params.dispensaries_processed, + jobs_queued: params.jobs_queued, + duration_ms: params.duration_ms, + }); + } +} +// Export singleton instance +exports.crawlerLogger = new CrawlerLoggerService(); diff --git a/backend/dist/services/dispensary-orchestrator.js b/backend/dist/services/dispensary-orchestrator.js new file mode 100644 index 00000000..0917c2b1 --- /dev/null +++ b/backend/dist/services/dispensary-orchestrator.js @@ -0,0 +1,383 @@ +"use strict"; +/** + * Dispensary Crawl Orchestrator + * + * Orchestrates the complete crawl workflow for a dispensary: + * 1. Load dispensary data + * 2. Check if provider detection is needed + * 3. Run provider detection if needed + * 4. Queue appropriate crawl jobs based on provider/mode + * 5. Update dispensary_crawl_schedule with meaningful status + * + * This works DIRECTLY with dispensaries (not through stores table). + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runDispensaryOrchestrator = runDispensaryOrchestrator; +exports.runBatchDispensaryOrchestrator = runBatchDispensaryOrchestrator; +exports.getDispensariesDueForOrchestration = getDispensariesDueForOrchestration; +exports.ensureAllDispensariesHaveSchedules = ensureAllDispensariesHaveSchedules; +exports.processDispensaryScheduler = processDispensaryScheduler; +const uuid_1 = require("uuid"); +const migrate_1 = require("../db/migrate"); +const crawler_logger_1 = require("./crawler-logger"); +const intelligence_detector_1 = require("./intelligence-detector"); +const category_crawler_jobs_1 = require("./category-crawler-jobs"); +// ======================================== +// Main Orchestrator Function +// ======================================== +/** + * Run the complete crawl orchestration for a dispensary + * + * Behavior: + * 1. Load the dispensary info + * 2. If product_provider is missing or stale (>7 days), run detection + * 3. After detection: + * - If product_provider = 'dutchie' and product_crawler_mode = 'production': Run production crawl + * - Otherwise: Run sandbox crawl + * 4. Update dispensary_crawl_schedule with status/summary + */ +async function runDispensaryOrchestrator(dispensaryId, scheduleId) { + const startTime = Date.now(); + const runId = (0, uuid_1.v4)(); + let result = { + status: 'pending', + summary: '', + runId, + dispensaryId, + dispensaryName: '', + detectionRan: false, + crawlRan: false, + durationMs: 0, + }; + try { + // Mark schedule as running + await updateScheduleStatus(dispensaryId, 'running', 'Starting orchestrator...', null, runId); + // 1. Load dispensary info + const dispensary = await getDispensaryInfo(dispensaryId); + if (!dispensary) { + throw new Error(`Dispensary ${dispensaryId} not found`); + } + result.dispensaryName = dispensary.name; + // 2. Check if provider detection is needed + const needsDetection = await checkNeedsDetection(dispensary); + if (needsDetection) { + // Run provider detection + const websiteUrl = dispensary.menu_url || dispensary.website; + if (!websiteUrl) { + result.status = 'error'; + result.summary = 'No website URL available for detection'; + result.error = 'Dispensary has no menu_url or website configured'; + await updateScheduleStatus(dispensaryId, 'error', result.summary, result.error, runId); + result.durationMs = Date.now() - startTime; + await createJobRecord(dispensaryId, scheduleId, result); + return result; + } + await updateScheduleStatus(dispensaryId, 'running', 'Running provider detection...', null, runId); + const detectionResult = await (0, intelligence_detector_1.detectMultiCategoryProviders)(websiteUrl); + result.detectionRan = true; + result.detectionResult = detectionResult; + // Save detection results to dispensary + await (0, intelligence_detector_1.updateAllCategoryProviders)(dispensaryId, detectionResult); + crawler_logger_1.crawlerLogger.providerDetected({ + dispensary_id: dispensaryId, + dispensary_name: dispensary.name, + detected_provider: detectionResult.product.provider, + confidence: detectionResult.product.confidence, + detection_method: 'dispensary_orchestrator', + menu_url: websiteUrl, + category: 'product', + }); + // Refresh dispensary info after detection + const updatedDispensary = await getDispensaryInfo(dispensaryId); + if (updatedDispensary) { + Object.assign(dispensary, updatedDispensary); + } + } + // 3. Determine crawl type and run + const provider = dispensary.product_provider; + const mode = dispensary.product_crawler_mode; + if (provider === 'dutchie' && mode === 'production') { + // Production Dutchie crawl + await updateScheduleStatus(dispensaryId, 'running', 'Running Dutchie production crawl...', null, runId); + try { + // Run the category-specific crawl job + const crawlResult = await (0, category_crawler_jobs_1.runCrawlProductsJob)(dispensaryId); + result.crawlRan = true; + result.crawlType = 'production'; + if (crawlResult.success) { + result.productsFound = crawlResult.data?.productsFound || 0; + const detectionPart = result.detectionRan ? 'Detection + ' : ''; + result.summary = `${detectionPart}Dutchie products crawl completed`; + result.status = 'success'; + crawler_logger_1.crawlerLogger.jobCompleted({ + job_id: 0, + store_id: 0, + store_name: dispensary.name, + duration_ms: Date.now() - startTime, + products_found: result.productsFound || 0, + products_new: 0, + products_updated: 0, + provider: 'dutchie', + }); + } + else { + result.status = 'error'; + result.error = crawlResult.message; + result.summary = `Dutchie crawl failed: ${crawlResult.message.slice(0, 100)}`; + } + } + catch (crawlError) { + result.status = 'error'; + result.error = crawlError.message; + result.summary = `Dutchie crawl failed: ${crawlError.message.slice(0, 100)}`; + result.crawlRan = true; + result.crawlType = 'production'; + crawler_logger_1.crawlerLogger.jobFailed({ + job_id: 0, + store_id: 0, + store_name: dispensary.name, + duration_ms: Date.now() - startTime, + error_message: crawlError.message, + provider: 'dutchie', + }); + } + } + else if (provider && provider !== 'unknown') { + // Sandbox crawl for non-Dutchie or sandbox mode + await updateScheduleStatus(dispensaryId, 'running', `Running ${provider} sandbox crawl...`, null, runId); + try { + const sandboxResult = await (0, category_crawler_jobs_1.runSandboxProductsJob)(dispensaryId); + result.crawlRan = true; + result.crawlType = 'sandbox'; + result.productsFound = sandboxResult.data?.productsExtracted || 0; + const detectionPart = result.detectionRan ? 'Detection + ' : ''; + if (sandboxResult.success) { + result.summary = `${detectionPart}${provider} sandbox crawl (${result.productsFound} items, quality ${sandboxResult.data?.qualityScore || 0}%)`; + result.status = 'sandbox_only'; + } + else { + result.summary = `${detectionPart}${provider} sandbox failed: ${sandboxResult.message}`; + result.status = 'error'; + result.error = sandboxResult.message; + } + } + catch (sandboxError) { + result.status = 'error'; + result.error = sandboxError.message; + result.summary = `Sandbox crawl failed: ${sandboxError.message.slice(0, 100)}`; + result.crawlRan = true; + result.crawlType = 'sandbox'; + } + } + else { + // No provider detected - detection only + if (result.detectionRan) { + result.summary = `Detection complete: provider=${dispensary.product_provider || 'unknown'}, confidence=${dispensary.product_confidence || 0}%`; + result.status = 'detection_only'; + } + else { + result.summary = 'No provider detected and no crawl possible'; + result.status = 'error'; + result.error = 'Could not determine menu provider'; + } + } + } + catch (error) { + result.status = 'error'; + result.error = error.message; + result.summary = `Orchestrator error: ${error.message.slice(0, 100)}`; + crawler_logger_1.crawlerLogger.queueFailure({ + queue_type: 'dispensary_orchestrator', + error_message: error.message, + }); + } + result.durationMs = Date.now() - startTime; + // Update final schedule status + await updateScheduleStatus(dispensaryId, result.status, result.summary, result.error || null, runId); + // Create job record + await createJobRecord(dispensaryId, scheduleId, result); + return result; +} +// ======================================== +// Helper Functions +// ======================================== +async function getDispensaryInfo(dispensaryId) { + const result = await migrate_1.pool.query(`SELECT id, name, city, website, menu_url, + product_provider, product_confidence, product_crawler_mode, last_product_scan_at + FROM dispensaries + WHERE id = $1`, [dispensaryId]); + return result.rows[0] || null; +} +async function checkNeedsDetection(dispensary) { + // No provider = definitely needs detection + if (!dispensary.product_provider) + return true; + // Unknown provider = needs detection + if (dispensary.product_provider === 'unknown') + return true; + // Low confidence = needs re-detection + if (dispensary.product_confidence !== null && dispensary.product_confidence < 50) + return true; + // Stale detection (> 7 days) = needs refresh + if (dispensary.last_product_scan_at) { + const daysSince = (Date.now() - new Date(dispensary.last_product_scan_at).getTime()) / (1000 * 60 * 60 * 24); + if (daysSince > 7) + return true; + } + return false; +} +async function updateScheduleStatus(dispensaryId, status, summary, error, runId) { + await migrate_1.pool.query(`INSERT INTO dispensary_crawl_schedule (dispensary_id, last_status, last_summary, last_error, last_run_at, updated_at) + VALUES ($1, $2, $3, $4, NOW(), NOW()) + ON CONFLICT (dispensary_id) DO UPDATE SET + last_status = $2, + last_summary = $3, + last_error = $4, + last_run_at = NOW(), + updated_at = NOW()`, [dispensaryId, status, summary, error]); +} +async function createJobRecord(dispensaryId, scheduleId, result) { + await migrate_1.pool.query(`INSERT INTO dispensary_crawl_jobs ( + dispensary_id, schedule_id, job_type, trigger_type, status, priority, + scheduled_at, started_at, completed_at, duration_ms, + detection_ran, crawl_ran, crawl_type, + products_found, products_new, products_updated, + detected_provider, detected_confidence, detected_mode, + error_message, run_id + ) VALUES ( + $1, $2, 'orchestrator', 'manual', $3, 100, + NOW(), NOW(), NOW(), $4, + $5, $6, $7, + $8, $9, $10, + $11, $12, $13, + $14, $15 + )`, [ + dispensaryId, + scheduleId || null, + result.status === 'success' ? 'completed' : result.status === 'error' ? 'failed' : 'completed', + result.durationMs, + result.detectionRan, + result.crawlRan, + result.crawlType || null, + result.productsFound || null, + result.productsNew || null, + result.productsUpdated || null, + result.detectionResult?.product.provider || null, + result.detectionResult?.product.confidence || null, + result.detectionResult?.product.mode || null, + result.error || null, + result.runId, + ]); + // Update schedule stats + if (result.status === 'success' || result.status === 'sandbox_only' || result.status === 'detection_only') { + await migrate_1.pool.query(`UPDATE dispensary_crawl_schedule SET + total_runs = COALESCE(total_runs, 0) + 1, + successful_runs = COALESCE(successful_runs, 0) + 1, + consecutive_failures = 0, + next_run_at = NOW() + (interval_minutes || ' minutes')::INTERVAL, + last_duration_ms = $2 + WHERE dispensary_id = $1`, [dispensaryId, result.durationMs]); + } + else if (result.status === 'error') { + await migrate_1.pool.query(`UPDATE dispensary_crawl_schedule SET + total_runs = COALESCE(total_runs, 0) + 1, + consecutive_failures = COALESCE(consecutive_failures, 0) + 1, + next_run_at = NOW() + (interval_minutes || ' minutes')::INTERVAL, + last_duration_ms = $2 + WHERE dispensary_id = $1`, [dispensaryId, result.durationMs]); + } +} +// ======================================== +// Batch Processing +// ======================================== +/** + * Run orchestrator for multiple dispensaries + */ +async function runBatchDispensaryOrchestrator(dispensaryIds, concurrency = 3) { + const results = []; + // Process in batches + for (let i = 0; i < dispensaryIds.length; i += concurrency) { + const batch = dispensaryIds.slice(i, i + concurrency); + console.log(`Processing batch ${Math.floor(i / concurrency) + 1}: dispensaries ${batch.join(', ')}`); + const batchResults = await Promise.all(batch.map(id => runDispensaryOrchestrator(id))); + results.push(...batchResults); + // Small delay between batches to avoid overwhelming the system + if (i + concurrency < dispensaryIds.length) { + await new Promise(r => setTimeout(r, 1000)); + } + } + return results; +} +/** + * Get dispensaries that are due for orchestration + */ +async function getDispensariesDueForOrchestration(limit = 10) { + const result = await migrate_1.pool.query(`SELECT d.id + FROM dispensaries d + LEFT JOIN dispensary_crawl_schedule dcs ON dcs.dispensary_id = d.id + WHERE COALESCE(dcs.is_active, TRUE) = TRUE + AND ( + dcs.next_run_at IS NULL + OR dcs.next_run_at <= NOW() + ) + AND (dcs.last_status IS NULL OR dcs.last_status NOT IN ('running', 'pending')) + ORDER BY COALESCE(dcs.priority, 0) DESC, dcs.last_run_at ASC NULLS FIRST + LIMIT $1`, [limit]); + return result.rows.map(row => row.id); +} +/** + * Ensure all dispensaries have schedule entries + */ +async function ensureAllDispensariesHaveSchedules(intervalMinutes = 240) { + // Get all dispensary IDs that don't have a schedule + const result = await migrate_1.pool.query(`INSERT INTO dispensary_crawl_schedule (dispensary_id, is_active, interval_minutes, priority) + SELECT d.id, TRUE, $1, 0 + FROM dispensaries d + WHERE NOT EXISTS ( + SELECT 1 FROM dispensary_crawl_schedule dcs WHERE dcs.dispensary_id = d.id + ) + RETURNING id`, [intervalMinutes]); + const existingCount = await migrate_1.pool.query('SELECT COUNT(*) FROM dispensary_crawl_schedule'); + return { + created: result.rowCount || 0, + existing: parseInt(existingCount.rows[0].count) - (result.rowCount || 0), + }; +} +// ======================================== +// Scheduler Integration +// ======================================== +let dispensarySchedulerRunning = false; +/** + * Process dispensaries using the intelligent orchestrator + * Called periodically by the scheduler + */ +async function processDispensaryScheduler() { + if (dispensarySchedulerRunning) { + console.log('Dispensary scheduler already running, skipping...'); + return; + } + dispensarySchedulerRunning = true; + try { + // Get dispensaries due for orchestration + const dispensaryIds = await getDispensariesDueForOrchestration(3); + if (dispensaryIds.length === 0) { + return; + } + console.log(`Dispensary Scheduler: Processing ${dispensaryIds.length} dispensaries due for crawl`); + // Process each dispensary through the orchestrator + for (const dispensaryId of dispensaryIds) { + try { + console.log(`Dispensary Scheduler: Starting crawl for dispensary ${dispensaryId}`); + const result = await runDispensaryOrchestrator(dispensaryId); + console.log(`Dispensary Scheduler: Dispensary ${dispensaryId} completed - ${result.summary}`); + } + catch (error) { + console.error(`Dispensary Scheduler: Dispensary ${dispensaryId} failed - ${error.message}`); + } + } + console.log(`Dispensary Scheduler: Finished processing ${dispensaryIds.length} dispensaries`); + } + finally { + dispensarySchedulerRunning = false; + } +} diff --git a/backend/dist/services/geolocation.js b/backend/dist/services/geolocation.js new file mode 100644 index 00000000..32917440 --- /dev/null +++ b/backend/dist/services/geolocation.js @@ -0,0 +1,125 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.lookupProxyLocation = lookupProxyLocation; +exports.updateProxyLocation = updateProxyLocation; +exports.updateAllProxyLocations = updateAllProxyLocations; +exports.queueProxyLocationUpdate = queueProxyLocationUpdate; +const axios_1 = __importDefault(require("axios")); +const migrate_1 = require("../db/migrate"); +// Free API - 45 requests/minute limit +const GEOLOCATION_API = 'http://ip-api.com/json/'; +async function lookupProxyLocation(host) { + try { + const response = await axios_1.default.get(`${GEOLOCATION_API}${host}?fields=status,message,country,countryCode,regionName,city,query`); + const data = response.data; + if (data.status === 'fail') { + console.log(`❌ Geolocation lookup failed for ${host}: ${data.message}`); + return null; + } + return data; + } + catch (error) { + console.error(`❌ Error looking up location for ${host}:`, error.message); + return null; + } +} +async function updateProxyLocation(proxyId, location) { + await migrate_1.pool.query(` + UPDATE proxies + SET city = $1, + state = $2, + country = $3, + country_code = $4, + location_updated_at = CURRENT_TIMESTAMP + WHERE id = $5 + `, [ + location.city, + location.regionName, + location.country, + location.countryCode, + proxyId + ]); +} +async function updateAllProxyLocations(batchSize = 45) { + console.log('🌍 Starting proxy location update job...'); + // Get all proxies without location data + const result = await migrate_1.pool.query(` + SELECT id, host + FROM proxies + WHERE location_updated_at IS NULL + OR location_updated_at < CURRENT_TIMESTAMP - INTERVAL '30 days' + ORDER BY id + `); + const proxies = result.rows; + console.log(`📊 Found ${proxies.length} proxies to update`); + let updated = 0; + let failed = 0; + // Process in batches to respect rate limit (45 req/min) + for (let i = 0; i < proxies.length; i += batchSize) { + const batch = proxies.slice(i, i + batchSize); + console.log(`🔄 Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(proxies.length / batchSize)} (${batch.length} proxies)`); + // Process batch + for (const proxy of batch) { + const location = await lookupProxyLocation(proxy.host); + if (location) { + await updateProxyLocation(proxy.id, location); + console.log(`✅ Updated ${proxy.id}: ${location.city}, ${location.regionName} - ${location.country}`); + updated++; + } + else { + console.log(`⚠️ Failed to get location for proxy ${proxy.id} (${proxy.host})`); + failed++; + } + // Small delay between requests + await new Promise(resolve => setTimeout(resolve, 100)); + } + // Wait 60 seconds before next batch to respect rate limit + if (i + batchSize < proxies.length) { + console.log(`⏳ Waiting 60s before next batch (rate limit: 45 req/min)...`); + await new Promise(resolve => setTimeout(resolve, 60000)); + } + } + console.log(`✅ Proxy location update complete!`); + console.log(` Updated: ${updated}`); + console.log(` Failed: ${failed}`); +} +// Queue for background processing +const locationUpdateQueue = new Set(); +let isProcessing = false; +function queueProxyLocationUpdate(proxyId) { + locationUpdateQueue.add(proxyId); + processLocationQueue(); +} +async function processLocationQueue() { + if (isProcessing || locationUpdateQueue.size === 0) + return; + isProcessing = true; + try { + const proxyIds = Array.from(locationUpdateQueue); + locationUpdateQueue.clear(); + console.log(`🌍 Processing ${proxyIds.length} proxy location updates from queue`); + for (const proxyId of proxyIds) { + const result = await migrate_1.pool.query('SELECT host FROM proxies WHERE id = $1', [proxyId]); + if (result.rows.length === 0) + continue; + const host = result.rows[0].host; + const location = await lookupProxyLocation(host); + if (location) { + await updateProxyLocation(proxyId, location); + console.log(`✅ Queue: Updated ${proxyId}: ${location.city}, ${location.regionName} - ${location.country}`); + } + // Respect rate limit + await new Promise(resolve => setTimeout(resolve, 1500)); // ~40 req/min + } + } + finally { + isProcessing = false; + // Process any new items that were added while we were processing + if (locationUpdateQueue.size > 0) { + processLocationQueue(); + } + } +} diff --git a/backend/dist/services/intelligence-detector.js b/backend/dist/services/intelligence-detector.js new file mode 100644 index 00000000..0f5993b6 --- /dev/null +++ b/backend/dist/services/intelligence-detector.js @@ -0,0 +1,493 @@ +"use strict"; +/** + * Multi-Category Intelligence Detector + * + * Detects providers for each intelligence category independently: + * - Products: Which provider serves product data + * - Specials: Which provider serves deals/specials + * - Brand: Which provider serves brand information + * - Metadata: Which provider serves taxonomy/category data + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.detectMultiCategoryProviders = detectMultiCategoryProviders; +exports.detectCategoryProviderChange = detectCategoryProviderChange; +exports.updateDispensaryCategoryProvider = updateDispensaryCategoryProvider; +exports.updateAllCategoryProviders = updateAllCategoryProviders; +exports.moveCategoryToSandbox = moveCategoryToSandbox; +const migrate_1 = require("../db/migrate"); +const logger_1 = require("./logger"); +const puppeteer_1 = __importDefault(require("puppeteer")); +// Production-ready providers per category +// Only these combinations can be set to production mode +const PRODUCTION_READY = { + product: ['dutchie'], // Only Dutchie products are production-ready + specials: [], // None yet + brand: [], // None yet + metadata: [], // None yet +}; +// Provider detection patterns +const PROVIDER_PATTERNS = { + dutchie: { + scripts: [ + /dutchie\.com/i, + /dutchie-plus/i, + /dutchie\.js/i, + /__DUTCHIE__/i, + /dutchie-embed/i, + ], + iframes: [ + /dutchie\.com/i, + /dutchie-plus\.com/i, + /embed\.dutchie/i, + ], + html: [ + /class="dutchie/i, + /id="dutchie/i, + /data-dutchie/i, + /"menuType":\s*"dutchie"/i, + ], + apiEndpoints: [ + /dutchie\.com\/graphql/i, + /plus\.dutchie\.com/i, + ], + metaTags: [ + /dutchie/i, + ], + }, + treez: { + scripts: [ + /treez\.io/i, + /treez-ecommerce/i, + /treez\.js/i, + ], + iframes: [ + /treez\.io/i, + /shop\.treez/i, + ], + html: [ + /class="treez/i, + /data-treez/i, + /treez-menu/i, + ], + apiEndpoints: [ + /api\.treez\.io/i, + /treez\.io\/api/i, + ], + metaTags: [], + }, + jane: { + scripts: [ + /jane\.co/i, + /iheartjane\.com/i, + /jane-frame/i, + /jane\.js/i, + ], + iframes: [ + /jane\.co/i, + /iheartjane\.com/i, + /embed\.iheartjane/i, + ], + html: [ + /class="jane/i, + /data-jane/i, + /jane-embed/i, + ], + apiEndpoints: [ + /api\.iheartjane/i, + /jane\.co\/api/i, + ], + metaTags: [], + }, + weedmaps: { + scripts: [ + /weedmaps\.com/i, + /wm-menu/i, + ], + iframes: [ + /weedmaps\.com/i, + /menu\.weedmaps/i, + ], + html: [ + /data-weedmaps/i, + /wm-menu/i, + ], + apiEndpoints: [ + /api-g\.weedmaps/i, + /weedmaps\.com\/api/i, + ], + metaTags: [], + }, + leafly: { + scripts: [ + /leafly\.com/i, + /leafly-menu/i, + ], + iframes: [ + /leafly\.com/i, + /order\.leafly/i, + ], + html: [ + /data-leafly/i, + /leafly-embed/i, + ], + apiEndpoints: [ + /api\.leafly/i, + ], + metaTags: [], + }, +}; +// Category-specific detection signals +const CATEGORY_SIGNALS = { + product: { + urlPatterns: [/\/menu/i, /\/products/i, /\/shop/i, /\/order/i], + htmlPatterns: [/product-card/i, /menu-item/i, /product-list/i, /product-grid/i], + jsonKeys: ['products', 'menuItems', 'items', 'inventory'], + }, + specials: { + urlPatterns: [/\/specials/i, /\/deals/i, /\/promotions/i, /\/offers/i], + htmlPatterns: [/special/i, /deal/i, /promotion/i, /discount/i, /sale/i], + jsonKeys: ['specials', 'deals', 'promotions', 'offers'], + }, + brand: { + urlPatterns: [/\/brands/i, /\/vendors/i, /\/producers/i], + htmlPatterns: [/brand-list/i, /vendor/i, /producer/i, /manufacturer/i], + jsonKeys: ['brands', 'vendors', 'producers', 'manufacturers'], + }, + metadata: { + urlPatterns: [/\/categories/i, /\/taxonomy/i], + htmlPatterns: [/category-nav/i, /menu-categories/i, /filter-category/i], + jsonKeys: ['categories', 'taxonomy', 'filters', 'types'], + }, +}; +// ======================================== +// Main Detection Function +// ======================================== +async function detectMultiCategoryProviders(websiteUrl, options = {}) { + const { timeout = 30000, headless = true, existingBrowser } = options; + let browser = null; + let page = null; + const urlsTested = []; + const rawSignals = {}; + try { + browser = existingBrowser || await puppeteer_1.default.launch({ + headless, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], + }); + page = await browser.newPage(); + await page.setViewport({ width: 1920, height: 1080 }); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + // Navigate to main site + const baseUrl = normalizeUrl(websiteUrl); + urlsTested.push(baseUrl); + await page.goto(baseUrl, { waitUntil: 'networkidle2', timeout }); + // Collect signals from main page + const mainPageSignals = await collectPageSignals(page); + rawSignals.mainPage = mainPageSignals; + // Try common menu URLs + const menuUrls = ['/menu', '/shop', '/products', '/order', '/specials', '/deals', '/brands']; + for (const path of menuUrls) { + try { + const fullUrl = new URL(path, baseUrl).toString(); + urlsTested.push(fullUrl); + await page.goto(fullUrl, { waitUntil: 'networkidle2', timeout: 15000 }); + const signals = await collectPageSignals(page); + rawSignals[path] = signals; + } + catch { + // URL doesn't exist or timed out + } + } + // Analyze signals for each category + const result = { + product: analyzeCategorySignals('product', rawSignals), + specials: analyzeCategorySignals('specials', rawSignals), + brand: analyzeCategorySignals('brand', rawSignals), + metadata: analyzeCategorySignals('metadata', rawSignals), + urlsTested, + rawSignals, + }; + logger_1.logger.info('provider-detection', `Multi-category detection complete for ${websiteUrl}`); + return result; + } + catch (error) { + logger_1.logger.error('provider-detection', `Detection failed for ${websiteUrl}: ${error.message}`); + // Return unknown results for all categories + return { + product: createUnknownResult(), + specials: createUnknownResult(), + brand: createUnknownResult(), + metadata: createUnknownResult(), + urlsTested, + rawSignals: { error: error.message }, + }; + } + finally { + if (page) + await page.close().catch(() => { }); + if (browser && !existingBrowser) + await browser.close().catch(() => { }); + } +} +// ======================================== +// Helper Functions +// ======================================== +function normalizeUrl(url) { + if (!url.startsWith('http')) { + url = 'https://' + url; + } + return url.replace(/\/$/, ''); +} +async function collectPageSignals(page) { + return page.evaluate(() => { + const signals = { + scripts: [], + iframes: [], + links: [], + metaTags: [], + bodyClasses: document.body?.className || '', + bodyId: document.body?.id || '', + htmlSnippet: document.documentElement.outerHTML.slice(0, 10000), + }; + // Collect script sources + document.querySelectorAll('script[src]').forEach((el) => { + signals.scripts.push(el.src); + }); + // Collect inline scripts + document.querySelectorAll('script:not([src])').forEach((el) => { + const content = el.textContent || ''; + if (content.length < 5000) { + signals.scripts.push(`inline:${content.slice(0, 500)}`); + } + }); + // Collect iframes + document.querySelectorAll('iframe').forEach((el) => { + signals.iframes.push(el.src); + }); + // Collect links + document.querySelectorAll('a[href]').forEach((el) => { + signals.links.push(el.href); + }); + // Collect meta tags + document.querySelectorAll('meta').forEach((el) => { + const content = el.getAttribute('content') || ''; + const name = el.getAttribute('name') || el.getAttribute('property') || ''; + if (content || name) { + signals.metaTags.push(`${name}:${content}`); + } + }); + // Look for JSON data + const jsonBlocks = []; + document.querySelectorAll('script[type="application/json"]').forEach((el) => { + jsonBlocks.push(el.textContent?.slice(0, 2000) || ''); + }); + signals.jsonBlocks = jsonBlocks; + return signals; + }); +} +function analyzeCategorySignals(category, allSignals) { + const providerScores = {}; + const detectedSignals = {}; + // Initialize scores + for (const provider of Object.keys(PROVIDER_PATTERNS)) { + providerScores[provider] = 0; + } + // Analyze each page's signals + for (const [pagePath, signals] of Object.entries(allSignals)) { + if (!signals || typeof signals !== 'object') + continue; + // Check for provider-specific patterns + for (const [provider, patterns] of Object.entries(PROVIDER_PATTERNS)) { + let score = 0; + // Check scripts + if (signals.scripts) { + for (const script of signals.scripts) { + for (const pattern of patterns.scripts) { + if (pattern.test(script)) { + score += 20; + detectedSignals[`${provider}_script_${pagePath}`] = script; + } + } + } + } + // Check iframes + if (signals.iframes) { + for (const iframe of signals.iframes) { + for (const pattern of patterns.iframes) { + if (pattern.test(iframe)) { + score += 25; + detectedSignals[`${provider}_iframe_${pagePath}`] = iframe; + } + } + } + } + // Check HTML content + if (signals.htmlSnippet) { + for (const pattern of patterns.html) { + if (pattern.test(signals.htmlSnippet)) { + score += 15; + detectedSignals[`${provider}_html_${pagePath}`] = true; + } + } + } + providerScores[provider] += score; + } + // Check for category-specific signals on relevant pages + const categorySignals = CATEGORY_SIGNALS[category]; + const isRelevantPage = categorySignals.urlPatterns.some((p) => p.test(pagePath)); + if (isRelevantPage && signals.htmlSnippet) { + for (const pattern of categorySignals.htmlPatterns) { + if (pattern.test(signals.htmlSnippet)) { + detectedSignals[`${category}_html_pattern`] = true; + } + } + } + // Check JSON blocks for category data + if (signals.jsonBlocks) { + for (const json of signals.jsonBlocks) { + for (const key of categorySignals.jsonKeys) { + if (json.toLowerCase().includes(`"${key}"`)) { + detectedSignals[`${category}_json_key_${key}`] = true; + } + } + } + } + } + // Determine winning provider + let bestProvider = 'unknown'; + let bestScore = 0; + for (const [provider, score] of Object.entries(providerScores)) { + if (score > bestScore) { + bestScore = score; + bestProvider = provider; + } + } + // Calculate confidence (0-100) + const confidence = Math.min(100, bestScore); + // Determine mode based on provider and confidence + const isProductionReady = PRODUCTION_READY[category].includes(bestProvider); + const mode = isProductionReady && confidence >= 70 + ? 'production' + : 'sandbox'; + // Get template name if available + let templateName; + if (bestProvider === 'dutchie' && category === 'product') { + templateName = 'dutchie_standard'; + } + else if (bestProvider === 'treez') { + templateName = 'treez_products_v0'; + } + return { + provider: bestProvider, + confidence, + mode, + signals: detectedSignals, + templateName, + }; +} +function createUnknownResult() { + return { + provider: 'unknown', + confidence: 0, + mode: 'sandbox', + signals: {}, + }; +} +// ======================================== +// Lightweight Per-Category Change Detection +// ======================================== +async function detectCategoryProviderChange(page, category, expectedProvider) { + try { + const signals = await collectPageSignals(page); + const result = analyzeCategorySignals(category, { currentPage: signals }); + if (result.provider !== expectedProvider && result.confidence > 50) { + logger_1.logger.warn('provider-detection', `Provider change detected for ${category}: ${expectedProvider} -> ${result.provider}`); + return { + changed: true, + newProvider: result.provider, + confidence: result.confidence, + }; + } + return { changed: false }; + } + catch (error) { + logger_1.logger.error('provider-detection', `Change detection failed: ${error.message}`); + return { changed: false }; + } +} +// ======================================== +// Database Operations +// ======================================== +async function updateDispensaryCategoryProvider(dispensaryId, category, result) { + const columnPrefix = category === 'product' ? 'product' : + category === 'specials' ? 'specials' : + category === 'brand' ? 'brand' : 'metadata'; + await migrate_1.pool.query(`UPDATE dispensaries SET + ${columnPrefix}_provider = $1, + ${columnPrefix}_confidence = $2, + ${columnPrefix}_crawler_mode = $3, + ${columnPrefix}_detection_data = $4, + updated_at = NOW() + WHERE id = $5`, [ + result.provider, + result.confidence, + result.mode, + JSON.stringify(result.signals), + dispensaryId, + ]); +} +async function updateAllCategoryProviders(dispensaryId, result) { + await migrate_1.pool.query(`UPDATE dispensaries SET + product_provider = $1, + product_confidence = $2, + product_crawler_mode = $3, + product_detection_data = $4, + specials_provider = $5, + specials_confidence = $6, + specials_crawler_mode = $7, + specials_detection_data = $8, + brand_provider = $9, + brand_confidence = $10, + brand_crawler_mode = $11, + brand_detection_data = $12, + metadata_provider = $13, + metadata_confidence = $14, + metadata_crawler_mode = $15, + metadata_detection_data = $16, + updated_at = NOW() + WHERE id = $17`, [ + result.product.provider, + result.product.confidence, + result.product.mode, + JSON.stringify(result.product.signals), + result.specials.provider, + result.specials.confidence, + result.specials.mode, + JSON.stringify(result.specials.signals), + result.brand.provider, + result.brand.confidence, + result.brand.mode, + JSON.stringify(result.brand.signals), + result.metadata.provider, + result.metadata.confidence, + result.metadata.mode, + JSON.stringify(result.metadata.signals), + dispensaryId, + ]); +} +async function moveCategoryToSandbox(dispensaryId, category, reason) { + const columnPrefix = category === 'product' ? 'product' : + category === 'specials' ? 'specials' : + category === 'brand' ? 'brand' : 'metadata'; + await migrate_1.pool.query(`UPDATE dispensaries SET + ${columnPrefix}_crawler_mode = 'sandbox', + ${columnPrefix}_detection_data = ${columnPrefix}_detection_data || $1::jsonb, + updated_at = NOW() + WHERE id = $2`, [ + JSON.stringify({ sandbox_reason: reason, sandbox_at: new Date().toISOString() }), + dispensaryId, + ]); + logger_1.logger.info('provider-detection', `Moved dispensary ${dispensaryId} ${category} to sandbox: ${reason}`); +} diff --git a/backend/dist/services/menu-provider-detector.js b/backend/dist/services/menu-provider-detector.js new file mode 100644 index 00000000..f3faa9a9 --- /dev/null +++ b/backend/dist/services/menu-provider-detector.js @@ -0,0 +1,612 @@ +"use strict"; +/** + * Menu Provider Detection Service + * + * Detects which menu platform a dispensary is using by analyzing: + * - HTML content patterns (scripts, iframes, classes) + * - URL patterns (embedded menu paths) + * - API endpoint signatures + * - Meta tags and headers + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.detectMenuProvider = detectMenuProvider; +exports.quickDutchieCheck = quickDutchieCheck; +exports.detectProviderChange = detectProviderChange; +const puppeteer_1 = __importDefault(require("puppeteer")); +const logger_1 = require("./logger"); +// Provider detection patterns +const PROVIDER_PATTERNS = { + dutchie: { + scripts: [ + /dutchie/i, + /dutchie-plus/i, + /dutchie\.com/i, + /dutchie-embed/i, + ], + iframes: [ + /dutchie\.com/i, + /embed\.dutchie/i, + /iframe\.dutchie/i, + ], + classes: [ + /dutchie-/i, + /DutchieEmbed/i, + ], + urls: [ + /dutchie\.com/i, + /\.dutchie\./i, + ], + meta: [ + /dutchie/i, + ], + apiEndpoints: [ + /graphql.*dutchie/i, + /api\.dutchie/i, + ], + htmlPatterns: [ + /data-dutchie/i, + /__DUTCHIE__/i, + /dutchie-plus-iframe/i, + ], + }, + treez: { + scripts: [ + /treez/i, + /treez\.io/i, + /treezpay/i, + ], + iframes: [ + /treez\.io/i, + /menu\.treez/i, + ], + classes: [ + /treez-/i, + ], + urls: [ + /treez\.io/i, + /\.treez\./i, + ], + meta: [ + /treez/i, + ], + apiEndpoints: [ + /api\.treez/i, + ], + htmlPatterns: [ + /data-treez/i, + /treez-embed/i, + ], + }, + jane: { + scripts: [ + /jane\.co/i, + /iheartjane/i, + /jane-embed/i, + /janetechnologies/i, + ], + iframes: [ + /jane\.co/i, + /iheartjane\.com/i, + /menu\.jane/i, + ], + classes: [ + /jane-/i, + /iheartjane/i, + ], + urls: [ + /jane\.co/i, + /iheartjane\.com/i, + ], + meta: [ + /jane/i, + /iheartjane/i, + ], + apiEndpoints: [ + /api\.iheartjane/i, + /api\.jane\.co/i, + ], + htmlPatterns: [ + /data-jane/i, + /jane-root/i, + /jane-embed/i, + ], + }, + weedmaps: { + scripts: [ + /weedmaps/i, + /wm\.com/i, + ], + iframes: [ + /weedmaps\.com/i, + /menu\.weedmaps/i, + ], + classes: [ + /weedmaps-/i, + /wm-/i, + ], + urls: [ + /weedmaps\.com/i, + ], + meta: [ + /weedmaps/i, + ], + apiEndpoints: [ + /api.*weedmaps/i, + ], + htmlPatterns: [ + /data-weedmaps/i, + ], + }, + leafly: { + scripts: [ + /leafly/i, + /leafly\.com/i, + ], + iframes: [ + /leafly\.com/i, + /menu\.leafly/i, + ], + classes: [ + /leafly-/i, + ], + urls: [ + /leafly\.com/i, + ], + meta: [ + /leafly/i, + ], + apiEndpoints: [ + /api\.leafly/i, + ], + htmlPatterns: [ + /data-leafly/i, + ], + }, + meadow: { + scripts: [ + /meadow/i, + /getmeadow/i, + ], + iframes: [ + /getmeadow\.com/i, + ], + classes: [ + /meadow-/i, + ], + urls: [ + /getmeadow\.com/i, + ], + meta: [], + apiEndpoints: [ + /api\.getmeadow/i, + ], + htmlPatterns: [], + }, + greenlight: { + scripts: [ + /greenlight/i, + /greenlightmenu/i, + ], + iframes: [ + /greenlight/i, + ], + classes: [ + /greenlight-/i, + ], + urls: [ + /greenlight/i, + ], + meta: [], + apiEndpoints: [], + htmlPatterns: [], + }, + blaze: { + scripts: [ + /blaze\.me/i, + /blazepos/i, + ], + iframes: [ + /blaze\.me/i, + ], + classes: [ + /blaze-/i, + ], + urls: [ + /blaze\.me/i, + ], + meta: [], + apiEndpoints: [ + /api\.blaze/i, + ], + htmlPatterns: [], + }, + flowhub: { + scripts: [ + /flowhub/i, + ], + iframes: [ + /flowhub\.com/i, + ], + classes: [ + /flowhub-/i, + ], + urls: [ + /flowhub\.com/i, + ], + meta: [], + apiEndpoints: [], + htmlPatterns: [], + }, + dispense: { + scripts: [ + /dispenseapp/i, + ], + iframes: [ + /dispenseapp\.com/i, + ], + classes: [ + /dispense-/i, + ], + urls: [ + /dispenseapp\.com/i, + ], + meta: [], + apiEndpoints: [], + htmlPatterns: [], + }, + cova: { + scripts: [ + /covasoftware/i, + /cova\.software/i, + ], + iframes: [ + /cova/i, + ], + classes: [ + /cova-/i, + ], + urls: [ + /cova/i, + ], + meta: [], + apiEndpoints: [], + htmlPatterns: [], + }, +}; +// Common menu URL paths to check +const MENU_PATHS = [ + '/menu', + '/shop', + '/products', + '/order', + '/store', + '/dispensary-menu', + '/online-menu', + '/shop-all', + '/browse', + '/catalog', +]; +/** + * Analyze a single page for provider signals + */ +async function analyzePageForProviders(page, url) { + const signals = []; + try { + // Get page HTML + const html = await page.content(); + const lowerHtml = html.toLowerCase(); + // Check each provider's patterns + for (const [provider, patterns] of Object.entries(PROVIDER_PATTERNS)) { + // Check script sources + const scripts = await page.$$eval('script[src]', els => els.map(el => el.getAttribute('src') || '')); + for (const script of scripts) { + for (const pattern of patterns.scripts) { + if (pattern.test(script)) { + signals.push({ + provider: provider, + confidence: 90, + source: 'script_src', + details: script, + }); + } + } + } + // Check inline scripts + const inlineScripts = await page.$$eval('script:not([src])', els => els.map(el => el.textContent || '')); + for (const scriptContent of inlineScripts) { + for (const pattern of patterns.scripts) { + if (pattern.test(scriptContent)) { + signals.push({ + provider: provider, + confidence: 70, + source: 'inline_script', + details: `Pattern: ${pattern}`, + }); + } + } + } + // Check iframes + const iframes = await page.$$eval('iframe', els => els.map(el => el.getAttribute('src') || '')); + for (const iframe of iframes) { + for (const pattern of patterns.iframes) { + if (pattern.test(iframe)) { + signals.push({ + provider: provider, + confidence: 95, + source: 'iframe_src', + details: iframe, + }); + } + } + } + // Check HTML patterns + for (const pattern of patterns.htmlPatterns) { + if (pattern.test(html)) { + signals.push({ + provider: provider, + confidence: 85, + source: 'html_pattern', + details: `Pattern: ${pattern}`, + }); + } + } + // Check CSS classes + for (const pattern of patterns.classes) { + if (pattern.test(html)) { + signals.push({ + provider: provider, + confidence: 60, + source: 'css_class', + details: `Pattern: ${pattern}`, + }); + } + } + // Check meta tags + const metaTags = await page.$$eval('meta', els => els.map(el => `${el.getAttribute('name')} ${el.getAttribute('content')}`)); + for (const meta of metaTags) { + for (const pattern of patterns.meta) { + if (pattern.test(meta)) { + signals.push({ + provider: provider, + confidence: 80, + source: 'meta_tag', + details: meta, + }); + } + } + } + } + // Check for network requests (if we intercepted them) + // This would be enhanced with request interception + } + catch (error) { + logger_1.logger.error('provider-detection', `Error analyzing page ${url}: ${error}`); + } + return signals; +} +/** + * Aggregate signals into a final detection result + */ +function aggregateSignals(signals) { + if (signals.length === 0) { + return { provider: 'unknown', confidence: 0 }; + } + // Group signals by provider + const providerScores = {}; + for (const signal of signals) { + if (!providerScores[signal.provider]) { + providerScores[signal.provider] = []; + } + providerScores[signal.provider].push(signal.confidence); + } + // Calculate weighted score for each provider + const scores = []; + for (const [provider, confidences] of Object.entries(providerScores)) { + // Use max confidence + bonus for multiple signals + const maxConf = Math.max(...confidences); + const multiSignalBonus = Math.min(10, (confidences.length - 1) * 3); + const score = Math.min(100, maxConf + multiSignalBonus); + scores.push({ provider: provider, score }); + } + // Sort by score descending + scores.sort((a, b) => b.score - a.score); + const best = scores[0]; + // If there's a clear winner (20+ point lead), use it + if (scores.length === 1 || best.score - scores[1].score >= 20) { + return { provider: best.provider, confidence: best.score }; + } + // Multiple contenders - reduce confidence + return { provider: best.provider, confidence: Math.max(50, best.score - 20) }; +} +/** + * Detect the menu provider for a dispensary + */ +async function detectMenuProvider(websiteUrl, options = {}) { + const { checkMenuPaths = true, timeout = 30000 } = options; + const result = { + provider: 'unknown', + confidence: 0, + signals: [], + urlsTested: [], + menuEntryPoints: [], + rawSignals: {}, + }; + let browser = null; + try { + // Normalize URL + let baseUrl = websiteUrl.trim(); + if (!baseUrl.startsWith('http')) { + baseUrl = `https://${baseUrl}`; + } + baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash + // Launch browser + browser = await puppeteer_1.default.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + ], + }); + const page = await browser.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + // Track network requests for API detection + const apiRequests = []; + await page.setRequestInterception(true); + page.on('request', (request) => { + const url = request.url(); + if (url.includes('api') || url.includes('graphql')) { + apiRequests.push(url); + } + request.continue(); + }); + // URLs to check + const urlsToCheck = [baseUrl]; + if (checkMenuPaths) { + for (const path of MENU_PATHS) { + urlsToCheck.push(`${baseUrl}${path}`); + } + } + // Check each URL + for (const url of urlsToCheck) { + try { + result.urlsTested.push(url); + await page.goto(url, { + waitUntil: 'networkidle2', + timeout, + }); + // Wait a bit for dynamic content + await new Promise(r => setTimeout(r, 2000)); + // Analyze page + const pageSignals = await analyzePageForProviders(page, url); + result.signals.push(...pageSignals); + // Track if this URL has menu content + const hasMenuContent = await page.evaluate(() => { + const text = document.body.innerText.toLowerCase(); + return (text.includes('add to cart') || + text.includes('add to bag') || + text.includes('product') || + text.includes('indica') || + text.includes('sativa') || + text.includes('hybrid') || + text.includes('thc') || + text.includes('cbd')); + }); + if (hasMenuContent && url !== baseUrl) { + result.menuEntryPoints.push(url); + } + } + catch (pageError) { + // 404s are fine, just skip + if (!pageError.message?.includes('404')) { + logger_1.logger.warn('provider-detection', `Could not load ${url}: ${pageError.message}`); + } + } + } + // Check API requests for provider hints + for (const apiUrl of apiRequests) { + for (const [provider, patterns] of Object.entries(PROVIDER_PATTERNS)) { + for (const pattern of patterns.apiEndpoints) { + if (pattern.test(apiUrl)) { + result.signals.push({ + provider: provider, + confidence: 95, + source: 'api_request', + details: apiUrl, + }); + } + } + } + } + // Record raw signals + result.rawSignals = { + apiRequestsFound: apiRequests.length, + menuEntryPointsFound: result.menuEntryPoints.length, + totalSignals: result.signals.length, + uniqueProviders: [...new Set(result.signals.map(s => s.provider))].length, + }; + // Aggregate signals into final result + const aggregated = aggregateSignals(result.signals); + result.provider = aggregated.provider; + result.confidence = aggregated.confidence; + } + catch (error) { + result.error = error.message; + logger_1.logger.error('provider-detection', `Detection failed for ${websiteUrl}: ${error.message}`); + } + finally { + if (browser) { + await browser.close(); + } + } + return result; +} +/** + * Quick check if a site has Dutchie - used during production crawls + */ +async function quickDutchieCheck(page) { + try { + const html = await page.content(); + // Check for Dutchie-specific patterns + const dutchiePatterns = [ + /dutchie/i, + /dutchie-plus/i, + /__DUTCHIE__/i, + /data-dutchie/i, + /embed\.dutchie/i, + ]; + for (const pattern of dutchiePatterns) { + if (pattern.test(html)) { + return true; + } + } + // Check iframes + const iframes = await page.$$eval('iframe', els => els.map(el => el.getAttribute('src') || '')); + for (const iframe of iframes) { + if (/dutchie/i.test(iframe)) { + return true; + } + } + return false; + } + catch { + return false; + } +} +/** + * Check if provider has changed from expected + */ +async function detectProviderChange(page, expectedProvider) { + try { + const signals = await analyzePageForProviders(page, page.url()); + const aggregated = aggregateSignals(signals); + // If we expected Dutchie but found something else with high confidence + if (expectedProvider === 'dutchie' && aggregated.provider !== 'dutchie' && aggregated.confidence >= 70) { + return { + changed: true, + newProvider: aggregated.provider, + confidence: aggregated.confidence, + }; + } + // If we expected Dutchie and found nothing/low confidence, might have switched + if (expectedProvider === 'dutchie' && aggregated.confidence < 30) { + // Check if Dutchie is definitely NOT present + const hasDutchie = await quickDutchieCheck(page); + if (!hasDutchie) { + return { + changed: true, + newProvider: aggregated.provider !== 'unknown' ? aggregated.provider : 'other', + confidence: Math.max(30, aggregated.confidence), + }; + } + } + return { changed: false }; + } + catch { + return { changed: false }; + } +} diff --git a/backend/dist/services/proxy.js b/backend/dist/services/proxy.js index 6cbc003a..0989c314 100644 --- a/backend/dist/services/proxy.js +++ b/backend/dist/services/proxy.js @@ -3,22 +3,92 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.isBotDetectionError = isBotDetectionError; +exports.putProxyInTimeout = putProxyInTimeout; +exports.isProxyInTimeout = isProxyInTimeout; +exports.getActiveProxy = getActiveProxy; exports.testProxy = testProxy; exports.saveProxyTestResult = saveProxyTestResult; exports.testAllProxies = testAllProxies; exports.addProxy = addProxy; exports.addProxiesFromList = addProxiesFromList; +exports.moveProxyToFailed = moveProxyToFailed; +exports.incrementProxyFailure = incrementProxyFailure; const axios_1 = __importDefault(require("axios")); const socks_proxy_agent_1 = require("socks-proxy-agent"); const https_proxy_agent_1 = require("https-proxy-agent"); const migrate_1 = require("../db/migrate"); +// In-memory proxy timeout tracking +// Maps proxy ID to timestamp when timeout expires +const proxyTimeouts = new Map(); +const PROXY_TIMEOUT_MS = 35000; // 35 seconds timeout for bot-detected proxies +// Check if error message indicates bot detection +function isBotDetectionError(errorMsg) { + const botPatterns = [ + /bot detection/i, + /captcha/i, + /challenge/i, + /cloudflare/i, + /access denied/i, + /rate limit/i, + /too many requests/i, + /temporarily blocked/i, + /suspicious activity/i, + ]; + return botPatterns.some(pattern => pattern.test(errorMsg)); +} +// Put proxy in timeout (bot detection cooldown) +function putProxyInTimeout(proxyId, reason) { + const timeoutUntil = Date.now() + PROXY_TIMEOUT_MS; + proxyTimeouts.set(proxyId, timeoutUntil); + console.log(`🚫 Proxy ${proxyId} in timeout for ${PROXY_TIMEOUT_MS / 1000}s: ${reason}`); +} +// Check if proxy is currently in timeout +function isProxyInTimeout(proxyId) { + const timeoutUntil = proxyTimeouts.get(proxyId); + if (!timeoutUntil) + return false; + if (Date.now() >= timeoutUntil) { + // Timeout expired, remove it + proxyTimeouts.delete(proxyId); + console.log(`✅ Proxy ${proxyId} timeout expired, back in rotation`); + return false; + } + return true; +} +// Get active proxy that's not in timeout +async function getActiveProxy() { + const result = await migrate_1.pool.query(` + SELECT id, host, port, protocol, username, password + FROM proxies + WHERE active = true + ORDER BY RANDOM() + `); + // Filter out proxies in timeout + for (const proxy of result.rows) { + if (!isProxyInTimeout(proxy.id)) { + return proxy; + } + } + // All proxies are in timeout, wait for first one to expire + if (proxyTimeouts.size > 0) { + const nextAvailable = Math.min(...Array.from(proxyTimeouts.values())); + const waitTime = Math.max(0, nextAvailable - Date.now()); + console.log(`⏳ All proxies in timeout, waiting ${Math.ceil(waitTime / 1000)}s for next available...`); + await new Promise(resolve => setTimeout(resolve, waitTime)); + // Try again after waiting + return getActiveProxy(); + } + console.log('⚠️ No active proxies available'); + return null; +} async function getSettings() { const result = await migrate_1.pool.query(` SELECT key, value FROM settings WHERE key IN ('proxy_timeout_ms', 'proxy_test_url') `); const settings = {}; - result.rows.forEach(row => { + result.rows.forEach((row) => { settings[row.key] = row.value; }); return { @@ -146,12 +216,44 @@ async function addProxy(host, port, protocol, username, password) { async function addProxiesFromList(proxies) { let added = 0; let failed = 0; + let duplicates = 0; const errors = []; + console.log(`📥 Importing ${proxies.length} proxies without testing...`); for (const proxy of proxies) { try { - await addProxy(proxy.host, proxy.port, proxy.protocol, proxy.username, proxy.password); - added++; - console.log(`✅ Added proxy: ${proxy.protocol}://${proxy.host}:${proxy.port}`); + // Insert without testing first + await migrate_1.pool.query(` + INSERT INTO proxies (host, port, protocol, username, password, active) + VALUES ($1, $2, $3, $4, $5, false) + ON CONFLICT (host, port, protocol) DO NOTHING + `, [ + proxy.host, + proxy.port, + proxy.protocol, + proxy.username, + proxy.password + ]); + // Check if it was actually inserted + const result = await migrate_1.pool.query(` + SELECT id FROM proxies + WHERE host = $1 AND port = $2 AND protocol = $3 + `, [proxy.host, proxy.port, proxy.protocol]); + if (result.rows.length > 0) { + // Check if it was just inserted (no last_tested_at means new) + const checkResult = await migrate_1.pool.query(` + SELECT last_tested_at FROM proxies + WHERE host = $1 AND port = $2 AND protocol = $3 + `, [proxy.host, proxy.port, proxy.protocol]); + if (checkResult.rows[0].last_tested_at === null) { + added++; + if (added % 100 === 0) { + console.log(`📥 Imported ${added} proxies...`); + } + } + else { + duplicates++; + } + } } catch (error) { failed++; @@ -159,8 +261,63 @@ async function addProxiesFromList(proxies) { errors.push(errorMsg); console.log(`❌ Failed to add proxy: ${errorMsg}`); } - // Small delay between adds - await new Promise(resolve => setTimeout(resolve, 500)); } - return { added, failed, errors }; + console.log(`✅ Import complete: ${added} added, ${duplicates} duplicates, ${failed} failed`); + return { added, failed, duplicates, errors }; +} +async function moveProxyToFailed(proxyId, errorMsg) { + // Get proxy details + const proxyResult = await migrate_1.pool.query(` + SELECT host, port, protocol, username, password, failure_count + FROM proxies + WHERE id = $1 + `, [proxyId]); + if (proxyResult.rows.length === 0) { + return; + } + const proxy = proxyResult.rows[0]; + // Insert into failed_proxies table + await migrate_1.pool.query(` + INSERT INTO failed_proxies (host, port, protocol, username, password, failure_count, last_error) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (host, port, protocol) + DO UPDATE SET + failure_count = $6, + last_error = $7, + failed_at = CURRENT_TIMESTAMP + `, [ + proxy.host, + proxy.port, + proxy.protocol, + proxy.username, + proxy.password, + proxy.failure_count, + errorMsg + ]); + // Delete from active proxies + await migrate_1.pool.query(`DELETE FROM proxies WHERE id = $1`, [proxyId]); + console.log(`🔴 Moved proxy to failed: ${proxy.protocol}://${proxy.host}:${proxy.port} (${proxy.failure_count} failures)`); +} +async function incrementProxyFailure(proxyId, errorMsg) { + // Increment failure count + const result = await migrate_1.pool.query(` + UPDATE proxies + SET failure_count = failure_count + 1, + active = false, + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + RETURNING failure_count, host, port, protocol + `, [proxyId]); + if (result.rows.length === 0) { + return false; + } + const proxy = result.rows[0]; + const failureCount = proxy.failure_count; + console.log(`⚠️ Proxy failure #${failureCount}: ${proxy.protocol}://${proxy.host}:${proxy.port}`); + // If failed 3 times, move to failed table + if (failureCount >= 3) { + await moveProxyToFailed(proxyId, errorMsg); + return true; // Moved to failed + } + return false; // Still in active proxies } diff --git a/backend/dist/services/proxyTestQueue.js b/backend/dist/services/proxyTestQueue.js new file mode 100644 index 00000000..e79c5735 --- /dev/null +++ b/backend/dist/services/proxyTestQueue.js @@ -0,0 +1,174 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.cleanupOrphanedJobs = cleanupOrphanedJobs; +exports.createProxyTestJob = createProxyTestJob; +exports.getProxyTestJob = getProxyTestJob; +exports.getActiveProxyTestJob = getActiveProxyTestJob; +exports.cancelProxyTestJob = cancelProxyTestJob; +const migrate_1 = require("../db/migrate"); +const proxy_1 = require("./proxy"); +// Simple in-memory queue - could be replaced with Bull/Bee-Queue for production +const activeJobs = new Map(); +// Clean up orphaned jobs on server startup +async function cleanupOrphanedJobs() { + try { + const result = await migrate_1.pool.query(` + UPDATE proxy_test_jobs + SET status = 'cancelled', + completed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE status IN ('pending', 'running') + RETURNING id + `); + if (result.rows.length > 0) { + console.log(`🧹 Cleaned up ${result.rows.length} orphaned proxy test jobs`); + } + } + catch (error) { + console.error('Error cleaning up orphaned jobs:', error); + } +} +async function createProxyTestJob() { + // Check for existing running jobs first + const existingJob = await getActiveProxyTestJob(); + if (existingJob) { + throw new Error('A proxy test job is already running. Please cancel it first.'); + } + const result = await migrate_1.pool.query(` + SELECT COUNT(*) as count FROM proxies + `); + const totalProxies = parseInt(result.rows[0].count); + const jobResult = await migrate_1.pool.query(` + INSERT INTO proxy_test_jobs (status, total_proxies) + VALUES ('pending', $1) + RETURNING id + `, [totalProxies]); + const jobId = jobResult.rows[0].id; + // Start job in background + runProxyTestJob(jobId).catch(err => { + console.error(`❌ Proxy test job ${jobId} failed:`, err); + }); + return jobId; +} +async function getProxyTestJob(jobId) { + const result = await migrate_1.pool.query(` + SELECT id, status, total_proxies, tested_proxies, passed_proxies, failed_proxies + FROM proxy_test_jobs + WHERE id = $1 + `, [jobId]); + if (result.rows.length === 0) { + return null; + } + return result.rows[0]; +} +async function getActiveProxyTestJob() { + const result = await migrate_1.pool.query(` + SELECT id, status, total_proxies, tested_proxies, passed_proxies, failed_proxies + FROM proxy_test_jobs + WHERE status IN ('pending', 'running') + ORDER BY created_at DESC + LIMIT 1 + `); + if (result.rows.length === 0) { + return null; + } + return result.rows[0]; +} +async function cancelProxyTestJob(jobId) { + // Try to cancel in-memory job first + const jobControl = activeJobs.get(jobId); + if (jobControl) { + jobControl.cancelled = true; + } + // Always update database to handle orphaned jobs + const result = await migrate_1.pool.query(` + UPDATE proxy_test_jobs + SET status = 'cancelled', + completed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 AND status IN ('pending', 'running') + RETURNING id + `, [jobId]); + return result.rows.length > 0; +} +async function runProxyTestJob(jobId) { + // Register job as active + activeJobs.set(jobId, { cancelled: false }); + try { + // Update status to running + await migrate_1.pool.query(` + UPDATE proxy_test_jobs + SET status = 'running', + started_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + `, [jobId]); + console.log(`🔍 Starting proxy test job ${jobId}...`); + // Get all proxies + const result = await migrate_1.pool.query(` + SELECT id, host, port, protocol, username, password + FROM proxies + ORDER BY id + `); + let tested = 0; + let passed = 0; + let failed = 0; + for (const proxy of result.rows) { + // Check if job was cancelled + const jobControl = activeJobs.get(jobId); + if (jobControl?.cancelled) { + console.log(`⏸️ Proxy test job ${jobId} cancelled`); + break; + } + // Test the proxy + const testResult = await (0, proxy_1.testProxy)(proxy.host, proxy.port, proxy.protocol, proxy.username, proxy.password); + // Save result + await (0, proxy_1.saveProxyTestResult)(proxy.id, testResult); + tested++; + if (testResult.success) { + passed++; + } + else { + failed++; + } + // Update job progress + await migrate_1.pool.query(` + UPDATE proxy_test_jobs + SET tested_proxies = $1, + passed_proxies = $2, + failed_proxies = $3, + updated_at = CURRENT_TIMESTAMP + WHERE id = $4 + `, [tested, passed, failed, jobId]); + // Log progress every 10 proxies + if (tested % 10 === 0) { + console.log(`📊 Job ${jobId}: ${tested}/${result.rows.length} proxies tested (${passed} passed, ${failed} failed)`); + } + } + // Mark job as completed + const jobControl = activeJobs.get(jobId); + const finalStatus = jobControl?.cancelled ? 'cancelled' : 'completed'; + await migrate_1.pool.query(` + UPDATE proxy_test_jobs + SET status = $1, + completed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE id = $2 + `, [finalStatus, jobId]); + console.log(`✅ Proxy test job ${jobId} ${finalStatus}: ${tested} tested, ${passed} passed, ${failed} failed`); + } + catch (error) { + console.error(`❌ Proxy test job ${jobId} error:`, error); + await migrate_1.pool.query(` + UPDATE proxy_test_jobs + SET status = 'failed', + completed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + `, [jobId]); + } + finally { + // Remove from active jobs + activeJobs.delete(jobId); + } +} diff --git a/backend/dist/services/scheduler.js b/backend/dist/services/scheduler.js index 9c97cab9..dfa670a4 100644 --- a/backend/dist/services/scheduler.js +++ b/backend/dist/services/scheduler.js @@ -18,7 +18,7 @@ async function getSettings() { WHERE key IN ('scrape_interval_hours', 'scrape_specials_time') `); const settings = {}; - result.rows.forEach(row => { + result.rows.forEach((row) => { settings[row.key] = row.value; }); return { diff --git a/backend/dist/services/scraper-debug.js b/backend/dist/services/scraper-debug.js index a1caa9dd..2050279f 100644 --- a/backend/dist/services/scraper-debug.js +++ b/backend/dist/services/scraper-debug.js @@ -4,10 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.debugDutchiePage = debugDutchiePage; -const puppeteer_1 = __importDefault(require("puppeteer")); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); const logger_1 = require("./logger"); +// Apply stealth plugin +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); async function debugDutchiePage(url) { - const browser = await puppeteer_1.default.launch({ + const browser = await puppeteer_extra_1.default.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] }); diff --git a/backend/dist/services/scraper-playwright.js b/backend/dist/services/scraper-playwright.js new file mode 100644 index 00000000..ad2ec2fa --- /dev/null +++ b/backend/dist/services/scraper-playwright.js @@ -0,0 +1,236 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.scrapeCategoryPlaywright = scrapeCategoryPlaywright; +exports.testScrapeCategoryPlaywright = testScrapeCategoryPlaywright; +const age_gate_playwright_1 = require("../utils/age-gate-playwright"); +const logger_1 = require("./logger"); +const stealthBrowser_1 = require("../utils/stealthBrowser"); +const dutchie_1 = require("../scrapers/templates/dutchie"); +/** + * Scrapes a category page using Playwright with stealth mode to extract product information + */ +async function scrapeCategoryPlaywright(categoryUrl, categoryName, state = 'Arizona', proxy) { + logger_1.logger.info('scraper', `Scraping category: ${categoryName}`); + logger_1.logger.info('scraper', `URL: ${categoryUrl}`); + // Create stealth browser with optional proxy + const browser = await (0, stealthBrowser_1.createStealthBrowser)({ proxy, headless: true }); + try { + // Create stealth context with age gate cookies + const context = await (0, stealthBrowser_1.createStealthContext)(browser, { state }); + // Try to load saved session cookies + const cookiesPath = `/tmp/dutchie-session-${state.toLowerCase()}.json`; + await (0, stealthBrowser_1.loadCookies)(context, cookiesPath); + const page = await context.newPage(); + // Navigate to category page + logger_1.logger.info('scraper', `Loading page: ${categoryUrl}`); + await page.goto(categoryUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); + // Random delay to appear more human + await (0, stealthBrowser_1.randomDelay)(1000, 2000); + // Check for Cloudflare challenge + if (await (0, stealthBrowser_1.isCloudflareChallenge)(page)) { + logger_1.logger.info('scraper', '🛡️ Cloudflare challenge detected, waiting...'); + const passed = await (0, stealthBrowser_1.waitForCloudflareChallenge)(page, 30000); + if (!passed) { + logger_1.logger.error('scraper', '❌ Failed to pass Cloudflare challenge'); + await browser.close(); + return []; + } + // Save successful session cookies + await (0, stealthBrowser_1.saveCookies)(context, cookiesPath); + } + // Wait for page to be fully loaded + await (0, stealthBrowser_1.waitForPageLoad)(page); + // Simulate human behavior + await (0, stealthBrowser_1.simulateHumanBehavior)(page); + // Check for and bypass age gate + const bypassed = await (0, age_gate_playwright_1.bypassAgeGatePlaywright)(page, state); + if (!bypassed) { + logger_1.logger.error('scraper', 'Failed to bypass age gate'); + await browser.close(); + return []; + } + // Wait for products to load with random delay + logger_1.logger.info('scraper', 'Waiting for products to load...'); + await (0, stealthBrowser_1.randomDelay)(2000, 4000); + // Scroll to load all products with human-like behavior + logger_1.logger.info('scraper', 'Scrolling to load all products...'); + await scrollToBottomHuman(page); + // Extract products + logger_1.logger.info('scraper', 'Extracting products from page...'); + const products = await extractProducts(page, categoryUrl, categoryName); + logger_1.logger.info('scraper', `Found ${products.length} products`); + await browser.close(); + return products; + } + catch (error) { + logger_1.logger.error('scraper', `Error scraping category: ${error}`); + await browser.close(); + return []; + } +} +/** + * Scrolls to the bottom of the page with human-like behavior + */ +async function scrollToBottomHuman(page) { + let previousHeight = 0; + let currentHeight = await page.evaluate(() => document.body.scrollHeight); + let attempts = 0; + const maxAttempts = 20; + while (previousHeight < currentHeight && attempts < maxAttempts) { + previousHeight = currentHeight; + // Scroll down in chunks with randomized delays + const scrollAmount = Math.floor(Math.random() * 200) + 300; // 300-500px + await (0, stealthBrowser_1.humanScroll)(page, scrollAmount); + // Random pause like a human reading + await (0, stealthBrowser_1.randomDelay)(500, 1500); + // Check new height + currentHeight = await page.evaluate(() => document.body.scrollHeight); + attempts++; + } + // Final wait for any lazy-loaded content + await (0, stealthBrowser_1.randomDelay)(1000, 2000); +} +/** + * Extracts product information from the page + */ +async function extractProducts(page, categoryUrl, categoryName) { + let products = []; + // Check if we have a template for this URL + const template = (0, dutchie_1.getTemplateForUrl)(categoryUrl); + if (template) { + logger_1.logger.info('scraper', `Using ${template.name} template for extraction`); + try { + const templateProducts = await template.extractProducts(page); + // Add category to products from template + products = templateProducts.map(p => ({ + ...p, + category: categoryName, + })); + logger_1.logger.info('scraper', `Template extracted ${products.length} products`); + return products; + } + catch (err) { + logger_1.logger.error('scraper', `Template extraction failed: ${err}`); + // Fall through to fallback methods + } + } + // Fallback Method 1: Dutchie products (for Sol Flower, etc.) + try { + const dutchieProducts = await page.locator('[data-testid^="product-"], .product-card, [class*="ProductCard"]').all(); + if (dutchieProducts.length > 0) { + logger_1.logger.info('scraper', `Found ${dutchieProducts.length} Dutchie-style products`); + for (const productEl of dutchieProducts) { + try { + const name = await productEl.locator('[data-testid="product-name"], .product-name, h3, h4').first().textContent() || ''; + const brand = await productEl.locator('[data-testid="product-brand"], .product-brand, .brand').first().textContent().catch(() => ''); + const priceText = await productEl.locator('[data-testid="product-price"], .product-price, .price').first().textContent().catch(() => ''); + const imageUrl = await productEl.locator('img').first().getAttribute('src').catch(() => ''); + const productLink = await productEl.locator('a').first().getAttribute('href').catch(() => ''); + // Parse price + const price = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '')) : undefined; + if (name) { + products.push({ + name: name.trim(), + brand: brand ? brand.trim() : undefined, + category: categoryName, + price, + image_url: imageUrl || undefined, + product_url: productLink ? new URL(productLink, categoryUrl).toString() : categoryUrl, + in_stock: true + }); + } + } + catch (err) { + logger_1.logger.warn('scraper', `Error extracting Dutchie product: ${err}`); + } + } + } + } + catch (err) { + logger_1.logger.warn('scraper', `Dutchie product extraction failed: ${err}`); + } + // Method 2: Curaleaf products + if (products.length === 0) { + try { + const curaleafProducts = await page.locator('.product, [class*="Product"], [class*="item"]').all(); + if (curaleafProducts.length > 0) { + logger_1.logger.info('scraper', `Found ${curaleafProducts.length} Curaleaf-style products`); + for (const productEl of curaleafProducts) { + try { + const name = await productEl.locator('h1, h2, h3, h4, .title, .name').first().textContent() || ''; + const priceText = await productEl.locator('.price, [class*="price"]').first().textContent().catch(() => ''); + const imageUrl = await productEl.locator('img').first().getAttribute('src').catch(() => ''); + const price = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '')) : undefined; + if (name && name.length > 3) { + products.push({ + name: name.trim(), + category: categoryName, + price, + image_url: imageUrl || undefined, + product_url: categoryUrl, + in_stock: true + }); + } + } + catch (err) { + logger_1.logger.warn('scraper', `Error extracting Curaleaf product: ${err}`); + } + } + } + } + catch (err) { + logger_1.logger.warn('scraper', `Curaleaf product extraction failed: ${err}`); + } + } + // Method 3: Generic product cards + if (products.length === 0) { + try { + const genericProducts = await page.locator('article, [role="article"], .card, [class*="card"]').all(); + logger_1.logger.info('scraper', `Trying generic selectors, found ${genericProducts.length} elements`); + for (const productEl of genericProducts) { + try { + const text = await productEl.textContent() || ''; + // Only consider elements that look like products + if (text.includes('$') || text.toLowerCase().includes('price') || text.toLowerCase().includes('thc')) { + const name = await productEl.locator('h1, h2, h3, h4').first().textContent() || ''; + if (name && name.length > 3) { + products.push({ + name: name.trim(), + category: categoryName, + product_url: categoryUrl, + in_stock: true + }); + } + } + } + catch (err) { + // Skip this element + } + } + } + catch (err) { + logger_1.logger.warn('scraper', `Generic product extraction failed: ${err}`); + } + } + return products; +} +/** + * Test function to scrape a single category + */ +async function testScrapeCategoryPlaywright(url, categoryName, state = 'Arizona') { + console.log(`\n🎭 Testing Playwright Category Scraper\n`); + console.log(`Category: ${categoryName}`); + console.log(`URL: ${url}\n`); + const products = await scrapeCategoryPlaywright(url, categoryName, state); + console.log(`\n✅ Found ${products.length} products\n`); + products.slice(0, 5).forEach((p, i) => { + console.log(`${i + 1}. ${p.name}`); + if (p.brand) + console.log(` Brand: ${p.brand}`); + if (p.price) + console.log(` Price: $${p.price}`); + console.log(` URL: ${p.product_url}`); + console.log(''); + }); + return products; +} diff --git a/backend/dist/services/scraper.js b/backend/dist/services/scraper.js index 40725930..aaaa917d 100644 --- a/backend/dist/services/scraper.js +++ b/backend/dist/services/scraper.js @@ -3,20 +3,52 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.USER_AGENT_GROUPS = exports.USER_AGENTS = void 0; +exports.getUserAgent = getUserAgent; exports.scrapeCategory = scrapeCategory; exports.saveProducts = saveProducts; exports.scrapeStore = scrapeStore; -const puppeteer_1 = __importDefault(require("puppeteer")); +const puppeteer_extra_1 = __importDefault(require("puppeteer-extra")); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); const migrate_1 = require("../db/migrate"); const minio_1 = require("../utils/minio"); const logger_1 = require("./logger"); -const USER_AGENTS = [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' -]; -function getRandomUserAgent() { - return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]; +const scraper_monitor_1 = require("../routes/scraper-monitor"); +const proxy_1 = require("./proxy"); +const age_gate_1 = require("../utils/age-gate"); +const availability_1 = require("./availability"); +// Apply stealth plugin for antidetect/anti-fingerprinting +puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)()); +exports.USER_AGENTS = { + 'chrome-windows': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'chrome-mac': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'chrome-linux': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'mobile-ios': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + 'mobile-android': 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36', + 'googlebot': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + 'bingbot': 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' +}; +exports.USER_AGENT_GROUPS = { + desktop: ['chrome-windows', 'chrome-mac', 'chrome-linux'], + mobile: ['mobile-ios', 'mobile-android'], + serp: ['googlebot', 'bingbot'] +}; +function getRandomUserAgentFromGroup(group) { + const randomKey = group[Math.floor(Math.random() * group.length)]; + return exports.USER_AGENTS[randomKey]; +} +function getUserAgent(key) { + if (!key) + return getRandomUserAgentFromGroup(exports.USER_AGENT_GROUPS.desktop); + // Check if it's a group + if (key === 'rotate-desktop') + return getRandomUserAgentFromGroup(exports.USER_AGENT_GROUPS.desktop); + if (key === 'rotate-mobile') + return getRandomUserAgentFromGroup(exports.USER_AGENT_GROUPS.mobile); + if (key === 'rotate-serp') + return getRandomUserAgentFromGroup(exports.USER_AGENT_GROUPS.serp); + // Otherwise treat as specific UA + return exports.USER_AGENTS[key] || getRandomUserAgentFromGroup(exports.USER_AGENT_GROUPS.desktop); } function extractImageIdFromUrl(url) { try { @@ -44,19 +76,6 @@ function sanitizeProductData(product) { cbd: product.cbd && product.cbd < 100 ? product.cbd : null }; } -async function getActiveProxy() { - const result = await migrate_1.pool.query(` - SELECT host, port, protocol, username, password - FROM proxies - WHERE active = true AND is_anonymous = true - ORDER BY RANDOM() - LIMIT 1 - `); - if (result.rows.length === 0) { - return null; - } - return result.rows[0]; -} async function makePageStealthy(page) { await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', { @@ -86,12 +105,11 @@ async function makePageStealthy(page) { }); } async function scrapeProductDetails(page, productUrl, productName) { - const maxRetries = 2; + const maxRetries = 3; let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - await page.goto(productUrl, { waitUntil: 'domcontentloaded', timeout: 20000 }); - await page.waitForTimeout(3000); + await page.goto(productUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }); const details = await page.evaluate(() => { const allText = document.body.textContent || ''; let fullSizeImage = null; @@ -233,9 +251,7 @@ async function scrapeProductDetails(page, productUrl, productName) { catch (error) { lastError = error; logger_1.logger.warn('scraper', ` Attempt ${attempt}/${maxRetries} failed for ${productName}: ${error}`); - if (attempt < maxRetries) { - await page.waitForTimeout(2000); - } + // No delays - just retry immediately } } logger_1.logger.error('scraper', ` ✗ All attempts failed for ${productName}`); @@ -253,8 +269,10 @@ async function scrapeProductDetails(page, productUrl, productName) { weights: [] }; } -async function scrapeCategory(storeId, categoryId) { +async function scrapeCategory(storeId, categoryId, userAgent) { let browser = null; + const scraperId = `cat-${categoryId}-${Date.now()}`; + let proxyId = null; try { const categoryResult = await migrate_1.pool.query(` SELECT c.*, s.slug as store_slug, s.name as store_name @@ -267,7 +285,12 @@ async function scrapeCategory(storeId, categoryId) { } const category = categoryResult.rows[0]; logger_1.logger.info('scraper', `Scraping category: ${category.name} for ${category.store_name}`); - const proxy = await getActiveProxy(); + // Register scraper with monitoring system + (0, scraper_monitor_1.registerScraper)(scraperId, storeId, category.store_name, categoryId, category.name); + const proxy = await (0, proxy_1.getActiveProxy)(); + if (proxy) { + proxyId = proxy.id; + } const launchOptions = { headless: 'new', args: [ @@ -287,24 +310,51 @@ async function scrapeCategory(storeId, categoryId) { } logger_1.logger.info('scraper', `Using proxy: ${proxy.protocol}://${proxy.host}:${proxy.port}`); } - browser = await puppeteer_1.default.launch(launchOptions); + browser = await puppeteer_extra_1.default.launch(launchOptions); const page = await browser.newPage(); await makePageStealthy(page); await page.setViewport({ width: 1920, height: 1080 }); - await page.setUserAgent(getRandomUserAgent()); + // Use provided userAgent or random if not specified + const ua = getUserAgent(userAgent); + await page.setUserAgent(ua); + // Set age gate bypass cookies BEFORE navigation (standard for all cannabis sites) + const state = (0, age_gate_1.detectStateFromUrl)(category.dutchie_url); + await (0, age_gate_1.setAgeGateCookies)(page, category.dutchie_url, state); logger_1.logger.info('scraper', `Loading page: ${category.dutchie_url}`); try { await page.goto(category.dutchie_url, { - waitUntil: 'domcontentloaded', + waitUntil: 'networkidle2', timeout: 60000 }); - await page.waitForTimeout(5000); + // If age gate still appears, try to bypass it + await (0, age_gate_1.bypassAgeGate)(page, state); + // Wait for products to load + await page.waitForSelector('[data-testid="product-list-item"], a[href*="/product/"]', { + timeout: 30000, + }).catch(() => { + logger_1.logger.warn('scraper', 'No product selectors found, trying anyway...'); + }); logger_1.logger.info('scraper', 'Scrolling to load all products...'); await autoScroll(page); - await page.waitForTimeout(3000); } catch (navError) { logger_1.logger.error('scraper', `Navigation error: ${navError}`); + // Check if this is bot detection - put proxy in timeout instead of hard failure + if (proxyId) { + const errorMsg = String(navError); + if ((0, proxy_1.isBotDetectionError)(errorMsg)) { + // Bot detection! Put this proxy in timeout and get a new one + logger_1.logger.warn('scraper', `🤖 Bot detection triggered for proxy ${proxyId}!`); + (0, proxy_1.putProxyInTimeout)(proxyId, errorMsg); + throw new Error(`Bot detection: ${errorMsg}`); + } + else if (errorMsg.includes('timeout') || errorMsg.includes('net::') || + errorMsg.includes('ERR_') || errorMsg.includes('Navigation')) { + // Regular proxy failure - increment failure count + logger_1.logger.warn('scraper', `Proxy failure detected, incrementing failure count for proxy ${proxyId}`); + await (0, proxy_1.incrementProxyFailure)(proxyId, errorMsg); + } + } throw navError; } logger_1.logger.info('scraper', 'Extracting product list from page...'); @@ -336,6 +386,21 @@ async function scrapeCategory(storeId, categoryId) { originalPrice = parseFloat(priceMatches[1].replace('$', '')); } } + // Extract variant (weight/size) - look for common patterns + let variant = null; + const variantPatterns = [ + /(\d+\.?\d*\s*(?:g|oz|mg|ml|gram|ounce))/i, // Weight units + /(\d+\s*pack)/i, // Pack sizes + /(\d+\s*ct)/i, // Count + /(\d+\s*x\s*\d+\.?\d*\s*(?:g|mg|ml))/i // Multi-pack (e.g., 5x0.5g) + ]; + for (const pattern of variantPatterns) { + const match = allText.match(pattern); + if (match) { + variant = match[1].trim(); + break; + } + } const linkEl = card.querySelector('a[href*="/product/"]'); let href = linkEl?.href || linkEl?.getAttribute('href') || ''; if (href && href.startsWith('/')) { @@ -343,6 +408,7 @@ async function scrapeCategory(storeId, categoryId) { } items.push({ name, + variant, price, originalPrice, href: href || window.location.href @@ -358,10 +424,19 @@ async function scrapeCategory(storeId, categoryId) { logger_1.logger.info('scraper', `Now visiting each product page for complete details...`); let successCount = 0; let failCount = 0; + // Update initial stats + (0, scraper_monitor_1.updateScraperStats)(scraperId, { + productsProcessed: 0, + productsTotal: products.length + }); for (let i = 0; i < products.length; i++) { const product = products[i]; try { logger_1.logger.info('scraper', ` [${i + 1}/${products.length}] ${product.name}`); + (0, scraper_monitor_1.updateScraperStats)(scraperId, { + productsProcessed: i + 1, + productsTotal: products.length + }, `Processing: ${product.name}`); if (!product.href) { logger_1.logger.warn('scraper', ` ⚠ No product URL, skipping details`); product.metadata = {}; @@ -391,7 +466,7 @@ async function scrapeCategory(storeId, categoryId) { logger_1.logger.warn('scraper', ` ⚠ Limited data extracted`); failCount++; } - await page.waitForTimeout(1500); + // No delays - scrape fast! } catch (error) { logger_1.logger.error('scraper', ` ✗ Unexpected error: ${error}`); @@ -411,11 +486,16 @@ async function scrapeCategory(storeId, categoryId) { SET last_scraped_at = CURRENT_TIMESTAMP WHERE id = $1 `, [categoryId]); + // Mark scraper as complete + (0, scraper_monitor_1.completeScraper)(scraperId); const formattedProducts = products.map((p, index) => { const sanitized = sanitizeProductData(p); + // Normalize availability from Dutchie product data + const availability = (0, availability_1.normalizeAvailability)(p); return { dutchieProductId: `${category.store_slug}-${category.slug}-${Date.now()}-${index}`, name: sanitized.name, + variant: p.variant || null, description: sanitized.description, price: p.price, originalPrice: p.originalPrice, @@ -426,13 +506,34 @@ async function scrapeCategory(storeId, categoryId) { weight: sanitized.weight, imageUrl: p.imageUrl, dutchieUrl: p.href, - metadata: p.metadata || {} + metadata: p.metadata || {}, + availabilityStatus: availability.status, + availabilityRaw: availability.raw, + stockQuantity: availability.quantity }; }); return formattedProducts; } catch (error) { logger_1.logger.error('scraper', `❌ Category scraping error: ${error}`); + // Smart proxy error handling + if (proxyId) { + const errorMsg = String(error); + if ((0, proxy_1.isBotDetectionError)(errorMsg)) { + // Bot detection! Put this proxy in timeout + logger_1.logger.warn('scraper', `🤖 Bot detection triggered for proxy ${proxyId}!`); + (0, proxy_1.putProxyInTimeout)(proxyId, errorMsg); + } + else if (errorMsg.includes('timeout') || errorMsg.includes('net::') || + errorMsg.includes('ERR_') || errorMsg.includes('Navigation') || + errorMsg.includes('Protocol error') || errorMsg.includes('Target closed')) { + // Regular proxy failure - increment failure count + logger_1.logger.warn('scraper', `Proxy failure detected, incrementing failure count for proxy ${proxyId}`); + await (0, proxy_1.incrementProxyFailure)(proxyId, errorMsg); + } + } + // Mark scraper as failed + (0, scraper_monitor_1.completeScraper)(scraperId, String(error)); if (browser) { try { await browser.close(); @@ -466,51 +567,84 @@ async function saveProducts(storeId, categoryId, products) { try { await client.query('BEGIN'); logger_1.logger.info('scraper', `Saving ${products.length} products to database...`); + // Mark all products as out-of-stock before processing (they'll be re-marked if found) + // Also update availability_status and last_seen_out_of_stock_at for state transition tracking await client.query(` UPDATE products - SET in_stock = false - WHERE store_id = $1 AND category_id = $2 + SET in_stock = false, + availability_status = 'out_of_stock', + last_seen_out_of_stock_at = CASE + WHEN availability_status != 'out_of_stock' THEN CURRENT_TIMESTAMP + ELSE last_seen_out_of_stock_at + END + WHERE store_id = $1 AND category_id = $2 AND in_stock = true `, [storeId, categoryId]); for (const product of products) { try { + // Get availability from product (defaults to in_stock if product exists in scraped data) + const availStatus = product.availabilityStatus || 'in_stock'; + const availRaw = product.availabilityRaw ? JSON.stringify(product.availabilityRaw) : null; + const stockQty = product.stockQuantity ?? null; const existingResult = await client.query(` - SELECT id, image_url, local_image_path + SELECT id, image_url, local_image_path, availability_status FROM products WHERE store_id = $1 AND name = $2 AND category_id = $3 - `, [storeId, product.name, categoryId]); + AND (variant = $4 OR (variant IS NULL AND $4 IS NULL)) + `, [storeId, product.name, categoryId, product.variant || null]); let localImagePath = null; let productId; if (existingResult.rows.length > 0) { productId = existingResult.rows[0].id; localImagePath = existingResult.rows[0].local_image_path; + const prevStatus = existingResult.rows[0].availability_status; + // Determine if we need to update last_seen_in_stock_at + const isNowInStock = availStatus === 'in_stock' || availStatus === 'limited'; + const wasOutOfStock = prevStatus === 'out_of_stock' || prevStatus === 'unknown'; await client.query(` UPDATE products - SET name = $1, description = $2, price = $3, - strain_type = $4, thc_percentage = $5, cbd_percentage = $6, - brand = $7, weight = $8, image_url = $9, dutchie_url = $10, - in_stock = true, metadata = $11, last_seen_at = CURRENT_TIMESTAMP, - updated_at = CURRENT_TIMESTAMP - WHERE id = $12 + SET name = $1, variant = $2, description = $3, price = $4, + strain_type = $5, thc_percentage = $6, cbd_percentage = $7, + brand = $8, weight = $9, image_url = $10, dutchie_url = $11, + in_stock = true, metadata = $12, last_seen_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP, + availability_status = $14, + availability_raw = $15, + stock_quantity = $16, + last_seen_in_stock_at = CASE + WHEN $17 THEN CURRENT_TIMESTAMP + ELSE last_seen_in_stock_at + END + WHERE id = $13 `, [ - product.name, product.description, product.price, + product.name, product.variant, product.description, product.price, product.strainType, product.thcPercentage, product.cbdPercentage, product.brand, product.weight, product.imageUrl, product.dutchieUrl, - JSON.stringify(product.metadata), productId + JSON.stringify(product.metadata), productId, availStatus, availRaw, stockQty, + isNowInStock && wasOutOfStock ]); } else { + // Generate unique slug from product name + timestamp + random suffix + const baseSlug = product.name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .substring(0, 150); + const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).substr(2, 6)}`; + const slug = `${baseSlug}-${uniqueSuffix}`; const insertResult = await client.query(` INSERT INTO products ( - store_id, category_id, dutchie_product_id, name, description, + store_id, category_id, dutchie_product_id, name, slug, variant, description, price, strain_type, thc_percentage, cbd_percentage, - brand, weight, image_url, dutchie_url, in_stock, metadata - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, true, $14) + brand, weight, image_url, dutchie_url, in_stock, metadata, + availability_status, availability_raw, stock_quantity, last_seen_in_stock_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, true, $16, $17, $18, $19, CURRENT_TIMESTAMP) RETURNING id `, [ - storeId, categoryId, product.dutchieProductId, product.name, product.description, + storeId, categoryId, product.dutchieProductId, product.name, slug, product.variant, product.description, product.price, product.strainType, product.thcPercentage, product.cbdPercentage, product.brand, product.weight, product.imageUrl, product.dutchieUrl, - JSON.stringify(product.metadata) + JSON.stringify(product.metadata), availStatus, availRaw, stockQty ]); productId = insertResult.rows[0].id; } @@ -544,19 +678,15 @@ async function saveProducts(storeId, categoryId, products) { client.release(); } } -async function scrapeStore(storeId) { +async function scrapeStore(storeId, parallel = 3, userAgent) { try { - logger_1.logger.info('scraper', `🏪 Starting scrape for store ID: ${storeId}`); + logger_1.logger.info('scraper', `🏪 Starting scrape for store ID: ${storeId} (${parallel} parallel, UA: ${userAgent || 'random'})`); const categoriesResult = await migrate_1.pool.query(` SELECT c.id, c.name, c.slug, c.dutchie_url FROM categories c - WHERE c.store_id = $1 - AND c.scrape_enabled = true - AND NOT EXISTS ( - SELECT 1 FROM categories child - WHERE child.parent_id = c.id - ) - ORDER BY c.display_order, c.name + WHERE c.store_id = $1 + AND c.scrape_enabled = true + ORDER BY c.name `, [storeId]); logger_1.logger.info('scraper', `Found ${categoriesResult.rows.length} categories to scrape`); for (const category of categoriesResult.rows) { @@ -564,14 +694,14 @@ async function scrapeStore(storeId) { logger_1.logger.info('scraper', `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); logger_1.logger.info('scraper', `📂 Scraping: ${category.name}`); logger_1.logger.info('scraper', `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - const products = await scrapeCategory(storeId, category.id); + const products = await scrapeCategory(storeId, category.id, userAgent); await saveProducts(storeId, category.id, products); logger_1.logger.info('scraper', `✅ Completed ${category.name} - ${products.length} products saved`); } catch (error) { logger_1.logger.error('scraper', `❌ Failed to scrape ${category.name}: ${error}`); } - await new Promise(resolve => setTimeout(resolve, 5000)); + // No delays - scrape fast! } await migrate_1.pool.query(` UPDATE stores diff --git a/backend/dist/services/store-crawl-orchestrator.js b/backend/dist/services/store-crawl-orchestrator.js new file mode 100644 index 00000000..11831849 --- /dev/null +++ b/backend/dist/services/store-crawl-orchestrator.js @@ -0,0 +1,351 @@ +"use strict"; +/** + * Store Crawl Orchestrator + * + * Orchestrates the complete crawl workflow for a store: + * 1. Load store and its linked dispensary + * 2. Check if provider detection is needed + * 3. Run provider detection if needed + * 4. Queue appropriate crawl jobs based on provider/mode + * 5. Update store_crawl_schedule with meaningful status + * + * This replaces the simple "triggerManualCrawl" with intelligent orchestration. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runStoreCrawlOrchestrator = runStoreCrawlOrchestrator; +exports.runBatchOrchestrator = runBatchOrchestrator; +exports.getStoresDueForOrchestration = getStoresDueForOrchestration; +const uuid_1 = require("uuid"); +const migrate_1 = require("../db/migrate"); +const crawler_logger_1 = require("./crawler-logger"); +const intelligence_detector_1 = require("./intelligence-detector"); +const category_crawler_jobs_1 = require("./category-crawler-jobs"); +// DEPRECATED: scrapeStore writes to legacy products table +// import { scrapeStore } from '../scraper-v2'; +// Import the new dutchie-az pipeline for Dutchie crawling +const product_crawler_1 = require("../dutchie-az/services/product-crawler"); +const connection_1 = require("../dutchie-az/db/connection"); +// ======================================== +// Main Orchestrator Function +// ======================================== +/** + * Run the complete crawl orchestration for a store + * + * Behavior: + * 1. Load the store and its linked dispensary + * 2. If no dispensary is linked, report error + * 3. If product_provider is missing or stale (>7 days), run detection + * 4. After detection: + * - If product_provider = 'dutchie' and product_crawler_mode = 'production': Run production crawl + * - Otherwise: Run sandbox crawl + * 5. Update store_crawl_schedule with status/summary + */ +async function runStoreCrawlOrchestrator(storeId) { + const startTime = Date.now(); + const runId = (0, uuid_1.v4)(); + let result = { + status: 'pending', + summary: '', + runId, + storeId, + dispensaryId: null, + detectionRan: false, + crawlRan: false, + durationMs: 0, + }; + try { + // Mark schedule as running + await updateScheduleStatus(storeId, 'running', 'Starting orchestrator...', runId); + // 1. Load store with dispensary info + const store = await getStoreWithDispensary(storeId); + if (!store) { + throw new Error(`Store ${storeId} not found`); + } + result.dispensaryId = store.dispensary_id; + // 2. Check if dispensary is linked + if (!store.dispensary_id) { + result.status = 'error'; + result.summary = 'No dispensary linked - cannot determine provider'; + result.error = 'Store is not linked to a dispensary. Link it in the Dispensaries page.'; + await updateScheduleStatus(storeId, 'error', result.summary, runId, result.error); + result.durationMs = Date.now() - startTime; + return result; + } + // 3. Check if provider detection is needed + const needsDetection = await checkNeedsDetection(store); + if (needsDetection) { + // Run provider detection + const websiteUrl = store.dispensary_menu_url || store.dispensary_website; + if (!websiteUrl) { + result.status = 'error'; + result.summary = 'No website URL available for detection'; + result.error = 'Dispensary has no menu_url or website configured'; + await updateScheduleStatus(storeId, 'error', result.summary, runId, result.error); + result.durationMs = Date.now() - startTime; + return result; + } + await updateScheduleStatus(storeId, 'running', 'Running provider detection...', runId); + const detectionResult = await (0, intelligence_detector_1.detectMultiCategoryProviders)(websiteUrl); + result.detectionRan = true; + result.detectionResult = detectionResult; + // Save detection results to dispensary + await (0, intelligence_detector_1.updateAllCategoryProviders)(store.dispensary_id, detectionResult); + crawler_logger_1.crawlerLogger.providerDetected({ + dispensary_id: store.dispensary_id, + dispensary_name: store.dispensary_name || store.name, + detected_provider: detectionResult.product.provider, + confidence: detectionResult.product.confidence, + detection_method: 'orchestrator_run', + menu_url: websiteUrl, + category: 'product', + }); + // Refresh store info after detection + const updatedStore = await getStoreWithDispensary(storeId); + if (updatedStore) { + Object.assign(store, updatedStore); + } + } + // 4. Determine crawl type and run + const provider = store.product_provider; + const mode = store.product_crawler_mode; + if (provider === 'dutchie' && mode === 'production') { + // Production Dutchie crawl - now uses the new dutchie-az GraphQL pipeline + await updateScheduleStatus(storeId, 'running', 'Running Dutchie GraphQL crawl (dutchie-az)...', runId); + try { + // Look up the dispensary in the dutchie-az database + // The dutchie-az pipeline has its own dispensaries table + // We try multiple matching strategies: name, slug, or platform_dispensary_id + const dispensaryResult = await (0, connection_1.query)(`SELECT * FROM dispensaries + WHERE name ILIKE $1 + OR slug ILIKE $2 + LIMIT 1`, [store.dispensary_name, store.slug]); + if (dispensaryResult.rows.length === 0) { + throw new Error(`Dispensary not found in dutchie-az database. ` + + `You must add this dispensary to the dutchie-az pipeline first. ` + + `Store: ${store.name} (${store.dispensary_name})`); + } + const dutchieDispensary = dispensaryResult.rows[0]; + // Run the new dutchie-az GraphQL crawler + const crawlResult = await (0, product_crawler_1.crawlDispensaryProducts)(dutchieDispensary, 'rec', { useBothModes: true }); + result.crawlRan = true; + result.crawlType = 'production'; + result.productsFound = crawlResult.productsFound ?? undefined; + result.productsNew = crawlResult.productsUpserted ?? undefined; + result.productsUpdated = crawlResult.snapshotsCreated ?? undefined; + if (crawlResult.success) { + const detectionPart = result.detectionRan ? 'Detection + ' : ''; + result.summary = `${detectionPart}Dutchie GraphQL crawl (${crawlResult.productsFound || 0} items, ${crawlResult.productsUpserted || 0} upserted, ${crawlResult.snapshotsCreated || 0} snapshots)`; + result.status = 'success'; + // Update store's last_scraped_at + await migrate_1.pool.query('UPDATE stores SET last_scraped_at = NOW() WHERE id = $1', [storeId]); + crawler_logger_1.crawlerLogger.jobCompleted({ + job_id: 0, // Orchestrator doesn't create traditional jobs + store_id: storeId, + store_name: store.name, + duration_ms: crawlResult.durationMs, + products_found: crawlResult.productsFound || 0, + products_new: crawlResult.productsUpserted || 0, + products_updated: crawlResult.snapshotsCreated || 0, + provider: 'dutchie', + }); + } + else { + throw new Error(crawlResult.errorMessage || 'Crawl failed'); + } + } + catch (crawlError) { + result.status = 'error'; + result.error = crawlError.message; + result.summary = `Dutchie crawl failed: ${crawlError.message.slice(0, 100)}`; + result.crawlRan = true; + result.crawlType = 'production'; + crawler_logger_1.crawlerLogger.jobFailed({ + job_id: 0, + store_id: storeId, + store_name: store.name, + duration_ms: Date.now() - startTime, + error_message: crawlError.message, + provider: 'dutchie', + }); + } + } + else if (provider && provider !== 'unknown') { + // Sandbox crawl for non-Dutchie or sandbox mode + await updateScheduleStatus(storeId, 'running', `Running ${provider} sandbox crawl...`, runId); + try { + const sandboxResult = await (0, category_crawler_jobs_1.runSandboxProductsJob)(store.dispensary_id); + result.crawlRan = true; + result.crawlType = 'sandbox'; + result.productsFound = sandboxResult.data?.productsExtracted || 0; + const detectionPart = result.detectionRan ? 'Detection + ' : ''; + if (sandboxResult.success) { + result.summary = `${detectionPart}${provider} sandbox crawl (${result.productsFound} items, quality ${sandboxResult.data?.qualityScore || 0}%)`; + result.status = 'sandbox_only'; + } + else { + result.summary = `${detectionPart}${provider} sandbox failed: ${sandboxResult.message}`; + result.status = 'error'; + result.error = sandboxResult.message; + } + } + catch (sandboxError) { + result.status = 'error'; + result.error = sandboxError.message; + result.summary = `Sandbox crawl failed: ${sandboxError.message.slice(0, 100)}`; + result.crawlRan = true; + result.crawlType = 'sandbox'; + } + } + else { + // No provider detected - detection only + if (result.detectionRan) { + result.summary = `Detection complete: provider=${store.product_provider || 'unknown'}, confidence=${store.product_confidence || 0}%`; + result.status = 'detection_only'; + } + else { + result.summary = 'No provider detected and no crawl possible'; + result.status = 'error'; + result.error = 'Could not determine menu provider'; + } + } + } + catch (error) { + result.status = 'error'; + result.error = error.message; + result.summary = `Orchestrator error: ${error.message.slice(0, 100)}`; + crawler_logger_1.crawlerLogger.queueFailure({ + queue_type: 'orchestrator', + error_message: error.message, + }); + } + result.durationMs = Date.now() - startTime; + // Update final schedule status + await updateScheduleStatus(storeId, result.status, result.summary, runId, result.error); + // Create a crawl_job record for tracking + await createOrchestratorJobRecord(storeId, result); + return result; +} +// ======================================== +// Helper Functions +// ======================================== +async function getStoreWithDispensary(storeId) { + const result = await migrate_1.pool.query(`SELECT + s.id, s.name, s.slug, s.timezone, s.dispensary_id, + d.name as dispensary_name, + d.menu_url as dispensary_menu_url, + d.website as dispensary_website, + d.product_provider, + d.product_confidence, + d.product_crawler_mode, + d.last_product_scan_at + FROM stores s + LEFT JOIN dispensaries d ON d.id = s.dispensary_id + WHERE s.id = $1`, [storeId]); + return result.rows[0] || null; +} +async function checkNeedsDetection(store) { + // No dispensary = can't detect + if (!store.dispensary_id) + return false; + // No provider = definitely needs detection + if (!store.product_provider) + return true; + // Unknown provider = needs detection + if (store.product_provider === 'unknown') + return true; + // Low confidence = needs re-detection + if (store.product_confidence !== null && store.product_confidence < 50) + return true; + // Stale detection (> 7 days) = needs refresh + if (store.last_product_scan_at) { + const daysSince = (Date.now() - new Date(store.last_product_scan_at).getTime()) / (1000 * 60 * 60 * 24); + if (daysSince > 7) + return true; + } + return false; +} +async function updateScheduleStatus(storeId, status, summary, runId, error) { + await migrate_1.pool.query(`INSERT INTO store_crawl_schedule (store_id, last_status, last_summary, last_run_at, last_error) + VALUES ($1, $2, $3, NOW(), $4) + ON CONFLICT (store_id) DO UPDATE SET + last_status = $2, + last_summary = $3, + last_run_at = NOW(), + last_error = $4, + updated_at = NOW()`, [storeId, status, summary, error || null]); +} +async function getLatestCrawlStats(storeId) { + // Get count of products for this store + const result = await migrate_1.pool.query(`SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '1 hour') as recent_new, + COUNT(*) FILTER (WHERE updated_at > NOW() - INTERVAL '1 hour' AND created_at < NOW() - INTERVAL '1 hour') as recent_updated + FROM products + WHERE store_id = $1`, [storeId]); + return { + products_found: parseInt(result.rows[0]?.total || '0'), + products_new: parseInt(result.rows[0]?.recent_new || '0'), + products_updated: parseInt(result.rows[0]?.recent_updated || '0'), + }; +} +async function createOrchestratorJobRecord(storeId, result) { + await migrate_1.pool.query(`INSERT INTO crawl_jobs ( + store_id, job_type, trigger_type, status, priority, + scheduled_at, started_at, completed_at, + products_found, products_new, products_updated, + error_message, orchestrator_run_id, detection_result + ) VALUES ( + $1, 'orchestrator', 'manual', $2, 100, + NOW(), NOW(), NOW(), + $3, $4, $5, + $6, $7, $8 + )`, [ + storeId, + result.status === 'success' ? 'completed' : result.status === 'error' ? 'failed' : 'completed', + result.productsFound || null, + result.productsNew || null, + result.productsUpdated || null, + result.error || null, + result.runId, + result.detectionResult ? JSON.stringify({ + product_provider: result.detectionResult.product.provider, + product_confidence: result.detectionResult.product.confidence, + product_mode: result.detectionResult.product.mode, + }) : null, + ]); +} +// ======================================== +// Batch Orchestration +// ======================================== +/** + * Run orchestrator for multiple stores + */ +async function runBatchOrchestrator(storeIds, concurrency = 3) { + const results = []; + // Process in batches + for (let i = 0; i < storeIds.length; i += concurrency) { + const batch = storeIds.slice(i, i + concurrency); + const batchResults = await Promise.all(batch.map(storeId => runStoreCrawlOrchestrator(storeId))); + results.push(...batchResults); + } + return results; +} +/** + * Get stores that are due for orchestration + */ +async function getStoresDueForOrchestration(limit = 10) { + const result = await migrate_1.pool.query(`SELECT s.id + FROM stores s + LEFT JOIN store_crawl_schedule scs ON scs.store_id = s.id + WHERE s.active = TRUE + AND s.scrape_enabled = TRUE + AND COALESCE(scs.enabled, TRUE) = TRUE + AND ( + scs.last_run_at IS NULL + OR scs.last_run_at < NOW() - (COALESCE(scs.interval_hours, 4) || ' hours')::INTERVAL + ) + AND (scs.last_status IS NULL OR scs.last_status NOT IN ('running', 'pending')) + ORDER BY COALESCE(scs.priority, 0) DESC, scs.last_run_at ASC NULLS FIRST + LIMIT $1`, [limit]); + return result.rows.map(row => row.id); +} diff --git a/backend/dist/utils/age-gate-playwright.js b/backend/dist/utils/age-gate-playwright.js new file mode 100644 index 00000000..ac32cce4 --- /dev/null +++ b/backend/dist/utils/age-gate-playwright.js @@ -0,0 +1,175 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.hasAgeGatePlaywright = hasAgeGatePlaywright; +exports.bypassAgeGatePlaywright = bypassAgeGatePlaywright; +exports.detectStateFromUrlPlaywright = detectStateFromUrlPlaywright; +const logger_1 = require("../services/logger"); +/** + * Detects if a Playwright page has an age verification gate + */ +async function hasAgeGatePlaywright(page) { + try { + const url = page.url(); + const bodyText = await page.textContent('body') || ''; + const hasAgeVerification = url.includes('/age-gate') || + bodyText.includes('age verification') || + bodyText.includes('Please select your state') || + bodyText.includes('are you 21') || + bodyText.includes('are you 18') || + bodyText.includes('Enter your date of birth') || + bodyText.toLowerCase().includes('verify your age'); + return hasAgeVerification; + } + catch (err) { + logger_1.logger.warn('age-gate', `Error detecting age gate: ${err}`); + return false; + } +} +/** + * Attempts to bypass an age gate using Playwright + * Handles multiple age gate patterns including Curaleaf's complex React-based gate + * + * @param page - Playwright page object + * @param state - State to select (e.g., 'Arizona', 'California') + * @returns Promise - true if bypass succeeded, false otherwise + */ +async function bypassAgeGatePlaywright(page, state = 'Arizona') { + try { + const hasGate = await hasAgeGatePlaywright(page); + if (!hasGate) { + logger_1.logger.info('age-gate', 'No age gate detected'); + return true; + } + logger_1.logger.info('age-gate', `Age gate detected - attempting to bypass with state: ${state}...`); + // Wait for age gate to fully render + await page.waitForTimeout(2000); + // Method 1: Curaleaf-style (state dropdown + "I'm over 21" button) + try { + const stateButton = page.locator('button#state, button[id="state"]').first(); + const stateButtonExists = await stateButton.count() > 0; + if (stateButtonExists) { + logger_1.logger.info('age-gate', 'Found Curaleaf-style state dropdown...'); + await stateButton.click(); + await page.waitForTimeout(1000); + // Select state + const stateOption = page.locator('[role="option"]').filter({ hasText: new RegExp(`^${state}$`, 'i') }); + const stateExists = await stateOption.count() > 0; + if (stateExists) { + logger_1.logger.info('age-gate', `Clicking ${state} option...`); + await stateOption.first().click(); + await page.waitForTimeout(2000); + // Look for "I'm over 21" button + const ageButton = page.locator('button').filter({ hasText: /I'm over 21|I am 21|I'm 21|over 21/i }); + const ageButtonExists = await ageButton.count() > 0; + if (ageButtonExists) { + logger_1.logger.info('age-gate', 'Clicking age verification button...'); + await ageButton.first().click(); + await page.waitForLoadState('domcontentloaded', { timeout: 15000 }); + await page.waitForTimeout(3000); + // Check if we successfully bypassed + const finalUrl = page.url(); + if (!finalUrl.includes('/age-gate')) { + logger_1.logger.info('age-gate', `✅ Age gate bypass successful`); + return true; + } + } + } + } + } + catch (e) { + logger_1.logger.warn('age-gate', `Curaleaf method failed: ${e}`); + } + // Method 2: Simple "Yes" or "I'm 21" button (for simpler age gates) + try { + const simpleButton = page.locator('button, a, [role="button"]').filter({ + hasText: /yes|i am 21|i'm 21|enter the site|continue|confirm/i + }); + const simpleExists = await simpleButton.count() > 0; + if (simpleExists) { + logger_1.logger.info('age-gate', 'Found simple age gate button...'); + await simpleButton.first().click(); + await page.waitForLoadState('domcontentloaded', { timeout: 10000 }); + await page.waitForTimeout(2000); + const finalUrl = page.url(); + if (!finalUrl.includes('/age-gate')) { + logger_1.logger.info('age-gate', `✅ Age gate bypass successful`); + return true; + } + } + } + catch (e) { + logger_1.logger.warn('age-gate', `Simple button method failed: ${e}`); + } + // Method 3: Standard select dropdown + try { + const selectExists = await page.locator('select').count() > 0; + if (selectExists) { + logger_1.logger.info('age-gate', 'Found select dropdown...'); + const select = page.locator('select').first(); + await select.selectOption({ label: state }); + await page.waitForTimeout(1000); + // Look for submit button + const submitButton = page.locator('button[type="submit"], input[type="submit"]'); + const submitExists = await submitButton.count() > 0; + if (submitExists) { + await submitButton.first().click(); + await page.waitForLoadState('domcontentloaded', { timeout: 10000 }); + await page.waitForTimeout(2000); + const finalUrl = page.url(); + if (!finalUrl.includes('/age-gate')) { + logger_1.logger.info('age-gate', `✅ Age gate bypass successful`); + return true; + } + } + } + } + catch (e) { + logger_1.logger.warn('age-gate', `Select dropdown method failed: ${e}`); + } + // Verify final state + const finalUrl = page.url(); + if (finalUrl.includes('/age-gate')) { + logger_1.logger.error('age-gate', `❌ Age gate bypass failed - still at: ${finalUrl}`); + return false; + } + logger_1.logger.info('age-gate', `✅ Age gate bypass successful`); + return true; + } + catch (err) { + logger_1.logger.error('age-gate', `Error bypassing age gate: ${err}`); + return false; + } +} +/** + * Helper to detect the state from a store URL + */ +function detectStateFromUrlPlaywright(url) { + const stateMap = { + '-az-': 'Arizona', + 'arizona': 'Arizona', + '-ca-': 'California', + 'california': 'California', + '-co-': 'Colorado', + 'colorado': 'Colorado', + '-fl-': 'Florida', + 'florida': 'Florida', + '-il-': 'Illinois', + 'illinois': 'Illinois', + '-ma-': 'Massachusetts', + '-mi-': 'Michigan', + '-nv-': 'Nevada', + '-nj-': 'New Jersey', + '-ny-': 'New York', + '-or-': 'Oregon', + '-pa-': 'Pennsylvania', + '-wa-': 'Washington', + }; + const lowerUrl = url.toLowerCase(); + for (const [pattern, stateName] of Object.entries(stateMap)) { + if (lowerUrl.includes(pattern)) { + return stateName; + } + } + // Default to Arizona + return 'Arizona'; +} diff --git a/backend/dist/utils/age-gate.js b/backend/dist/utils/age-gate.js new file mode 100644 index 00000000..392e7b6e --- /dev/null +++ b/backend/dist/utils/age-gate.js @@ -0,0 +1,263 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setAgeGateCookies = setAgeGateCookies; +exports.hasAgeGate = hasAgeGate; +exports.bypassAgeGate = bypassAgeGate; +exports.detectStateFromUrl = detectStateFromUrl; +const logger_1 = require("../services/logger"); +/** + * Sets age gate bypass cookies before navigating to a page + * This should be called BEFORE page.goto() to prevent the age gate from showing + * + * @param page - Puppeteer page object + * @param url - URL to extract domain from + * @param state - State to set in cookie + */ +async function setAgeGateCookies(page, url, state = 'Arizona') { + try { + const urlObj = new URL(url); + const domain = urlObj.hostname.replace('www.', ''); + // Set cookies that bypass age gates + await page.setCookie({ + name: 'age_gate_passed', + value: 'true', + domain: `.${domain}`, + path: '/', + expires: Date.now() / 1000 + 365 * 24 * 60 * 60, // 1 year + httpOnly: false, + secure: false, + sameSite: 'Lax' + }, { + name: 'selected_state', + value: state, + domain: `.${domain}`, + path: '/', + expires: Date.now() / 1000 + 365 * 24 * 60 * 60, // 1 year + httpOnly: false, + secure: false, + sameSite: 'Lax' + }, { + name: 'age_verified', + value: 'true', + domain: `.${domain}`, + path: '/', + expires: Date.now() / 1000 + 365 * 24 * 60 * 60, + httpOnly: false, + secure: false, + sameSite: 'Lax' + }); + logger_1.logger.info('age-gate', `Set age gate bypass cookies for ${domain} (state: ${state})`); + } + catch (err) { + logger_1.logger.warn('age-gate', `Failed to set age gate cookies: ${err}`); + } +} +/** + * Detects if a page has an age verification gate + */ +async function hasAgeGate(page) { + return await page.evaluate(() => { + const bodyText = document.body.textContent || ''; + const hasAgeVerification = bodyText.includes('age verification') || + bodyText.includes('Please select your state') || + bodyText.includes('are you 21') || + bodyText.includes('are you 18') || + bodyText.includes('Enter your date of birth') || + bodyText.toLowerCase().includes('verify'); + return hasAgeVerification; + }); +} +/** + * Attempts to bypass an age gate by selecting the appropriate state + * Works with multiple age gate patterns used by cannabis dispensaries + * + * @param page - Puppeteer page object + * @param state - State to select (e.g., 'Arizona', 'California'). Defaults to 'Arizona' + * @returns Promise - true if bypass was attempted, false if no age gate found + */ +async function bypassAgeGate(page, state = 'Arizona', useSavedCookies = true) { + try { + const hasGate = await hasAgeGate(page); + if (!hasGate) { + logger_1.logger.info('age-gate', 'No age gate detected'); + return false; + } + logger_1.logger.info('age-gate', `Age gate detected - attempting to bypass with state: ${state}...`); + // Wait a bit for React components to fully render + await page.waitForTimeout(2000); + // Try Method 0: Custom dropdown button (shadcn/radix style - Curaleaf) + let customDropdownWorked = false; + try { + // Click button to open dropdown + const dropdownButton = await page.$('button#state, button[id="state"]'); + if (dropdownButton) { + logger_1.logger.info('age-gate', 'Found state dropdown button, clicking...'); + await dropdownButton.click(); + await page.waitForTimeout(800); + // Click the state option and trigger React events + const stateClicked = await page.evaluate((selectedState) => { + const options = Array.from(document.querySelectorAll('[role="option"]')); + const stateOption = options.find(el => el.textContent?.toLowerCase() === selectedState.toLowerCase()); + if (stateOption instanceof HTMLElement) { + // Trigger multiple events that React might be listening for + stateOption.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); + stateOption.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); + stateOption.click(); + stateOption.dispatchEvent(new MouseEvent('click', { bubbles: true })); + stateOption.dispatchEvent(new Event('change', { bubbles: true })); + stateOption.dispatchEvent(new Event('input', { bubbles: true })); + return true; + } + return false; + }, state); + if (stateClicked) { + logger_1.logger.info('age-gate', `Clicked ${state} option with React events`); + await page.waitForTimeout(1000); + // Look for and click any submit/continue button that appeared + const submitClicked = await page.evaluate(() => { + const buttons = Array.from(document.querySelectorAll('button, [role="button"], a')); + const submitBtn = buttons.find(el => { + const text = el.textContent?.toLowerCase() || ''; + const ariaLabel = el.getAttribute('aria-label')?.toLowerCase() || ''; + return text.includes('continue') || text.includes('submit') || + text.includes('enter') || text.includes('confirm') || + ariaLabel.includes('continue') || ariaLabel.includes('submit'); + }); + if (submitBtn instanceof HTMLElement && submitBtn.offsetParent !== null) { + submitBtn.click(); + return true; + } + return false; + }); + if (submitClicked) { + logger_1.logger.info('age-gate', `Found and clicked submit button`); + } + customDropdownWorked = true; + } + } + } + catch (e) { + logger_1.logger.warn('age-gate', `Dropdown method failed: ${e}`); + } + // Try Method 1: Dropdown select + const selectFound = await page.evaluate((selectedState) => { + const selects = Array.from(document.querySelectorAll('select')); + for (const select of selects) { + const options = Array.from(select.options); + const stateOption = options.find(opt => opt.text.toLowerCase().includes(selectedState.toLowerCase()) || + opt.value.toLowerCase().includes(selectedState.toLowerCase())); + if (stateOption) { + select.value = stateOption.value; + select.dispatchEvent(new Event('change', { bubbles: true })); + select.dispatchEvent(new Event('input', { bubbles: true })); + return true; + } + } + return false; + }, state); + // Try Method 2: State button/card (click state, then click confirm) + let stateClicked = false; + if (!selectFound) { + stateClicked = await page.evaluate((selectedState) => { + const allElements = Array.from(document.querySelectorAll('button, a, div, span, [role="button"], [class*="state"], [class*="State"], [class*="card"], [class*="option"]')); + const stateButton = allElements.find(el => el.textContent?.toLowerCase().includes(selectedState.toLowerCase())); + if (stateButton instanceof HTMLElement) { + stateButton.click(); + return true; + } + return false; + }, state); + if (stateClicked) { + // Wait for confirm button to appear and click it + await page.waitForTimeout(1000); + await page.evaluate(() => { + const confirmBtns = Array.from(document.querySelectorAll('button, a, [role="button"]')); + const confirmBtn = confirmBtns.find(el => { + const text = el.textContent?.toLowerCase() || ''; + return text.includes('enter') || text.includes('continue') || text.includes('yes') || text.includes('confirm'); + }); + if (confirmBtn instanceof HTMLElement) { + confirmBtn.click(); + } + }); + } + } + // Try Method 3: Direct "Yes" or age confirmation button + const yesClicked = await page.evaluate(() => { + const confirmButtons = Array.from(document.querySelectorAll('button, a, [role="button"]')); + const yesButton = confirmButtons.find(el => { + const text = el.textContent?.toLowerCase() || ''; + return text.includes('yes') || + text.includes('i am 21') || + text.includes('i am 18') || + text.includes('enter the site') || + text.includes('enter') || + text.includes('continue'); + }); + if (yesButton instanceof HTMLElement) { + yesButton.click(); + return true; + } + return false; + }); + const bypassed = customDropdownWorked || selectFound || stateClicked || yesClicked; + if (bypassed) { + // Wait for navigation to complete after clicking age gate button + logger_1.logger.info('age-gate', `Waiting for navigation after age gate bypass...`); + try { + await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 15000 }); + } + catch (e) { + // Navigation might not trigger, that's ok - wait a bit anyway + await page.waitForTimeout(3000); + } + // Give the page extra time to load content + await page.waitForTimeout(3000); + // Verify we actually bypassed by checking the URL + const finalUrl = page.url(); + if (finalUrl.includes('/age-gate')) { + logger_1.logger.error('age-gate', `❌ Age gate bypass failed - still at age gate URL: ${finalUrl}`); + return false; + } + logger_1.logger.info('age-gate', `✅ Age gate bypass completed - now at: ${finalUrl}`); + return true; + } + else { + logger_1.logger.warn('age-gate', `Could not find ${state} option or confirmation button in age gate`); + return false; + } + } + catch (err) { + logger_1.logger.error('age-gate', `Error bypassing age gate: ${err}`); + return false; + } +} +/** + * Helper to detect the state from a store URL + * @param url - Store URL + * @returns State name (e.g., 'Arizona', 'California') + */ +function detectStateFromUrl(url) { + const stateMap = { + '-az-': 'Arizona', + '-ca-': 'California', + '-co-': 'Colorado', + '-fl-': 'Florida', + '-il-': 'Illinois', + '-ma-': 'Massachusetts', + '-mi-': 'Michigan', + '-nv-': 'Nevada', + '-nj-': 'New Jersey', + '-ny-': 'New York', + '-or-': 'Oregon', + '-pa-': 'Pennsylvania', + '-wa-': 'Washington', + }; + for (const [pattern, stateName] of Object.entries(stateMap)) { + if (url.toLowerCase().includes(pattern)) { + return stateName; + } + } + // Default to Arizona if state not detected + return 'Arizona'; +} diff --git a/backend/dist/utils/image-storage.js b/backend/dist/utils/image-storage.js new file mode 100644 index 00000000..8f346232 --- /dev/null +++ b/backend/dist/utils/image-storage.js @@ -0,0 +1,296 @@ +"use strict"; +/** + * Local Image Storage Utility + * + * Downloads and stores product images to local filesystem. + * Replaces MinIO-based storage with simple local file storage. + * + * Directory structure: + * /images/products//.webp + * /images/products//-thumb.webp + * /images/products//-medium.webp + * /images/brands/.webp + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.downloadProductImage = downloadProductImage; +exports.downloadBrandLogo = downloadBrandLogo; +exports.imageExists = imageExists; +exports.deleteProductImages = deleteProductImages; +exports.initializeImageStorage = initializeImageStorage; +exports.getStorageStats = getStorageStats; +const axios_1 = __importDefault(require("axios")); +const sharp_1 = __importDefault(require("sharp")); +const fs = __importStar(require("fs/promises")); +const path = __importStar(require("path")); +const crypto_1 = require("crypto"); +// Base path for image storage - configurable via env +const IMAGES_BASE_PATH = process.env.IMAGES_PATH || '/app/public/images'; +// Public URL base for serving images +const IMAGES_PUBLIC_URL = process.env.IMAGES_PUBLIC_URL || '/images'; +/** + * Ensure a directory exists + */ +async function ensureDir(dirPath) { + try { + await fs.mkdir(dirPath, { recursive: true }); + } + catch (error) { + if (error.code !== 'EEXIST') + throw error; + } +} +/** + * Generate a short hash from a URL for deduplication + */ +function hashUrl(url) { + return (0, crypto_1.createHash)('md5').update(url).digest('hex').substring(0, 8); +} +/** + * Download an image from a URL and return the buffer + */ +async function downloadImage(imageUrl) { + const response = await axios_1.default.get(imageUrl, { + responseType: 'arraybuffer', + timeout: 30000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8', + }, + }); + return Buffer.from(response.data); +} +/** + * Process and save image in multiple sizes + * Returns the file paths relative to IMAGES_BASE_PATH + */ +async function processAndSaveImage(buffer, outputDir, baseFilename) { + await ensureDir(outputDir); + const fullPath = path.join(outputDir, `${baseFilename}.webp`); + const mediumPath = path.join(outputDir, `${baseFilename}-medium.webp`); + const thumbPath = path.join(outputDir, `${baseFilename}-thumb.webp`); + // Process images in parallel + const [fullBuffer, mediumBuffer, thumbBuffer] = await Promise.all([ + // Full: max 1200x1200, high quality + (0, sharp_1.default)(buffer) + .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true }) + .webp({ quality: 85 }) + .toBuffer(), + // Medium: 600x600 + (0, sharp_1.default)(buffer) + .resize(600, 600, { fit: 'inside', withoutEnlargement: true }) + .webp({ quality: 80 }) + .toBuffer(), + // Thumb: 200x200 + (0, sharp_1.default)(buffer) + .resize(200, 200, { fit: 'inside', withoutEnlargement: true }) + .webp({ quality: 75 }) + .toBuffer(), + ]); + // Save all sizes + await Promise.all([ + fs.writeFile(fullPath, fullBuffer), + fs.writeFile(mediumPath, mediumBuffer), + fs.writeFile(thumbPath, thumbBuffer), + ]); + const totalBytes = fullBuffer.length + mediumBuffer.length + thumbBuffer.length; + return { + full: fullPath, + medium: mediumPath, + thumb: thumbPath, + totalBytes, + }; +} +/** + * Convert a file path to a public URL + */ +function pathToUrl(filePath) { + const relativePath = filePath.replace(IMAGES_BASE_PATH, ''); + return `${IMAGES_PUBLIC_URL}${relativePath}`; +} +/** + * Download and store a product image locally + * + * @param imageUrl - The third-party image URL to download + * @param dispensaryId - The dispensary ID (for directory organization) + * @param productId - The product ID or external ID (for filename) + * @returns Download result with local URLs + */ +async function downloadProductImage(imageUrl, dispensaryId, productId) { + try { + if (!imageUrl) { + return { success: false, error: 'No image URL provided' }; + } + // Download the image + const buffer = await downloadImage(imageUrl); + // Organize by dispensary ID + const outputDir = path.join(IMAGES_BASE_PATH, 'products', String(dispensaryId)); + // Use product ID + URL hash for uniqueness + const urlHash = hashUrl(imageUrl); + const baseFilename = `${productId}-${urlHash}`; + // Process and save + const result = await processAndSaveImage(buffer, outputDir, baseFilename); + return { + success: true, + urls: { + full: pathToUrl(result.full), + medium: pathToUrl(result.medium), + thumb: pathToUrl(result.thumb), + }, + bytesDownloaded: result.totalBytes, + }; + } + catch (error) { + return { + success: false, + error: error.message || 'Failed to download image', + }; + } +} +/** + * Download and store a brand logo locally + * + * @param logoUrl - The brand logo URL + * @param brandId - The brand ID or slug + * @returns Download result with local URL + */ +async function downloadBrandLogo(logoUrl, brandId) { + try { + if (!logoUrl) { + return { success: false, error: 'No logo URL provided' }; + } + // Download the image + const buffer = await downloadImage(logoUrl); + // Brand logos go in /images/brands/ + const outputDir = path.join(IMAGES_BASE_PATH, 'brands'); + // Sanitize brand ID for filename + const safeBrandId = brandId.replace(/[^a-zA-Z0-9-_]/g, '_'); + const urlHash = hashUrl(logoUrl); + const baseFilename = `${safeBrandId}-${urlHash}`; + // Process and save (single size for logos) + await ensureDir(outputDir); + const logoPath = path.join(outputDir, `${baseFilename}.webp`); + const logoBuffer = await (0, sharp_1.default)(buffer) + .resize(400, 400, { fit: 'inside', withoutEnlargement: true }) + .webp({ quality: 85 }) + .toBuffer(); + await fs.writeFile(logoPath, logoBuffer); + return { + success: true, + urls: { + full: pathToUrl(logoPath), + medium: pathToUrl(logoPath), + thumb: pathToUrl(logoPath), + }, + bytesDownloaded: logoBuffer.length, + }; + } + catch (error) { + return { + success: false, + error: error.message || 'Failed to download brand logo', + }; + } +} +/** + * Check if a local image already exists + */ +async function imageExists(dispensaryId, productId, imageUrl) { + const urlHash = hashUrl(imageUrl); + const imagePath = path.join(IMAGES_BASE_PATH, 'products', String(dispensaryId), `${productId}-${urlHash}.webp`); + try { + await fs.access(imagePath); + return true; + } + catch { + return false; + } +} +/** + * Delete a product's local images + */ +async function deleteProductImages(dispensaryId, productId, imageUrl) { + const productDir = path.join(IMAGES_BASE_PATH, 'products', String(dispensaryId)); + const prefix = imageUrl + ? `${productId}-${hashUrl(imageUrl)}` + : String(productId); + try { + const files = await fs.readdir(productDir); + const toDelete = files.filter(f => f.startsWith(prefix)); + await Promise.all(toDelete.map(f => fs.unlink(path.join(productDir, f)))); + } + catch { + // Directory might not exist, that's fine + } +} +/** + * Initialize the image storage directories + */ +async function initializeImageStorage() { + await ensureDir(path.join(IMAGES_BASE_PATH, 'products')); + await ensureDir(path.join(IMAGES_BASE_PATH, 'brands')); + console.log(`✅ Image storage initialized at ${IMAGES_BASE_PATH}`); +} +/** + * Get storage stats + */ +async function getStorageStats() { + const productsDir = path.join(IMAGES_BASE_PATH, 'products'); + const brandsDir = path.join(IMAGES_BASE_PATH, 'brands'); + let productCount = 0; + let brandCount = 0; + try { + const productDirs = await fs.readdir(productsDir); + for (const dir of productDirs) { + const files = await fs.readdir(path.join(productsDir, dir)); + productCount += files.filter(f => f.endsWith('.webp') && !f.includes('-')).length; + } + } + catch { /* ignore */ } + try { + const brandFiles = await fs.readdir(brandsDir); + brandCount = brandFiles.filter(f => f.endsWith('.webp')).length; + } + catch { /* ignore */ } + return { + productsDir, + brandsDir, + productCount, + brandCount, + }; +} diff --git a/backend/dist/utils/minio.js b/backend/dist/utils/minio.js index 96bca1ed..552cdffb 100644 --- a/backend/dist/utils/minio.js +++ b/backend/dist/utils/minio.js @@ -36,30 +36,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.minioClient = void 0; +exports.isMinioEnabled = isMinioEnabled; exports.initializeMinio = initializeMinio; exports.uploadImageFromUrl = uploadImageFromUrl; exports.getImageUrl = getImageUrl; exports.deleteImage = deleteImage; +exports.minioClient = getMinioClient; const Minio = __importStar(require("minio")); const axios_1 = __importDefault(require("axios")); const uuid_1 = require("uuid"); -const minioClient = new Minio.Client({ - endPoint: process.env.MINIO_ENDPOINT || 'minio', - port: parseInt(process.env.MINIO_PORT || '9000'), - useSSL: process.env.MINIO_USE_SSL === 'true', - accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin', - secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin', -}); -exports.minioClient = minioClient; +const sharp_1 = __importDefault(require("sharp")); +const fs = __importStar(require("fs/promises")); +const path = __importStar(require("path")); +let minioClient = null; +// Check if MinIO is configured +function isMinioEnabled() { + return !!process.env.MINIO_ENDPOINT; +} +// Local storage path for images when MinIO is not configured +const LOCAL_IMAGES_PATH = process.env.LOCAL_IMAGES_PATH || '/app/public/images'; +function getMinioClient() { + if (!minioClient) { + minioClient = new Minio.Client({ + endPoint: process.env.MINIO_ENDPOINT || 'minio', + port: parseInt(process.env.MINIO_PORT || '9000'), + useSSL: process.env.MINIO_USE_SSL === 'true', + accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin', + secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin', + }); + } + return minioClient; +} const BUCKET_NAME = process.env.MINIO_BUCKET || 'dutchie'; async function initializeMinio() { + // Skip MinIO initialization if not configured + if (!isMinioEnabled()) { + console.log('ℹ️ MinIO not configured (MINIO_ENDPOINT not set), using local filesystem storage'); + // Ensure local images directory exists + try { + await fs.mkdir(LOCAL_IMAGES_PATH, { recursive: true }); + await fs.mkdir(path.join(LOCAL_IMAGES_PATH, 'products'), { recursive: true }); + console.log(`✅ Local images directory ready: ${LOCAL_IMAGES_PATH}`); + } + catch (error) { + console.error('❌ Failed to create local images directory:', error); + throw error; + } + return; + } try { + const client = getMinioClient(); // Check if bucket exists - const exists = await minioClient.bucketExists(BUCKET_NAME); + const exists = await client.bucketExists(BUCKET_NAME); if (!exists) { // Create bucket - await minioClient.makeBucket(BUCKET_NAME, 'us-east-1'); + await client.makeBucket(BUCKET_NAME, 'us-east-1'); console.log(`✅ Minio bucket created: ${BUCKET_NAME}`); // Set public read policy const policy = { @@ -73,7 +104,7 @@ async function initializeMinio() { }, ], }; - await minioClient.setBucketPolicy(BUCKET_NAME, JSON.stringify(policy)); + await client.setBucketPolicy(BUCKET_NAME, JSON.stringify(policy)); console.log(`✅ Bucket policy set to public read`); } else { @@ -85,36 +116,145 @@ async function initializeMinio() { throw error; } } -async function uploadImageFromUrl(imageUrl, productId) { +async function removeBackground(buffer) { + try { + // Get image metadata to check if it has an alpha channel + const metadata = await (0, sharp_1.default)(buffer).metadata(); + // If image already has transparency, trim and optimize it + if (metadata.hasAlpha) { + return await (0, sharp_1.default)(buffer) + .trim() // Remove transparent borders + .toBuffer(); + } + // For images without alpha (like JPEGs with solid backgrounds), + // we'll use a threshold-based approach to detect and remove solid backgrounds + // This works well for product images on solid color backgrounds + // Convert to PNG with alpha channel, then flatten with transparency + const withAlpha = await (0, sharp_1.default)(buffer) + .ensureAlpha() // Add alpha channel + .toBuffer(); + // Use threshold to make similar colors transparent (targets solid backgrounds) + // This is a simple approach - for better results, use remove.bg API or ML models + return await (0, sharp_1.default)(withAlpha) + .flatten({ background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .trim() + .toBuffer(); + } + catch (error) { + console.warn('Background removal failed, using original image:', error); + return buffer; + } +} +async function uploadToLocalFilesystem(thumbnailBuffer, mediumBuffer, fullBuffer, baseFilename) { + const thumbnailPath = `${baseFilename}-thumb.png`; + const mediumPath = `${baseFilename}-medium.png`; + const fullPath = `${baseFilename}-full.png`; + // Ensure the target directory exists (in case initializeMinio wasn't called) + // Extract directory from baseFilename (e.g., 'products/store-slug' or just 'products') + const targetDir = path.join(LOCAL_IMAGES_PATH, path.dirname(baseFilename)); + await fs.mkdir(targetDir, { recursive: true }); + await Promise.all([ + fs.writeFile(path.join(LOCAL_IMAGES_PATH, thumbnailPath), thumbnailBuffer), + fs.writeFile(path.join(LOCAL_IMAGES_PATH, mediumPath), mediumBuffer), + fs.writeFile(path.join(LOCAL_IMAGES_PATH, fullPath), fullBuffer), + ]); + return { + thumbnail: thumbnailPath, + medium: mediumPath, + full: fullPath, + }; +} +async function uploadToMinio(thumbnailBuffer, mediumBuffer, fullBuffer, baseFilename) { + const client = getMinioClient(); + const thumbnailPath = `${baseFilename}-thumb.png`; + const mediumPath = `${baseFilename}-medium.png`; + const fullPath = `${baseFilename}-full.png`; + await Promise.all([ + client.putObject(BUCKET_NAME, thumbnailPath, thumbnailBuffer, thumbnailBuffer.length, { + 'Content-Type': 'image/png', + }), + client.putObject(BUCKET_NAME, mediumPath, mediumBuffer, mediumBuffer.length, { + 'Content-Type': 'image/png', + }), + client.putObject(BUCKET_NAME, fullPath, fullBuffer, fullBuffer.length, { + 'Content-Type': 'image/png', + }), + ]); + return { + thumbnail: thumbnailPath, + medium: mediumPath, + full: fullPath, + }; +} +async function uploadImageFromUrl(imageUrl, productId, storeSlug, removeBackgrounds = true) { try { // Download image const response = await axios_1.default.get(imageUrl, { responseType: 'arraybuffer' }); - const buffer = Buffer.from(response.data); - // Generate unique filename - const ext = imageUrl.split('.').pop()?.split('?')[0] || 'jpg'; - const filename = `products/${productId}-${(0, uuid_1.v4)()}.${ext}`; - // Get content type - const contentType = response.headers['content-type'] || 'image/jpeg'; - // Upload to Minio - await minioClient.putObject(BUCKET_NAME, filename, buffer, buffer.length, { - 'Content-Type': contentType, - }); - // Return the path (URL will be constructed when serving) - return filename; + let buffer = Buffer.from(response.data); + // Remove background if enabled + if (removeBackgrounds) { + buffer = await removeBackground(buffer); + } + // Generate unique base filename - organize by store if slug provided + const storeDir = storeSlug ? `products/${storeSlug}` : 'products'; + const baseFilename = `${storeDir}/${productId}-${(0, uuid_1.v4)()}`; + // Create multiple sizes with Sharp and convert to WebP/PNG for better compression + // Use PNG for images with transparency + const [thumbnailBuffer, mediumBuffer, fullBuffer] = await Promise.all([ + // Thumbnail: 300x300 + (0, sharp_1.default)(buffer) + .resize(300, 300, { fit: 'inside', background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png({ quality: 80, compressionLevel: 9 }) + .toBuffer(), + // Medium: 800x800 + (0, sharp_1.default)(buffer) + .resize(800, 800, { fit: 'inside', background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png({ quality: 85, compressionLevel: 9 }) + .toBuffer(), + // Full: 2000x2000 (optimized) + (0, sharp_1.default)(buffer) + .resize(2000, 2000, { fit: 'inside', withoutEnlargement: true, background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png({ quality: 90, compressionLevel: 9 }) + .toBuffer(), + ]); + // Upload to appropriate storage backend + let result; + if (isMinioEnabled()) { + result = await uploadToMinio(thumbnailBuffer, mediumBuffer, fullBuffer, baseFilename); + } + else { + result = await uploadToLocalFilesystem(thumbnailBuffer, mediumBuffer, fullBuffer, baseFilename); + } + console.log(`✅ Uploaded 3 sizes for product ${productId}: ${thumbnailBuffer.length + mediumBuffer.length + fullBuffer.length} bytes total`); + return result; } catch (error) { console.error('Error uploading image:', error); throw error; } } -function getImageUrl(path) { - // Use localhost:9020 for browser access since Minio is exposed on host port 9020 - const endpoint = process.env.MINIO_PUBLIC_ENDPOINT || 'http://localhost:9020'; - return `${endpoint}/${BUCKET_NAME}/${path}`; +function getImageUrl(imagePath) { + if (isMinioEnabled()) { + // Use MinIO endpoint for browser access + const endpoint = process.env.MINIO_PUBLIC_ENDPOINT || 'http://localhost:9020'; + return `${endpoint}/${BUCKET_NAME}/${imagePath}`; + } + else { + // Use local path - served via Express static middleware + const publicUrl = process.env.PUBLIC_URL || ''; + return `${publicUrl}/images/${imagePath}`; + } } -async function deleteImage(path) { +async function deleteImage(imagePath) { try { - await minioClient.removeObject(BUCKET_NAME, path); + if (isMinioEnabled()) { + const client = getMinioClient(); + await client.removeObject(BUCKET_NAME, imagePath); + } + else { + const fullPath = path.join(LOCAL_IMAGES_PATH, imagePath); + await fs.unlink(fullPath); + } } catch (error) { console.error('Error deleting image:', error); diff --git a/backend/dist/utils/product-normalizer.js b/backend/dist/utils/product-normalizer.js new file mode 100644 index 00000000..6d98adcd --- /dev/null +++ b/backend/dist/utils/product-normalizer.js @@ -0,0 +1,181 @@ +"use strict"; +/** + * Product Normalizer Utility + * + * Functions for normalizing product data to enable consistent matching + * and prevent duplicate product entries. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeProductName = normalizeProductName; +exports.normalizeBrandName = normalizeBrandName; +exports.normalizeWeight = normalizeWeight; +exports.generateProductFingerprint = generateProductFingerprint; +exports.stringSimilarity = stringSimilarity; +exports.areProductsSimilar = areProductsSimilar; +/** + * Normalize product name for matching + * - Lowercase + * - Remove punctuation + * - Remove THC/CBD percentages often appended to names + * - Remove weight suffixes + * - Remove emoji + * - Normalize whitespace + */ +function normalizeProductName(name) { + if (!name) + return ''; + return name + .toLowerCase() + .trim() + // Remove special characters except alphanumeric and spaces + .replace(/[^\w\s]/g, ' ') + // Remove common suffixes like THC/CBD percentages appended to names + .replace(/\s*(thc|cbd|cbg|cbn|tac)\s*[:=]?\s*[\d.]+\s*%?/gi, '') + // Remove weight/size suffixes often appended + .replace(/\s*\d+(\.\d+)?\s*(mg|g|oz|ml|gram|grams|ounce|ounces)\b/gi, '') + // Remove emoji + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') + // Remove "special offer" type suffixes + .replace(/\s*special\s*offer\s*/gi, '') + // Normalize multiple spaces to single space + .replace(/\s+/g, ' ') + .trim(); +} +/** + * Normalize brand name for matching + */ +function normalizeBrandName(brand) { + if (!brand) + return ''; + return brand + .toLowerCase() + .trim() + // Remove special characters + .replace(/[^\w\s]/g, ' ') + // Normalize whitespace + .replace(/\s+/g, ' ') + .trim(); +} +/** + * Normalize weight string to standard format + * e.g., "3.5 grams" -> "3.5g", "1/8 oz" -> "3.5g" + */ +function normalizeWeight(weight) { + if (!weight) + return ''; + const w = weight.toLowerCase().trim(); + // Handle fractional ounces + if (w.includes('1/8') || w.includes('eighth')) { + return '3.5g'; + } + if (w.includes('1/4') || w.includes('quarter')) { + return '7g'; + } + if (w.includes('1/2') || w.includes('half')) { + return '14g'; + } + if (w.includes('1 oz') || w === 'oz' || w === '1oz') { + return '28g'; + } + // Extract numeric value and unit + const match = w.match(/([\d.]+)\s*(mg|g|oz|ml|gram|grams?|ounce|ounces?)?/i); + if (!match) + return w; + const value = parseFloat(match[1]); + let unit = (match[2] || 'g').toLowerCase(); + // Normalize unit names + unit = unit.replace(/gram(s)?/, 'g').replace(/ounce(s)?/, 'oz'); + // Convert oz to grams for consistency + if (unit === 'oz') { + return `${(value * 28).toFixed(1)}g`; + } + return `${value}${unit}`; +} +/** + * Generate a matching fingerprint for a product + * Used for deduplication + */ +function generateProductFingerprint(name, brand, weight, categoryId) { + const parts = [ + normalizeProductName(name), + normalizeBrandName(brand), + normalizeWeight(weight), + categoryId?.toString() || '' + ]; + return parts.filter(Boolean).join('|'); +} +/** + * Calculate similarity between two strings (0-100) + * Uses Levenshtein distance + */ +function stringSimilarity(str1, str2) { + if (str1 === str2) + return 100; + if (!str1 || !str2) + return 0; + const s1 = str1.toLowerCase(); + const s2 = str2.toLowerCase(); + if (s1 === s2) + return 100; + const longer = s1.length > s2.length ? s1 : s2; + const shorter = s1.length > s2.length ? s2 : s1; + const longerLength = longer.length; + if (longerLength === 0) + return 100; + const distance = levenshteinDistance(longer, shorter); + return Math.round(((longerLength - distance) / longerLength) * 100); +} +/** + * Levenshtein distance between two strings + */ +function levenshteinDistance(str1, str2) { + const m = str1.length; + const n = str2.length; + // Create distance matrix + const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)); + // Initialize first row and column + for (let i = 0; i <= m; i++) + dp[i][0] = i; + for (let j = 0; j <= n; j++) + dp[0][j] = j; + // Fill in the rest + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; + dp[i][j] = Math.min(dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + cost // substitution + ); + } + } + return dp[m][n]; +} +/** + * Check if two products are likely the same + * Returns confidence score (0-100) + */ +function areProductsSimilar(product1, product2, threshold = 92) { + const name1 = normalizeProductName(product1.name); + const name2 = normalizeProductName(product2.name); + const nameSimilarity = stringSimilarity(name1, name2); + // If names are very similar, likely same product + if (nameSimilarity >= threshold) { + return { isSimilar: true, confidence: nameSimilarity }; + } + // Check brand match for additional confidence + const brand1 = normalizeBrandName(product1.brand); + const brand2 = normalizeBrandName(product2.brand); + if (brand1 && brand2 && brand1 === brand2) { + // Same brand, lower threshold for name match + if (nameSimilarity >= threshold - 10) { + return { isSimilar: true, confidence: nameSimilarity + 5 }; + } + } + // Check weight match + const weight1 = normalizeWeight(product1.weight); + const weight2 = normalizeWeight(product2.weight); + if (weight1 && weight2 && weight1 === weight2 && nameSimilarity >= threshold - 15) { + return { isSimilar: true, confidence: nameSimilarity + 3 }; + } + return { isSimilar: false, confidence: nameSimilarity }; +} diff --git a/backend/dist/utils/proxyManager.js b/backend/dist/utils/proxyManager.js new file mode 100644 index 00000000..688939b4 --- /dev/null +++ b/backend/dist/utils/proxyManager.js @@ -0,0 +1,112 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getProxy = getProxy; +exports.getPhoenixProxy = getPhoenixProxy; +exports.getStateProxy = getStateProxy; +exports.getCityProxy = getCityProxy; +exports.getRandomProxy = getRandomProxy; +exports.getProxyLocationStats = getProxyLocationStats; +const migrate_1 = require("../db/migrate"); +const logger_1 = require("../services/logger"); +/** + * Get an active proxy from the database, optionally filtered by location + */ +async function getProxy(locationFilter) { + try { + let query = ` + SELECT protocol, host, port, username, password + FROM proxies + WHERE active = true + `; + const params = []; + let paramIndex = 1; + if (locationFilter) { + if (locationFilter.city) { + query += ` AND LOWER(city) = LOWER($${paramIndex})`; + params.push(locationFilter.city); + paramIndex++; + } + if (locationFilter.state) { + query += ` AND LOWER(state) = LOWER($${paramIndex})`; + params.push(locationFilter.state); + paramIndex++; + } + if (locationFilter.country) { + query += ` AND LOWER(country) = LOWER($${paramIndex})`; + params.push(locationFilter.country); + paramIndex++; + } + if (locationFilter.countryCode) { + query += ` AND LOWER(country_code) = LOWER($${paramIndex})`; + params.push(locationFilter.countryCode); + paramIndex++; + } + } + // Use RANDOM() for true randomization instead of least recently used + query += ` ORDER BY RANDOM() LIMIT 1`; + const result = await migrate_1.pool.query(query, params); + if (result.rows.length === 0) { + logger_1.logger.warn('proxy', `No active proxies found with filter: ${JSON.stringify(locationFilter)}`); + return null; + } + const proxy = result.rows[0]; + return { + server: `${proxy.protocol}://${proxy.host}:${proxy.port}`, + username: proxy.username || undefined, + password: proxy.password || undefined, + }; + } + catch (error) { + logger_1.logger.error('proxy', `Error fetching proxy: ${error}`); + return null; + } +} +/** + * Get a proxy from Phoenix, AZ, USA (ideal for Arizona dispensaries) + */ +async function getPhoenixProxy() { + return getProxy({ city: 'Phoenix', state: 'Arizona', country: 'United States' }); +} +/** + * Get a proxy from a specific US state + */ +async function getStateProxy(state) { + return getProxy({ state, country: 'United States' }); +} +/** + * Get a proxy from a specific city + */ +async function getCityProxy(city, state) { + return getProxy({ city, state }); +} +/** + * Get a random active proxy (no location filter) + */ +async function getRandomProxy() { + return getProxy(); +} +/** + * Get proxy location statistics + */ +async function getProxyLocationStats() { + try { + const result = await migrate_1.pool.query(` + SELECT + country, + state, + city, + COUNT(*) as count, + SUM(CASE WHEN active THEN 1 ELSE 0 END) as active_count + FROM proxies + WHERE country IS NOT NULL + GROUP BY country, state, city + ORDER BY count DESC + LIMIT 50 + `); + return result.rows; + } + catch (error) { + logger_1.logger.error('proxy', `Error fetching proxy stats: ${error}`); + return []; + } +} diff --git a/backend/dist/utils/stealthBrowser.js b/backend/dist/utils/stealthBrowser.js new file mode 100644 index 00000000..c6161cac --- /dev/null +++ b/backend/dist/utils/stealthBrowser.js @@ -0,0 +1,264 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createStealthBrowser = createStealthBrowser; +exports.createStealthContext = createStealthContext; +exports.randomDelay = randomDelay; +exports.humanMouseMove = humanMouseMove; +exports.humanScroll = humanScroll; +exports.humanType = humanType; +exports.simulateHumanBehavior = simulateHumanBehavior; +exports.waitForPageLoad = waitForPageLoad; +exports.isCloudflareChallenge = isCloudflareChallenge; +exports.waitForCloudflareChallenge = waitForCloudflareChallenge; +exports.saveCookies = saveCookies; +exports.loadCookies = loadCookies; +const playwright_extra_1 = require("playwright-extra"); +const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth")); +// Add stealth plugin +playwright_extra_1.chromium.use((0, puppeteer_extra_plugin_stealth_1.default)()); +/** + * Create a stealth browser instance with anti-detection measures + */ +async function createStealthBrowser(options = {}) { + const launchOptions = { + headless: options.headless !== false, + args: [ + '--disable-blink-features=AutomationControlled', + '--disable-features=IsolateOrigins,site-per-process', + '--disable-web-security', + '--disable-features=VizDisplayCompositor', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--no-first-run', + '--no-zygote', + '--disable-gpu', + ], + }; + if (options.proxy) { + launchOptions.proxy = options.proxy; + } + const browser = await playwright_extra_1.chromium.launch(launchOptions); + return browser; +} +/** + * Create a stealth context with realistic browser fingerprint + */ +async function createStealthContext(browser, options = {}) { + const userAgent = options.userAgent || + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + const context = await browser.newContext({ + userAgent, + viewport: { width: 1920, height: 1080 }, + locale: 'en-US', + timezoneId: 'America/Phoenix', + permissions: ['geolocation'], + geolocation: { latitude: 33.4484, longitude: -112.074 }, // Phoenix, AZ + colorScheme: 'light', + deviceScaleFactor: 1, + hasTouch: false, + isMobile: false, + javaScriptEnabled: true, + extraHTTPHeaders: { + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Upgrade-Insecure-Requests': '1', + }, + }); + // Set age verification cookies for Dutchie + await context.addCookies([ + { + name: 'age_verified', + value: 'true', + domain: '.dutchie.com', + path: '/', + expires: Math.floor(Date.now() / 1000) + 86400 * 30, // 30 days + }, + { + name: 'initial_location', + value: JSON.stringify({ state: options.state || 'Arizona' }), + domain: '.dutchie.com', + path: '/', + expires: Math.floor(Date.now() / 1000) + 86400 * 30, + }, + ]); + return context; +} +/** + * Random delay between min and max milliseconds + */ +function randomDelay(min, max) { + const delay = Math.floor(Math.random() * (max - min + 1)) + min; + return new Promise((resolve) => setTimeout(resolve, delay)); +} +/** + * Simulate human-like mouse movement + */ +async function humanMouseMove(page, x, y) { + const steps = 20; + const currentPos = await page.evaluate(() => ({ x: 0, y: 0 })); + for (let i = 0; i <= steps; i++) { + const progress = i / steps; + const easeProgress = easeInOutQuad(progress); + const nextX = currentPos.x + (x - currentPos.x) * easeProgress; + const nextY = currentPos.y + (y - currentPos.y) * easeProgress; + await page.mouse.move(nextX, nextY); + await randomDelay(5, 15); + } +} +/** + * Easing function for smooth mouse movement + */ +function easeInOutQuad(t) { + return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; +} +/** + * Simulate human-like scrolling + */ +async function humanScroll(page, scrollAmount = 500) { + const scrollSteps = 10; + const stepSize = scrollAmount / scrollSteps; + for (let i = 0; i < scrollSteps; i++) { + await page.mouse.wheel(0, stepSize); + await randomDelay(50, 150); + } +} +/** + * Simulate human-like typing + */ +async function humanType(page, selector, text) { + await page.click(selector); + await randomDelay(100, 300); + for (const char of text) { + await page.keyboard.type(char); + await randomDelay(50, 150); + } +} +/** + * Random realistic behavior before interacting with page + */ +async function simulateHumanBehavior(page) { + // Random small mouse movements + for (let i = 0; i < 3; i++) { + const x = Math.random() * 500 + 100; + const y = Math.random() * 300 + 100; + await humanMouseMove(page, x, y); + await randomDelay(200, 500); + } + // Small scroll + await humanScroll(page, 100); + await randomDelay(300, 700); +} +/** + * Wait for page to be fully loaded with human-like delay + */ +async function waitForPageLoad(page, timeout = 60000) { + try { + await page.waitForLoadState('networkidle', { timeout }); + await randomDelay(500, 1500); // Random delay after load + } + catch (error) { + // If networkidle times out, try domcontentloaded as fallback + console.log('⚠️ networkidle timeout, waiting for domcontentloaded...'); + await page.waitForLoadState('domcontentloaded', { timeout: 30000 }); + await randomDelay(1000, 2000); + } +} +/** + * Check if we're on a Cloudflare challenge page + */ +async function isCloudflareChallenge(page) { + const title = await page.title(); + const content = await page.content(); + return (title.includes('Cloudflare') || + title.includes('Just a moment') || + title.includes('Attention Required') || + content.includes('challenge-platform') || + content.includes('cf-challenge') || + content.includes('Checking your browser')); +} +/** + * Wait for Cloudflare challenge to complete + */ +async function waitForCloudflareChallenge(page, maxWaitMs = 60000) { + const startTime = Date.now(); + let attempts = 0; + while (Date.now() - startTime < maxWaitMs) { + attempts++; + if (!(await isCloudflareChallenge(page))) { + console.log(`✅ Cloudflare challenge passed after ${attempts} attempts (${Math.floor((Date.now() - startTime) / 1000)}s)`); + return true; + } + const remaining = Math.floor((maxWaitMs - (Date.now() - startTime)) / 1000); + console.log(`⏳ Waiting for Cloudflare challenge... (attempt ${attempts}, ${remaining}s remaining)`); + // Random delay between checks + await randomDelay(2000, 3000); + } + console.log('❌ Cloudflare challenge timeout - may need residential proxy or manual intervention'); + return false; +} +/** + * Save session cookies to file + */ +async function saveCookies(context, filepath) { + const cookies = await context.cookies(); + const fs = await Promise.resolve().then(() => __importStar(require('fs/promises'))); + await fs.writeFile(filepath, JSON.stringify(cookies, null, 2)); +} +/** + * Load session cookies from file + */ +async function loadCookies(context, filepath) { + try { + const fs = await Promise.resolve().then(() => __importStar(require('fs/promises'))); + const cookiesString = await fs.readFile(filepath, 'utf-8'); + const cookies = JSON.parse(cookiesString); + await context.addCookies(cookies); + return true; + } + catch (error) { + return false; + } +} diff --git a/backend/migrations/033_job_queue_claiming.sql b/backend/migrations/033_job_queue_claiming.sql new file mode 100644 index 00000000..88d15054 --- /dev/null +++ b/backend/migrations/033_job_queue_claiming.sql @@ -0,0 +1,67 @@ +-- Migration: Add job queue claiming/locking fields to dispensary_crawl_jobs +-- This enables multiple workers to claim and process jobs without conflicts + +-- Add claiming fields +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS claimed_by VARCHAR(100); +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ; +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS max_retries INTEGER DEFAULT 3; +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS retry_count INTEGER DEFAULT 0; +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS locked_until TIMESTAMPTZ; + +-- Add worker tracking +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS worker_hostname VARCHAR(255); + +-- Add progress tracking for live monitoring +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS products_upserted INTEGER DEFAULT 0; +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS snapshots_created INTEGER DEFAULT 0; +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS current_page INTEGER DEFAULT 0; +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS total_pages INTEGER; +ALTER TABLE dispensary_crawl_jobs ADD COLUMN IF NOT EXISTS last_heartbeat_at TIMESTAMPTZ; + +-- Create index for queue polling (pending jobs ordered by priority and created_at) +CREATE INDEX IF NOT EXISTS idx_dispensary_crawl_jobs_queue +ON dispensary_crawl_jobs(status, priority DESC, created_at ASC) +WHERE status = 'pending'; + +-- Create index for worker claiming (to prevent double-claims on same dispensary) +CREATE INDEX IF NOT EXISTS idx_dispensary_crawl_jobs_dispensary_active +ON dispensary_crawl_jobs(dispensary_id, status) +WHERE status IN ('pending', 'running'); + +-- Create index for claimed_by lookup +CREATE INDEX IF NOT EXISTS idx_dispensary_crawl_jobs_claimed_by +ON dispensary_crawl_jobs(claimed_by) WHERE claimed_by IS NOT NULL; + +-- Create index for heartbeat monitoring (stale workers) +CREATE INDEX IF NOT EXISTS idx_dispensary_crawl_jobs_heartbeat +ON dispensary_crawl_jobs(last_heartbeat_at) WHERE status = 'running'; + +-- Add worker_id to job_run_logs for tracking which worker ran scheduled jobs +ALTER TABLE job_run_logs ADD COLUMN IF NOT EXISTS worker_id VARCHAR(100); +ALTER TABLE job_run_logs ADD COLUMN IF NOT EXISTS worker_hostname VARCHAR(255); + +-- Create a view for queue stats +CREATE OR REPLACE VIEW v_queue_stats AS +SELECT + COUNT(*) FILTER (WHERE status = 'pending') as pending_jobs, + COUNT(*) FILTER (WHERE status = 'running') as running_jobs, + COUNT(*) FILTER (WHERE status = 'completed' AND completed_at > NOW() - INTERVAL '1 hour') as completed_1h, + COUNT(*) FILTER (WHERE status = 'failed' AND completed_at > NOW() - INTERVAL '1 hour') as failed_1h, + COUNT(DISTINCT claimed_by) FILTER (WHERE status = 'running') as active_workers, + AVG(EXTRACT(EPOCH FROM (completed_at - started_at))) FILTER (WHERE status = 'completed' AND completed_at > NOW() - INTERVAL '1 hour') as avg_duration_seconds +FROM dispensary_crawl_jobs; + +-- Create a view for active workers +CREATE OR REPLACE VIEW v_active_workers AS +SELECT + claimed_by as worker_id, + worker_hostname, + COUNT(*) as current_jobs, + SUM(products_found) as total_products_found, + SUM(products_upserted) as total_products_upserted, + SUM(snapshots_created) as total_snapshots, + MIN(claimed_at) as first_claimed_at, + MAX(last_heartbeat_at) as last_heartbeat +FROM dispensary_crawl_jobs +WHERE status = 'running' AND claimed_by IS NOT NULL +GROUP BY claimed_by, worker_hostname; diff --git a/backend/migrations/034_crawl_status_fields.sql b/backend/migrations/034_crawl_status_fields.sql new file mode 100644 index 00000000..2541cb3b --- /dev/null +++ b/backend/migrations/034_crawl_status_fields.sql @@ -0,0 +1,53 @@ +-- Migration 034: Add crawl status fields for dispensary detection +-- Tracks provider detection state and not_crawlable status + +-- Add crawl_status column: ready, not_ready, not_crawlable +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS crawl_status VARCHAR(30) DEFAULT 'not_ready'; + +-- Add reason for current status (e.g., "removed from Dutchie", "unsupported provider") +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS crawl_status_reason TEXT; + +-- When the status was last updated +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS crawl_status_updated_at TIMESTAMPTZ; + +-- The menu_url that was tested (for tracking when it changes) +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS last_tested_menu_url TEXT; + +-- HTTP status code from last test (403, 404, 200, etc.) +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS last_http_status INTEGER; + +-- Index for filtering by crawl status +CREATE INDEX IF NOT EXISTS idx_dispensaries_crawl_status ON dispensaries(crawl_status); + +-- Index for ready dispensaries with dutchie type +CREATE INDEX IF NOT EXISTS idx_dispensaries_ready_dutchie +ON dispensaries(id) +WHERE crawl_status = 'ready' AND menu_type = 'dutchie' AND platform_dispensary_id IS NOT NULL; + +-- Update existing dispensaries based on current state: +-- 1. Deeply Rooted has platform_dispensary_id, so it's ready +UPDATE dispensaries +SET crawl_status = 'ready', + crawl_status_reason = 'Platform ID resolved', + crawl_status_updated_at = NOW() +WHERE menu_type = 'dutchie' + AND platform_dispensary_id IS NOT NULL; + +-- 2. Dispensaries with not_crawlable in provider_detection_data +UPDATE dispensaries +SET crawl_status = 'not_crawlable', + crawl_status_reason = provider_detection_data->>'not_crawlable_reason', + crawl_status_updated_at = NOW() +WHERE provider_detection_data->>'not_crawlable' = 'true'; + +-- 3. All other dutchie stores are not_ready +UPDATE dispensaries +SET crawl_status = 'not_ready', + crawl_status_reason = 'Platform ID not resolved', + crawl_status_updated_at = NOW() +WHERE menu_type = 'dutchie' + AND platform_dispensary_id IS NULL + AND crawl_status IS NULL OR crawl_status = 'not_ready'; + +COMMENT ON COLUMN dispensaries.crawl_status IS 'Crawl readiness: ready (can crawl), not_ready (needs setup), not_crawlable (removed/unsupported)'; +COMMENT ON COLUMN dispensaries.crawl_status_reason IS 'Human-readable reason for current crawl status'; diff --git a/backend/migrations/034_dispensary_failure_tracking.sql b/backend/migrations/034_dispensary_failure_tracking.sql new file mode 100644 index 00000000..e81d8696 --- /dev/null +++ b/backend/migrations/034_dispensary_failure_tracking.sql @@ -0,0 +1,56 @@ +-- Migration: Add failure tracking to dispensaries +-- Tracks consecutive crawl failures and flags problematic dispensaries for review + +-- Add failure tracking columns to dispensaries +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS consecutive_failures INTEGER DEFAULT 0; +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS last_failure_at TIMESTAMPTZ; +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS last_failure_reason TEXT; +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS failed_at TIMESTAMPTZ; -- NULL = active, set = failed/suspended +ALTER TABLE dispensaries ADD COLUMN IF NOT EXISTS failure_notes TEXT; -- Admin notes about the failure + +-- Index for finding failed dispensaries +CREATE INDEX IF NOT EXISTS idx_dispensaries_failed +ON dispensaries(failed_at) WHERE failed_at IS NOT NULL; + +-- Index for finding dispensaries with failures +CREATE INDEX IF NOT EXISTS idx_dispensaries_consecutive_failures +ON dispensaries(consecutive_failures) WHERE consecutive_failures > 0; + +-- View for failed dispensaries (for admin dashboard) +CREATE OR REPLACE VIEW v_failed_dispensaries AS +SELECT + id, + name, + city, + state, + menu_url, + menu_type, + platform_dispensary_id, + consecutive_failures, + last_failure_at, + last_failure_reason, + failed_at, + failure_notes, + last_crawl_at, + updated_at +FROM dispensaries +WHERE failed_at IS NOT NULL +ORDER BY failed_at DESC; + +-- View for dispensaries needing attention (high failures but not yet failed) +CREATE OR REPLACE VIEW v_dispensaries_at_risk AS +SELECT + id, + name, + city, + state, + menu_url, + menu_type, + consecutive_failures, + last_failure_at, + last_failure_reason, + last_crawl_at +FROM dispensaries +WHERE consecutive_failures >= 2 + AND failed_at IS NULL +ORDER BY consecutive_failures DESC, last_failure_at DESC; diff --git a/backend/new-scrapers/fetch-dutchie-product.ts b/backend/new-scrapers/fetch-dutchie-product.ts new file mode 100644 index 00000000..628e0985 --- /dev/null +++ b/backend/new-scrapers/fetch-dutchie-product.ts @@ -0,0 +1,214 @@ +import { chromium as playwright } from 'playwright-extra'; +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; +import fs from 'fs/promises'; +import path from 'path'; + +playwright.use(StealthPlugin()); + +type ProductVariant = { + label: string; + price?: number; + inventory?: string; +}; + +type ProductData = { + name: string; + brand?: string; + price?: number; + description?: string; + thc?: string; + cbd?: string; + category?: string; + variants?: ProductVariant[]; + images: string[]; + productUrl: string; +}; + +const PRODUCT_URL = + 'https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-stoopid-gas'; +const OUTPUT_DIR = path.join(process.cwd(), 'scrape-output', 'dutchie-product'); +const IMAGE_DIR = path.join(OUTPUT_DIR, 'images'); +const JSON_PATH = path.join(OUTPUT_DIR, 'product.json'); + +async function ensureDirs() { + await fs.mkdir(IMAGE_DIR, { recursive: true }); +} + +async function waitForCloudflare(page: any, maxWaitMs = 60000): Promise { + const start = Date.now(); + while (Date.now() - start < maxWaitMs) { + const title = await page.title().catch(() => ''); + const content = await page.content().catch(() => ''); + const challenge = + title.includes('Attention Required') || + title.includes('Just a moment') || + content.includes('challenge-platform') || + content.includes('cf-challenge'); + if (!challenge) return true; + await page.waitForTimeout(2000); + } + return false; +} + +async function extractProduct(page: any): Promise { + return page.evaluate(() => { + const pickText = (selectorList: string[]): string | undefined => { + for (const sel of selectorList) { + const el = document.querySelector(sel) as HTMLElement | null; + const txt = el?.innerText?.trim(); + if (txt) return txt; + } + return undefined; + }; + + const pickAllTexts = (selector: string): string[] => + Array.from(document.querySelectorAll(selector)) + .map(el => (el as HTMLElement).innerText?.trim()) + .filter(Boolean) as string[]; + + const parsePrice = (text?: string | null): number | undefined => { + if (!text) return undefined; + const match = text.match(/\$?(\d+(?:\.\d{1,2})?)/); + return match ? parseFloat(match[1]) : undefined; + }; + + const name = + pickText(['[data-testid="product-name"]', 'h1', '[class*="ProductTitle"]']) || ''; + const brand = pickText(['[data-testid="product-brand"]', '[class*="Brand"]']); + const priceText = + pickText([ + '[data-testid="product-price"]', + '[data-testid*="price"]', + '[class*="Price"]' + ]) || ''; + const description = pickText(['[data-testid="product-description"]', 'article', '[class*="Description"]']); + + const potencyTexts = pickAllTexts('[data-testid*="thc"], [data-testid*="cbd"], [class*="Potency"]'); + const thc = potencyTexts.find(t => t.toLowerCase().includes('thc')) || undefined; + const cbd = potencyTexts.find(t => t.toLowerCase().includes('cbd')) || undefined; + + const category = + pickText(['[data-testid="breadcrumb"]', '[class*="Breadcrumb"]', '[data-testid*="category"]']) || undefined; + + const variantEls = Array.from( + document.querySelectorAll('[data-testid*="variant"], [data-testid*="option"], [class*="Variant"]') + ); + const variants = variantEls.map(el => { + const label = + (el.querySelector('span,div') as HTMLElement | null)?.innerText?.trim() || + el.textContent?.trim() || + ''; + const price = parsePrice(el.textContent || undefined); + return { label, price }; + }).filter(v => v.label); + + const imageUrls = Array.from( + document.querySelectorAll('img[src*="images.dutchie.com"], source[srcset*="images.dutchie.com"], img[src*="https://images.dutchie.com"]') + ).map(el => { + if (el instanceof HTMLImageElement) return el.src; + const srcset = (el as HTMLSourceElement).srcset || ''; + return srcset.split(',')[0]?.trim().split(' ')[0]; + }).filter((u): u is string => !!u); + + return { + name, + brand, + price: parsePrice(priceText), + description, + thc, + cbd, + category, + variants, + images: Array.from(new Set(imageUrls)), + productUrl: window.location.href, + }; + }); +} + +function safeFileName(base: string, ext: string): string { + return `${base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'image'}.${ext}`; +} + +async function downloadImages(urls: string[]): Promise { + const saved: string[] = []; + for (const url of urls) { + try { + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const buf = Buffer.from(await res.arrayBuffer()); + const contentType = res.headers.get('content-type') || ''; + const urlExt = path.extname(new URL(url).pathname).replace('.', ''); + const ext = + urlExt || + (contentType.includes('png') + ? 'png' + : contentType.includes('jpeg') + ? 'jpg' + : contentType.includes('webp') + ? 'webp' + : 'bin'); + const fileName = safeFileName(path.basename(url).split('.')[0] || 'image', ext); + const filePath = path.join(IMAGE_DIR, fileName); + await fs.writeFile(filePath, buf); + saved.push(filePath); + } catch (err) { + console.warn(`Failed to download image ${url}:`, err); + } + } + return saved; +} + +async function main() { + await ensureDirs(); + + const browser = await playwright.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-blink-features=AutomationControlled', + ], + }); + + const context = await browser.newContext({ + viewport: { width: 1280, height: 900 }, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); + + const page = await context.newPage(); + + try { + console.log(`Navigating to product page...`); + await page.goto(PRODUCT_URL, { waitUntil: 'domcontentloaded', timeout: 90000 }); + + const cfOk = await waitForCloudflare(page, 60000); + if (!cfOk) { + throw new Error('Cloudflare challenge not passed in time'); + } + + await page.waitForSelector('[data-testid*="product"]', { timeout: 60000 }).catch(() => undefined); + await page.waitForTimeout(2000); + + const product = await extractProduct(page); + console.log('Extracted product:'); + console.log(product); + + const imagePaths = await downloadImages(product.images); + const finalProduct = { ...product, imagePaths }; + + await fs.writeFile(JSON_PATH, JSON.stringify(finalProduct, null, 2)); + + console.log(`Saved product JSON to ${JSON_PATH}`); + if (imagePaths.length) { + console.log(`Saved ${imagePaths.length} images to ${IMAGE_DIR}`); + } + } catch (err) { + console.error('Failed to scrape product:', err); + process.exitCode = 1; + } finally { + await browser.close(); + } +} + +main(); diff --git a/backend/new-scrapers/graphql-deeply-rooted-products.ts b/backend/new-scrapers/graphql-deeply-rooted-products.ts new file mode 100644 index 00000000..6ae2c75e --- /dev/null +++ b/backend/new-scrapers/graphql-deeply-rooted-products.ts @@ -0,0 +1,227 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { chromium } from 'playwright-extra'; +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; + +chromium.use(StealthPlugin()); + +type Option = { + option?: string | null; + price?: number | null; + quantity?: number | null; + kioskQuantity?: number | null; +}; + +type Product = { + id: string; + slug?: string; + name: string; + brand?: string; + type?: string; + category?: string; + strainType?: string | null; + status?: string | null; + price?: number | null; + specialPrice?: number | null; + image?: string | null; + inStock: boolean; + options: Option[]; + raw?: any; +}; + +const DISPENSARY_SLUG = 'AZ-Deeply-Rooted'; +const DISPENSARY_ID = '6405ef617056e8014d79101b'; +const HASH_FILTERED_PRODUCTS = 'ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0'; +const OUTPUT_DIR = path.join(process.cwd(), 'scrape-output', 'deeply-rooted'); +const OUTPUT_FILE = path.join(OUTPUT_DIR, 'graphql-products.json'); + +async function ensureOutputDir() { + await fs.mkdir(OUTPUT_DIR, { recursive: true }); +} + +async function fetchAllProducts(): Promise { + const browser = await chromium.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + + const context = await browser.newContext({ + viewport: { width: 1300, height: 900 }, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.37 Safari/537.36', + }); + + const page = await context.newPage(); + await page.goto(`https://dutchie.com/embedded-menu/${DISPENSARY_SLUG}`, { + waitUntil: 'domcontentloaded', + timeout: 90000, + }); + await page.waitForTimeout(3000); + + const products: any[] = await page.evaluate( + async ({ dispensaryId, hash }) => { + const sessionRaw = localStorage.getItem('dutchie-session'); + const session = sessionRaw ? sessionRaw.replace(/^\"|\"$/g, '') : ''; + + const all: any[] = []; + const perPage = 100; + + for (let pageIdx = 0; pageIdx < 40; pageIdx++) { + const variables = { + includeEnterpriseSpecials: false, + productsFilter: { + dispensaryId, + pricingType: 'rec', + Status: 'Active', // set to null to try to include inactive if exposed + types: [], + useCache: true, + isDefaultSort: true, + sortBy: 'popularSortIdx', + sortDirection: 1, + bypassOnlineThresholds: true, + isKioskMenu: false, + removeProductsBelowOptionThresholds: false, + }, + page: pageIdx, + perPage, + }; + + const qs = new URLSearchParams({ + operationName: 'FilteredProducts', + variables: JSON.stringify(variables), + extensions: JSON.stringify({ + persistedQuery: { version: 1, sha256Hash: hash }, + }), + }); + + const url = `https://dutchie.com/api-3/graphql?${qs.toString()}`; + const res = await fetch(url, { + headers: { + 'apollographql-client-name': 'Marketplace (production)', + 'x-dutchie-session': session, + 'content-type': 'application/json', + }, + credentials: 'include', + }); + + if (!res.ok) { + console.warn(`Request failed ${res.status} on page ${pageIdx}`); + break; + } + + const json = await res.json(); + const chunk = json?.data?.filteredProducts?.products || []; + all.push(...chunk); + + if (chunk.length < perPage) break; + } + + return all; + }, + { dispensaryId: DISPENSARY_ID, hash: HASH_FILTERED_PRODUCTS } + ); + + await browser.close(); + return normalizeProducts(products); +} + +function normalizeProducts(items: any[]): Product[] { + return items.map((p) => { + const options: Option[] = + p?.POSMetaData?.children?.map((child: any) => ({ + option: child.option ?? null, + price: + child.recPrice ?? + child.price ?? + child.medPrice ?? + null, + quantity: + child.quantity ?? + child.quantityAvailable ?? + null, + kioskQuantity: child.kioskQuantityAvailable ?? null, + })) || []; + + const basePrice = + (p.recSpecialPrices && p.recSpecialPrices[0]) ?? + (p.recPrices && p.recPrices[0]) ?? + (p.Prices && p.Prices[0]) ?? + null; + + const image = + p.Image || + (p.images && p.images.find((img: any) => img.active)?.url) || + null; + + const inStock = + options.some( + (o) => + (o.quantity ?? 0) > 0 || + (o.kioskQuantity ?? 0) > 0 + ) || + !p.isBelowThreshold; + + return { + id: p.id || p._id, + slug: p.cName, + name: p.Name, + brand: p.brandName || p.brand?.name, + type: p.type, + category: p.subcategory, + strainType: p.strainType, + status: p.Status, + price: basePrice, + specialPrice: + (p.recSpecialPrices && p.recSpecialPrices[0]) || + (p.medicalSpecialPrices && p.medicalSpecialPrices[0]) || + null, + image, + inStock, + options, + raw: undefined, + }; + }); +} + +function summarize(products: Product[]) { + const total = products.length; + const inStock = products.filter((p) => p.inStock).length; + const outOfStock = total - inStock; + const byBrand = new Map(); + for (const p of products) { + const key = (p.brand || 'Unknown').trim(); + byBrand.set(key, (byBrand.get(key) || 0) + 1); + } + const topBrands = Array.from(byBrand.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + return { total, inStock, outOfStock, topBrands }; +} + +function formatSample(products: Product[], n = 5) { + return products.slice(0, n).map((p) => ({ + name: p.name, + brand: p.brand, + price: p.price, + specialPrice: p.specialPrice, + inStock: p.inStock, + options: p.options, + })); +} + +async function main() { + await ensureOutputDir(); + const products = await fetchAllProducts(); + await fs.writeFile(OUTPUT_FILE, JSON.stringify(products, null, 2)); + + const summary = summarize(products); + console.log(`Saved ${products.length} products to ${OUTPUT_FILE}`); + console.log(`In stock: ${summary.inStock} | Out of stock: ${summary.outOfStock}`); + console.log('Top brands:', summary.topBrands); + console.log('Sample:', JSON.stringify(formatSample(products, 5), null, 2)); +} + +main().catch((err) => { + console.error('GraphQL scrape failed:', err); + process.exit(1); +}); diff --git a/backend/new-scrapers/postprocess-deeply-rooted-clean.js b/backend/new-scrapers/postprocess-deeply-rooted-clean.js new file mode 100644 index 00000000..27555815 --- /dev/null +++ b/backend/new-scrapers/postprocess-deeply-rooted-clean.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const path = require('path'); + +const INPUT = path.join(process.cwd(), 'scrape-output', 'deeply-rooted', 'inventory-by-brand.json'); +const OUTPUT = path.join(process.cwd(), 'scrape-output', 'deeply-rooted', 'cleaned-inventory.json'); + +function extractPrice(text, fallback) { + const prices = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]{2})?)/g)).map((m) => parseFloat(m[1])); + if (prices.length > 0) { + return Math.min(...prices); + } + return fallback; +} + +function cleanBrandAndName(rawName, rawBrand) { + const parts = rawName.split('…').map((p) => p.trim()).filter(Boolean); + const name = parts[0] || rawName.trim(); + const inferredBrand = parts[1]?.replace(/[^a-z0-9\s\-\&']/gi, ' ').replace(/\s+/g, ' ').trim(); + const brand = normalizeBrand((rawBrand || inferredBrand || 'Unknown').trim()); + return { name, brand }; +} + +function cleanProduct(p) { + const { name, brand } = cleanBrandAndName(p.name, p.brand); + const price = extractPrice(p.name, p.price); + return { + name, + brand: brand || 'Unknown', + price, + size: p.size, + category: p.category, + url: p.url, + imageUrl: p.imageUrl, + inStock: p.inStock !== false, + }; +} + +function dedupe(products) { + const seen = new Map(); + for (const p of products) { + const key = (p.url || `${p.name.toLowerCase()}|${p.brand.toLowerCase()}`).trim(); + if (!seen.has(key)) { + seen.set(key, p); + } + } + return Array.from(seen.values()); +} + +function groupByBrand(products) { + const map = new Map(); + for (const p of products) { + const key = p.brand || 'Unknown'; + if (!map.has(key)) map.set(key, []); + map.get(key).push(p); + } + return Array.from(map.entries()).map(([brand, prods]) => ({ brand, products: prods })); +} + +function normalizeBrand(brand) { + const replacements = { + 'Gr n': 'Gron', + }; + return replacements[brand] || brand; +} + +function main() { + const raw = JSON.parse(fs.readFileSync(INPUT, 'utf8')); + const flattened = []; + + for (const group of raw) { + for (const p of group.products) { + flattened.push(cleanProduct(p)); + } + } + + const unique = dedupe(flattened); + const grouped = groupByBrand(unique); + + fs.writeFileSync(OUTPUT, JSON.stringify(grouped, null, 2)); + + const total = unique.length; + const outOfStock = unique.filter((p) => !p.inStock).length; + + console.log(`Cleaned products: ${total}`); + console.log(`Out of stock: ${outOfStock}`); + console.log(`Brands: ${grouped.length}`); + console.log(`Saved to ${OUTPUT}`); +} + +main(); diff --git a/backend/new-scrapers/postprocess-deeply-rooted-clean.ts b/backend/new-scrapers/postprocess-deeply-rooted-clean.ts new file mode 100644 index 00000000..d7e9a7e1 --- /dev/null +++ b/backend/new-scrapers/postprocess-deeply-rooted-clean.ts @@ -0,0 +1,114 @@ +import fs from 'fs/promises'; +import path from 'path'; + +type RawProduct = { + name: string; + brand?: string; + price?: number; + size?: string; + category?: string; + url?: string; + imageUrl?: string; + inStock?: boolean; +}; + +type BrandGroup = { + brand: string; + products: CleanProduct[]; +}; + +type CleanProduct = { + name: string; + brand: string; + price?: number; + size?: string; + category?: string; + url?: string; + imageUrl?: string; + inStock: boolean; +}; + +const INPUT = path.join(process.cwd(), 'scrape-output', 'deeply-rooted', 'inventory-by-brand.json'); +const OUTPUT = path.join(process.cwd(), 'scrape-output', 'deeply-rooted', 'cleaned-inventory.json'); + +function extractPrice(text: string, fallback?: number): number | undefined { + const prices = [...text.matchAll(/\$([0-9]+(?:\.[0-9]{2})?)/g)].map((m) => parseFloat(m[1])); + if (prices.length > 0) { + // Use the lowest price (usually the sale price) + return Math.min(...prices); + } + return fallback; +} + +function cleanBrandAndName(rawName: string, rawBrand?: string): { name: string; brand: string } { + const parts = rawName.split('…').map((p) => p.trim()).filter(Boolean); + const name = parts[0] || rawName.trim(); + const inferredBrand = parts[1]?.replace(/[^a-z0-9\s\-\&']/gi, ' ').replace(/\s+/g, ' ').trim(); + const brand = (rawBrand || inferredBrand || 'Unknown').trim(); + return { name, brand }; +} + +function cleanProduct(p: RawProduct): CleanProduct { + const { name, brand } = cleanBrandAndName(p.name, p.brand); + const price = extractPrice(p.name, p.price); + return { + name, + brand: brand || 'Unknown', + price, + size: p.size, + category: p.category, + url: p.url, + imageUrl: p.imageUrl, + inStock: p.inStock !== false, + }; +} + +function dedupe(products: CleanProduct[]): CleanProduct[] { + const seen = new Map(); + for (const p of products) { + const key = (p.url || `${p.name.toLowerCase()}|${p.brand.toLowerCase()}`).trim(); + if (!seen.has(key)) { + seen.set(key, p); + } + } + return Array.from(seen.values()); +} + +function groupByBrand(products: CleanProduct[]): BrandGroup[] { + const map = new Map(); + for (const p of products) { + const key = p.brand || 'Unknown'; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(p); + } + return Array.from(map.entries()).map(([brand, prods]) => ({ brand, products: prods })); +} + +async function main() { + const raw = JSON.parse(await fs.readFile(INPUT, 'utf8')) as { brand: string; products: RawProduct[] }[]; + const flattened: CleanProduct[] = []; + + for (const group of raw) { + for (const p of group.products) { + flattened.push(cleanProduct(p)); + } + } + + const unique = dedupe(flattened); + const grouped = groupByBrand(unique); + + await fs.writeFile(OUTPUT, JSON.stringify(grouped, null, 2)); + + const total = unique.length; + const outOfStock = unique.filter((p) => !p.inStock).length; + + console.log(`Cleaned products: ${total}`); + console.log(`Out of stock: ${outOfStock}`); + console.log(`Brands: ${grouped.length}`); + console.log(`Saved to ${OUTPUT}`); +} + +main().catch((err) => { + console.error('Post-process failed:', err); + process.exitCode = 1; +}); diff --git a/backend/new-scrapers/scrape-deeply-rooted-inventory-by-brand.ts b/backend/new-scrapers/scrape-deeply-rooted-inventory-by-brand.ts new file mode 100644 index 00000000..522983e6 --- /dev/null +++ b/backend/new-scrapers/scrape-deeply-rooted-inventory-by-brand.ts @@ -0,0 +1,183 @@ +import { chromium as playwright } from 'playwright-extra'; +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; +import fs from 'fs/promises'; +import path from 'path'; + +playwright.use(StealthPlugin()); + +type Product = { + name: string; + brand?: string; + price?: number; + size?: string; + category?: string; + url?: string; + imageUrl?: string; + inStock: boolean; +}; + +type BrandGroup = { + brand: string; + products: Product[]; +}; + +const TARGET_URL = 'https://dutchie.com/embedded-menu/AZ-Deeply-Rooted'; +const OUTPUT_DIR = path.join(process.cwd(), 'scrape-output', 'deeply-rooted'); +const JSON_PATH = path.join(OUTPUT_DIR, 'inventory-by-brand.json'); + +async function ensureDirs(): Promise { + await fs.mkdir(OUTPUT_DIR, { recursive: true }); +} + +async function waitForCloudflare(page: any, maxWaitMs = 60000): Promise { + const start = Date.now(); + while (Date.now() - start < maxWaitMs) { + const title = await page.title().catch(() => ''); + const content = await page.content().catch(() => ''); + const challenge = + title.includes('Attention Required') || + title.includes('Just a moment') || + content.includes('challenge-platform') || + content.includes('cf-challenge'); + if (!challenge) return true; + await page.waitForTimeout(2000); + } + return false; +} + +async function loadAllProducts(page: any): Promise { + const maxScrolls = 40; + for (let i = 0; i < maxScrolls; i++) { + const beforeCount = await page.$$eval('[data-testid*="product"], [data-testid*="card"]', (els) => els.length); + await page.mouse.wheel(0, 1400); + await page.waitForTimeout(900); + const afterCount = await page.$$eval('[data-testid*="product"], [data-testid*="card"]', (els) => els.length); + if (afterCount <= beforeCount) break; + } + await page.evaluate(() => window.scrollTo({ top: 0 })); +} + +async function extractProducts(page: any): Promise { + const script = ` + (() => { + function parsePrice(text) { + if (!text) return undefined; + const match = text.match(/\\$?(\\d+(?:\\.\\d{1,2})?)/); + return match ? parseFloat(match[1]) : undefined; + } + + function pickImage(card) { + const imgEl = + card.querySelector('img[src^="http"]') || + card.querySelector('source[srcset]'); + if (imgEl && imgEl.src && imgEl.src.startsWith('http')) { + return imgEl.src; + } + if (imgEl && imgEl.srcset) { + const first = imgEl.srcset.split(',')[0]?.trim().split(' ')[0]; + if (first && first.startsWith('http')) return first; + } + const dataSrc = card.querySelector('img[data-src]')?.getAttribute('data-src'); + if (dataSrc && dataSrc.startsWith('http')) return dataSrc; + return undefined; + } + + const cards = Array.from( + document.querySelectorAll('[data-testid="product-list-item"], [data-testid="card-link"], [data-testid*="product-card"]') + ); + + return cards + .map((card) => { + const name = + card.querySelector('[data-testid="product-card-name"]')?.innerText?.trim() || + card.querySelector('[data-testid="product-name"]')?.innerText?.trim() || + card.querySelector('h3, h4')?.innerText?.trim() || + (card.textContent || '').split('\\n').map((t) => t.trim()).find((t) => t.length > 3) || + ''; + + const brand = + card.querySelector('[data-testid="product-card-brand"]')?.innerText?.trim() || + card.querySelector('[data-testid="product-brand"]')?.innerText?.trim() || + undefined; + + const priceText = + card.querySelector('[data-testid="product-card-price"]')?.innerText || + card.textContent || + ''; + const price = parsePrice(priceText); + + const size = + card.querySelector('[data-testid*="size"]')?.innerText?.trim() || + card.querySelector('[data-testid*="weight"]')?.innerText?.trim() || + undefined; + + const category = + card.querySelector('[data-testid*="category"]')?.innerText?.trim() || + undefined; + + const link = card.querySelector('a[href*="/product/"]'); + const url = link?.href; + + const imageUrl = pickImage(card); + + const cardText = (card.textContent || '').toLowerCase(); + const inStock = !(cardText.includes('sold out') || cardText.includes('out of stock')); + + return { name, brand, price, size, category, url, imageUrl, inStock }; + }) + .filter((p) => p.name); + })(); + `; + + return page.evaluate(script); +} + +function groupByBrand(products: Product[]): BrandGroup[] { + const map = new Map(); + for (const p of products) { + const key = p.brand || 'Unknown'; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(p); + } + return Array.from(map.entries()).map(([brand, prods]) => ({ brand, products: prods })); +} + +async function main() { + await ensureDirs(); + + const browser = await playwright.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled'], + }); + + const page = await browser.newPage({ + viewport: { width: 1300, height: 900 }, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); + + try { + console.log(`Navigating to ${TARGET_URL}...`); + await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 90000 }); + + const cfOk = await waitForCloudflare(page, 60000); + if (!cfOk) throw new Error('Cloudflare challenge not passed in time'); + + await page.waitForSelector('[data-testid*="product"], [data-testid*="card"]', { timeout: 60000 }).catch(() => undefined); + + await loadAllProducts(page); + const products = await extractProducts(page); + const grouped = groupByBrand(products); + + await fs.writeFile(JSON_PATH, JSON.stringify(grouped, null, 2)); + console.log(`Found ${products.length} products across ${grouped.length} brands`); + console.log(`Saved grouped inventory to ${JSON_PATH}`); + } catch (err) { + console.error('Inventory scrape failed:', err); + process.exitCode = 1; + } finally { + await page.context().browser()?.close(); + } +} + +main(); diff --git a/backend/new-scrapers/scrape-deeply-rooted-playwright.ts b/backend/new-scrapers/scrape-deeply-rooted-playwright.ts new file mode 100644 index 00000000..1bf99b18 --- /dev/null +++ b/backend/new-scrapers/scrape-deeply-rooted-playwright.ts @@ -0,0 +1,115 @@ +import { chromium, Frame } from 'playwright'; + +type Product = { + name: string; + brand?: string; + price?: number; + size?: string; + category?: string; + url?: string; +}; + +async function getDutchieFrame(page: any): Promise { + const iframeHandle = await page.waitForSelector( + 'iframe[src*="dutchie"], iframe[srcdoc*="dutchie"], iframe[id^="iframe-"]', + { timeout: 45000 } + ); + + const frame = await iframeHandle.contentFrame(); + if (!frame) { + throw new Error('Unable to access embedded Dutchie iframe.'); + } + + await frame.waitForLoadState('domcontentloaded', { timeout: 30000 }); + return frame; +} + +async function loadAllProducts(frame: Frame): Promise { + const maxScrolls = 20; + for (let i = 0; i < maxScrolls; i++) { + const beforeCount = await frame.$$eval('[data-testid*="product"], [data-testid*="card"]', els => els.length); + await frame.mouse.wheel(0, 1200); + await frame.waitForTimeout(800); + const afterCount = await frame.$$eval('[data-testid*="product"], [data-testid*="card"]', els => els.length); + if (afterCount <= beforeCount) break; + } + await frame.evaluate(() => window.scrollTo({ top: 0 })); +} + +async function extractProducts(frame: Frame): Promise { + return frame.evaluate(() => { + const cards = Array.from( + document.querySelectorAll('[data-testid="product-list-item"], [data-testid="card-link"], [data-testid*="product-card"]') + ); + + return cards.map((card: Element) => { + const name = + (card.querySelector('[data-testid="product-card-name"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('[data-testid="product-name"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('h3, h4') as HTMLElement)?.innerText?.trim() || + (card.textContent || '').split('\n').map(t => t.trim()).find(t => t.length > 3) || + ''; + + const brand = + (card.querySelector('[data-testid="product-card-brand"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('[data-testid="product-brand"]') as HTMLElement)?.innerText?.trim() || + undefined; + + const priceText = + (card.querySelector('[data-testid="product-card-price"]') as HTMLElement)?.innerText || + (card.textContent || ''); + const priceMatch = priceText.match(/\$?(\d+(?:\.\d{2})?)/); + const price = priceMatch ? parseFloat(priceMatch[1]) : undefined; + + const size = + (card.querySelector('[data-testid*="size"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('[data-testid*="weight"]') as HTMLElement)?.innerText?.trim() || + undefined; + + const category = + (card.querySelector('[data-testid*="category"]') as HTMLElement)?.innerText?.trim() || + undefined; + + const link = card.querySelector('a[href*="/product/"]') as HTMLAnchorElement | null; + const url = link?.href; + + return { name, brand, price, size, category, url }; + }).filter(p => p.name); + }); +} + +async function main() { + const targetUrl = 'https://azdeeplyrooted.com/menu'; + + const browser = await chromium.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled'], + }); + + const page = await browser.newPage({ + viewport: { width: 1300, height: 900 }, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); + + try { + console.log(`Navigating to ${targetUrl}...`); + await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); + + const frame = await getDutchieFrame(page); + await frame.waitForSelector('[data-testid*="product"], [data-testid*="card"]', { timeout: 60000 }).catch(() => undefined); + + await loadAllProducts(frame); + const products = await extractProducts(frame); + + console.log(`Found ${products.length} products`); + console.log(JSON.stringify(products.slice(0, 20), null, 2)); + } catch (err) { + console.error('Scrape failed:', err); + process.exitCode = 1; + } finally { + await browser.close(); + } +} + +main(); diff --git a/backend/new-scrapers/scrape-deeply-rooted-with-images.ts b/backend/new-scrapers/scrape-deeply-rooted-with-images.ts new file mode 100644 index 00000000..c3c52f39 --- /dev/null +++ b/backend/new-scrapers/scrape-deeply-rooted-with-images.ts @@ -0,0 +1,191 @@ +import { chromium, Frame } from 'playwright'; +import fs from 'fs/promises'; +import path from 'path'; + +type Product = { + name: string; + brand?: string; + price?: number; + size?: string; + category?: string; + url?: string; + imageUrl?: string; +}; + +type ProductWithImagePath = Product & { imagePath?: string }; + +const TARGET_URL = 'https://azdeeplyrooted.com/menu'; +const OUTPUT_DIR = path.join(process.cwd(), 'scrape-output', 'deeply-rooted'); +const IMAGE_DIR = path.join(OUTPUT_DIR, 'images'); +const JSON_PATH = path.join(OUTPUT_DIR, 'products.json'); + +async function ensureDirs(): Promise { + await fs.mkdir(IMAGE_DIR, { recursive: true }); +} + +async function getDutchieFrame(page: any): Promise { + const iframeHandle = await page.waitForSelector( + 'iframe[src*="dutchie"], iframe[srcdoc*="dutchie"], iframe[id^="iframe-"]', + { timeout: 45000 } + ); + + const frame = await iframeHandle.contentFrame(); + if (!frame) { + throw new Error('Unable to access embedded Dutchie iframe.'); + } + + await frame.waitForLoadState('domcontentloaded', { timeout: 30000 }); + return frame; +} + +async function loadAllProducts(frame: Frame): Promise { + const maxScrolls = 30; + for (let i = 0; i < maxScrolls; i++) { + const beforeCount = await frame.$$eval('[data-testid*="product"], [data-testid*="card"]', els => els.length); + await frame.mouse.wheel(0, 1200); + await frame.waitForTimeout(900); + const afterCount = await frame.$$eval('[data-testid*="product"], [data-testid*="card"]', els => els.length); + if (afterCount <= beforeCount) break; + } + await frame.evaluate(() => window.scrollTo({ top: 0 })); +} + +async function extractProducts(frame: Frame): Promise { + return frame.evaluate(() => { + const cards = Array.from( + document.querySelectorAll('[data-testid="product-list-item"], [data-testid="card-link"], [data-testid*="product-card"]') + ); + + const pickImage = (card: Element): string | undefined => { + const imgEl = + (card.querySelector('img[src^="http"]') as HTMLImageElement | null) || + (card.querySelector('source[srcset]') as HTMLSourceElement | null); + if (imgEl && 'src' in imgEl && typeof imgEl.src === 'string' && imgEl.src.startsWith('http')) { + return imgEl.src; + } + if (imgEl && 'srcset' in imgEl && typeof (imgEl as any).srcset === 'string') { + const first = (imgEl as any).srcset.split(',')[0]?.trim().split(' ')[0]; + if (first?.startsWith('http')) return first; + } + const dataSrc = (card.querySelector('img[data-src]') as HTMLImageElement | null)?.getAttribute('data-src'); + if (dataSrc?.startsWith('http')) return dataSrc; + return undefined; + }; + + return cards + .map((card: Element) => { + const name = + (card.querySelector('[data-testid="product-card-name"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('[data-testid="product-name"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('h3, h4') as HTMLElement)?.innerText?.trim() || + (card.textContent || '').split('\n').map(t => t.trim()).find(t => t.length > 3) || + ''; + + const brand = + (card.querySelector('[data-testid="product-card-brand"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('[data-testid="product-brand"]') as HTMLElement)?.innerText?.trim() || + undefined; + + const priceText = + (card.querySelector('[data-testid="product-card-price"]') as HTMLElement)?.innerText || + (card.textContent || ''); + const priceMatch = priceText.match(/\$?(\d+(?:\.\d{2})?)/); + const price = priceMatch ? parseFloat(priceMatch[1]) : undefined; + + const size = + (card.querySelector('[data-testid*="size"]') as HTMLElement)?.innerText?.trim() || + (card.querySelector('[data-testid*="weight"]') as HTMLElement)?.innerText?.trim() || + undefined; + + const category = + (card.querySelector('[data-testid*="category"]') as HTMLElement)?.innerText?.trim() || + undefined; + + const link = card.querySelector('a[href*="/product/"]') as HTMLAnchorElement | null; + const url = link?.href; + + const imageUrl = pickImage(card); + + return { name, brand, price, size, category, url, imageUrl }; + }) + .filter(p => p.name); + }); +} + +function safeFileName(base: string, ext: string): string { + return `${base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'product'}.${ext}`; +} + +async function downloadImages(products: Product[]): Promise { + const results: ProductWithImagePath[] = []; + for (const product of products) { + if (!product.imageUrl) { + results.push(product); + continue; + } + + try { + const res = await fetch(product.imageUrl); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const arrayBuffer = await res.arrayBuffer(); + const contentType = res.headers.get('content-type') || ''; + const extFromType = + contentType.includes('png') ? 'png' : + contentType.includes('jpeg') ? 'jpg' : + contentType.includes('jpg') ? 'jpg' : + contentType.includes('webp') ? 'webp' : + contentType.includes('gif') ? 'gif' : 'bin'; + + const urlExt = path.extname(new URL(product.imageUrl).pathname).replace('.', ''); + const ext = urlExt || extFromType || 'bin'; + const fileName = safeFileName(product.name || 'product', ext); + const filePath = path.join(IMAGE_DIR, fileName); + await fs.writeFile(filePath, Buffer.from(arrayBuffer)); + results.push({ ...product, imagePath: filePath }); + } catch (err) { + console.warn(`Failed to download image for ${product.name}: ${err}`); + results.push(product); + } + } + return results; +} + +async function main() { + await ensureDirs(); + + const browser = await chromium.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled'], + }); + + const page = await browser.newPage({ + viewport: { width: 1300, height: 900 }, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); + + try { + console.log(`Navigating to ${TARGET_URL}...`); + await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 60000 }); + + const frame = await getDutchieFrame(page); + await frame.waitForSelector('[data-testid*="product"], [data-testid*="card"]', { timeout: 60000 }).catch(() => undefined); + + await loadAllProducts(frame); + const products = await extractProducts(frame); + console.log(`Found ${products.length} products, downloading images...`); + + const withImages = await downloadImages(products); + await fs.writeFile(JSON_PATH, JSON.stringify(withImages, null, 2)); + + console.log(`Saved data to ${JSON_PATH}`); + console.log(`Images stored in ${IMAGE_DIR}`); + } catch (err) { + console.error('Scrape failed:', err); + process.exitCode = 1; + } finally { + await browser.close(); + } +} + +main(); diff --git a/backend/node_modules/.bin/prebuild-install b/backend/node_modules/.bin/prebuild-install new file mode 120000 index 00000000..12a458dd --- /dev/null +++ b/backend/node_modules/.bin/prebuild-install @@ -0,0 +1 @@ +../prebuild-install/bin.js \ No newline at end of file diff --git a/backend/node_modules/.bin/rc b/backend/node_modules/.bin/rc new file mode 120000 index 00000000..48b3cda7 --- /dev/null +++ b/backend/node_modules/.bin/rc @@ -0,0 +1 @@ +../rc/cli.js \ No newline at end of file diff --git a/backend/node_modules/.package-lock.json b/backend/node_modules/.package-lock.json index 7f4aa339..37f4ccbb 100644 --- a/backend/node_modules/.package-lock.json +++ b/backend/node_modules/.package-lock.json @@ -41,86 +41,6 @@ "node": ">=18" } }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -341,6 +261,17 @@ "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", "dev": true }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -603,6 +534,16 @@ "node": ">= 10.0.0" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/block-stream2": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", @@ -792,6 +733,18 @@ "node": ">=0.10.0" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -808,6 +761,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -938,6 +900,28 @@ "node": ">=0.10" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1251,6 +1235,14 @@ "bare-events": "^2.7.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -1488,6 +1480,11 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -1667,6 +1664,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1904,6 +1906,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -2291,6 +2298,17 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2302,6 +2320,14 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minio": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minio/-/minio-7.1.3.tgz", @@ -2403,6 +2429,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2419,6 +2450,17 @@ "node": ">= 0.4.0" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -2832,6 +2874,62 @@ "node": ">=0.10.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -3257,6 +3355,20 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3476,49 +3588,32 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=14.15.0" }, "funding": { "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/sharp/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -3592,6 +3687,62 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -3731,6 +3882,14 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strnum": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", @@ -3836,6 +3995,17 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/backend/node_modules/@img/colour/LICENSE.md b/backend/node_modules/@img/colour/LICENSE.md deleted file mode 100644 index 292c550d..00000000 --- a/backend/node_modules/@img/colour/LICENSE.md +++ /dev/null @@ -1,82 +0,0 @@ -# Licensing - -## color - -Copyright (c) 2012 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## color-convert - -Copyright (c) 2011-2016 Heather Arthur . -Copyright (c) 2016-2021 Josh Junon . - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## color-string - -Copyright (c) 2011 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## color-name - -The MIT License (MIT) -Copyright (c) 2015 Dmitry Ivanov - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/@img/colour/README.md b/backend/node_modules/@img/colour/README.md deleted file mode 100644 index a33e4eb8..00000000 --- a/backend/node_modules/@img/colour/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# `@img/colour` - -The latest version of the -[color](https://www.npmjs.com/package/color) -package is now ESM-only, -however some JavaScript runtimes do not yet support this, -which includes versions of Node.js prior to 20.19.0. - -This package converts the `color` package and its dependencies, -all of which are MIT-licensed, to CommonJS. - -- [color](https://www.npmjs.com/package/color) -- [color-convert](https://www.npmjs.com/package/color-convert) -- [color-string](https://www.npmjs.com/package/color-string) -- [color-name](https://www.npmjs.com/package/color-name) diff --git a/backend/node_modules/@img/colour/color.cjs b/backend/node_modules/@img/colour/color.cjs deleted file mode 100644 index ac055fa9..00000000 --- a/backend/node_modules/@img/colour/color.cjs +++ /dev/null @@ -1,1594 +0,0 @@ -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// node_modules/color/index.js -var index_exports = {}; -__export(index_exports, { - default: () => index_default -}); -module.exports = __toCommonJS(index_exports); - -// node_modules/color-name/index.js -var color_name_default = { - aliceblue: [240, 248, 255], - antiquewhite: [250, 235, 215], - aqua: [0, 255, 255], - aquamarine: [127, 255, 212], - azure: [240, 255, 255], - beige: [245, 245, 220], - bisque: [255, 228, 196], - black: [0, 0, 0], - blanchedalmond: [255, 235, 205], - blue: [0, 0, 255], - blueviolet: [138, 43, 226], - brown: [165, 42, 42], - burlywood: [222, 184, 135], - cadetblue: [95, 158, 160], - chartreuse: [127, 255, 0], - chocolate: [210, 105, 30], - coral: [255, 127, 80], - cornflowerblue: [100, 149, 237], - cornsilk: [255, 248, 220], - crimson: [220, 20, 60], - cyan: [0, 255, 255], - darkblue: [0, 0, 139], - darkcyan: [0, 139, 139], - darkgoldenrod: [184, 134, 11], - darkgray: [169, 169, 169], - darkgreen: [0, 100, 0], - darkgrey: [169, 169, 169], - darkkhaki: [189, 183, 107], - darkmagenta: [139, 0, 139], - darkolivegreen: [85, 107, 47], - darkorange: [255, 140, 0], - darkorchid: [153, 50, 204], - darkred: [139, 0, 0], - darksalmon: [233, 150, 122], - darkseagreen: [143, 188, 143], - darkslateblue: [72, 61, 139], - darkslategray: [47, 79, 79], - darkslategrey: [47, 79, 79], - darkturquoise: [0, 206, 209], - darkviolet: [148, 0, 211], - deeppink: [255, 20, 147], - deepskyblue: [0, 191, 255], - dimgray: [105, 105, 105], - dimgrey: [105, 105, 105], - dodgerblue: [30, 144, 255], - firebrick: [178, 34, 34], - floralwhite: [255, 250, 240], - forestgreen: [34, 139, 34], - fuchsia: [255, 0, 255], - gainsboro: [220, 220, 220], - ghostwhite: [248, 248, 255], - gold: [255, 215, 0], - goldenrod: [218, 165, 32], - gray: [128, 128, 128], - green: [0, 128, 0], - greenyellow: [173, 255, 47], - grey: [128, 128, 128], - honeydew: [240, 255, 240], - hotpink: [255, 105, 180], - indianred: [205, 92, 92], - indigo: [75, 0, 130], - ivory: [255, 255, 240], - khaki: [240, 230, 140], - lavender: [230, 230, 250], - lavenderblush: [255, 240, 245], - lawngreen: [124, 252, 0], - lemonchiffon: [255, 250, 205], - lightblue: [173, 216, 230], - lightcoral: [240, 128, 128], - lightcyan: [224, 255, 255], - lightgoldenrodyellow: [250, 250, 210], - lightgray: [211, 211, 211], - lightgreen: [144, 238, 144], - lightgrey: [211, 211, 211], - lightpink: [255, 182, 193], - lightsalmon: [255, 160, 122], - lightseagreen: [32, 178, 170], - lightskyblue: [135, 206, 250], - lightslategray: [119, 136, 153], - lightslategrey: [119, 136, 153], - lightsteelblue: [176, 196, 222], - lightyellow: [255, 255, 224], - lime: [0, 255, 0], - limegreen: [50, 205, 50], - linen: [250, 240, 230], - magenta: [255, 0, 255], - maroon: [128, 0, 0], - mediumaquamarine: [102, 205, 170], - mediumblue: [0, 0, 205], - mediumorchid: [186, 85, 211], - mediumpurple: [147, 112, 219], - mediumseagreen: [60, 179, 113], - mediumslateblue: [123, 104, 238], - mediumspringgreen: [0, 250, 154], - mediumturquoise: [72, 209, 204], - mediumvioletred: [199, 21, 133], - midnightblue: [25, 25, 112], - mintcream: [245, 255, 250], - mistyrose: [255, 228, 225], - moccasin: [255, 228, 181], - navajowhite: [255, 222, 173], - navy: [0, 0, 128], - oldlace: [253, 245, 230], - olive: [128, 128, 0], - olivedrab: [107, 142, 35], - orange: [255, 165, 0], - orangered: [255, 69, 0], - orchid: [218, 112, 214], - palegoldenrod: [238, 232, 170], - palegreen: [152, 251, 152], - paleturquoise: [175, 238, 238], - palevioletred: [219, 112, 147], - papayawhip: [255, 239, 213], - peachpuff: [255, 218, 185], - peru: [205, 133, 63], - pink: [255, 192, 203], - plum: [221, 160, 221], - powderblue: [176, 224, 230], - purple: [128, 0, 128], - rebeccapurple: [102, 51, 153], - red: [255, 0, 0], - rosybrown: [188, 143, 143], - royalblue: [65, 105, 225], - saddlebrown: [139, 69, 19], - salmon: [250, 128, 114], - sandybrown: [244, 164, 96], - seagreen: [46, 139, 87], - seashell: [255, 245, 238], - sienna: [160, 82, 45], - silver: [192, 192, 192], - skyblue: [135, 206, 235], - slateblue: [106, 90, 205], - slategray: [112, 128, 144], - slategrey: [112, 128, 144], - snow: [255, 250, 250], - springgreen: [0, 255, 127], - steelblue: [70, 130, 180], - tan: [210, 180, 140], - teal: [0, 128, 128], - thistle: [216, 191, 216], - tomato: [255, 99, 71], - turquoise: [64, 224, 208], - violet: [238, 130, 238], - wheat: [245, 222, 179], - white: [255, 255, 255], - whitesmoke: [245, 245, 245], - yellow: [255, 255, 0], - yellowgreen: [154, 205, 50] -}; - -// node_modules/color-string/index.js -var reverseNames = /* @__PURE__ */ Object.create(null); -for (const name in color_name_default) { - if (Object.hasOwn(color_name_default, name)) { - reverseNames[color_name_default[name]] = name; - } -} -var cs = { - to: {}, - get: {} -}; -cs.get = function(string) { - const prefix = string.slice(0, 3).toLowerCase(); - let value; - let model; - switch (prefix) { - case "hsl": { - value = cs.get.hsl(string); - model = "hsl"; - break; - } - case "hwb": { - value = cs.get.hwb(string); - model = "hwb"; - break; - } - default: { - value = cs.get.rgb(string); - model = "rgb"; - break; - } - } - if (!value) { - return null; - } - return { model, value }; -}; -cs.get.rgb = function(string) { - if (!string) { - return null; - } - const abbr = /^#([a-f\d]{3,4})$/i; - const hex = /^#([a-f\d]{6})([a-f\d]{2})?$/i; - const rgba = /^rgba?\(\s*([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)\s*(?:[\s,|/]\s*([+-]?[\d.]+)(%?)\s*)?\)$/; - const per = /^rgba?\(\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*(?:[\s,|/]\s*([+-]?[\d.]+)(%?)\s*)?\)$/; - const keyword = /^(\w+)$/; - let rgb = [0, 0, 0, 1]; - let match; - let i; - let hexAlpha; - if (match = string.match(hex)) { - hexAlpha = match[2]; - match = match[1]; - for (i = 0; i < 3; i++) { - const i2 = i * 2; - rgb[i] = Number.parseInt(match.slice(i2, i2 + 2), 16); - } - if (hexAlpha) { - rgb[3] = Number.parseInt(hexAlpha, 16) / 255; - } - } else if (match = string.match(abbr)) { - match = match[1]; - hexAlpha = match[3]; - for (i = 0; i < 3; i++) { - rgb[i] = Number.parseInt(match[i] + match[i], 16); - } - if (hexAlpha) { - rgb[3] = Number.parseInt(hexAlpha + hexAlpha, 16) / 255; - } - } else if (match = string.match(rgba)) { - for (i = 0; i < 3; i++) { - rgb[i] = Number.parseInt(match[i + 1], 10); - } - if (match[4]) { - rgb[3] = match[5] ? Number.parseFloat(match[4]) * 0.01 : Number.parseFloat(match[4]); - } - } else if (match = string.match(per)) { - for (i = 0; i < 3; i++) { - rgb[i] = Math.round(Number.parseFloat(match[i + 1]) * 2.55); - } - if (match[4]) { - rgb[3] = match[5] ? Number.parseFloat(match[4]) * 0.01 : Number.parseFloat(match[4]); - } - } else if (match = string.match(keyword)) { - if (match[1] === "transparent") { - return [0, 0, 0, 0]; - } - if (!Object.hasOwn(color_name_default, match[1])) { - return null; - } - rgb = color_name_default[match[1]]; - rgb[3] = 1; - return rgb; - } else { - return null; - } - for (i = 0; i < 3; i++) { - rgb[i] = clamp(rgb[i], 0, 255); - } - rgb[3] = clamp(rgb[3], 0, 1); - return rgb; -}; -cs.get.hsl = function(string) { - if (!string) { - return null; - } - const hsl = /^hsla?\(\s*([+-]?(?:\d{0,3}\.)?\d+)(?:deg)?\s*,?\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*(?:[,|/]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; - const match = string.match(hsl); - if (match) { - const alpha = Number.parseFloat(match[4]); - const h = (Number.parseFloat(match[1]) % 360 + 360) % 360; - const s = clamp(Number.parseFloat(match[2]), 0, 100); - const l = clamp(Number.parseFloat(match[3]), 0, 100); - const a = clamp(Number.isNaN(alpha) ? 1 : alpha, 0, 1); - return [h, s, l, a]; - } - return null; -}; -cs.get.hwb = function(string) { - if (!string) { - return null; - } - const hwb = /^hwb\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*[\s,]\s*([+-]?[\d.]+)%\s*[\s,]\s*([+-]?[\d.]+)%\s*(?:[\s,]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; - const match = string.match(hwb); - if (match) { - const alpha = Number.parseFloat(match[4]); - const h = (Number.parseFloat(match[1]) % 360 + 360) % 360; - const w = clamp(Number.parseFloat(match[2]), 0, 100); - const b = clamp(Number.parseFloat(match[3]), 0, 100); - const a = clamp(Number.isNaN(alpha) ? 1 : alpha, 0, 1); - return [h, w, b, a]; - } - return null; -}; -cs.to.hex = function(...rgba) { - return "#" + hexDouble(rgba[0]) + hexDouble(rgba[1]) + hexDouble(rgba[2]) + (rgba[3] < 1 ? hexDouble(Math.round(rgba[3] * 255)) : ""); -}; -cs.to.rgb = function(...rgba) { - return rgba.length < 4 || rgba[3] === 1 ? "rgb(" + Math.round(rgba[0]) + ", " + Math.round(rgba[1]) + ", " + Math.round(rgba[2]) + ")" : "rgba(" + Math.round(rgba[0]) + ", " + Math.round(rgba[1]) + ", " + Math.round(rgba[2]) + ", " + rgba[3] + ")"; -}; -cs.to.rgb.percent = function(...rgba) { - const r = Math.round(rgba[0] / 255 * 100); - const g = Math.round(rgba[1] / 255 * 100); - const b = Math.round(rgba[2] / 255 * 100); - return rgba.length < 4 || rgba[3] === 1 ? "rgb(" + r + "%, " + g + "%, " + b + "%)" : "rgba(" + r + "%, " + g + "%, " + b + "%, " + rgba[3] + ")"; -}; -cs.to.hsl = function(...hsla) { - return hsla.length < 4 || hsla[3] === 1 ? "hsl(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%)" : "hsla(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%, " + hsla[3] + ")"; -}; -cs.to.hwb = function(...hwba) { - let a = ""; - if (hwba.length >= 4 && hwba[3] !== 1) { - a = ", " + hwba[3]; - } - return "hwb(" + hwba[0] + ", " + hwba[1] + "%, " + hwba[2] + "%" + a + ")"; -}; -cs.to.keyword = function(...rgb) { - return reverseNames[rgb.slice(0, 3)]; -}; -function clamp(number_, min, max) { - return Math.min(Math.max(min, number_), max); -} -function hexDouble(number_) { - const string_ = Math.round(number_).toString(16).toUpperCase(); - return string_.length < 2 ? "0" + string_ : string_; -} -var color_string_default = cs; - -// node_modules/color-convert/conversions.js -var reverseKeywords = {}; -for (const key of Object.keys(color_name_default)) { - reverseKeywords[color_name_default[key]] = key; -} -var convert = { - rgb: { channels: 3, labels: "rgb" }, - hsl: { channels: 3, labels: "hsl" }, - hsv: { channels: 3, labels: "hsv" }, - hwb: { channels: 3, labels: "hwb" }, - cmyk: { channels: 4, labels: "cmyk" }, - xyz: { channels: 3, labels: "xyz" }, - lab: { channels: 3, labels: "lab" }, - oklab: { channels: 3, labels: ["okl", "oka", "okb"] }, - lch: { channels: 3, labels: "lch" }, - oklch: { channels: 3, labels: ["okl", "okc", "okh"] }, - hex: { channels: 1, labels: ["hex"] }, - keyword: { channels: 1, labels: ["keyword"] }, - ansi16: { channels: 1, labels: ["ansi16"] }, - ansi256: { channels: 1, labels: ["ansi256"] }, - hcg: { channels: 3, labels: ["h", "c", "g"] }, - apple: { channels: 3, labels: ["r16", "g16", "b16"] }, - gray: { channels: 1, labels: ["gray"] } -}; -var conversions_default = convert; -var LAB_FT = (6 / 29) ** 3; -function srgbNonlinearTransform(c) { - const cc = c > 31308e-7 ? 1.055 * c ** (1 / 2.4) - 0.055 : c * 12.92; - return Math.min(Math.max(0, cc), 1); -} -function srgbNonlinearTransformInv(c) { - return c > 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92; -} -for (const model of Object.keys(convert)) { - if (!("channels" in convert[model])) { - throw new Error("missing channels property: " + model); - } - if (!("labels" in convert[model])) { - throw new Error("missing channel labels property: " + model); - } - if (convert[model].labels.length !== convert[model].channels) { - throw new Error("channel and label counts mismatch: " + model); - } - const { channels, labels } = convert[model]; - delete convert[model].channels; - delete convert[model].labels; - Object.defineProperty(convert[model], "channels", { value: channels }); - Object.defineProperty(convert[model], "labels", { value: labels }); -} -convert.rgb.hsl = function(rgb) { - const r = rgb[0] / 255; - const g = rgb[1] / 255; - const b = rgb[2] / 255; - const min = Math.min(r, g, b); - const max = Math.max(r, g, b); - const delta = max - min; - let h; - let s; - switch (max) { - case min: { - h = 0; - break; - } - case r: { - h = (g - b) / delta; - break; - } - case g: { - h = 2 + (b - r) / delta; - break; - } - case b: { - h = 4 + (r - g) / delta; - break; - } - } - h = Math.min(h * 60, 360); - if (h < 0) { - h += 360; - } - const l = (min + max) / 2; - if (max === min) { - s = 0; - } else if (l <= 0.5) { - s = delta / (max + min); - } else { - s = delta / (2 - max - min); - } - return [h, s * 100, l * 100]; -}; -convert.rgb.hsv = function(rgb) { - let rdif; - let gdif; - let bdif; - let h; - let s; - const r = rgb[0] / 255; - const g = rgb[1] / 255; - const b = rgb[2] / 255; - const v = Math.max(r, g, b); - const diff = v - Math.min(r, g, b); - const diffc = function(c) { - return (v - c) / 6 / diff + 1 / 2; - }; - if (diff === 0) { - h = 0; - s = 0; - } else { - s = diff / v; - rdif = diffc(r); - gdif = diffc(g); - bdif = diffc(b); - switch (v) { - case r: { - h = bdif - gdif; - break; - } - case g: { - h = 1 / 3 + rdif - bdif; - break; - } - case b: { - h = 2 / 3 + gdif - rdif; - break; - } - } - if (h < 0) { - h += 1; - } else if (h > 1) { - h -= 1; - } - } - return [ - h * 360, - s * 100, - v * 100 - ]; -}; -convert.rgb.hwb = function(rgb) { - const r = rgb[0]; - const g = rgb[1]; - let b = rgb[2]; - const h = convert.rgb.hsl(rgb)[0]; - const w = 1 / 255 * Math.min(r, Math.min(g, b)); - b = 1 - 1 / 255 * Math.max(r, Math.max(g, b)); - return [h, w * 100, b * 100]; -}; -convert.rgb.oklab = function(rgb) { - const r = srgbNonlinearTransformInv(rgb[0] / 255); - const g = srgbNonlinearTransformInv(rgb[1] / 255); - const b = srgbNonlinearTransformInv(rgb[2] / 255); - const lp = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b); - const mp = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b); - const sp = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b); - const l = 0.2104542553 * lp + 0.793617785 * mp - 0.0040720468 * sp; - const aa = 1.9779984951 * lp - 2.428592205 * mp + 0.4505937099 * sp; - const bb = 0.0259040371 * lp + 0.7827717662 * mp - 0.808675766 * sp; - return [l * 100, aa * 100, bb * 100]; -}; -convert.rgb.cmyk = function(rgb) { - const r = rgb[0] / 255; - const g = rgb[1] / 255; - const b = rgb[2] / 255; - const k = Math.min(1 - r, 1 - g, 1 - b); - const c = (1 - r - k) / (1 - k) || 0; - const m = (1 - g - k) / (1 - k) || 0; - const y = (1 - b - k) / (1 - k) || 0; - return [c * 100, m * 100, y * 100, k * 100]; -}; -function comparativeDistance(x, y) { - return (x[0] - y[0]) ** 2 + (x[1] - y[1]) ** 2 + (x[2] - y[2]) ** 2; -} -convert.rgb.keyword = function(rgb) { - const reversed = reverseKeywords[rgb]; - if (reversed) { - return reversed; - } - let currentClosestDistance = Number.POSITIVE_INFINITY; - let currentClosestKeyword; - for (const keyword of Object.keys(color_name_default)) { - const value = color_name_default[keyword]; - const distance = comparativeDistance(rgb, value); - if (distance < currentClosestDistance) { - currentClosestDistance = distance; - currentClosestKeyword = keyword; - } - } - return currentClosestKeyword; -}; -convert.keyword.rgb = function(keyword) { - return color_name_default[keyword]; -}; -convert.rgb.xyz = function(rgb) { - const r = srgbNonlinearTransformInv(rgb[0] / 255); - const g = srgbNonlinearTransformInv(rgb[1] / 255); - const b = srgbNonlinearTransformInv(rgb[2] / 255); - const x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; - const y = r * 0.2126729 + g * 0.7151522 + b * 0.072175; - const z = r * 0.0193339 + g * 0.119192 + b * 0.9503041; - return [x * 100, y * 100, z * 100]; -}; -convert.rgb.lab = function(rgb) { - const xyz = convert.rgb.xyz(rgb); - let x = xyz[0]; - let y = xyz[1]; - let z = xyz[2]; - x /= 95.047; - y /= 100; - z /= 108.883; - x = x > LAB_FT ? x ** (1 / 3) : 7.787 * x + 16 / 116; - y = y > LAB_FT ? y ** (1 / 3) : 7.787 * y + 16 / 116; - z = z > LAB_FT ? z ** (1 / 3) : 7.787 * z + 16 / 116; - const l = 116 * y - 16; - const a = 500 * (x - y); - const b = 200 * (y - z); - return [l, a, b]; -}; -convert.hsl.rgb = function(hsl) { - const h = hsl[0] / 360; - const s = hsl[1] / 100; - const l = hsl[2] / 100; - let t3; - let value; - if (s === 0) { - value = l * 255; - return [value, value, value]; - } - const t2 = l < 0.5 ? l * (1 + s) : l + s - l * s; - const t1 = 2 * l - t2; - const rgb = [0, 0, 0]; - for (let i = 0; i < 3; i++) { - t3 = h + 1 / 3 * -(i - 1); - if (t3 < 0) { - t3++; - } - if (t3 > 1) { - t3--; - } - if (6 * t3 < 1) { - value = t1 + (t2 - t1) * 6 * t3; - } else if (2 * t3 < 1) { - value = t2; - } else if (3 * t3 < 2) { - value = t1 + (t2 - t1) * (2 / 3 - t3) * 6; - } else { - value = t1; - } - rgb[i] = value * 255; - } - return rgb; -}; -convert.hsl.hsv = function(hsl) { - const h = hsl[0]; - let s = hsl[1] / 100; - let l = hsl[2] / 100; - let smin = s; - const lmin = Math.max(l, 0.01); - l *= 2; - s *= l <= 1 ? l : 2 - l; - smin *= lmin <= 1 ? lmin : 2 - lmin; - const v = (l + s) / 2; - const sv = l === 0 ? 2 * smin / (lmin + smin) : 2 * s / (l + s); - return [h, sv * 100, v * 100]; -}; -convert.hsv.rgb = function(hsv) { - const h = hsv[0] / 60; - const s = hsv[1] / 100; - let v = hsv[2] / 100; - const hi = Math.floor(h) % 6; - const f = h - Math.floor(h); - const p = 255 * v * (1 - s); - const q = 255 * v * (1 - s * f); - const t = 255 * v * (1 - s * (1 - f)); - v *= 255; - switch (hi) { - case 0: { - return [v, t, p]; - } - case 1: { - return [q, v, p]; - } - case 2: { - return [p, v, t]; - } - case 3: { - return [p, q, v]; - } - case 4: { - return [t, p, v]; - } - case 5: { - return [v, p, q]; - } - } -}; -convert.hsv.hsl = function(hsv) { - const h = hsv[0]; - const s = hsv[1] / 100; - const v = hsv[2] / 100; - const vmin = Math.max(v, 0.01); - let sl; - let l; - l = (2 - s) * v; - const lmin = (2 - s) * vmin; - sl = s * vmin; - sl /= lmin <= 1 ? lmin : 2 - lmin; - sl = sl || 0; - l /= 2; - return [h, sl * 100, l * 100]; -}; -convert.hwb.rgb = function(hwb) { - const h = hwb[0] / 360; - let wh = hwb[1] / 100; - let bl = hwb[2] / 100; - const ratio = wh + bl; - let f; - if (ratio > 1) { - wh /= ratio; - bl /= ratio; - } - const i = Math.floor(6 * h); - const v = 1 - bl; - f = 6 * h - i; - if ((i & 1) !== 0) { - f = 1 - f; - } - const n = wh + f * (v - wh); - let r; - let g; - let b; - switch (i) { - default: - case 6: - case 0: { - r = v; - g = n; - b = wh; - break; - } - case 1: { - r = n; - g = v; - b = wh; - break; - } - case 2: { - r = wh; - g = v; - b = n; - break; - } - case 3: { - r = wh; - g = n; - b = v; - break; - } - case 4: { - r = n; - g = wh; - b = v; - break; - } - case 5: { - r = v; - g = wh; - b = n; - break; - } - } - return [r * 255, g * 255, b * 255]; -}; -convert.cmyk.rgb = function(cmyk) { - const c = cmyk[0] / 100; - const m = cmyk[1] / 100; - const y = cmyk[2] / 100; - const k = cmyk[3] / 100; - const r = 1 - Math.min(1, c * (1 - k) + k); - const g = 1 - Math.min(1, m * (1 - k) + k); - const b = 1 - Math.min(1, y * (1 - k) + k); - return [r * 255, g * 255, b * 255]; -}; -convert.xyz.rgb = function(xyz) { - const x = xyz[0] / 100; - const y = xyz[1] / 100; - const z = xyz[2] / 100; - let r; - let g; - let b; - r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314; - g = x * -0.969266 + y * 1.8760108 + z * 0.041556; - b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252; - r = srgbNonlinearTransform(r); - g = srgbNonlinearTransform(g); - b = srgbNonlinearTransform(b); - return [r * 255, g * 255, b * 255]; -}; -convert.xyz.lab = function(xyz) { - let x = xyz[0]; - let y = xyz[1]; - let z = xyz[2]; - x /= 95.047; - y /= 100; - z /= 108.883; - x = x > LAB_FT ? x ** (1 / 3) : 7.787 * x + 16 / 116; - y = y > LAB_FT ? y ** (1 / 3) : 7.787 * y + 16 / 116; - z = z > LAB_FT ? z ** (1 / 3) : 7.787 * z + 16 / 116; - const l = 116 * y - 16; - const a = 500 * (x - y); - const b = 200 * (y - z); - return [l, a, b]; -}; -convert.xyz.oklab = function(xyz) { - const x = xyz[0] / 100; - const y = xyz[1] / 100; - const z = xyz[2] / 100; - const lp = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z); - const mp = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z); - const sp = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z); - const l = 0.2104542553 * lp + 0.793617785 * mp - 0.0040720468 * sp; - const a = 1.9779984951 * lp - 2.428592205 * mp + 0.4505937099 * sp; - const b = 0.0259040371 * lp + 0.7827717662 * mp - 0.808675766 * sp; - return [l * 100, a * 100, b * 100]; -}; -convert.oklab.oklch = function(oklab) { - return convert.lab.lch(oklab); -}; -convert.oklab.xyz = function(oklab) { - const ll = oklab[0] / 100; - const a = oklab[1] / 100; - const b = oklab[2] / 100; - const l = (0.999999998 * ll + 0.396337792 * a + 0.215803758 * b) ** 3; - const m = (1.000000008 * ll - 0.105561342 * a - 0.063854175 * b) ** 3; - const s = (1.000000055 * ll - 0.089484182 * a - 1.291485538 * b) ** 3; - const x = 1.227013851 * l - 0.55779998 * m + 0.281256149 * s; - const y = -0.040580178 * l + 1.11225687 * m - 0.071676679 * s; - const z = -0.076381285 * l - 0.421481978 * m + 1.58616322 * s; - return [x * 100, y * 100, z * 100]; -}; -convert.oklab.rgb = function(oklab) { - const ll = oklab[0] / 100; - const aa = oklab[1] / 100; - const bb = oklab[2] / 100; - const l = (ll + 0.3963377774 * aa + 0.2158037573 * bb) ** 3; - const m = (ll - 0.1055613458 * aa - 0.0638541728 * bb) ** 3; - const s = (ll - 0.0894841775 * aa - 1.291485548 * bb) ** 3; - const r = srgbNonlinearTransform(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s); - const g = srgbNonlinearTransform(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s); - const b = srgbNonlinearTransform(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s); - return [r * 255, g * 255, b * 255]; -}; -convert.oklch.oklab = function(oklch) { - return convert.lch.lab(oklch); -}; -convert.lab.xyz = function(lab) { - const l = lab[0]; - const a = lab[1]; - const b = lab[2]; - let x; - let y; - let z; - y = (l + 16) / 116; - x = a / 500 + y; - z = y - b / 200; - const y2 = y ** 3; - const x2 = x ** 3; - const z2 = z ** 3; - y = y2 > LAB_FT ? y2 : (y - 16 / 116) / 7.787; - x = x2 > LAB_FT ? x2 : (x - 16 / 116) / 7.787; - z = z2 > LAB_FT ? z2 : (z - 16 / 116) / 7.787; - x *= 95.047; - y *= 100; - z *= 108.883; - return [x, y, z]; -}; -convert.lab.lch = function(lab) { - const l = lab[0]; - const a = lab[1]; - const b = lab[2]; - let h; - const hr = Math.atan2(b, a); - h = hr * 360 / 2 / Math.PI; - if (h < 0) { - h += 360; - } - const c = Math.sqrt(a * a + b * b); - return [l, c, h]; -}; -convert.lch.lab = function(lch) { - const l = lch[0]; - const c = lch[1]; - const h = lch[2]; - const hr = h / 360 * 2 * Math.PI; - const a = c * Math.cos(hr); - const b = c * Math.sin(hr); - return [l, a, b]; -}; -convert.rgb.ansi16 = function(args, saturation = null) { - const [r, g, b] = args; - let value = saturation === null ? convert.rgb.hsv(args)[2] : saturation; - value = Math.round(value / 50); - if (value === 0) { - return 30; - } - let ansi = 30 + (Math.round(b / 255) << 2 | Math.round(g / 255) << 1 | Math.round(r / 255)); - if (value === 2) { - ansi += 60; - } - return ansi; -}; -convert.hsv.ansi16 = function(args) { - return convert.rgb.ansi16(convert.hsv.rgb(args), args[2]); -}; -convert.rgb.ansi256 = function(args) { - const r = args[0]; - const g = args[1]; - const b = args[2]; - if (r >> 4 === g >> 4 && g >> 4 === b >> 4) { - if (r < 8) { - return 16; - } - if (r > 248) { - return 231; - } - return Math.round((r - 8) / 247 * 24) + 232; - } - const ansi = 16 + 36 * Math.round(r / 255 * 5) + 6 * Math.round(g / 255 * 5) + Math.round(b / 255 * 5); - return ansi; -}; -convert.ansi16.rgb = function(args) { - args = args[0]; - let color = args % 10; - if (color === 0 || color === 7) { - if (args > 50) { - color += 3.5; - } - color = color / 10.5 * 255; - return [color, color, color]; - } - const mult = (Math.trunc(args > 50) + 1) * 0.5; - const r = (color & 1) * mult * 255; - const g = (color >> 1 & 1) * mult * 255; - const b = (color >> 2 & 1) * mult * 255; - return [r, g, b]; -}; -convert.ansi256.rgb = function(args) { - args = args[0]; - if (args >= 232) { - const c = (args - 232) * 10 + 8; - return [c, c, c]; - } - args -= 16; - let rem; - const r = Math.floor(args / 36) / 5 * 255; - const g = Math.floor((rem = args % 36) / 6) / 5 * 255; - const b = rem % 6 / 5 * 255; - return [r, g, b]; -}; -convert.rgb.hex = function(args) { - const integer = ((Math.round(args[0]) & 255) << 16) + ((Math.round(args[1]) & 255) << 8) + (Math.round(args[2]) & 255); - const string = integer.toString(16).toUpperCase(); - return "000000".slice(string.length) + string; -}; -convert.hex.rgb = function(args) { - const match = args.toString(16).match(/[a-f\d]{6}|[a-f\d]{3}/i); - if (!match) { - return [0, 0, 0]; - } - let colorString = match[0]; - if (match[0].length === 3) { - colorString = [...colorString].map((char) => char + char).join(""); - } - const integer = Number.parseInt(colorString, 16); - const r = integer >> 16 & 255; - const g = integer >> 8 & 255; - const b = integer & 255; - return [r, g, b]; -}; -convert.rgb.hcg = function(rgb) { - const r = rgb[0] / 255; - const g = rgb[1] / 255; - const b = rgb[2] / 255; - const max = Math.max(Math.max(r, g), b); - const min = Math.min(Math.min(r, g), b); - const chroma = max - min; - let hue; - const grayscale = chroma < 1 ? min / (1 - chroma) : 0; - if (chroma <= 0) { - hue = 0; - } else if (max === r) { - hue = (g - b) / chroma % 6; - } else if (max === g) { - hue = 2 + (b - r) / chroma; - } else { - hue = 4 + (r - g) / chroma; - } - hue /= 6; - hue %= 1; - return [hue * 360, chroma * 100, grayscale * 100]; -}; -convert.hsl.hcg = function(hsl) { - const s = hsl[1] / 100; - const l = hsl[2] / 100; - const c = l < 0.5 ? 2 * s * l : 2 * s * (1 - l); - let f = 0; - if (c < 1) { - f = (l - 0.5 * c) / (1 - c); - } - return [hsl[0], c * 100, f * 100]; -}; -convert.hsv.hcg = function(hsv) { - const s = hsv[1] / 100; - const v = hsv[2] / 100; - const c = s * v; - let f = 0; - if (c < 1) { - f = (v - c) / (1 - c); - } - return [hsv[0], c * 100, f * 100]; -}; -convert.hcg.rgb = function(hcg) { - const h = hcg[0] / 360; - const c = hcg[1] / 100; - const g = hcg[2] / 100; - if (c === 0) { - return [g * 255, g * 255, g * 255]; - } - const pure = [0, 0, 0]; - const hi = h % 1 * 6; - const v = hi % 1; - const w = 1 - v; - let mg = 0; - switch (Math.floor(hi)) { - case 0: { - pure[0] = 1; - pure[1] = v; - pure[2] = 0; - break; - } - case 1: { - pure[0] = w; - pure[1] = 1; - pure[2] = 0; - break; - } - case 2: { - pure[0] = 0; - pure[1] = 1; - pure[2] = v; - break; - } - case 3: { - pure[0] = 0; - pure[1] = w; - pure[2] = 1; - break; - } - case 4: { - pure[0] = v; - pure[1] = 0; - pure[2] = 1; - break; - } - default: { - pure[0] = 1; - pure[1] = 0; - pure[2] = w; - } - } - mg = (1 - c) * g; - return [ - (c * pure[0] + mg) * 255, - (c * pure[1] + mg) * 255, - (c * pure[2] + mg) * 255 - ]; -}; -convert.hcg.hsv = function(hcg) { - const c = hcg[1] / 100; - const g = hcg[2] / 100; - const v = c + g * (1 - c); - let f = 0; - if (v > 0) { - f = c / v; - } - return [hcg[0], f * 100, v * 100]; -}; -convert.hcg.hsl = function(hcg) { - const c = hcg[1] / 100; - const g = hcg[2] / 100; - const l = g * (1 - c) + 0.5 * c; - let s = 0; - if (l > 0 && l < 0.5) { - s = c / (2 * l); - } else if (l >= 0.5 && l < 1) { - s = c / (2 * (1 - l)); - } - return [hcg[0], s * 100, l * 100]; -}; -convert.hcg.hwb = function(hcg) { - const c = hcg[1] / 100; - const g = hcg[2] / 100; - const v = c + g * (1 - c); - return [hcg[0], (v - c) * 100, (1 - v) * 100]; -}; -convert.hwb.hcg = function(hwb) { - const w = hwb[1] / 100; - const b = hwb[2] / 100; - const v = 1 - b; - const c = v - w; - let g = 0; - if (c < 1) { - g = (v - c) / (1 - c); - } - return [hwb[0], c * 100, g * 100]; -}; -convert.apple.rgb = function(apple) { - return [apple[0] / 65535 * 255, apple[1] / 65535 * 255, apple[2] / 65535 * 255]; -}; -convert.rgb.apple = function(rgb) { - return [rgb[0] / 255 * 65535, rgb[1] / 255 * 65535, rgb[2] / 255 * 65535]; -}; -convert.gray.rgb = function(args) { - return [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255]; -}; -convert.gray.hsl = function(args) { - return [0, 0, args[0]]; -}; -convert.gray.hsv = convert.gray.hsl; -convert.gray.hwb = function(gray) { - return [0, 100, gray[0]]; -}; -convert.gray.cmyk = function(gray) { - return [0, 0, 0, gray[0]]; -}; -convert.gray.lab = function(gray) { - return [gray[0], 0, 0]; -}; -convert.gray.hex = function(gray) { - const value = Math.round(gray[0] / 100 * 255) & 255; - const integer = (value << 16) + (value << 8) + value; - const string = integer.toString(16).toUpperCase(); - return "000000".slice(string.length) + string; -}; -convert.rgb.gray = function(rgb) { - const value = (rgb[0] + rgb[1] + rgb[2]) / 3; - return [value / 255 * 100]; -}; - -// node_modules/color-convert/route.js -function buildGraph() { - const graph = {}; - const models2 = Object.keys(conversions_default); - for (let { length } = models2, i = 0; i < length; i++) { - graph[models2[i]] = { - // http://jsperf.com/1-vs-infinity - // micro-opt, but this is simple. - distance: -1, - parent: null - }; - } - return graph; -} -function deriveBFS(fromModel) { - const graph = buildGraph(); - const queue = [fromModel]; - graph[fromModel].distance = 0; - while (queue.length > 0) { - const current = queue.pop(); - const adjacents = Object.keys(conversions_default[current]); - for (let { length } = adjacents, i = 0; i < length; i++) { - const adjacent = adjacents[i]; - const node = graph[adjacent]; - if (node.distance === -1) { - node.distance = graph[current].distance + 1; - node.parent = current; - queue.unshift(adjacent); - } - } - } - return graph; -} -function link(from, to) { - return function(args) { - return to(from(args)); - }; -} -function wrapConversion(toModel, graph) { - const path = [graph[toModel].parent, toModel]; - let fn = conversions_default[graph[toModel].parent][toModel]; - let cur = graph[toModel].parent; - while (graph[cur].parent) { - path.unshift(graph[cur].parent); - fn = link(conversions_default[graph[cur].parent][cur], fn); - cur = graph[cur].parent; - } - fn.conversion = path; - return fn; -} -function route(fromModel) { - const graph = deriveBFS(fromModel); - const conversion = {}; - const models2 = Object.keys(graph); - for (let { length } = models2, i = 0; i < length; i++) { - const toModel = models2[i]; - const node = graph[toModel]; - if (node.parent === null) { - continue; - } - conversion[toModel] = wrapConversion(toModel, graph); - } - return conversion; -} -var route_default = route; - -// node_modules/color-convert/index.js -var convert2 = {}; -var models = Object.keys(conversions_default); -function wrapRaw(fn) { - const wrappedFn = function(...args) { - const arg0 = args[0]; - if (arg0 === void 0 || arg0 === null) { - return arg0; - } - if (arg0.length > 1) { - args = arg0; - } - return fn(args); - }; - if ("conversion" in fn) { - wrappedFn.conversion = fn.conversion; - } - return wrappedFn; -} -function wrapRounded(fn) { - const wrappedFn = function(...args) { - const arg0 = args[0]; - if (arg0 === void 0 || arg0 === null) { - return arg0; - } - if (arg0.length > 1) { - args = arg0; - } - const result = fn(args); - if (typeof result === "object") { - for (let { length } = result, i = 0; i < length; i++) { - result[i] = Math.round(result[i]); - } - } - return result; - }; - if ("conversion" in fn) { - wrappedFn.conversion = fn.conversion; - } - return wrappedFn; -} -for (const fromModel of models) { - convert2[fromModel] = {}; - Object.defineProperty(convert2[fromModel], "channels", { value: conversions_default[fromModel].channels }); - Object.defineProperty(convert2[fromModel], "labels", { value: conversions_default[fromModel].labels }); - const routes = route_default(fromModel); - const routeModels = Object.keys(routes); - for (const toModel of routeModels) { - const fn = routes[toModel]; - convert2[fromModel][toModel] = wrapRounded(fn); - convert2[fromModel][toModel].raw = wrapRaw(fn); - } -} -var color_convert_default = convert2; - -// node_modules/color/index.js -var skippedModels = [ - // To be honest, I don't really feel like keyword belongs in color convert, but eh. - "keyword", - // Gray conflicts with some method names, and has its own method defined. - "gray", - // Shouldn't really be in color-convert either... - "hex" -]; -var hashedModelKeys = {}; -for (const model of Object.keys(color_convert_default)) { - hashedModelKeys[[...color_convert_default[model].labels].sort().join("")] = model; -} -var limiters = {}; -function Color(object, model) { - if (!(this instanceof Color)) { - return new Color(object, model); - } - if (model && model in skippedModels) { - model = null; - } - if (model && !(model in color_convert_default)) { - throw new Error("Unknown model: " + model); - } - let i; - let channels; - if (object == null) { - this.model = "rgb"; - this.color = [0, 0, 0]; - this.valpha = 1; - } else if (object instanceof Color) { - this.model = object.model; - this.color = [...object.color]; - this.valpha = object.valpha; - } else if (typeof object === "string") { - const result = color_string_default.get(object); - if (result === null) { - throw new Error("Unable to parse color from string: " + object); - } - this.model = result.model; - channels = color_convert_default[this.model].channels; - this.color = result.value.slice(0, channels); - this.valpha = typeof result.value[channels] === "number" ? result.value[channels] : 1; - } else if (object.length > 0) { - this.model = model || "rgb"; - channels = color_convert_default[this.model].channels; - const newArray = Array.prototype.slice.call(object, 0, channels); - this.color = zeroArray(newArray, channels); - this.valpha = typeof object[channels] === "number" ? object[channels] : 1; - } else if (typeof object === "number") { - this.model = "rgb"; - this.color = [ - object >> 16 & 255, - object >> 8 & 255, - object & 255 - ]; - this.valpha = 1; - } else { - this.valpha = 1; - const keys = Object.keys(object); - if ("alpha" in object) { - keys.splice(keys.indexOf("alpha"), 1); - this.valpha = typeof object.alpha === "number" ? object.alpha : 0; - } - const hashedKeys = keys.sort().join(""); - if (!(hashedKeys in hashedModelKeys)) { - throw new Error("Unable to parse color from object: " + JSON.stringify(object)); - } - this.model = hashedModelKeys[hashedKeys]; - const { labels } = color_convert_default[this.model]; - const color = []; - for (i = 0; i < labels.length; i++) { - color.push(object[labels[i]]); - } - this.color = zeroArray(color); - } - if (limiters[this.model]) { - channels = color_convert_default[this.model].channels; - for (i = 0; i < channels; i++) { - const limit = limiters[this.model][i]; - if (limit) { - this.color[i] = limit(this.color[i]); - } - } - } - this.valpha = Math.max(0, Math.min(1, this.valpha)); - if (Object.freeze) { - Object.freeze(this); - } -} -Color.prototype = { - toString() { - return this.string(); - }, - toJSON() { - return this[this.model](); - }, - string(places) { - let self = this.model in color_string_default.to ? this : this.rgb(); - self = self.round(typeof places === "number" ? places : 1); - const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha]; - return color_string_default.to[self.model](...arguments_); - }, - percentString(places) { - const self = this.rgb().round(typeof places === "number" ? places : 1); - const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha]; - return color_string_default.to.rgb.percent(...arguments_); - }, - array() { - return this.valpha === 1 ? [...this.color] : [...this.color, this.valpha]; - }, - object() { - const result = {}; - const { channels } = color_convert_default[this.model]; - const { labels } = color_convert_default[this.model]; - for (let i = 0; i < channels; i++) { - result[labels[i]] = this.color[i]; - } - if (this.valpha !== 1) { - result.alpha = this.valpha; - } - return result; - }, - unitArray() { - const rgb = this.rgb().color; - rgb[0] /= 255; - rgb[1] /= 255; - rgb[2] /= 255; - if (this.valpha !== 1) { - rgb.push(this.valpha); - } - return rgb; - }, - unitObject() { - const rgb = this.rgb().object(); - rgb.r /= 255; - rgb.g /= 255; - rgb.b /= 255; - if (this.valpha !== 1) { - rgb.alpha = this.valpha; - } - return rgb; - }, - round(places) { - places = Math.max(places || 0, 0); - return new Color([...this.color.map(roundToPlace(places)), this.valpha], this.model); - }, - alpha(value) { - if (value !== void 0) { - return new Color([...this.color, Math.max(0, Math.min(1, value))], this.model); - } - return this.valpha; - }, - // Rgb - red: getset("rgb", 0, maxfn(255)), - green: getset("rgb", 1, maxfn(255)), - blue: getset("rgb", 2, maxfn(255)), - hue: getset(["hsl", "hsv", "hsl", "hwb", "hcg"], 0, (value) => (value % 360 + 360) % 360), - saturationl: getset("hsl", 1, maxfn(100)), - lightness: getset("hsl", 2, maxfn(100)), - saturationv: getset("hsv", 1, maxfn(100)), - value: getset("hsv", 2, maxfn(100)), - chroma: getset("hcg", 1, maxfn(100)), - gray: getset("hcg", 2, maxfn(100)), - white: getset("hwb", 1, maxfn(100)), - wblack: getset("hwb", 2, maxfn(100)), - cyan: getset("cmyk", 0, maxfn(100)), - magenta: getset("cmyk", 1, maxfn(100)), - yellow: getset("cmyk", 2, maxfn(100)), - black: getset("cmyk", 3, maxfn(100)), - x: getset("xyz", 0, maxfn(95.047)), - y: getset("xyz", 1, maxfn(100)), - z: getset("xyz", 2, maxfn(108.833)), - l: getset("lab", 0, maxfn(100)), - a: getset("lab", 1), - b: getset("lab", 2), - keyword(value) { - if (value !== void 0) { - return new Color(value); - } - return color_convert_default[this.model].keyword(this.color); - }, - hex(value) { - if (value !== void 0) { - return new Color(value); - } - return color_string_default.to.hex(...this.rgb().round().color); - }, - hexa(value) { - if (value !== void 0) { - return new Color(value); - } - const rgbArray = this.rgb().round().color; - let alphaHex = Math.round(this.valpha * 255).toString(16).toUpperCase(); - if (alphaHex.length === 1) { - alphaHex = "0" + alphaHex; - } - return color_string_default.to.hex(...rgbArray) + alphaHex; - }, - rgbNumber() { - const rgb = this.rgb().color; - return (rgb[0] & 255) << 16 | (rgb[1] & 255) << 8 | rgb[2] & 255; - }, - luminosity() { - const rgb = this.rgb().color; - const lum = []; - for (const [i, element] of rgb.entries()) { - const chan = element / 255; - lum[i] = chan <= 0.04045 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4; - } - return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; - }, - contrast(color2) { - const lum1 = this.luminosity(); - const lum2 = color2.luminosity(); - if (lum1 > lum2) { - return (lum1 + 0.05) / (lum2 + 0.05); - } - return (lum2 + 0.05) / (lum1 + 0.05); - }, - level(color2) { - const contrastRatio = this.contrast(color2); - if (contrastRatio >= 7) { - return "AAA"; - } - return contrastRatio >= 4.5 ? "AA" : ""; - }, - isDark() { - const rgb = this.rgb().color; - const yiq = (rgb[0] * 2126 + rgb[1] * 7152 + rgb[2] * 722) / 1e4; - return yiq < 128; - }, - isLight() { - return !this.isDark(); - }, - negate() { - const rgb = this.rgb(); - for (let i = 0; i < 3; i++) { - rgb.color[i] = 255 - rgb.color[i]; - } - return rgb; - }, - lighten(ratio) { - const hsl = this.hsl(); - hsl.color[2] += hsl.color[2] * ratio; - return hsl; - }, - darken(ratio) { - const hsl = this.hsl(); - hsl.color[2] -= hsl.color[2] * ratio; - return hsl; - }, - saturate(ratio) { - const hsl = this.hsl(); - hsl.color[1] += hsl.color[1] * ratio; - return hsl; - }, - desaturate(ratio) { - const hsl = this.hsl(); - hsl.color[1] -= hsl.color[1] * ratio; - return hsl; - }, - whiten(ratio) { - const hwb = this.hwb(); - hwb.color[1] += hwb.color[1] * ratio; - return hwb; - }, - blacken(ratio) { - const hwb = this.hwb(); - hwb.color[2] += hwb.color[2] * ratio; - return hwb; - }, - grayscale() { - const rgb = this.rgb().color; - const value = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; - return Color.rgb(value, value, value); - }, - fade(ratio) { - return this.alpha(this.valpha - this.valpha * ratio); - }, - opaquer(ratio) { - return this.alpha(this.valpha + this.valpha * ratio); - }, - rotate(degrees) { - const hsl = this.hsl(); - let hue = hsl.color[0]; - hue = (hue + degrees) % 360; - hue = hue < 0 ? 360 + hue : hue; - hsl.color[0] = hue; - return hsl; - }, - mix(mixinColor, weight) { - if (!mixinColor || !mixinColor.rgb) { - throw new Error('Argument to "mix" was not a Color instance, but rather an instance of ' + typeof mixinColor); - } - const color1 = mixinColor.rgb(); - const color2 = this.rgb(); - const p = weight === void 0 ? 0.5 : weight; - const w = 2 * p - 1; - const a = color1.alpha() - color2.alpha(); - const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2; - const w2 = 1 - w1; - return Color.rgb( - w1 * color1.red() + w2 * color2.red(), - w1 * color1.green() + w2 * color2.green(), - w1 * color1.blue() + w2 * color2.blue(), - color1.alpha() * p + color2.alpha() * (1 - p) - ); - } -}; -for (const model of Object.keys(color_convert_default)) { - if (skippedModels.includes(model)) { - continue; - } - const { channels } = color_convert_default[model]; - Color.prototype[model] = function(...arguments_) { - if (this.model === model) { - return new Color(this); - } - if (arguments_.length > 0) { - return new Color(arguments_, model); - } - return new Color([...assertArray(color_convert_default[this.model][model].raw(this.color)), this.valpha], model); - }; - Color[model] = function(...arguments_) { - let color = arguments_[0]; - if (typeof color === "number") { - color = zeroArray(arguments_, channels); - } - return new Color(color, model); - }; -} -function roundTo(number, places) { - return Number(number.toFixed(places)); -} -function roundToPlace(places) { - return function(number) { - return roundTo(number, places); - }; -} -function getset(model, channel, modifier) { - model = Array.isArray(model) ? model : [model]; - for (const m of model) { - (limiters[m] ||= [])[channel] = modifier; - } - model = model[0]; - return function(value) { - let result; - if (value !== void 0) { - if (modifier) { - value = modifier(value); - } - result = this[model](); - result.color[channel] = value; - return result; - } - result = this[model]().color[channel]; - if (modifier) { - result = modifier(result); - } - return result; - }; -} -function maxfn(max) { - return function(v) { - return Math.max(0, Math.min(max, v)); - }; -} -function assertArray(value) { - return Array.isArray(value) ? value : [value]; -} -function zeroArray(array, length) { - for (let i = 0; i < length; i++) { - if (typeof array[i] !== "number") { - array[i] = 0; - } - } - return array; -} -var index_default = Color; diff --git a/backend/node_modules/@img/colour/index.cjs b/backend/node_modules/@img/colour/index.cjs deleted file mode 100644 index 25596b2b..00000000 --- a/backend/node_modules/@img/colour/index.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./color.cjs").default; diff --git a/backend/node_modules/@img/colour/package.json b/backend/node_modules/@img/colour/package.json deleted file mode 100644 index dc991b22..00000000 --- a/backend/node_modules/@img/colour/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@img/colour", - "version": "1.0.0", - "description": "The ESM-only 'color' package made compatible for use with CommonJS runtimes", - "license": "MIT", - "main": "index.cjs", - "authors": [ - "Heather Arthur ", - "Josh Junon ", - "Maxime Thirouin", - "Dyma Ywanov ", - "LitoMore (https://github.com/LitoMore)" - ], - "engines": { - "node": ">=18" - }, - "files": [ - "color.cjs" - ], - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/lovell/colour.git" - }, - "type": "commonjs", - "keywords": [ - "color", - "colour", - "cjs", - "commonjs" - ], - "scripts": { - "build": "esbuild node_modules/color/index.js --bundle --platform=node --outfile=color.cjs", - "test": "node --test" - }, - "devDependencies": { - "color": "5.0.0", - "color-convert": "3.1.0", - "color-name": "2.0.0", - "color-string": "2.1.0", - "esbuild": "^0.25.9" - } -} diff --git a/backend/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h b/backend/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h deleted file mode 100644 index d0cf4714..00000000 --- a/backend/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +++ /dev/null @@ -1,221 +0,0 @@ -/* glibconfig.h - * - * This is a generated file. Please modify 'glibconfig.h.in' - */ - -#ifndef __GLIBCONFIG_H__ -#define __GLIBCONFIG_H__ - -#include - -#include -#include -#define GLIB_HAVE_ALLOCA_H - -#define GLIB_STATIC_COMPILATION 1 -#define GOBJECT_STATIC_COMPILATION 1 -#define GIO_STATIC_COMPILATION 1 -#define GMODULE_STATIC_COMPILATION 1 -#define GI_STATIC_COMPILATION 1 -#define G_INTL_STATIC_COMPILATION 1 -#define FFI_STATIC_BUILD 1 - -/* Specifies that GLib's g_print*() functions wrap the - * system printf functions. This is useful to know, for example, - * when using glibc's register_printf_function(). - */ -#define GLIB_USING_SYSTEM_PRINTF - -G_BEGIN_DECLS - -#define G_MINFLOAT FLT_MIN -#define G_MAXFLOAT FLT_MAX -#define G_MINDOUBLE DBL_MIN -#define G_MAXDOUBLE DBL_MAX -#define G_MINSHORT SHRT_MIN -#define G_MAXSHORT SHRT_MAX -#define G_MAXUSHORT USHRT_MAX -#define G_MININT INT_MIN -#define G_MAXINT INT_MAX -#define G_MAXUINT UINT_MAX -#define G_MINLONG LONG_MIN -#define G_MAXLONG LONG_MAX -#define G_MAXULONG ULONG_MAX - -typedef signed char gint8; -typedef unsigned char guint8; - -typedef signed short gint16; -typedef unsigned short guint16; - -#define G_GINT16_MODIFIER "h" -#define G_GINT16_FORMAT "hi" -#define G_GUINT16_FORMAT "hu" - - -typedef signed int gint32; -typedef unsigned int guint32; - -#define G_GINT32_MODIFIER "" -#define G_GINT32_FORMAT "i" -#define G_GUINT32_FORMAT "u" - - -#define G_HAVE_GINT64 1 /* deprecated, always true */ - -typedef signed long gint64; -typedef unsigned long guint64; - -#define G_GINT64_CONSTANT(val) (val##L) -#define G_GUINT64_CONSTANT(val) (val##UL) - -#define G_GINT64_MODIFIER "l" -#define G_GINT64_FORMAT "li" -#define G_GUINT64_FORMAT "lu" - - -#define GLIB_SIZEOF_VOID_P 8 -#define GLIB_SIZEOF_LONG 8 -#define GLIB_SIZEOF_SIZE_T 8 -#define GLIB_SIZEOF_SSIZE_T 8 - -typedef signed long gssize; -typedef unsigned long gsize; -#define G_GSIZE_MODIFIER "l" -#define G_GSSIZE_MODIFIER "l" -#define G_GSIZE_FORMAT "lu" -#define G_GSSIZE_FORMAT "li" - -#define G_MAXSIZE G_MAXULONG -#define G_MINSSIZE G_MINLONG -#define G_MAXSSIZE G_MAXLONG - -typedef gint64 goffset; -#define G_MINOFFSET G_MININT64 -#define G_MAXOFFSET G_MAXINT64 - -#define G_GOFFSET_MODIFIER G_GINT64_MODIFIER -#define G_GOFFSET_FORMAT G_GINT64_FORMAT -#define G_GOFFSET_CONSTANT(val) G_GINT64_CONSTANT(val) - -#define G_POLLFD_FORMAT "%d" - -#define GPOINTER_TO_INT(p) ((gint) (glong) (p)) -#define GPOINTER_TO_UINT(p) ((guint) (gulong) (p)) - -#define GINT_TO_POINTER(i) ((gpointer) (glong) (i)) -#define GUINT_TO_POINTER(u) ((gpointer) (gulong) (u)) - -typedef signed long gintptr; -typedef unsigned long guintptr; - -#define G_GINTPTR_MODIFIER "l" -#define G_GINTPTR_FORMAT "li" -#define G_GUINTPTR_FORMAT "lu" - -#define GLIB_MAJOR_VERSION 2 -#define GLIB_MINOR_VERSION 86 -#define GLIB_MICRO_VERSION 1 - -#define G_OS_UNIX - -#define G_VA_COPY va_copy - -#define G_VA_COPY_AS_ARRAY 1 - -#define G_HAVE_ISO_VARARGS 1 - -/* gcc-2.95.x supports both gnu style and ISO varargs, but if -ansi - * is passed ISO vararg support is turned off, and there is no work - * around to turn it on, so we unconditionally turn it off. - */ -#if __GNUC__ == 2 && __GNUC_MINOR__ == 95 -# undef G_HAVE_ISO_VARARGS -#endif - -#define G_HAVE_GROWING_STACK 0 - -#ifndef _MSC_VER -# define G_HAVE_GNUC_VARARGS 1 -#endif - -#if defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590) -#define G_GNUC_INTERNAL __attribute__((visibility("hidden"))) -#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550) -#define G_GNUC_INTERNAL __hidden -#elif defined (__GNUC__) && defined (G_HAVE_GNUC_VISIBILITY) -#define G_GNUC_INTERNAL __attribute__((visibility("hidden"))) -#else -#define G_GNUC_INTERNAL -#endif - -#define G_THREADS_ENABLED -#define G_THREADS_IMPL_POSIX - -#define G_ATOMIC_LOCK_FREE - -#define GINT16_TO_LE(val) ((gint16) (val)) -#define GUINT16_TO_LE(val) ((guint16) (val)) -#define GINT16_TO_BE(val) ((gint16) GUINT16_SWAP_LE_BE (val)) -#define GUINT16_TO_BE(val) (GUINT16_SWAP_LE_BE (val)) - -#define GINT32_TO_LE(val) ((gint32) (val)) -#define GUINT32_TO_LE(val) ((guint32) (val)) -#define GINT32_TO_BE(val) ((gint32) GUINT32_SWAP_LE_BE (val)) -#define GUINT32_TO_BE(val) (GUINT32_SWAP_LE_BE (val)) - -#define GINT64_TO_LE(val) ((gint64) (val)) -#define GUINT64_TO_LE(val) ((guint64) (val)) -#define GINT64_TO_BE(val) ((gint64) GUINT64_SWAP_LE_BE (val)) -#define GUINT64_TO_BE(val) (GUINT64_SWAP_LE_BE (val)) - -#define GLONG_TO_LE(val) ((glong) GINT64_TO_LE (val)) -#define GULONG_TO_LE(val) ((gulong) GUINT64_TO_LE (val)) -#define GLONG_TO_BE(val) ((glong) GINT64_TO_BE (val)) -#define GULONG_TO_BE(val) ((gulong) GUINT64_TO_BE (val)) -#define GINT_TO_LE(val) ((gint) GINT32_TO_LE (val)) -#define GUINT_TO_LE(val) ((guint) GUINT32_TO_LE (val)) -#define GINT_TO_BE(val) ((gint) GINT32_TO_BE (val)) -#define GUINT_TO_BE(val) ((guint) GUINT32_TO_BE (val)) -#define GSIZE_TO_LE(val) ((gsize) GUINT64_TO_LE (val)) -#define GSSIZE_TO_LE(val) ((gssize) GINT64_TO_LE (val)) -#define GSIZE_TO_BE(val) ((gsize) GUINT64_TO_BE (val)) -#define GSSIZE_TO_BE(val) ((gssize) GINT64_TO_BE (val)) -#define G_BYTE_ORDER G_LITTLE_ENDIAN - -#define GLIB_SYSDEF_POLLIN =1 -#define GLIB_SYSDEF_POLLOUT =4 -#define GLIB_SYSDEF_POLLPRI =2 -#define GLIB_SYSDEF_POLLHUP =16 -#define GLIB_SYSDEF_POLLERR =8 -#define GLIB_SYSDEF_POLLNVAL =32 - -/* No way to disable deprecation warnings for macros, so only emit deprecation - * warnings on platforms where usage of this macro is broken */ -#if defined(__APPLE__) || defined(_MSC_VER) || defined(__CYGWIN__) -#define G_MODULE_SUFFIX "so" GLIB_DEPRECATED_MACRO_IN_2_76 -#else -#define G_MODULE_SUFFIX "so" -#endif - -typedef int GPid; -#define G_PID_FORMAT "i" - -#define GLIB_SYSDEF_AF_UNIX 1 -#define GLIB_SYSDEF_AF_INET 2 -#define GLIB_SYSDEF_AF_INET6 10 - -#define GLIB_SYSDEF_MSG_OOB 1 -#define GLIB_SYSDEF_MSG_PEEK 2 -#define GLIB_SYSDEF_MSG_DONTROUTE 4 - -#define G_DIR_SEPARATOR '/' -#define G_DIR_SEPARATOR_S "/" -#define G_SEARCHPATH_SEPARATOR ':' -#define G_SEARCHPATH_SEPARATOR_S ":" - -#undef G_HAVE_FREE_SIZED - -G_END_DECLS - -#endif /* __GLIBCONFIG_H__ */ diff --git a/backend/node_modules/@img/sharp-libvips-linux-x64/lib/index.js b/backend/node_modules/@img/sharp-libvips-linux-x64/lib/index.js deleted file mode 100644 index 5092b4dd..00000000 --- a/backend/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = __dirname; diff --git a/backend/node_modules/@img/sharp-libvips-linux-x64/package.json b/backend/node_modules/@img/sharp-libvips-linux-x64/package.json deleted file mode 100644 index 332e5e47..00000000 --- a/backend/node_modules/@img/sharp-libvips-linux-x64/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@img/sharp-libvips-linux-x64", - "version": "1.2.4", - "description": "Prebuilt libvips and dependencies for use with sharp on Linux (glibc) x64", - "author": "Lovell Fuller ", - "homepage": "https://sharp.pixelplumbing.com", - "repository": { - "type": "git", - "url": "git+https://github.com/lovell/sharp-libvips.git", - "directory": "npm/linux-x64" - }, - "license": "LGPL-3.0-or-later", - "funding": { - "url": "https://opencollective.com/libvips" - }, - "preferUnplugged": true, - "publishConfig": { - "access": "public" - }, - "files": [ - "lib", - "versions.json" - ], - "type": "commonjs", - "exports": { - "./lib": "./lib/index.js", - "./package": "./package.json", - "./versions": "./versions.json" - }, - "config": { - "glibc": ">=2.26" - }, - "os": [ - "linux" - ], - "libc": [ - "glibc" - ], - "cpu": [ - "x64" - ] -} diff --git a/backend/node_modules/@img/sharp-libvips-linux-x64/versions.json b/backend/node_modules/@img/sharp-libvips-linux-x64/versions.json deleted file mode 100644 index fec67b15..00000000 --- a/backend/node_modules/@img/sharp-libvips-linux-x64/versions.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "aom": "3.13.1", - "archive": "3.8.2", - "cairo": "1.18.4", - "cgif": "0.5.0", - "exif": "0.6.25", - "expat": "2.7.3", - "ffi": "3.5.2", - "fontconfig": "2.17.1", - "freetype": "2.14.1", - "fribidi": "1.0.16", - "glib": "2.86.1", - "harfbuzz": "12.1.0", - "heif": "1.20.2", - "highway": "1.3.0", - "imagequant": "2.4.1", - "lcms": "2.17", - "mozjpeg": "0826579", - "pango": "1.57.0", - "pixman": "0.46.4", - "png": "1.6.50", - "proxy-libintl": "0.5", - "rsvg": "2.61.2", - "spng": "0.7.4", - "tiff": "4.7.1", - "vips": "8.17.3", - "webp": "1.6.0", - "xml2": "2.15.1", - "zlib-ng": "2.2.5" -} \ No newline at end of file diff --git a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md b/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md deleted file mode 100644 index a220ae47..00000000 --- a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# `@img/sharp-libvips-linuxmusl-x64` - -Prebuilt libvips and dependencies for use with sharp on Linux (musl) x64. - -## Licensing - -This software contains third-party libraries -used under the terms of the following licences: - -| Library | Used under the terms of | -|---------------|-----------------------------------------------------------------------------------------------------------| -| aom | BSD 2-Clause + [Alliance for Open Media Patent License 1.0](https://aomedia.org/license/patent-license/) | -| cairo | Mozilla Public License 2.0 | -| cgif | MIT Licence | -| expat | MIT Licence | -| fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) | -| freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) | -| fribidi | LGPLv3 | -| glib | LGPLv3 | -| harfbuzz | MIT Licence | -| highway | Apache-2.0 License, BSD 3-Clause | -| lcms | MIT Licence | -| libarchive | BSD 2-Clause | -| libexif | LGPLv3 | -| libffi | MIT Licence | -| libheif | LGPLv3 | -| libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) | -| libnsgif | MIT Licence | -| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) | -| librsvg | LGPLv3 | -| libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) | -| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) | -| libvips | LGPLv3 | -| libwebp | New BSD License | -| libxml2 | MIT Licence | -| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) | -| pango | LGPLv3 | -| pixman | MIT Licence | -| proxy-libintl | LGPLv3 | -| zlib-ng | [zlib Licence](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) | - -Use of libraries under the terms of the LGPLv3 is via the -"any later version" clause of the LGPLv2 or LGPLv2.1. - -Please report any errors or omissions via -https://github.com/lovell/sharp-libvips/issues/new diff --git a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h b/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h deleted file mode 100644 index d0cf4714..00000000 --- a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +++ /dev/null @@ -1,221 +0,0 @@ -/* glibconfig.h - * - * This is a generated file. Please modify 'glibconfig.h.in' - */ - -#ifndef __GLIBCONFIG_H__ -#define __GLIBCONFIG_H__ - -#include - -#include -#include -#define GLIB_HAVE_ALLOCA_H - -#define GLIB_STATIC_COMPILATION 1 -#define GOBJECT_STATIC_COMPILATION 1 -#define GIO_STATIC_COMPILATION 1 -#define GMODULE_STATIC_COMPILATION 1 -#define GI_STATIC_COMPILATION 1 -#define G_INTL_STATIC_COMPILATION 1 -#define FFI_STATIC_BUILD 1 - -/* Specifies that GLib's g_print*() functions wrap the - * system printf functions. This is useful to know, for example, - * when using glibc's register_printf_function(). - */ -#define GLIB_USING_SYSTEM_PRINTF - -G_BEGIN_DECLS - -#define G_MINFLOAT FLT_MIN -#define G_MAXFLOAT FLT_MAX -#define G_MINDOUBLE DBL_MIN -#define G_MAXDOUBLE DBL_MAX -#define G_MINSHORT SHRT_MIN -#define G_MAXSHORT SHRT_MAX -#define G_MAXUSHORT USHRT_MAX -#define G_MININT INT_MIN -#define G_MAXINT INT_MAX -#define G_MAXUINT UINT_MAX -#define G_MINLONG LONG_MIN -#define G_MAXLONG LONG_MAX -#define G_MAXULONG ULONG_MAX - -typedef signed char gint8; -typedef unsigned char guint8; - -typedef signed short gint16; -typedef unsigned short guint16; - -#define G_GINT16_MODIFIER "h" -#define G_GINT16_FORMAT "hi" -#define G_GUINT16_FORMAT "hu" - - -typedef signed int gint32; -typedef unsigned int guint32; - -#define G_GINT32_MODIFIER "" -#define G_GINT32_FORMAT "i" -#define G_GUINT32_FORMAT "u" - - -#define G_HAVE_GINT64 1 /* deprecated, always true */ - -typedef signed long gint64; -typedef unsigned long guint64; - -#define G_GINT64_CONSTANT(val) (val##L) -#define G_GUINT64_CONSTANT(val) (val##UL) - -#define G_GINT64_MODIFIER "l" -#define G_GINT64_FORMAT "li" -#define G_GUINT64_FORMAT "lu" - - -#define GLIB_SIZEOF_VOID_P 8 -#define GLIB_SIZEOF_LONG 8 -#define GLIB_SIZEOF_SIZE_T 8 -#define GLIB_SIZEOF_SSIZE_T 8 - -typedef signed long gssize; -typedef unsigned long gsize; -#define G_GSIZE_MODIFIER "l" -#define G_GSSIZE_MODIFIER "l" -#define G_GSIZE_FORMAT "lu" -#define G_GSSIZE_FORMAT "li" - -#define G_MAXSIZE G_MAXULONG -#define G_MINSSIZE G_MINLONG -#define G_MAXSSIZE G_MAXLONG - -typedef gint64 goffset; -#define G_MINOFFSET G_MININT64 -#define G_MAXOFFSET G_MAXINT64 - -#define G_GOFFSET_MODIFIER G_GINT64_MODIFIER -#define G_GOFFSET_FORMAT G_GINT64_FORMAT -#define G_GOFFSET_CONSTANT(val) G_GINT64_CONSTANT(val) - -#define G_POLLFD_FORMAT "%d" - -#define GPOINTER_TO_INT(p) ((gint) (glong) (p)) -#define GPOINTER_TO_UINT(p) ((guint) (gulong) (p)) - -#define GINT_TO_POINTER(i) ((gpointer) (glong) (i)) -#define GUINT_TO_POINTER(u) ((gpointer) (gulong) (u)) - -typedef signed long gintptr; -typedef unsigned long guintptr; - -#define G_GINTPTR_MODIFIER "l" -#define G_GINTPTR_FORMAT "li" -#define G_GUINTPTR_FORMAT "lu" - -#define GLIB_MAJOR_VERSION 2 -#define GLIB_MINOR_VERSION 86 -#define GLIB_MICRO_VERSION 1 - -#define G_OS_UNIX - -#define G_VA_COPY va_copy - -#define G_VA_COPY_AS_ARRAY 1 - -#define G_HAVE_ISO_VARARGS 1 - -/* gcc-2.95.x supports both gnu style and ISO varargs, but if -ansi - * is passed ISO vararg support is turned off, and there is no work - * around to turn it on, so we unconditionally turn it off. - */ -#if __GNUC__ == 2 && __GNUC_MINOR__ == 95 -# undef G_HAVE_ISO_VARARGS -#endif - -#define G_HAVE_GROWING_STACK 0 - -#ifndef _MSC_VER -# define G_HAVE_GNUC_VARARGS 1 -#endif - -#if defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590) -#define G_GNUC_INTERNAL __attribute__((visibility("hidden"))) -#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550) -#define G_GNUC_INTERNAL __hidden -#elif defined (__GNUC__) && defined (G_HAVE_GNUC_VISIBILITY) -#define G_GNUC_INTERNAL __attribute__((visibility("hidden"))) -#else -#define G_GNUC_INTERNAL -#endif - -#define G_THREADS_ENABLED -#define G_THREADS_IMPL_POSIX - -#define G_ATOMIC_LOCK_FREE - -#define GINT16_TO_LE(val) ((gint16) (val)) -#define GUINT16_TO_LE(val) ((guint16) (val)) -#define GINT16_TO_BE(val) ((gint16) GUINT16_SWAP_LE_BE (val)) -#define GUINT16_TO_BE(val) (GUINT16_SWAP_LE_BE (val)) - -#define GINT32_TO_LE(val) ((gint32) (val)) -#define GUINT32_TO_LE(val) ((guint32) (val)) -#define GINT32_TO_BE(val) ((gint32) GUINT32_SWAP_LE_BE (val)) -#define GUINT32_TO_BE(val) (GUINT32_SWAP_LE_BE (val)) - -#define GINT64_TO_LE(val) ((gint64) (val)) -#define GUINT64_TO_LE(val) ((guint64) (val)) -#define GINT64_TO_BE(val) ((gint64) GUINT64_SWAP_LE_BE (val)) -#define GUINT64_TO_BE(val) (GUINT64_SWAP_LE_BE (val)) - -#define GLONG_TO_LE(val) ((glong) GINT64_TO_LE (val)) -#define GULONG_TO_LE(val) ((gulong) GUINT64_TO_LE (val)) -#define GLONG_TO_BE(val) ((glong) GINT64_TO_BE (val)) -#define GULONG_TO_BE(val) ((gulong) GUINT64_TO_BE (val)) -#define GINT_TO_LE(val) ((gint) GINT32_TO_LE (val)) -#define GUINT_TO_LE(val) ((guint) GUINT32_TO_LE (val)) -#define GINT_TO_BE(val) ((gint) GINT32_TO_BE (val)) -#define GUINT_TO_BE(val) ((guint) GUINT32_TO_BE (val)) -#define GSIZE_TO_LE(val) ((gsize) GUINT64_TO_LE (val)) -#define GSSIZE_TO_LE(val) ((gssize) GINT64_TO_LE (val)) -#define GSIZE_TO_BE(val) ((gsize) GUINT64_TO_BE (val)) -#define GSSIZE_TO_BE(val) ((gssize) GINT64_TO_BE (val)) -#define G_BYTE_ORDER G_LITTLE_ENDIAN - -#define GLIB_SYSDEF_POLLIN =1 -#define GLIB_SYSDEF_POLLOUT =4 -#define GLIB_SYSDEF_POLLPRI =2 -#define GLIB_SYSDEF_POLLHUP =16 -#define GLIB_SYSDEF_POLLERR =8 -#define GLIB_SYSDEF_POLLNVAL =32 - -/* No way to disable deprecation warnings for macros, so only emit deprecation - * warnings on platforms where usage of this macro is broken */ -#if defined(__APPLE__) || defined(_MSC_VER) || defined(__CYGWIN__) -#define G_MODULE_SUFFIX "so" GLIB_DEPRECATED_MACRO_IN_2_76 -#else -#define G_MODULE_SUFFIX "so" -#endif - -typedef int GPid; -#define G_PID_FORMAT "i" - -#define GLIB_SYSDEF_AF_UNIX 1 -#define GLIB_SYSDEF_AF_INET 2 -#define GLIB_SYSDEF_AF_INET6 10 - -#define GLIB_SYSDEF_MSG_OOB 1 -#define GLIB_SYSDEF_MSG_PEEK 2 -#define GLIB_SYSDEF_MSG_DONTROUTE 4 - -#define G_DIR_SEPARATOR '/' -#define G_DIR_SEPARATOR_S "/" -#define G_SEARCHPATH_SEPARATOR ':' -#define G_SEARCHPATH_SEPARATOR_S ":" - -#undef G_HAVE_FREE_SIZED - -G_END_DECLS - -#endif /* __GLIBCONFIG_H__ */ diff --git a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js b/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js deleted file mode 100644 index 5092b4dd..00000000 --- a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = __dirname; diff --git a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 b/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 deleted file mode 100644 index 41c605d1..00000000 Binary files a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 and /dev/null differ diff --git a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json b/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json deleted file mode 100644 index bcb9e6ca..00000000 --- a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@img/sharp-libvips-linuxmusl-x64", - "version": "1.2.4", - "description": "Prebuilt libvips and dependencies for use with sharp on Linux (musl) x64", - "author": "Lovell Fuller ", - "homepage": "https://sharp.pixelplumbing.com", - "repository": { - "type": "git", - "url": "git+https://github.com/lovell/sharp-libvips.git", - "directory": "npm/linuxmusl-x64" - }, - "license": "LGPL-3.0-or-later", - "funding": { - "url": "https://opencollective.com/libvips" - }, - "preferUnplugged": true, - "publishConfig": { - "access": "public" - }, - "files": [ - "lib", - "versions.json" - ], - "type": "commonjs", - "exports": { - "./lib": "./lib/index.js", - "./package": "./package.json", - "./versions": "./versions.json" - }, - "config": { - "musl": ">=1.2.2" - }, - "os": [ - "linux" - ], - "libc": [ - "musl" - ], - "cpu": [ - "x64" - ] -} diff --git a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json b/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json deleted file mode 100644 index fec67b15..00000000 --- a/backend/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "aom": "3.13.1", - "archive": "3.8.2", - "cairo": "1.18.4", - "cgif": "0.5.0", - "exif": "0.6.25", - "expat": "2.7.3", - "ffi": "3.5.2", - "fontconfig": "2.17.1", - "freetype": "2.14.1", - "fribidi": "1.0.16", - "glib": "2.86.1", - "harfbuzz": "12.1.0", - "heif": "1.20.2", - "highway": "1.3.0", - "imagequant": "2.4.1", - "lcms": "2.17", - "mozjpeg": "0826579", - "pango": "1.57.0", - "pixman": "0.46.4", - "png": "1.6.50", - "proxy-libintl": "0.5", - "rsvg": "2.61.2", - "spng": "0.7.4", - "tiff": "4.7.1", - "vips": "8.17.3", - "webp": "1.6.0", - "xml2": "2.15.1", - "zlib-ng": "2.2.5" -} \ No newline at end of file diff --git a/backend/node_modules/@img/sharp-linux-x64/LICENSE b/backend/node_modules/@img/sharp-linux-x64/LICENSE deleted file mode 100644 index 37ec93a1..00000000 --- a/backend/node_modules/@img/sharp-linux-x64/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/backend/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node b/backend/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node deleted file mode 100644 index 8c53a4c1..00000000 Binary files a/backend/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node and /dev/null differ diff --git a/backend/node_modules/@img/sharp-linux-x64/package.json b/backend/node_modules/@img/sharp-linux-x64/package.json deleted file mode 100644 index 95a8a035..00000000 --- a/backend/node_modules/@img/sharp-linux-x64/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@img/sharp-linux-x64", - "version": "0.34.5", - "description": "Prebuilt sharp for use with Linux (glibc) x64", - "author": "Lovell Fuller ", - "homepage": "https://sharp.pixelplumbing.com", - "repository": { - "type": "git", - "url": "git+https://github.com/lovell/sharp.git", - "directory": "npm/linux-x64" - }, - "license": "Apache-2.0", - "funding": { - "url": "https://opencollective.com/libvips" - }, - "preferUnplugged": true, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - }, - "files": [ - "lib" - ], - "publishConfig": { - "access": "public" - }, - "type": "commonjs", - "exports": { - "./sharp.node": "./lib/sharp-linux-x64.node", - "./package": "./package.json" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "config": { - "glibc": ">=2.26" - }, - "os": [ - "linux" - ], - "libc": [ - "glibc" - ], - "cpu": [ - "x64" - ] -} diff --git a/backend/node_modules/@img/sharp-linuxmusl-x64/LICENSE b/backend/node_modules/@img/sharp-linuxmusl-x64/LICENSE deleted file mode 100644 index 37ec93a1..00000000 --- a/backend/node_modules/@img/sharp-linuxmusl-x64/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/backend/node_modules/@img/sharp-linuxmusl-x64/README.md b/backend/node_modules/@img/sharp-linuxmusl-x64/README.md deleted file mode 100644 index f651f980..00000000 --- a/backend/node_modules/@img/sharp-linuxmusl-x64/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# `@img/sharp-linuxmusl-x64` - -Prebuilt sharp for use with Linux (musl) x64. - -## Licensing - -Copyright 2013 Lovell Fuller and others. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at -[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/backend/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node b/backend/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node deleted file mode 100644 index 6d8d5dd8..00000000 Binary files a/backend/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node and /dev/null differ diff --git a/backend/node_modules/@img/sharp-linuxmusl-x64/package.json b/backend/node_modules/@img/sharp-linuxmusl-x64/package.json deleted file mode 100644 index 8be92db0..00000000 --- a/backend/node_modules/@img/sharp-linuxmusl-x64/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@img/sharp-linuxmusl-x64", - "version": "0.34.5", - "description": "Prebuilt sharp for use with Linux (musl) x64", - "author": "Lovell Fuller ", - "homepage": "https://sharp.pixelplumbing.com", - "repository": { - "type": "git", - "url": "git+https://github.com/lovell/sharp.git", - "directory": "npm/linuxmusl-x64" - }, - "license": "Apache-2.0", - "funding": { - "url": "https://opencollective.com/libvips" - }, - "preferUnplugged": true, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - }, - "files": [ - "lib" - ], - "publishConfig": { - "access": "public" - }, - "type": "commonjs", - "exports": { - "./sharp.node": "./lib/sharp-linuxmusl-x64.node", - "./package": "./package.json" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "config": { - "musl": ">=1.2.2" - }, - "os": [ - "linux" - ], - "libc": [ - "musl" - ], - "cpu": [ - "x64" - ] -} diff --git a/backend/node_modules/@types/pg/LICENSE b/backend/node_modules/@types/pg/LICENSE new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/backend/node_modules/@types/pg/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/backend/node_modules/@types/pg/README.md b/backend/node_modules/@types/pg/README.md new file mode 100644 index 00000000..0730f054 --- /dev/null +++ b/backend/node_modules/@types/pg/README.md @@ -0,0 +1,15 @@ +# Installation +> `npm install --save @types/pg` + +# Summary +This package contains type definitions for pg (https://github.com/brianc/node-postgres). + +# Details +Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/pg. + +### Additional Details + * Last updated: Mon, 27 Oct 2025 23:32:24 GMT + * Dependencies: [@types/node](https://npmjs.com/package/@types/node), [pg-protocol](https://npmjs.com/package/pg-protocol), [pg-types](https://npmjs.com/package/pg-types) + +# Credits +These definitions were written by [Phips Peter](https://github.com/pspeter3). diff --git a/backend/node_modules/@types/pg/index.d.mts b/backend/node_modules/@types/pg/index.d.mts new file mode 100644 index 00000000..a56545ba --- /dev/null +++ b/backend/node_modules/@types/pg/index.d.mts @@ -0,0 +1,56 @@ +import * as pg from "./index.js"; + +declare const PG: { + defaults: typeof pg.defaults; + Client: typeof pg.Client; + ClientBase: typeof pg.ClientBase; + Events: typeof pg.Events; + Query: typeof pg.Query; + Pool: typeof pg.Pool; + Connection: typeof pg.Connection; + types: typeof pg.types; + DatabaseError: typeof pg.DatabaseError; + TypeOverrides: typeof pg.TypeOverrides; + escapeIdentifier: typeof pg.escapeIdentifier; + escapeLiteral: typeof pg.escapeLiteral; + Result: typeof pg.Result; + native: typeof pg.native; +}; + +declare namespace PG { + type QueryConfigValues = pg.QueryConfigValues; + type ClientConfig = pg.ClientConfig; + type ConnectionConfig = pg.ConnectionConfig; + type Defaults = pg.Defaults; + type PoolConfig = pg.PoolConfig; + type QueryConfig = pg.QueryConfig; + type CustomTypesConfig = pg.CustomTypesConfig; + type Submittable = pg.Submittable; + type QueryArrayConfig = pg.QueryArrayConfig; + type FieldDef = pg.FieldDef; + type QueryResultBase = pg.QueryResultBase; + type QueryResultRow = pg.QueryResultRow; + type QueryResult = pg.QueryResult; + type QueryArrayResult = pg.QueryArrayResult; + type Notification = pg.Notification; + type ResultBuilder = pg.ResultBuilder; + type QueryParse = pg.QueryParse; + type BindConfig = pg.BindConfig; + type ExecuteConfig = pg.ExecuteConfig; + type MessageConfig = pg.MessageConfig; + type PoolOptions = pg.PoolOptions; + type PoolClient = pg.PoolClient; + + type ClientBase = pg.ClientBase; + type Client = pg.Client; + type Query = pg.Query; + type Events = pg.Events; + type Pool = pg.Pool; + type Connection = pg.Connection; + type DatabaseError = pg.DatabaseError; + type TypeOverrides = pg.TypeOverrides; + type Result = pg.Result; +} + +export * from "./index.js"; +export default PG; diff --git a/backend/node_modules/@types/pg/index.d.ts b/backend/node_modules/@types/pg/index.d.ts new file mode 100644 index 00000000..026b116c --- /dev/null +++ b/backend/node_modules/@types/pg/index.d.ts @@ -0,0 +1,349 @@ +/// + +import events = require("events"); +import stream = require("stream"); +import pgTypes = require("pg-types"); +import { NoticeMessage } from "pg-protocol/dist/messages.js"; + +import { ConnectionOptions } from "tls"; + +export type QueryConfigValues = T extends Array ? T : never; + +export interface ClientConfig { + user?: string | undefined; + database?: string | undefined; + password?: string | (() => string | Promise) | undefined; + port?: number | undefined; + host?: string | undefined; + connectionString?: string | undefined; + keepAlive?: boolean | undefined; + stream?: () => stream.Duplex | undefined; + statement_timeout?: false | number | undefined; + ssl?: boolean | ConnectionOptions | undefined; + query_timeout?: number | undefined; + lock_timeout?: number | undefined; + keepAliveInitialDelayMillis?: number | undefined; + idle_in_transaction_session_timeout?: number | undefined; + application_name?: string | undefined; + fallback_application_name?: string | undefined; + connectionTimeoutMillis?: number | undefined; + types?: CustomTypesConfig | undefined; + options?: string | undefined; + client_encoding?: string | undefined; +} + +export type ConnectionConfig = ClientConfig; + +export interface Defaults extends ClientConfig { + poolSize?: number | undefined; + poolIdleTimeout?: number | undefined; + reapIntervalMillis?: number | undefined; + binary?: boolean | undefined; + parseInt8?: boolean | undefined; + parseInputDatesAsUTC?: boolean | undefined; +} + +export interface PoolConfig extends ClientConfig { + // properties from module 'pg-pool' + max?: number | undefined; + min?: number | undefined; + idleTimeoutMillis?: number | undefined | null; + log?: ((...messages: any[]) => void) | undefined; + Promise?: PromiseConstructorLike | undefined; + allowExitOnIdle?: boolean | undefined; + maxUses?: number | undefined; + maxLifetimeSeconds?: number | undefined; + Client?: (new() => ClientBase) | undefined; +} + +export interface QueryConfig { + name?: string | undefined; + text: string; + values?: QueryConfigValues; + types?: CustomTypesConfig | undefined; +} + +export interface CustomTypesConfig { + getTypeParser: typeof pgTypes.getTypeParser; +} + +export interface Submittable { + submit: (connection: Connection) => void; +} + +export interface QueryArrayConfig extends QueryConfig { + rowMode: "array"; +} + +export interface FieldDef { + name: string; + tableID: number; + columnID: number; + dataTypeID: number; + dataTypeSize: number; + dataTypeModifier: number; + format: string; +} + +export interface QueryResultBase { + command: string; + rowCount: number | null; + oid: number; + fields: FieldDef[]; +} + +export interface QueryResultRow { + [column: string]: any; +} + +export interface QueryResult extends QueryResultBase { + rows: R[]; +} + +export interface QueryArrayResult extends QueryResultBase { + rows: R[]; +} + +export interface Notification { + processId: number; + channel: string; + payload?: string | undefined; +} + +export interface ResultBuilder extends QueryResult { + addRow(row: R): void; +} + +export interface QueryParse { + name: string; + text: string; + types: string[]; +} + +type ValueMapper = (param: any, index: number) => any; + +export interface BindConfig { + portal?: string | undefined; + statement?: string | undefined; + binary?: string | undefined; + values?: Array | undefined; + valueMapper?: ValueMapper | undefined; +} + +export interface ExecuteConfig { + portal?: string | undefined; + rows?: string | undefined; +} + +export interface MessageConfig { + type: string; + name?: string | undefined; +} + +export function escapeIdentifier(str: string): string; + +export function escapeLiteral(str: string): string; + +export class Connection extends events.EventEmitter { + readonly stream: stream.Duplex; + + constructor(config?: ConnectionConfig); + + bind(config: BindConfig | null, more: boolean): void; + execute(config: ExecuteConfig | null, more: boolean): void; + parse(query: QueryParse, more: boolean): void; + + query(text: string): void; + + describe(msg: MessageConfig, more: boolean): void; + close(msg: MessageConfig, more: boolean): void; + + flush(): void; + sync(): void; + end(): void; +} + +export interface PoolOptions extends PoolConfig { + max: number; + maxUses: number; + allowExitOnIdle: boolean; + maxLifetimeSeconds: number; + idleTimeoutMillis: number | null; +} + +/** + * {@link https://node-postgres.com/apis/pool} + */ +export class Pool extends events.EventEmitter { + /** + * Every field of the config object is entirely optional. + * The config passed to the pool is also passed to every client + * instance within the pool when the pool creates that client. + */ + constructor(config?: PoolConfig); + + readonly totalCount: number; + readonly idleCount: number; + readonly waitingCount: number; + readonly expiredCount: number; + + readonly ending: boolean; + readonly ended: boolean; + + options: PoolOptions; + + connect(): Promise; + connect( + callback: (err: Error | undefined, client: PoolClient | undefined, done: (release?: any) => void) => void, + ): void; + + end(): Promise; + end(callback: () => void): void; + + query(queryStream: T): T; + // tslint:disable:no-unnecessary-generics + query( + queryConfig: QueryArrayConfig, + values?: QueryConfigValues, + ): Promise>; + query( + queryConfig: QueryConfig, + ): Promise>; + query( + queryTextOrConfig: string | QueryConfig, + values?: QueryConfigValues, + ): Promise>; + query( + queryConfig: QueryArrayConfig, + callback: (err: Error, result: QueryArrayResult) => void, + ): void; + query( + queryTextOrConfig: string | QueryConfig, + callback: (err: Error, result: QueryResult) => void, + ): void; + query( + queryText: string, + values: QueryConfigValues, + callback: (err: Error, result: QueryResult) => void, + ): void; + // tslint:enable:no-unnecessary-generics + + on(event: "release" | "error", listener: (err: Error, client: PoolClient) => void): this; + on(event: "connect" | "acquire" | "remove", listener: (client: PoolClient) => void): this; +} + +export class ClientBase extends events.EventEmitter { + constructor(config?: string | ClientConfig); + + connect(): Promise; + connect(callback: (err: Error) => void): void; + + query(queryStream: T): T; + // tslint:disable:no-unnecessary-generics + query( + queryConfig: QueryArrayConfig, + values?: QueryConfigValues, + ): Promise>; + query( + queryConfig: QueryConfig, + ): Promise>; + query( + queryTextOrConfig: string | QueryConfig, + values?: QueryConfigValues, + ): Promise>; + query( + queryConfig: QueryArrayConfig, + callback: (err: Error, result: QueryArrayResult) => void, + ): void; + query( + queryTextOrConfig: string | QueryConfig, + callback: (err: Error, result: QueryResult) => void, + ): void; + query( + queryText: string, + values: QueryConfigValues, + callback: (err: Error, result: QueryResult) => void, + ): void; + // tslint:enable:no-unnecessary-generics + + copyFrom(queryText: string): stream.Writable; + copyTo(queryText: string): stream.Readable; + + pauseDrain(): void; + resumeDrain(): void; + + escapeIdentifier: typeof escapeIdentifier; + escapeLiteral: typeof escapeLiteral; + setTypeParser: typeof pgTypes.setTypeParser; + getTypeParser: typeof pgTypes.getTypeParser; + + on(event: "drain", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "notice", listener: (notice: NoticeMessage) => void): this; + on(event: "notification", listener: (message: Notification) => void): this; + // tslint:disable-next-line unified-signatures + on(event: "end", listener: () => void): this; +} + +export class Client extends ClientBase { + user?: string | undefined; + database?: string | undefined; + port: number; + host: string; + password?: string | undefined; + ssl: boolean; + readonly connection: Connection; + + constructor(config?: string | ClientConfig); + + end(): Promise; + end(callback: (err: Error) => void): void; +} + +export interface PoolClient extends ClientBase { + release(err?: Error | boolean): void; +} + +export class Query extends events.EventEmitter + implements Submittable +{ + constructor( + queryTextOrConfig?: string | QueryConfig, + callback?: (error: Error | undefined, result: ResultBuilder) => void, + ); + constructor( + queryTextOrConfig?: string | QueryConfig, + values?: I, + callback?: (error: Error | undefined, result: ResultBuilder) => void, + ); + submit: (connection: Connection) => void; + on(event: "row", listener: (row: R, result?: ResultBuilder) => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "end", listener: (result: ResultBuilder) => void): this; +} + +export class Events extends events.EventEmitter { + on(event: "error", listener: (err: Error, client: Client) => void): this; +} + +export const types: typeof pgTypes; + +export const defaults: Defaults & ClientConfig; + +import * as Pg from "."; + +export const native: typeof Pg | null; + +export { DatabaseError } from "pg-protocol"; +import TypeOverrides = require("./lib/type-overrides"); +export { TypeOverrides }; + +export class Result implements QueryResult { + command: string; + rowCount: number | null; + oid: number; + fields: FieldDef[]; + rows: R[]; + + constructor(rowMode: string, t: typeof types); +} diff --git a/backend/node_modules/@types/pg/lib/connection-parameters.d.ts b/backend/node_modules/@types/pg/lib/connection-parameters.d.ts new file mode 100644 index 00000000..951d4813 --- /dev/null +++ b/backend/node_modules/@types/pg/lib/connection-parameters.d.ts @@ -0,0 +1,58 @@ +import { ConnectionOptions } from "tls"; +import { ConnectionConfig } from ".."; + +interface ConnectionParametersConfig extends + Pick< + ConnectionConfig, + | "user" + | "database" + | "password" + | "port" + | "host" + | "options" + | "ssl" + | "application_name" + | "statement_timeout" + | "idle_in_transaction_session_timeout" + | "query_timeout" + > +{ + binary?: unknown; + client_encoding?: unknown; + replication?: unknown; + isDomainSocket?: unknown; + fallback_application_name?: unknown; + lock_timeout?: unknown; + connect_timeout?: unknown; + keepalives?: unknown; + keepalives_idle?: unknown; +} + +export = ConnectionParameters; +declare class ConnectionParameters implements ConnectionParametersConfig { + user?: string | undefined; + database?: string | undefined; + password?: string | (() => string | Promise) | undefined; + port?: number | undefined; + host?: string | undefined; + statement_timeout?: false | number | undefined; + ssl?: boolean | ConnectionOptions | undefined; + query_timeout?: number | undefined; + idle_in_transaction_session_timeout?: number | undefined; + application_name?: string | undefined; + options?: string | undefined; + + binary?: unknown; + client_encoding?: unknown; + replication?: unknown; + isDomainSocket?: unknown; + fallback_application_name?: unknown; + lock_timeout?: unknown; + connect_timeout?: unknown; + keepalives?: unknown; + keepalives_idle?: unknown; + + constructor(config?: string | ConnectionParametersConfig); + + getLibpqConnectionString(cb: (err: Error | null, params: string | null) => TResult): TResult; +} diff --git a/backend/node_modules/@types/pg/lib/type-overrides.d.ts b/backend/node_modules/@types/pg/lib/type-overrides.d.ts new file mode 100644 index 00000000..972b5a98 --- /dev/null +++ b/backend/node_modules/@types/pg/lib/type-overrides.d.ts @@ -0,0 +1,77 @@ +import { CustomTypesConfig } from ".."; + +declare enum builtins { + BOOL = 16, + BYTEA = 17, + CHAR = 18, + INT8 = 20, + INT2 = 21, + INT4 = 23, + REGPROC = 24, + TEXT = 25, + OID = 26, + TID = 27, + XID = 28, + CID = 29, + JSON = 114, + XML = 142, + PG_NODE_TREE = 194, + SMGR = 210, + PATH = 602, + POLYGON = 604, + CIDR = 650, + FLOAT4 = 700, + FLOAT8 = 701, + ABSTIME = 702, + RELTIME = 703, + TINTERVAL = 704, + CIRCLE = 718, + MACADDR8 = 774, + MONEY = 790, + MACADDR = 829, + INET = 869, + ACLITEM = 1033, + BPCHAR = 1042, + VARCHAR = 1043, + DATE = 1082, + TIME = 1083, + TIMESTAMP = 1114, + TIMESTAMPTZ = 1184, + INTERVAL = 1186, + TIMETZ = 1266, + BIT = 1560, + VARBIT = 1562, + NUMERIC = 1700, + REFCURSOR = 1790, + REGPROCEDURE = 2202, + REGOPER = 2203, + REGOPERATOR = 2204, + REGCLASS = 2205, + REGTYPE = 2206, + UUID = 2950, + TXID_SNAPSHOT = 2970, + PG_LSN = 3220, + PG_NDISTINCT = 3361, + PG_DEPENDENCIES = 3402, + TSVECTOR = 3614, + TSQUERY = 3615, + GTSVECTOR = 3642, + REGCONFIG = 3734, + REGDICTIONARY = 3769, + JSONB = 3802, + REGNAMESPACE = 4089, + REGROLE = 4096, +} +type TypeId = builtins; +type TypeParser = (oid: TOid) => TReturn; +type TypeFormat = "text" | "binary"; + +export = TypeOverrides; +declare class TypeOverrides implements CustomTypesConfig { + constructor(types?: CustomTypesConfig); + setTypeParser(oid: number | TypeId, parseFn: TypeParser): void; + setTypeParser(oid: number | TypeId, format: "text", parseFn: TypeParser): void; + setTypeParser(oid: number | TypeId, format: "binary", parseFn: TypeParser): void; + + getTypeParser(oid: number | TypeId, format?: TypeFormat): TypeParser; +} diff --git a/backend/node_modules/@types/pg/package.json b/backend/node_modules/@types/pg/package.json new file mode 100644 index 00000000..4b862b1b --- /dev/null +++ b/backend/node_modules/@types/pg/package.json @@ -0,0 +1,38 @@ +{ + "name": "@types/pg", + "version": "8.15.6", + "description": "TypeScript definitions for pg", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/pg", + "license": "MIT", + "contributors": [ + { + "name": "Phips Peter", + "githubUsername": "pspeter3", + "url": "https://github.com/pspeter3" + } + ], + "main": "", + "types": "index.d.ts", + "exports": { + ".": { + "import": "./index.d.mts", + "require": "./index.d.ts" + }, + "./lib/*": "./lib/*.d.ts", + "./package.json": "./package.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/pg" + }, + "scripts": {}, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + }, + "peerDependencies": {}, + "typesPublisherContentHash": "bfe58cd33772b929b21b29a9496746a24084358ddea01e505b949995ff72d44e", + "typeScriptVersion": "5.2" +} \ No newline at end of file diff --git a/backend/node_modules/bl/.travis.yml b/backend/node_modules/bl/.travis.yml new file mode 100644 index 00000000..016eaf55 --- /dev/null +++ b/backend/node_modules/bl/.travis.yml @@ -0,0 +1,17 @@ +sudo: false +arch: + - amd64 + - ppc64le +language: node_js +node_js: + - '6' + - '8' + - '10' + - '12' + - '14' + - '15' + - lts/* +notifications: + email: + - rod@vagg.org + - matteo.collina@gmail.com diff --git a/backend/node_modules/bl/BufferList.js b/backend/node_modules/bl/BufferList.js new file mode 100644 index 00000000..471ee778 --- /dev/null +++ b/backend/node_modules/bl/BufferList.js @@ -0,0 +1,396 @@ +'use strict' + +const { Buffer } = require('buffer') +const symbol = Symbol.for('BufferList') + +function BufferList (buf) { + if (!(this instanceof BufferList)) { + return new BufferList(buf) + } + + BufferList._init.call(this, buf) +} + +BufferList._init = function _init (buf) { + Object.defineProperty(this, symbol, { value: true }) + + this._bufs = [] + this.length = 0 + + if (buf) { + this.append(buf) + } +} + +BufferList.prototype._new = function _new (buf) { + return new BufferList(buf) +} + +BufferList.prototype._offset = function _offset (offset) { + if (offset === 0) { + return [0, 0] + } + + let tot = 0 + + for (let i = 0; i < this._bufs.length; i++) { + const _t = tot + this._bufs[i].length + if (offset < _t || i === this._bufs.length - 1) { + return [i, offset - tot] + } + tot = _t + } +} + +BufferList.prototype._reverseOffset = function (blOffset) { + const bufferId = blOffset[0] + let offset = blOffset[1] + + for (let i = 0; i < bufferId; i++) { + offset += this._bufs[i].length + } + + return offset +} + +BufferList.prototype.get = function get (index) { + if (index > this.length || index < 0) { + return undefined + } + + const offset = this._offset(index) + + return this._bufs[offset[0]][offset[1]] +} + +BufferList.prototype.slice = function slice (start, end) { + if (typeof start === 'number' && start < 0) { + start += this.length + } + + if (typeof end === 'number' && end < 0) { + end += this.length + } + + return this.copy(null, 0, start, end) +} + +BufferList.prototype.copy = function copy (dst, dstStart, srcStart, srcEnd) { + if (typeof srcStart !== 'number' || srcStart < 0) { + srcStart = 0 + } + + if (typeof srcEnd !== 'number' || srcEnd > this.length) { + srcEnd = this.length + } + + if (srcStart >= this.length) { + return dst || Buffer.alloc(0) + } + + if (srcEnd <= 0) { + return dst || Buffer.alloc(0) + } + + const copy = !!dst + const off = this._offset(srcStart) + const len = srcEnd - srcStart + let bytes = len + let bufoff = (copy && dstStart) || 0 + let start = off[1] + + // copy/slice everything + if (srcStart === 0 && srcEnd === this.length) { + if (!copy) { + // slice, but full concat if multiple buffers + return this._bufs.length === 1 + ? this._bufs[0] + : Buffer.concat(this._bufs, this.length) + } + + // copy, need to copy individual buffers + for (let i = 0; i < this._bufs.length; i++) { + this._bufs[i].copy(dst, bufoff) + bufoff += this._bufs[i].length + } + + return dst + } + + // easy, cheap case where it's a subset of one of the buffers + if (bytes <= this._bufs[off[0]].length - start) { + return copy + ? this._bufs[off[0]].copy(dst, dstStart, start, start + bytes) + : this._bufs[off[0]].slice(start, start + bytes) + } + + if (!copy) { + // a slice, we need something to copy in to + dst = Buffer.allocUnsafe(len) + } + + for (let i = off[0]; i < this._bufs.length; i++) { + const l = this._bufs[i].length - start + + if (bytes > l) { + this._bufs[i].copy(dst, bufoff, start) + bufoff += l + } else { + this._bufs[i].copy(dst, bufoff, start, start + bytes) + bufoff += l + break + } + + bytes -= l + + if (start) { + start = 0 + } + } + + // safeguard so that we don't return uninitialized memory + if (dst.length > bufoff) return dst.slice(0, bufoff) + + return dst +} + +BufferList.prototype.shallowSlice = function shallowSlice (start, end) { + start = start || 0 + end = typeof end !== 'number' ? this.length : end + + if (start < 0) { + start += this.length + } + + if (end < 0) { + end += this.length + } + + if (start === end) { + return this._new() + } + + const startOffset = this._offset(start) + const endOffset = this._offset(end) + const buffers = this._bufs.slice(startOffset[0], endOffset[0] + 1) + + if (endOffset[1] === 0) { + buffers.pop() + } else { + buffers[buffers.length - 1] = buffers[buffers.length - 1].slice(0, endOffset[1]) + } + + if (startOffset[1] !== 0) { + buffers[0] = buffers[0].slice(startOffset[1]) + } + + return this._new(buffers) +} + +BufferList.prototype.toString = function toString (encoding, start, end) { + return this.slice(start, end).toString(encoding) +} + +BufferList.prototype.consume = function consume (bytes) { + // first, normalize the argument, in accordance with how Buffer does it + bytes = Math.trunc(bytes) + // do nothing if not a positive number + if (Number.isNaN(bytes) || bytes <= 0) return this + + while (this._bufs.length) { + if (bytes >= this._bufs[0].length) { + bytes -= this._bufs[0].length + this.length -= this._bufs[0].length + this._bufs.shift() + } else { + this._bufs[0] = this._bufs[0].slice(bytes) + this.length -= bytes + break + } + } + + return this +} + +BufferList.prototype.duplicate = function duplicate () { + const copy = this._new() + + for (let i = 0; i < this._bufs.length; i++) { + copy.append(this._bufs[i]) + } + + return copy +} + +BufferList.prototype.append = function append (buf) { + if (buf == null) { + return this + } + + if (buf.buffer) { + // append a view of the underlying ArrayBuffer + this._appendBuffer(Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength)) + } else if (Array.isArray(buf)) { + for (let i = 0; i < buf.length; i++) { + this.append(buf[i]) + } + } else if (this._isBufferList(buf)) { + // unwrap argument into individual BufferLists + for (let i = 0; i < buf._bufs.length; i++) { + this.append(buf._bufs[i]) + } + } else { + // coerce number arguments to strings, since Buffer(number) does + // uninitialized memory allocation + if (typeof buf === 'number') { + buf = buf.toString() + } + + this._appendBuffer(Buffer.from(buf)) + } + + return this +} + +BufferList.prototype._appendBuffer = function appendBuffer (buf) { + this._bufs.push(buf) + this.length += buf.length +} + +BufferList.prototype.indexOf = function (search, offset, encoding) { + if (encoding === undefined && typeof offset === 'string') { + encoding = offset + offset = undefined + } + + if (typeof search === 'function' || Array.isArray(search)) { + throw new TypeError('The "value" argument must be one of type string, Buffer, BufferList, or Uint8Array.') + } else if (typeof search === 'number') { + search = Buffer.from([search]) + } else if (typeof search === 'string') { + search = Buffer.from(search, encoding) + } else if (this._isBufferList(search)) { + search = search.slice() + } else if (Array.isArray(search.buffer)) { + search = Buffer.from(search.buffer, search.byteOffset, search.byteLength) + } else if (!Buffer.isBuffer(search)) { + search = Buffer.from(search) + } + + offset = Number(offset || 0) + + if (isNaN(offset)) { + offset = 0 + } + + if (offset < 0) { + offset = this.length + offset + } + + if (offset < 0) { + offset = 0 + } + + if (search.length === 0) { + return offset > this.length ? this.length : offset + } + + const blOffset = this._offset(offset) + let blIndex = blOffset[0] // index of which internal buffer we're working on + let buffOffset = blOffset[1] // offset of the internal buffer we're working on + + // scan over each buffer + for (; blIndex < this._bufs.length; blIndex++) { + const buff = this._bufs[blIndex] + + while (buffOffset < buff.length) { + const availableWindow = buff.length - buffOffset + + if (availableWindow >= search.length) { + const nativeSearchResult = buff.indexOf(search, buffOffset) + + if (nativeSearchResult !== -1) { + return this._reverseOffset([blIndex, nativeSearchResult]) + } + + buffOffset = buff.length - search.length + 1 // end of native search window + } else { + const revOffset = this._reverseOffset([blIndex, buffOffset]) + + if (this._match(revOffset, search)) { + return revOffset + } + + buffOffset++ + } + } + + buffOffset = 0 + } + + return -1 +} + +BufferList.prototype._match = function (offset, search) { + if (this.length - offset < search.length) { + return false + } + + for (let searchOffset = 0; searchOffset < search.length; searchOffset++) { + if (this.get(offset + searchOffset) !== search[searchOffset]) { + return false + } + } + return true +} + +;(function () { + const methods = { + readDoubleBE: 8, + readDoubleLE: 8, + readFloatBE: 4, + readFloatLE: 4, + readInt32BE: 4, + readInt32LE: 4, + readUInt32BE: 4, + readUInt32LE: 4, + readInt16BE: 2, + readInt16LE: 2, + readUInt16BE: 2, + readUInt16LE: 2, + readInt8: 1, + readUInt8: 1, + readIntBE: null, + readIntLE: null, + readUIntBE: null, + readUIntLE: null + } + + for (const m in methods) { + (function (m) { + if (methods[m] === null) { + BufferList.prototype[m] = function (offset, byteLength) { + return this.slice(offset, offset + byteLength)[m](0, byteLength) + } + } else { + BufferList.prototype[m] = function (offset = 0) { + return this.slice(offset, offset + methods[m])[m](0) + } + } + }(m)) + } +}()) + +// Used internally by the class and also as an indicator of this object being +// a `BufferList`. It's not possible to use `instanceof BufferList` in a browser +// environment because there could be multiple different copies of the +// BufferList class and some `BufferList`s might be `BufferList`s. +BufferList.prototype._isBufferList = function _isBufferList (b) { + return b instanceof BufferList || BufferList.isBufferList(b) +} + +BufferList.isBufferList = function isBufferList (b) { + return b != null && b[symbol] +} + +module.exports = BufferList diff --git a/backend/node_modules/bl/LICENSE.md b/backend/node_modules/bl/LICENSE.md new file mode 100644 index 00000000..ecbe5163 --- /dev/null +++ b/backend/node_modules/bl/LICENSE.md @@ -0,0 +1,13 @@ +The MIT License (MIT) +===================== + +Copyright (c) 2013-2019 bl contributors +---------------------------------- + +*bl contributors listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/bl/README.md b/backend/node_modules/bl/README.md new file mode 100644 index 00000000..9680b1dc --- /dev/null +++ b/backend/node_modules/bl/README.md @@ -0,0 +1,247 @@ +# bl *(BufferList)* + +[![Build Status](https://api.travis-ci.com/rvagg/bl.svg?branch=master)](https://travis-ci.com/rvagg/bl/) + +**A Node.js Buffer list collector, reader and streamer thingy.** + +[![NPM](https://nodei.co/npm/bl.svg)](https://nodei.co/npm/bl/) + +**bl** is a storage object for collections of Node Buffers, exposing them with the main Buffer readable API. Also works as a duplex stream so you can collect buffers from a stream that emits them and emit buffers to a stream that consumes them! + +The original buffers are kept intact and copies are only done as necessary. Any reads that require the use of a single original buffer will return a slice of that buffer only (which references the same memory as the original buffer). Reads that span buffers perform concatenation as required and return the results transparently. + +```js +const { BufferList } = require('bl') + +const bl = new BufferList() +bl.append(Buffer.from('abcd')) +bl.append(Buffer.from('efg')) +bl.append('hi') // bl will also accept & convert Strings +bl.append(Buffer.from('j')) +bl.append(Buffer.from([ 0x3, 0x4 ])) + +console.log(bl.length) // 12 + +console.log(bl.slice(0, 10).toString('ascii')) // 'abcdefghij' +console.log(bl.slice(3, 10).toString('ascii')) // 'defghij' +console.log(bl.slice(3, 6).toString('ascii')) // 'def' +console.log(bl.slice(3, 8).toString('ascii')) // 'defgh' +console.log(bl.slice(5, 10).toString('ascii')) // 'fghij' + +console.log(bl.indexOf('def')) // 3 +console.log(bl.indexOf('asdf')) // -1 + +// or just use toString! +console.log(bl.toString()) // 'abcdefghij\u0003\u0004' +console.log(bl.toString('ascii', 3, 8)) // 'defgh' +console.log(bl.toString('ascii', 5, 10)) // 'fghij' + +// other standard Buffer readables +console.log(bl.readUInt16BE(10)) // 0x0304 +console.log(bl.readUInt16LE(10)) // 0x0403 +``` + +Give it a callback in the constructor and use it just like **[concat-stream](https://github.com/maxogden/node-concat-stream)**: + +```js +const { BufferListStream } = require('bl') +const fs = require('fs') + +fs.createReadStream('README.md') + .pipe(BufferListStream((err, data) => { // note 'new' isn't strictly required + // `data` is a complete Buffer object containing the full data + console.log(data.toString()) + })) +``` + +Note that when you use the *callback* method like this, the resulting `data` parameter is a concatenation of all `Buffer` objects in the list. If you want to avoid the overhead of this concatenation (in cases of extreme performance consciousness), then avoid the *callback* method and just listen to `'end'` instead, like a standard Stream. + +Or to fetch a URL using [hyperquest](https://github.com/substack/hyperquest) (should work with [request](http://github.com/mikeal/request) and even plain Node http too!): + +```js +const hyperquest = require('hyperquest') +const { BufferListStream } = require('bl') + +const url = 'https://raw.github.com/rvagg/bl/master/README.md' + +hyperquest(url).pipe(BufferListStream((err, data) => { + console.log(data.toString()) +})) +``` + +Or, use it as a readable stream to recompose a list of Buffers to an output source: + +```js +const { BufferListStream } = require('bl') +const fs = require('fs') + +var bl = new BufferListStream() +bl.append(Buffer.from('abcd')) +bl.append(Buffer.from('efg')) +bl.append(Buffer.from('hi')) +bl.append(Buffer.from('j')) + +bl.pipe(fs.createWriteStream('gibberish.txt')) +``` + +## API + + * new BufferList([ buf ]) + * BufferList.isBufferList(obj) + * bl.length + * bl.append(buffer) + * bl.get(index) + * bl.indexOf(value[, byteOffset][, encoding]) + * bl.slice([ start[, end ] ]) + * bl.shallowSlice([ start[, end ] ]) + * bl.copy(dest, [ destStart, [ srcStart [, srcEnd ] ] ]) + * bl.duplicate() + * bl.consume(bytes) + * bl.toString([encoding, [ start, [ end ]]]) + * bl.readDoubleBE(), bl.readDoubleLE(), bl.readFloatBE(), bl.readFloatLE(), bl.readInt32BE(), bl.readInt32LE(), bl.readUInt32BE(), bl.readUInt32LE(), bl.readInt16BE(), bl.readInt16LE(), bl.readUInt16BE(), bl.readUInt16LE(), bl.readInt8(), bl.readUInt8() + * new BufferListStream([ callback ]) + +-------------------------------------------------------- + +### new BufferList([ Buffer | Buffer array | BufferList | BufferList array | String ]) +No arguments are _required_ for the constructor, but you can initialise the list by passing in a single `Buffer` object or an array of `Buffer` objects. + +`new` is not strictly required, if you don't instantiate a new object, it will be done automatically for you so you can create a new instance simply with: + +```js +const { BufferList } = require('bl') +const bl = BufferList() + +// equivalent to: + +const { BufferList } = require('bl') +const bl = new BufferList() +``` + +-------------------------------------------------------- + +### BufferList.isBufferList(obj) +Determines if the passed object is a `BufferList`. It will return `true` if the passed object is an instance of `BufferList` **or** `BufferListStream` and `false` otherwise. + +N.B. this won't return `true` for `BufferList` or `BufferListStream` instances created by versions of this library before this static method was added. + +-------------------------------------------------------- + +### bl.length +Get the length of the list in bytes. This is the sum of the lengths of all of the buffers contained in the list, minus any initial offset for a semi-consumed buffer at the beginning. Should accurately represent the total number of bytes that can be read from the list. + +-------------------------------------------------------- + +### bl.append(Buffer | Buffer array | BufferList | BufferList array | String) +`append(buffer)` adds an additional buffer or BufferList to the internal list. `this` is returned so it can be chained. + +-------------------------------------------------------- + +### bl.get(index) +`get()` will return the byte at the specified index. + +-------------------------------------------------------- + +### bl.indexOf(value[, byteOffset][, encoding]) +`get()` will return the byte at the specified index. +`indexOf()` method returns the first index at which a given element can be found in the BufferList, or -1 if it is not present. + +-------------------------------------------------------- + +### bl.slice([ start, [ end ] ]) +`slice()` returns a new `Buffer` object containing the bytes within the range specified. Both `start` and `end` are optional and will default to the beginning and end of the list respectively. + +If the requested range spans a single internal buffer then a slice of that buffer will be returned which shares the original memory range of that Buffer. If the range spans multiple buffers then copy operations will likely occur to give you a uniform Buffer. + +-------------------------------------------------------- + +### bl.shallowSlice([ start, [ end ] ]) +`shallowSlice()` returns a new `BufferList` object containing the bytes within the range specified. Both `start` and `end` are optional and will default to the beginning and end of the list respectively. + +No copies will be performed. All buffers in the result share memory with the original list. + +-------------------------------------------------------- + +### bl.copy(dest, [ destStart, [ srcStart [, srcEnd ] ] ]) +`copy()` copies the content of the list in the `dest` buffer, starting from `destStart` and containing the bytes within the range specified with `srcStart` to `srcEnd`. `destStart`, `start` and `end` are optional and will default to the beginning of the `dest` buffer, and the beginning and end of the list respectively. + +-------------------------------------------------------- + +### bl.duplicate() +`duplicate()` performs a **shallow-copy** of the list. The internal Buffers remains the same, so if you change the underlying Buffers, the change will be reflected in both the original and the duplicate. This method is needed if you want to call `consume()` or `pipe()` and still keep the original list.Example: + +```js +var bl = new BufferListStream() + +bl.append('hello') +bl.append(' world') +bl.append('\n') + +bl.duplicate().pipe(process.stdout, { end: false }) + +console.log(bl.toString()) +``` + +-------------------------------------------------------- + +### bl.consume(bytes) +`consume()` will shift bytes *off the start of the list*. The number of bytes consumed don't need to line up with the sizes of the internal Buffers—initial offsets will be calculated accordingly in order to give you a consistent view of the data. + +-------------------------------------------------------- + +### bl.toString([encoding, [ start, [ end ]]]) +`toString()` will return a string representation of the buffer. The optional `start` and `end` arguments are passed on to `slice()`, while the `encoding` is passed on to `toString()` of the resulting Buffer. See the [Buffer#toString()](http://nodejs.org/docs/latest/api/buffer.html#buffer_buf_tostring_encoding_start_end) documentation for more information. + +-------------------------------------------------------- + +### bl.readDoubleBE(), bl.readDoubleLE(), bl.readFloatBE(), bl.readFloatLE(), bl.readInt32BE(), bl.readInt32LE(), bl.readUInt32BE(), bl.readUInt32LE(), bl.readInt16BE(), bl.readInt16LE(), bl.readUInt16BE(), bl.readUInt16LE(), bl.readInt8(), bl.readUInt8() + +All of the standard byte-reading methods of the `Buffer` interface are implemented and will operate across internal Buffer boundaries transparently. + +See the [Buffer](http://nodejs.org/docs/latest/api/buffer.html) documentation for how these work. + +-------------------------------------------------------- + +### new BufferListStream([ callback | Buffer | Buffer array | BufferList | BufferList array | String ]) +**BufferListStream** is a Node **[Duplex Stream](http://nodejs.org/docs/latest/api/stream.html#stream_class_stream_duplex)**, so it can be read from and written to like a standard Node stream. You can also `pipe()` to and from a **BufferListStream** instance. + +The constructor takes an optional callback, if supplied, the callback will be called with an error argument followed by a reference to the **bl** instance, when `bl.end()` is called (i.e. from a piped stream). This is a convenient method of collecting the entire contents of a stream, particularly when the stream is *chunky*, such as a network stream. + +Normally, no arguments are required for the constructor, but you can initialise the list by passing in a single `Buffer` object or an array of `Buffer` object. + +`new` is not strictly required, if you don't instantiate a new object, it will be done automatically for you so you can create a new instance simply with: + +```js +const { BufferListStream } = require('bl') +const bl = BufferListStream() + +// equivalent to: + +const { BufferListStream } = require('bl') +const bl = new BufferListStream() +``` + +N.B. For backwards compatibility reasons, `BufferListStream` is the **default** export when you `require('bl')`: + +```js +const { BufferListStream } = require('bl') +// equivalent to: +const BufferListStream = require('bl') +``` + +-------------------------------------------------------- + +## Contributors + +**bl** is brought to you by the following hackers: + + * [Rod Vagg](https://github.com/rvagg) + * [Matteo Collina](https://github.com/mcollina) + * [Jarett Cruger](https://github.com/jcrugzz) + + +## License & copyright + +Copyright (c) 2013-2019 bl contributors (listed above). + +bl is licensed under the MIT license. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE.md file for more details. diff --git a/backend/node_modules/bl/bl.js b/backend/node_modules/bl/bl.js new file mode 100644 index 00000000..40228f87 --- /dev/null +++ b/backend/node_modules/bl/bl.js @@ -0,0 +1,84 @@ +'use strict' + +const DuplexStream = require('readable-stream').Duplex +const inherits = require('inherits') +const BufferList = require('./BufferList') + +function BufferListStream (callback) { + if (!(this instanceof BufferListStream)) { + return new BufferListStream(callback) + } + + if (typeof callback === 'function') { + this._callback = callback + + const piper = function piper (err) { + if (this._callback) { + this._callback(err) + this._callback = null + } + }.bind(this) + + this.on('pipe', function onPipe (src) { + src.on('error', piper) + }) + this.on('unpipe', function onUnpipe (src) { + src.removeListener('error', piper) + }) + + callback = null + } + + BufferList._init.call(this, callback) + DuplexStream.call(this) +} + +inherits(BufferListStream, DuplexStream) +Object.assign(BufferListStream.prototype, BufferList.prototype) + +BufferListStream.prototype._new = function _new (callback) { + return new BufferListStream(callback) +} + +BufferListStream.prototype._write = function _write (buf, encoding, callback) { + this._appendBuffer(buf) + + if (typeof callback === 'function') { + callback() + } +} + +BufferListStream.prototype._read = function _read (size) { + if (!this.length) { + return this.push(null) + } + + size = Math.min(size, this.length) + this.push(this.slice(0, size)) + this.consume(size) +} + +BufferListStream.prototype.end = function end (chunk) { + DuplexStream.prototype.end.call(this, chunk) + + if (this._callback) { + this._callback(null, this.slice()) + this._callback = null + } +} + +BufferListStream.prototype._destroy = function _destroy (err, cb) { + this._bufs.length = 0 + this.length = 0 + cb(err) +} + +BufferListStream.prototype._isBufferList = function _isBufferList (b) { + return b instanceof BufferListStream || b instanceof BufferList || BufferListStream.isBufferList(b) +} + +BufferListStream.isBufferList = BufferList.isBufferList + +module.exports = BufferListStream +module.exports.BufferListStream = BufferListStream +module.exports.BufferList = BufferList diff --git a/backend/node_modules/bl/package.json b/backend/node_modules/bl/package.json new file mode 100644 index 00000000..3b2be3f4 --- /dev/null +++ b/backend/node_modules/bl/package.json @@ -0,0 +1,37 @@ +{ + "name": "bl", + "version": "4.1.0", + "description": "Buffer List: collect buffers and access with a standard readable Buffer interface, streamable too!", + "license": "MIT", + "main": "bl.js", + "scripts": { + "lint": "standard *.js test/*.js", + "test": "npm run lint && node test/test.js | faucet" + }, + "repository": { + "type": "git", + "url": "https://github.com/rvagg/bl.git" + }, + "homepage": "https://github.com/rvagg/bl", + "authors": [ + "Rod Vagg (https://github.com/rvagg)", + "Matteo Collina (https://github.com/mcollina)", + "Jarett Cruger (https://github.com/jcrugzz)" + ], + "keywords": [ + "buffer", + "buffers", + "stream", + "awesomesauce" + ], + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "devDependencies": { + "faucet": "~0.0.1", + "standard": "^14.3.0", + "tape": "^4.11.0" + } +} diff --git a/backend/node_modules/bl/test/convert.js b/backend/node_modules/bl/test/convert.js new file mode 100644 index 00000000..9f3e2359 --- /dev/null +++ b/backend/node_modules/bl/test/convert.js @@ -0,0 +1,21 @@ +'use strict' + +const tape = require('tape') +const { BufferList, BufferListStream } = require('../') +const { Buffer } = require('buffer') + +tape('convert from BufferList to BufferListStream', (t) => { + const data = Buffer.from(`TEST-${Date.now()}`) + const bl = new BufferList(data) + const bls = new BufferListStream(bl) + t.ok(bl.slice().equals(bls.slice())) + t.end() +}) + +tape('convert from BufferListStream to BufferList', (t) => { + const data = Buffer.from(`TEST-${Date.now()}`) + const bls = new BufferListStream(data) + const bl = new BufferList(bls) + t.ok(bl.slice().equals(bls.slice())) + t.end() +}) diff --git a/backend/node_modules/bl/test/indexOf.js b/backend/node_modules/bl/test/indexOf.js new file mode 100644 index 00000000..62dcb01f --- /dev/null +++ b/backend/node_modules/bl/test/indexOf.js @@ -0,0 +1,492 @@ +'use strict' + +const tape = require('tape') +const BufferList = require('../') +const { Buffer } = require('buffer') + +tape('indexOf single byte needle', (t) => { + const bl = new BufferList(['abcdefg', 'abcdefg', '12345']) + + t.equal(bl.indexOf('e'), 4) + t.equal(bl.indexOf('e', 5), 11) + t.equal(bl.indexOf('e', 12), -1) + t.equal(bl.indexOf('5'), 18) + + t.end() +}) + +tape('indexOf multiple byte needle', (t) => { + const bl = new BufferList(['abcdefg', 'abcdefg']) + + t.equal(bl.indexOf('ef'), 4) + t.equal(bl.indexOf('ef', 5), 11) + + t.end() +}) + +tape('indexOf multiple byte needles across buffer boundaries', (t) => { + const bl = new BufferList(['abcdefg', 'abcdefg']) + + t.equal(bl.indexOf('fgabc'), 5) + + t.end() +}) + +tape('indexOf takes a Uint8Array search', (t) => { + const bl = new BufferList(['abcdefg', 'abcdefg']) + const search = new Uint8Array([102, 103, 97, 98, 99]) // fgabc + + t.equal(bl.indexOf(search), 5) + + t.end() +}) + +tape('indexOf takes a buffer list search', (t) => { + const bl = new BufferList(['abcdefg', 'abcdefg']) + const search = new BufferList('fgabc') + + t.equal(bl.indexOf(search), 5) + + t.end() +}) + +tape('indexOf a zero byte needle', (t) => { + const b = new BufferList('abcdef') + const bufEmpty = Buffer.from('') + + t.equal(b.indexOf(''), 0) + t.equal(b.indexOf('', 1), 1) + t.equal(b.indexOf('', b.length + 1), b.length) + t.equal(b.indexOf('', Infinity), b.length) + t.equal(b.indexOf(bufEmpty), 0) + t.equal(b.indexOf(bufEmpty, 1), 1) + t.equal(b.indexOf(bufEmpty, b.length + 1), b.length) + t.equal(b.indexOf(bufEmpty, Infinity), b.length) + + t.end() +}) + +tape('indexOf buffers smaller and larger than the needle', (t) => { + const bl = new BufferList(['abcdefg', 'a', 'bcdefg', 'a', 'bcfgab']) + + t.equal(bl.indexOf('fgabc'), 5) + t.equal(bl.indexOf('fgabc', 6), 12) + t.equal(bl.indexOf('fgabc', 13), -1) + + t.end() +}) + +// only present in node 6+ +;(process.version.substr(1).split('.')[0] >= 6) && tape('indexOf latin1 and binary encoding', (t) => { + const b = new BufferList('abcdef') + + // test latin1 encoding + t.equal( + new BufferList(Buffer.from(b.toString('latin1'), 'latin1')) + .indexOf('d', 0, 'latin1'), + 3 + ) + t.equal( + new BufferList(Buffer.from(b.toString('latin1'), 'latin1')) + .indexOf(Buffer.from('d', 'latin1'), 0, 'latin1'), + 3 + ) + t.equal( + new BufferList(Buffer.from('aa\u00e8aa', 'latin1')) + .indexOf('\u00e8', 'latin1'), + 2 + ) + t.equal( + new BufferList(Buffer.from('\u00e8', 'latin1')) + .indexOf('\u00e8', 'latin1'), + 0 + ) + t.equal( + new BufferList(Buffer.from('\u00e8', 'latin1')) + .indexOf(Buffer.from('\u00e8', 'latin1'), 'latin1'), + 0 + ) + + // test binary encoding + t.equal( + new BufferList(Buffer.from(b.toString('binary'), 'binary')) + .indexOf('d', 0, 'binary'), + 3 + ) + t.equal( + new BufferList(Buffer.from(b.toString('binary'), 'binary')) + .indexOf(Buffer.from('d', 'binary'), 0, 'binary'), + 3 + ) + t.equal( + new BufferList(Buffer.from('aa\u00e8aa', 'binary')) + .indexOf('\u00e8', 'binary'), + 2 + ) + t.equal( + new BufferList(Buffer.from('\u00e8', 'binary')) + .indexOf('\u00e8', 'binary'), + 0 + ) + t.equal( + new BufferList(Buffer.from('\u00e8', 'binary')) + .indexOf(Buffer.from('\u00e8', 'binary'), 'binary'), + 0 + ) + + t.end() +}) + +tape('indexOf the entire nodejs10 buffer test suite', (t) => { + const b = new BufferList('abcdef') + const bufA = Buffer.from('a') + const bufBc = Buffer.from('bc') + const bufF = Buffer.from('f') + const bufZ = Buffer.from('z') + + const stringComparison = 'abcdef' + + t.equal(b.indexOf('a'), 0) + t.equal(b.indexOf('a', 1), -1) + t.equal(b.indexOf('a', -1), -1) + t.equal(b.indexOf('a', -4), -1) + t.equal(b.indexOf('a', -b.length), 0) + t.equal(b.indexOf('a', NaN), 0) + t.equal(b.indexOf('a', -Infinity), 0) + t.equal(b.indexOf('a', Infinity), -1) + t.equal(b.indexOf('bc'), 1) + t.equal(b.indexOf('bc', 2), -1) + t.equal(b.indexOf('bc', -1), -1) + t.equal(b.indexOf('bc', -3), -1) + t.equal(b.indexOf('bc', -5), 1) + t.equal(b.indexOf('bc', NaN), 1) + t.equal(b.indexOf('bc', -Infinity), 1) + t.equal(b.indexOf('bc', Infinity), -1) + t.equal(b.indexOf('f'), b.length - 1) + t.equal(b.indexOf('z'), -1) + + // empty search tests + t.equal(b.indexOf(bufA), 0) + t.equal(b.indexOf(bufA, 1), -1) + t.equal(b.indexOf(bufA, -1), -1) + t.equal(b.indexOf(bufA, -4), -1) + t.equal(b.indexOf(bufA, -b.length), 0) + t.equal(b.indexOf(bufA, NaN), 0) + t.equal(b.indexOf(bufA, -Infinity), 0) + t.equal(b.indexOf(bufA, Infinity), -1) + t.equal(b.indexOf(bufBc), 1) + t.equal(b.indexOf(bufBc, 2), -1) + t.equal(b.indexOf(bufBc, -1), -1) + t.equal(b.indexOf(bufBc, -3), -1) + t.equal(b.indexOf(bufBc, -5), 1) + t.equal(b.indexOf(bufBc, NaN), 1) + t.equal(b.indexOf(bufBc, -Infinity), 1) + t.equal(b.indexOf(bufBc, Infinity), -1) + t.equal(b.indexOf(bufF), b.length - 1) + t.equal(b.indexOf(bufZ), -1) + t.equal(b.indexOf(0x61), 0) + t.equal(b.indexOf(0x61, 1), -1) + t.equal(b.indexOf(0x61, -1), -1) + t.equal(b.indexOf(0x61, -4), -1) + t.equal(b.indexOf(0x61, -b.length), 0) + t.equal(b.indexOf(0x61, NaN), 0) + t.equal(b.indexOf(0x61, -Infinity), 0) + t.equal(b.indexOf(0x61, Infinity), -1) + t.equal(b.indexOf(0x0), -1) + + // test offsets + t.equal(b.indexOf('d', 2), 3) + t.equal(b.indexOf('f', 5), 5) + t.equal(b.indexOf('f', -1), 5) + t.equal(b.indexOf('f', 6), -1) + + t.equal(b.indexOf(Buffer.from('d'), 2), 3) + t.equal(b.indexOf(Buffer.from('f'), 5), 5) + t.equal(b.indexOf(Buffer.from('f'), -1), 5) + t.equal(b.indexOf(Buffer.from('f'), 6), -1) + + t.equal(Buffer.from('ff').indexOf(Buffer.from('f'), 1, 'ucs2'), -1) + + // test invalid and uppercase encoding + t.equal(b.indexOf('b', 'utf8'), 1) + t.equal(b.indexOf('b', 'UTF8'), 1) + t.equal(b.indexOf('62', 'HEX'), 1) + t.throws(() => b.indexOf('bad', 'enc'), TypeError) + + // test hex encoding + t.equal( + Buffer.from(b.toString('hex'), 'hex') + .indexOf('64', 0, 'hex'), + 3 + ) + t.equal( + Buffer.from(b.toString('hex'), 'hex') + .indexOf(Buffer.from('64', 'hex'), 0, 'hex'), + 3 + ) + + // test base64 encoding + t.equal( + Buffer.from(b.toString('base64'), 'base64') + .indexOf('ZA==', 0, 'base64'), + 3 + ) + t.equal( + Buffer.from(b.toString('base64'), 'base64') + .indexOf(Buffer.from('ZA==', 'base64'), 0, 'base64'), + 3 + ) + + // test ascii encoding + t.equal( + Buffer.from(b.toString('ascii'), 'ascii') + .indexOf('d', 0, 'ascii'), + 3 + ) + t.equal( + Buffer.from(b.toString('ascii'), 'ascii') + .indexOf(Buffer.from('d', 'ascii'), 0, 'ascii'), + 3 + ) + + // test optional offset with passed encoding + t.equal(Buffer.from('aaaa0').indexOf('30', 'hex'), 4) + t.equal(Buffer.from('aaaa00a').indexOf('3030', 'hex'), 4) + + { + // test usc2 encoding + const twoByteString = Buffer.from('\u039a\u0391\u03a3\u03a3\u0395', 'ucs2') + + t.equal(8, twoByteString.indexOf('\u0395', 4, 'ucs2')) + t.equal(6, twoByteString.indexOf('\u03a3', -4, 'ucs2')) + t.equal(4, twoByteString.indexOf('\u03a3', -6, 'ucs2')) + t.equal(4, twoByteString.indexOf( + Buffer.from('\u03a3', 'ucs2'), -6, 'ucs2')) + t.equal(-1, twoByteString.indexOf('\u03a3', -2, 'ucs2')) + } + + const mixedByteStringUcs2 = + Buffer.from('\u039a\u0391abc\u03a3\u03a3\u0395', 'ucs2') + + t.equal(6, mixedByteStringUcs2.indexOf('bc', 0, 'ucs2')) + t.equal(10, mixedByteStringUcs2.indexOf('\u03a3', 0, 'ucs2')) + t.equal(-1, mixedByteStringUcs2.indexOf('\u0396', 0, 'ucs2')) + + t.equal( + 6, mixedByteStringUcs2.indexOf(Buffer.from('bc', 'ucs2'), 0, 'ucs2')) + t.equal( + 10, mixedByteStringUcs2.indexOf(Buffer.from('\u03a3', 'ucs2'), 0, 'ucs2')) + t.equal( + -1, mixedByteStringUcs2.indexOf(Buffer.from('\u0396', 'ucs2'), 0, 'ucs2')) + + { + const twoByteString = Buffer.from('\u039a\u0391\u03a3\u03a3\u0395', 'ucs2') + + // Test single char pattern + t.equal(0, twoByteString.indexOf('\u039a', 0, 'ucs2')) + let index = twoByteString.indexOf('\u0391', 0, 'ucs2') + t.equal(2, index, `Alpha - at index ${index}`) + index = twoByteString.indexOf('\u03a3', 0, 'ucs2') + t.equal(4, index, `First Sigma - at index ${index}`) + index = twoByteString.indexOf('\u03a3', 6, 'ucs2') + t.equal(6, index, `Second Sigma - at index ${index}`) + index = twoByteString.indexOf('\u0395', 0, 'ucs2') + t.equal(8, index, `Epsilon - at index ${index}`) + index = twoByteString.indexOf('\u0392', 0, 'ucs2') + t.equal(-1, index, `Not beta - at index ${index}`) + + // Test multi-char pattern + index = twoByteString.indexOf('\u039a\u0391', 0, 'ucs2') + t.equal(0, index, `Lambda Alpha - at index ${index}`) + index = twoByteString.indexOf('\u0391\u03a3', 0, 'ucs2') + t.equal(2, index, `Alpha Sigma - at index ${index}`) + index = twoByteString.indexOf('\u03a3\u03a3', 0, 'ucs2') + t.equal(4, index, `Sigma Sigma - at index ${index}`) + index = twoByteString.indexOf('\u03a3\u0395', 0, 'ucs2') + t.equal(6, index, `Sigma Epsilon - at index ${index}`) + } + + const mixedByteStringUtf8 = Buffer.from('\u039a\u0391abc\u03a3\u03a3\u0395') + + t.equal(5, mixedByteStringUtf8.indexOf('bc')) + t.equal(5, mixedByteStringUtf8.indexOf('bc', 5)) + t.equal(5, mixedByteStringUtf8.indexOf('bc', -8)) + t.equal(7, mixedByteStringUtf8.indexOf('\u03a3')) + t.equal(-1, mixedByteStringUtf8.indexOf('\u0396')) + + // Test complex string indexOf algorithms. Only trigger for long strings. + // Long string that isn't a simple repeat of a shorter string. + let longString = 'A' + for (let i = 66; i < 76; i++) { // from 'B' to 'K' + longString = longString + String.fromCharCode(i) + longString + } + + const longBufferString = Buffer.from(longString) + + // pattern of 15 chars, repeated every 16 chars in long + let pattern = 'ABACABADABACABA' + for (let i = 0; i < longBufferString.length - pattern.length; i += 7) { + const index = longBufferString.indexOf(pattern, i) + t.equal((i + 15) & ~0xf, index, + `Long ABACABA...-string at index ${i}`) + } + + let index = longBufferString.indexOf('AJABACA') + t.equal(510, index, `Long AJABACA, First J - at index ${index}`) + index = longBufferString.indexOf('AJABACA', 511) + t.equal(1534, index, `Long AJABACA, Second J - at index ${index}`) + + pattern = 'JABACABADABACABA' + index = longBufferString.indexOf(pattern) + t.equal(511, index, `Long JABACABA..., First J - at index ${index}`) + index = longBufferString.indexOf(pattern, 512) + t.equal( + 1535, index, `Long JABACABA..., Second J - at index ${index}`) + + // Search for a non-ASCII string in a pure ASCII string. + const asciiString = Buffer.from( + 'somethingnotatallsinisterwhichalsoworks') + t.equal(-1, asciiString.indexOf('\x2061')) + t.equal(3, asciiString.indexOf('eth', 0)) + + // Search in string containing many non-ASCII chars. + const allCodePoints = [] + for (let i = 0; i < 65536; i++) { + allCodePoints[i] = i + } + + const allCharsString = String.fromCharCode.apply(String, allCodePoints) + const allCharsBufferUtf8 = Buffer.from(allCharsString) + const allCharsBufferUcs2 = Buffer.from(allCharsString, 'ucs2') + + // Search for string long enough to trigger complex search with ASCII pattern + // and UC16 subject. + t.equal(-1, allCharsBufferUtf8.indexOf('notfound')) + t.equal(-1, allCharsBufferUcs2.indexOf('notfound')) + + // Needle is longer than haystack, but only because it's encoded as UTF-16 + t.equal(Buffer.from('aaaa').indexOf('a'.repeat(4), 'ucs2'), -1) + + t.equal(Buffer.from('aaaa').indexOf('a'.repeat(4), 'utf8'), 0) + t.equal(Buffer.from('aaaa').indexOf('你好', 'ucs2'), -1) + + // Haystack has odd length, but the needle is UCS2. + t.equal(Buffer.from('aaaaa').indexOf('b', 'ucs2'), -1) + + { + // Find substrings in Utf8. + const lengths = [1, 3, 15] // Single char, simple and complex. + const indices = [0x5, 0x60, 0x400, 0x680, 0x7ee, 0xFF02, 0x16610, 0x2f77b] + for (let lengthIndex = 0; lengthIndex < lengths.length; lengthIndex++) { + for (let i = 0; i < indices.length; i++) { + const index = indices[i] + let length = lengths[lengthIndex] + + if (index + length > 0x7F) { + length = 2 * length + } + + if (index + length > 0x7FF) { + length = 3 * length + } + + if (index + length > 0xFFFF) { + length = 4 * length + } + + const patternBufferUtf8 = allCharsBufferUtf8.slice(index, index + length) + t.equal(index, allCharsBufferUtf8.indexOf(patternBufferUtf8)) + + const patternStringUtf8 = patternBufferUtf8.toString() + t.equal(index, allCharsBufferUtf8.indexOf(patternStringUtf8)) + } + } + } + + { + // Find substrings in Usc2. + const lengths = [2, 4, 16] // Single char, simple and complex. + const indices = [0x5, 0x65, 0x105, 0x205, 0x285, 0x2005, 0x2085, 0xfff0] + + for (let lengthIndex = 0; lengthIndex < lengths.length; lengthIndex++) { + for (let i = 0; i < indices.length; i++) { + const index = indices[i] * 2 + const length = lengths[lengthIndex] + + const patternBufferUcs2 = + allCharsBufferUcs2.slice(index, index + length) + t.equal( + index, allCharsBufferUcs2.indexOf(patternBufferUcs2, 0, 'ucs2')) + + const patternStringUcs2 = patternBufferUcs2.toString('ucs2') + t.equal( + index, allCharsBufferUcs2.indexOf(patternStringUcs2, 0, 'ucs2')) + } + } + } + + [ + () => {}, + {}, + [] + ].forEach((val) => { + t.throws(() => b.indexOf(val), TypeError, `"${JSON.stringify(val)}" should throw`) + }) + + // Test weird offset arguments. + // The following offsets coerce to NaN or 0, searching the whole Buffer + t.equal(b.indexOf('b', undefined), 1) + t.equal(b.indexOf('b', {}), 1) + t.equal(b.indexOf('b', 0), 1) + t.equal(b.indexOf('b', null), 1) + t.equal(b.indexOf('b', []), 1) + + // The following offset coerces to 2, in other words +[2] === 2 + t.equal(b.indexOf('b', [2]), -1) + + // Behavior should match String.indexOf() + t.equal( + b.indexOf('b', undefined), + stringComparison.indexOf('b', undefined)) + t.equal( + b.indexOf('b', {}), + stringComparison.indexOf('b', {})) + t.equal( + b.indexOf('b', 0), + stringComparison.indexOf('b', 0)) + t.equal( + b.indexOf('b', null), + stringComparison.indexOf('b', null)) + t.equal( + b.indexOf('b', []), + stringComparison.indexOf('b', [])) + t.equal( + b.indexOf('b', [2]), + stringComparison.indexOf('b', [2])) + + // test truncation of Number arguments to uint8 + { + const buf = Buffer.from('this is a test') + + t.equal(buf.indexOf(0x6973), 3) + t.equal(buf.indexOf(0x697320), 4) + t.equal(buf.indexOf(0x69732069), 2) + t.equal(buf.indexOf(0x697374657374), 0) + t.equal(buf.indexOf(0x69737374), 0) + t.equal(buf.indexOf(0x69737465), 11) + t.equal(buf.indexOf(0x69737465), 11) + t.equal(buf.indexOf(-140), 0) + t.equal(buf.indexOf(-152), 1) + t.equal(buf.indexOf(0xff), -1) + t.equal(buf.indexOf(0xffff), -1) + } + + // Test that Uint8Array arguments are okay. + { + const needle = new Uint8Array([0x66, 0x6f, 0x6f]) + const haystack = new BufferList(Buffer.from('a foo b foo')) + t.equal(haystack.indexOf(needle), 2) + } + + t.end() +}) diff --git a/backend/node_modules/bl/test/isBufferList.js b/backend/node_modules/bl/test/isBufferList.js new file mode 100644 index 00000000..9d895d59 --- /dev/null +++ b/backend/node_modules/bl/test/isBufferList.js @@ -0,0 +1,32 @@ +'use strict' + +const tape = require('tape') +const { BufferList, BufferListStream } = require('../') +const { Buffer } = require('buffer') + +tape('isBufferList positives', (t) => { + t.ok(BufferList.isBufferList(new BufferList())) + t.ok(BufferList.isBufferList(new BufferListStream())) + + t.end() +}) + +tape('isBufferList negatives', (t) => { + const types = [ + null, + undefined, + NaN, + true, + false, + {}, + [], + Buffer.alloc(0), + [Buffer.alloc(0)] + ] + + for (const obj of types) { + t.notOk(BufferList.isBufferList(obj)) + } + + t.end() +}) diff --git a/backend/node_modules/bl/test/test.js b/backend/node_modules/bl/test/test.js new file mode 100644 index 00000000..e523d0c3 --- /dev/null +++ b/backend/node_modules/bl/test/test.js @@ -0,0 +1,869 @@ +'use strict' + +const tape = require('tape') +const crypto = require('crypto') +const fs = require('fs') +const path = require('path') +const BufferList = require('../') +const { Buffer } = require('buffer') + +const encodings = + ('hex utf8 utf-8 ascii binary base64' + + (process.browser ? '' : ' ucs2 ucs-2 utf16le utf-16le')).split(' ') + +require('./indexOf') +require('./isBufferList') +require('./convert') + +tape('single bytes from single buffer', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abcd')) + + t.equal(bl.length, 4) + t.equal(bl.get(-1), undefined) + t.equal(bl.get(0), 97) + t.equal(bl.get(1), 98) + t.equal(bl.get(2), 99) + t.equal(bl.get(3), 100) + t.equal(bl.get(4), undefined) + + t.end() +}) + +tape('single bytes from multiple buffers', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abcd')) + bl.append(Buffer.from('efg')) + bl.append(Buffer.from('hi')) + bl.append(Buffer.from('j')) + + t.equal(bl.length, 10) + + t.equal(bl.get(0), 97) + t.equal(bl.get(1), 98) + t.equal(bl.get(2), 99) + t.equal(bl.get(3), 100) + t.equal(bl.get(4), 101) + t.equal(bl.get(5), 102) + t.equal(bl.get(6), 103) + t.equal(bl.get(7), 104) + t.equal(bl.get(8), 105) + t.equal(bl.get(9), 106) + + t.end() +}) + +tape('multi bytes from single buffer', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abcd')) + + t.equal(bl.length, 4) + + t.equal(bl.slice(0, 4).toString('ascii'), 'abcd') + t.equal(bl.slice(0, 3).toString('ascii'), 'abc') + t.equal(bl.slice(1, 4).toString('ascii'), 'bcd') + t.equal(bl.slice(-4, -1).toString('ascii'), 'abc') + + t.end() +}) + +tape('multi bytes from single buffer (negative indexes)', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('buffer')) + + t.equal(bl.length, 6) + + t.equal(bl.slice(-6, -1).toString('ascii'), 'buffe') + t.equal(bl.slice(-6, -2).toString('ascii'), 'buff') + t.equal(bl.slice(-5, -2).toString('ascii'), 'uff') + + t.end() +}) + +tape('multiple bytes from multiple buffers', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abcd')) + bl.append(Buffer.from('efg')) + bl.append(Buffer.from('hi')) + bl.append(Buffer.from('j')) + + t.equal(bl.length, 10) + + t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') + t.equal(bl.slice(3, 10).toString('ascii'), 'defghij') + t.equal(bl.slice(3, 6).toString('ascii'), 'def') + t.equal(bl.slice(3, 8).toString('ascii'), 'defgh') + t.equal(bl.slice(5, 10).toString('ascii'), 'fghij') + t.equal(bl.slice(-7, -4).toString('ascii'), 'def') + + t.end() +}) + +tape('multiple bytes from multiple buffer lists', function (t) { + const bl = new BufferList() + + bl.append(new BufferList([Buffer.from('abcd'), Buffer.from('efg')])) + bl.append(new BufferList([Buffer.from('hi'), Buffer.from('j')])) + + t.equal(bl.length, 10) + + t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') + + t.equal(bl.slice(3, 10).toString('ascii'), 'defghij') + t.equal(bl.slice(3, 6).toString('ascii'), 'def') + t.equal(bl.slice(3, 8).toString('ascii'), 'defgh') + t.equal(bl.slice(5, 10).toString('ascii'), 'fghij') + + t.end() +}) + +// same data as previous test, just using nested constructors +tape('multiple bytes from crazy nested buffer lists', function (t) { + const bl = new BufferList() + + bl.append(new BufferList([ + new BufferList([ + new BufferList(Buffer.from('abc')), + Buffer.from('d'), + new BufferList(Buffer.from('efg')) + ]), + new BufferList([Buffer.from('hi')]), + new BufferList(Buffer.from('j')) + ])) + + t.equal(bl.length, 10) + + t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') + + t.equal(bl.slice(3, 10).toString('ascii'), 'defghij') + t.equal(bl.slice(3, 6).toString('ascii'), 'def') + t.equal(bl.slice(3, 8).toString('ascii'), 'defgh') + t.equal(bl.slice(5, 10).toString('ascii'), 'fghij') + + t.end() +}) + +tape('append accepts arrays of Buffers', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abc')) + bl.append([Buffer.from('def')]) + bl.append([Buffer.from('ghi'), Buffer.from('jkl')]) + bl.append([Buffer.from('mnop'), Buffer.from('qrstu'), Buffer.from('vwxyz')]) + t.equal(bl.length, 26) + t.equal(bl.slice().toString('ascii'), 'abcdefghijklmnopqrstuvwxyz') + + t.end() +}) + +tape('append accepts arrays of Uint8Arrays', function (t) { + const bl = new BufferList() + + bl.append(new Uint8Array([97, 98, 99])) + bl.append([Uint8Array.from([100, 101, 102])]) + bl.append([new Uint8Array([103, 104, 105]), new Uint8Array([106, 107, 108])]) + bl.append([new Uint8Array([109, 110, 111, 112]), new Uint8Array([113, 114, 115, 116, 117]), new Uint8Array([118, 119, 120, 121, 122])]) + t.equal(bl.length, 26) + t.equal(bl.slice().toString('ascii'), 'abcdefghijklmnopqrstuvwxyz') + + t.end() +}) + +tape('append accepts arrays of BufferLists', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abc')) + bl.append([new BufferList('def')]) + bl.append(new BufferList([Buffer.from('ghi'), new BufferList('jkl')])) + bl.append([Buffer.from('mnop'), new BufferList([Buffer.from('qrstu'), Buffer.from('vwxyz')])]) + t.equal(bl.length, 26) + t.equal(bl.slice().toString('ascii'), 'abcdefghijklmnopqrstuvwxyz') + + t.end() +}) + +tape('append chainable', function (t) { + const bl = new BufferList() + + t.ok(bl.append(Buffer.from('abcd')) === bl) + t.ok(bl.append([Buffer.from('abcd')]) === bl) + t.ok(bl.append(new BufferList(Buffer.from('abcd'))) === bl) + t.ok(bl.append([new BufferList(Buffer.from('abcd'))]) === bl) + + t.end() +}) + +tape('append chainable (test results)', function (t) { + const bl = new BufferList('abc') + .append([new BufferList('def')]) + .append(new BufferList([Buffer.from('ghi'), new BufferList('jkl')])) + .append([Buffer.from('mnop'), new BufferList([Buffer.from('qrstu'), Buffer.from('vwxyz')])]) + + t.equal(bl.length, 26) + t.equal(bl.slice().toString('ascii'), 'abcdefghijklmnopqrstuvwxyz') + + t.end() +}) + +tape('consuming from multiple buffers', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abcd')) + bl.append(Buffer.from('efg')) + bl.append(Buffer.from('hi')) + bl.append(Buffer.from('j')) + + t.equal(bl.length, 10) + + t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') + + bl.consume(3) + t.equal(bl.length, 7) + t.equal(bl.slice(0, 7).toString('ascii'), 'defghij') + + bl.consume(2) + t.equal(bl.length, 5) + t.equal(bl.slice(0, 5).toString('ascii'), 'fghij') + + bl.consume(1) + t.equal(bl.length, 4) + t.equal(bl.slice(0, 4).toString('ascii'), 'ghij') + + bl.consume(1) + t.equal(bl.length, 3) + t.equal(bl.slice(0, 3).toString('ascii'), 'hij') + + bl.consume(2) + t.equal(bl.length, 1) + t.equal(bl.slice(0, 1).toString('ascii'), 'j') + + t.end() +}) + +tape('complete consumption', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('a')) + bl.append(Buffer.from('b')) + + bl.consume(2) + + t.equal(bl.length, 0) + t.equal(bl._bufs.length, 0) + + t.end() +}) + +tape('test readUInt8 / readInt8', function (t) { + const buf1 = Buffer.alloc(1) + const buf2 = Buffer.alloc(3) + const buf3 = Buffer.alloc(3) + const bl = new BufferList() + + buf1[0] = 0x1 + buf2[1] = 0x3 + buf2[2] = 0x4 + buf3[0] = 0x23 + buf3[1] = 0x42 + + bl.append(buf1) + bl.append(buf2) + bl.append(buf3) + + t.equal(bl.readUInt8(), 0x1) + t.equal(bl.readUInt8(2), 0x3) + t.equal(bl.readInt8(2), 0x3) + t.equal(bl.readUInt8(3), 0x4) + t.equal(bl.readInt8(3), 0x4) + t.equal(bl.readUInt8(4), 0x23) + t.equal(bl.readInt8(4), 0x23) + t.equal(bl.readUInt8(5), 0x42) + t.equal(bl.readInt8(5), 0x42) + + t.end() +}) + +tape('test readUInt16LE / readUInt16BE / readInt16LE / readInt16BE', function (t) { + const buf1 = Buffer.alloc(1) + const buf2 = Buffer.alloc(3) + const buf3 = Buffer.alloc(3) + const bl = new BufferList() + + buf1[0] = 0x1 + buf2[1] = 0x3 + buf2[2] = 0x4 + buf3[0] = 0x23 + buf3[1] = 0x42 + + bl.append(buf1) + bl.append(buf2) + bl.append(buf3) + + t.equal(bl.readUInt16BE(), 0x0100) + t.equal(bl.readUInt16LE(), 0x0001) + t.equal(bl.readUInt16BE(2), 0x0304) + t.equal(bl.readUInt16LE(2), 0x0403) + t.equal(bl.readInt16BE(2), 0x0304) + t.equal(bl.readInt16LE(2), 0x0403) + t.equal(bl.readUInt16BE(3), 0x0423) + t.equal(bl.readUInt16LE(3), 0x2304) + t.equal(bl.readInt16BE(3), 0x0423) + t.equal(bl.readInt16LE(3), 0x2304) + t.equal(bl.readUInt16BE(4), 0x2342) + t.equal(bl.readUInt16LE(4), 0x4223) + t.equal(bl.readInt16BE(4), 0x2342) + t.equal(bl.readInt16LE(4), 0x4223) + + t.end() +}) + +tape('test readUInt32LE / readUInt32BE / readInt32LE / readInt32BE', function (t) { + const buf1 = Buffer.alloc(1) + const buf2 = Buffer.alloc(3) + const buf3 = Buffer.alloc(3) + const bl = new BufferList() + + buf1[0] = 0x1 + buf2[1] = 0x3 + buf2[2] = 0x4 + buf3[0] = 0x23 + buf3[1] = 0x42 + + bl.append(buf1) + bl.append(buf2) + bl.append(buf3) + + t.equal(bl.readUInt32BE(), 0x01000304) + t.equal(bl.readUInt32LE(), 0x04030001) + t.equal(bl.readUInt32BE(2), 0x03042342) + t.equal(bl.readUInt32LE(2), 0x42230403) + t.equal(bl.readInt32BE(2), 0x03042342) + t.equal(bl.readInt32LE(2), 0x42230403) + + t.end() +}) + +tape('test readUIntLE / readUIntBE / readIntLE / readIntBE', function (t) { + const buf1 = Buffer.alloc(1) + const buf2 = Buffer.alloc(3) + const buf3 = Buffer.alloc(3) + const bl = new BufferList() + + buf2[0] = 0x2 + buf2[1] = 0x3 + buf2[2] = 0x4 + buf3[0] = 0x23 + buf3[1] = 0x42 + buf3[2] = 0x61 + + bl.append(buf1) + bl.append(buf2) + bl.append(buf3) + + t.equal(bl.readUIntBE(1, 1), 0x02) + t.equal(bl.readUIntBE(1, 2), 0x0203) + t.equal(bl.readUIntBE(1, 3), 0x020304) + t.equal(bl.readUIntBE(1, 4), 0x02030423) + t.equal(bl.readUIntBE(1, 5), 0x0203042342) + t.equal(bl.readUIntBE(1, 6), 0x020304234261) + t.equal(bl.readUIntLE(1, 1), 0x02) + t.equal(bl.readUIntLE(1, 2), 0x0302) + t.equal(bl.readUIntLE(1, 3), 0x040302) + t.equal(bl.readUIntLE(1, 4), 0x23040302) + t.equal(bl.readUIntLE(1, 5), 0x4223040302) + t.equal(bl.readUIntLE(1, 6), 0x614223040302) + t.equal(bl.readIntBE(1, 1), 0x02) + t.equal(bl.readIntBE(1, 2), 0x0203) + t.equal(bl.readIntBE(1, 3), 0x020304) + t.equal(bl.readIntBE(1, 4), 0x02030423) + t.equal(bl.readIntBE(1, 5), 0x0203042342) + t.equal(bl.readIntBE(1, 6), 0x020304234261) + t.equal(bl.readIntLE(1, 1), 0x02) + t.equal(bl.readIntLE(1, 2), 0x0302) + t.equal(bl.readIntLE(1, 3), 0x040302) + t.equal(bl.readIntLE(1, 4), 0x23040302) + t.equal(bl.readIntLE(1, 5), 0x4223040302) + t.equal(bl.readIntLE(1, 6), 0x614223040302) + + t.end() +}) + +tape('test readFloatLE / readFloatBE', function (t) { + const buf1 = Buffer.alloc(1) + const buf2 = Buffer.alloc(3) + const buf3 = Buffer.alloc(3) + const bl = new BufferList() + + buf1[0] = 0x01 + buf2[1] = 0x00 + buf2[2] = 0x00 + buf3[0] = 0x80 + buf3[1] = 0x3f + + bl.append(buf1) + bl.append(buf2) + bl.append(buf3) + + const canonical = Buffer.concat([buf1, buf2, buf3]) + t.equal(bl.readFloatLE(), canonical.readFloatLE()) + t.equal(bl.readFloatBE(), canonical.readFloatBE()) + t.equal(bl.readFloatLE(2), canonical.readFloatLE(2)) + t.equal(bl.readFloatBE(2), canonical.readFloatBE(2)) + + t.end() +}) + +tape('test readDoubleLE / readDoubleBE', function (t) { + const buf1 = Buffer.alloc(1) + const buf2 = Buffer.alloc(3) + const buf3 = Buffer.alloc(10) + const bl = new BufferList() + + buf1[0] = 0x01 + buf2[1] = 0x55 + buf2[2] = 0x55 + buf3[0] = 0x55 + buf3[1] = 0x55 + buf3[2] = 0x55 + buf3[3] = 0x55 + buf3[4] = 0xd5 + buf3[5] = 0x3f + + bl.append(buf1) + bl.append(buf2) + bl.append(buf3) + + const canonical = Buffer.concat([buf1, buf2, buf3]) + t.equal(bl.readDoubleBE(), canonical.readDoubleBE()) + t.equal(bl.readDoubleLE(), canonical.readDoubleLE()) + t.equal(bl.readDoubleBE(2), canonical.readDoubleBE(2)) + t.equal(bl.readDoubleLE(2), canonical.readDoubleLE(2)) + + t.end() +}) + +tape('test toString', function (t) { + const bl = new BufferList() + + bl.append(Buffer.from('abcd')) + bl.append(Buffer.from('efg')) + bl.append(Buffer.from('hi')) + bl.append(Buffer.from('j')) + + t.equal(bl.toString('ascii', 0, 10), 'abcdefghij') + t.equal(bl.toString('ascii', 3, 10), 'defghij') + t.equal(bl.toString('ascii', 3, 6), 'def') + t.equal(bl.toString('ascii', 3, 8), 'defgh') + t.equal(bl.toString('ascii', 5, 10), 'fghij') + + t.end() +}) + +tape('test toString encoding', function (t) { + const bl = new BufferList() + const b = Buffer.from('abcdefghij\xff\x00') + + bl.append(Buffer.from('abcd')) + bl.append(Buffer.from('efg')) + bl.append(Buffer.from('hi')) + bl.append(Buffer.from('j')) + bl.append(Buffer.from('\xff\x00')) + + encodings.forEach(function (enc) { + t.equal(bl.toString(enc), b.toString(enc), enc) + }) + + t.end() +}) + +tape('uninitialized memory', function (t) { + const secret = crypto.randomBytes(256) + for (let i = 0; i < 1e6; i++) { + const clone = Buffer.from(secret) + const bl = new BufferList() + bl.append(Buffer.from('a')) + bl.consume(-1024) + const buf = bl.slice(1) + if (buf.indexOf(clone) !== -1) { + t.fail(`Match (at ${i})`) + break + } + } + t.end() +}) + +!process.browser && tape('test stream', function (t) { + const random = crypto.randomBytes(65534) + + const bl = new BufferList((err, buf) => { + t.ok(Buffer.isBuffer(buf)) + t.ok(err === null) + t.ok(random.equals(bl.slice())) + t.ok(random.equals(buf.slice())) + + bl.pipe(fs.createWriteStream('/tmp/bl_test_rnd_out.dat')) + .on('close', function () { + const rndhash = crypto.createHash('md5').update(random).digest('hex') + const md5sum = crypto.createHash('md5') + const s = fs.createReadStream('/tmp/bl_test_rnd_out.dat') + + s.on('data', md5sum.update.bind(md5sum)) + s.on('end', function () { + t.equal(rndhash, md5sum.digest('hex'), 'woohoo! correct hash!') + t.end() + }) + }) + }) + + fs.writeFileSync('/tmp/bl_test_rnd.dat', random) + fs.createReadStream('/tmp/bl_test_rnd.dat').pipe(bl) +}) + +tape('instantiation with Buffer', function (t) { + const buf = crypto.randomBytes(1024) + const buf2 = crypto.randomBytes(1024) + let b = BufferList(buf) + + t.equal(buf.toString('hex'), b.slice().toString('hex'), 'same buffer') + b = BufferList([buf, buf2]) + t.equal(b.slice().toString('hex'), Buffer.concat([buf, buf2]).toString('hex'), 'same buffer') + + t.end() +}) + +tape('test String appendage', function (t) { + const bl = new BufferList() + const b = Buffer.from('abcdefghij\xff\x00') + + bl.append('abcd') + bl.append('efg') + bl.append('hi') + bl.append('j') + bl.append('\xff\x00') + + encodings.forEach(function (enc) { + t.equal(bl.toString(enc), b.toString(enc)) + }) + + t.end() +}) + +tape('test Number appendage', function (t) { + const bl = new BufferList() + const b = Buffer.from('1234567890') + + bl.append(1234) + bl.append(567) + bl.append(89) + bl.append(0) + + encodings.forEach(function (enc) { + t.equal(bl.toString(enc), b.toString(enc)) + }) + + t.end() +}) + +tape('write nothing, should get empty buffer', function (t) { + t.plan(3) + BufferList(function (err, data) { + t.notOk(err, 'no error') + t.ok(Buffer.isBuffer(data), 'got a buffer') + t.equal(0, data.length, 'got a zero-length buffer') + t.end() + }).end() +}) + +tape('unicode string', function (t) { + t.plan(2) + + const inp1 = '\u2600' + const inp2 = '\u2603' + const exp = inp1 + ' and ' + inp2 + const bl = BufferList() + + bl.write(inp1) + bl.write(' and ') + bl.write(inp2) + t.equal(exp, bl.toString()) + t.equal(Buffer.from(exp).toString('hex'), bl.toString('hex')) +}) + +tape('should emit finish', function (t) { + const source = BufferList() + const dest = BufferList() + + source.write('hello') + source.pipe(dest) + + dest.on('finish', function () { + t.equal(dest.toString('utf8'), 'hello') + t.end() + }) +}) + +tape('basic copy', function (t) { + const buf = crypto.randomBytes(1024) + const buf2 = Buffer.alloc(1024) + const b = BufferList(buf) + + b.copy(buf2) + t.equal(b.slice().toString('hex'), buf2.toString('hex'), 'same buffer') + + t.end() +}) + +tape('copy after many appends', function (t) { + const buf = crypto.randomBytes(512) + const buf2 = Buffer.alloc(1024) + const b = BufferList(buf) + + b.append(buf) + b.copy(buf2) + t.equal(b.slice().toString('hex'), buf2.toString('hex'), 'same buffer') + + t.end() +}) + +tape('copy at a precise position', function (t) { + const buf = crypto.randomBytes(1004) + const buf2 = Buffer.alloc(1024) + const b = BufferList(buf) + + b.copy(buf2, 20) + t.equal(b.slice().toString('hex'), buf2.slice(20).toString('hex'), 'same buffer') + + t.end() +}) + +tape('copy starting from a precise location', function (t) { + const buf = crypto.randomBytes(10) + const buf2 = Buffer.alloc(5) + const b = BufferList(buf) + + b.copy(buf2, 0, 5) + t.equal(b.slice(5).toString('hex'), buf2.toString('hex'), 'same buffer') + + t.end() +}) + +tape('copy in an interval', function (t) { + const rnd = crypto.randomBytes(10) + const b = BufferList(rnd) // put the random bytes there + const actual = Buffer.alloc(3) + const expected = Buffer.alloc(3) + + rnd.copy(expected, 0, 5, 8) + b.copy(actual, 0, 5, 8) + + t.equal(actual.toString('hex'), expected.toString('hex'), 'same buffer') + + t.end() +}) + +tape('copy an interval between two buffers', function (t) { + const buf = crypto.randomBytes(10) + const buf2 = Buffer.alloc(10) + const b = BufferList(buf) + + b.append(buf) + b.copy(buf2, 0, 5, 15) + + t.equal(b.slice(5, 15).toString('hex'), buf2.toString('hex'), 'same buffer') + + t.end() +}) + +tape('shallow slice across buffer boundaries', function (t) { + const bl = new BufferList(['First', 'Second', 'Third']) + + t.equal(bl.shallowSlice(3, 13).toString(), 'stSecondTh') + + t.end() +}) + +tape('shallow slice within single buffer', function (t) { + t.plan(2) + + const bl = new BufferList(['First', 'Second', 'Third']) + + t.equal(bl.shallowSlice(5, 10).toString(), 'Secon') + t.equal(bl.shallowSlice(7, 10).toString(), 'con') + + t.end() +}) + +tape('shallow slice single buffer', function (t) { + t.plan(3) + + const bl = new BufferList(['First', 'Second', 'Third']) + + t.equal(bl.shallowSlice(0, 5).toString(), 'First') + t.equal(bl.shallowSlice(5, 11).toString(), 'Second') + t.equal(bl.shallowSlice(11, 16).toString(), 'Third') +}) + +tape('shallow slice with negative or omitted indices', function (t) { + t.plan(4) + + const bl = new BufferList(['First', 'Second', 'Third']) + + t.equal(bl.shallowSlice().toString(), 'FirstSecondThird') + t.equal(bl.shallowSlice(5).toString(), 'SecondThird') + t.equal(bl.shallowSlice(5, -3).toString(), 'SecondTh') + t.equal(bl.shallowSlice(-8).toString(), 'ondThird') +}) + +tape('shallow slice does not make a copy', function (t) { + t.plan(1) + + const buffers = [Buffer.from('First'), Buffer.from('Second'), Buffer.from('Third')] + const bl = (new BufferList(buffers)).shallowSlice(5, -3) + + buffers[1].fill('h') + buffers[2].fill('h') + + t.equal(bl.toString(), 'hhhhhhhh') +}) + +tape('shallow slice with 0 length', function (t) { + t.plan(1) + + const buffers = [Buffer.from('First'), Buffer.from('Second'), Buffer.from('Third')] + const bl = (new BufferList(buffers)).shallowSlice(0, 0) + + t.equal(bl.length, 0) +}) + +tape('shallow slice with 0 length from middle', function (t) { + t.plan(1) + + const buffers = [Buffer.from('First'), Buffer.from('Second'), Buffer.from('Third')] + const bl = (new BufferList(buffers)).shallowSlice(10, 10) + + t.equal(bl.length, 0) +}) + +tape('duplicate', function (t) { + t.plan(2) + + const bl = new BufferList('abcdefghij\xff\x00') + const dup = bl.duplicate() + + t.equal(bl.prototype, dup.prototype) + t.equal(bl.toString('hex'), dup.toString('hex')) +}) + +tape('destroy no pipe', function (t) { + t.plan(2) + + const bl = new BufferList('alsdkfja;lsdkfja;lsdk') + + bl.destroy() + + t.equal(bl._bufs.length, 0) + t.equal(bl.length, 0) +}) + +tape('destroy with error', function (t) { + t.plan(3) + + const bl = new BufferList('alsdkfja;lsdkfja;lsdk') + const err = new Error('kaboom') + + bl.destroy(err) + bl.on('error', function (_err) { + t.equal(_err, err) + }) + + t.equal(bl._bufs.length, 0) + t.equal(bl.length, 0) +}) + +!process.browser && tape('destroy with pipe before read end', function (t) { + t.plan(2) + + const bl = new BufferList() + fs.createReadStream(path.join(__dirname, '/test.js')) + .pipe(bl) + + bl.destroy() + + t.equal(bl._bufs.length, 0) + t.equal(bl.length, 0) +}) + +!process.browser && tape('destroy with pipe before read end with race', function (t) { + t.plan(2) + + const bl = new BufferList() + + fs.createReadStream(path.join(__dirname, '/test.js')) + .pipe(bl) + + setTimeout(function () { + bl.destroy() + setTimeout(function () { + t.equal(bl._bufs.length, 0) + t.equal(bl.length, 0) + }, 500) + }, 500) +}) + +!process.browser && tape('destroy with pipe after read end', function (t) { + t.plan(2) + + const bl = new BufferList() + + fs.createReadStream(path.join(__dirname, '/test.js')) + .on('end', onEnd) + .pipe(bl) + + function onEnd () { + bl.destroy() + + t.equal(bl._bufs.length, 0) + t.equal(bl.length, 0) + } +}) + +!process.browser && tape('destroy with pipe while writing to a destination', function (t) { + t.plan(4) + + const bl = new BufferList() + const ds = new BufferList() + + fs.createReadStream(path.join(__dirname, '/test.js')) + .on('end', onEnd) + .pipe(bl) + + function onEnd () { + bl.pipe(ds) + + setTimeout(function () { + bl.destroy() + + t.equals(bl._bufs.length, 0) + t.equals(bl.length, 0) + + ds.destroy() + + t.equals(bl._bufs.length, 0) + t.equals(bl.length, 0) + }, 100) + } +}) + +!process.browser && tape('handle error', function (t) { + t.plan(2) + + fs.createReadStream('/does/not/exist').pipe(BufferList(function (err, data) { + t.ok(err instanceof Error, 'has error') + t.notOk(data, 'no data') + })) +}) diff --git a/backend/node_modules/color-string/LICENSE b/backend/node_modules/color-string/LICENSE new file mode 100644 index 00000000..a8b08d4f --- /dev/null +++ b/backend/node_modules/color-string/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2011 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/backend/node_modules/color-string/README.md b/backend/node_modules/color-string/README.md new file mode 100644 index 00000000..e58670c6 --- /dev/null +++ b/backend/node_modules/color-string/README.md @@ -0,0 +1,62 @@ +# color-string + +> library for parsing and generating CSS color strings. + +## Install + +With [npm](http://npmjs.org/): + +```console +$ npm install color-string +``` + +## Usage + +### Parsing + +```js +colorString.get('#FFF') // {model: 'rgb', value: [255, 255, 255, 1]} +colorString.get('#FFFA') // {model: 'rgb', value: [255, 255, 255, 0.67]} +colorString.get('#FFFFFFAA') // {model: 'rgb', value: [255, 255, 255, 0.67]} +colorString.get('hsl(360, 100%, 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} +colorString.get('hsl(360 100% 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} +colorString.get('hwb(60, 3%, 60%)') // {model: 'hwb', value: [60, 3, 60, 1]} + +colorString.get.rgb('#FFF') // [255, 255, 255, 1] +colorString.get.rgb('blue') // [0, 0, 255, 1] +colorString.get.rgb('rgba(200, 60, 60, 0.3)') // [200, 60, 60, 0.3] +colorString.get.rgb('rgba(200 60 60 / 0.3)') // [200, 60, 60, 0.3] +colorString.get.rgb('rgba(200 60 60 / 30%)') // [200, 60, 60, 0.3] +colorString.get.rgb('rgb(200, 200, 200)') // [200, 200, 200, 1] +colorString.get.rgb('rgb(200 200 200)') // [200, 200, 200, 1] + +colorString.get.hsl('hsl(360, 100%, 50%)') // [0, 100, 50, 1] +colorString.get.hsl('hsl(360 100% 50%)') // [0, 100, 50, 1] +colorString.get.hsl('hsla(360, 60%, 50%, 0.4)') // [0, 60, 50, 0.4] +colorString.get.hsl('hsl(360 60% 50% / 0.4)') // [0, 60, 50, 0.4] + +colorString.get.hwb('hwb(60, 3%, 60%)') // [60, 3, 60, 1] +colorString.get.hwb('hwb(60, 3%, 60%, 0.6)') // [60, 3, 60, 0.6] + +colorString.get.rgb('invalid color string') // null +``` + +### Generation + +```js +colorString.to.hex([255, 255, 255]) // "#FFFFFF" +colorString.to.hex([0, 0, 255, 0.4]) // "#0000FF66" +colorString.to.hex([0, 0, 255], 0.4) // "#0000FF66" +colorString.to.rgb([255, 255, 255]) // "rgb(255, 255, 255)" +colorString.to.rgb([0, 0, 255, 0.4]) // "rgba(0, 0, 255, 0.4)" +colorString.to.rgb([0, 0, 255], 0.4) // "rgba(0, 0, 255, 0.4)" +colorString.to.rgb.percent([0, 0, 255]) // "rgb(0%, 0%, 100%)" +colorString.to.keyword([255, 255, 0]) // "yellow" +colorString.to.hsl([360, 100, 100]) // "hsl(360, 100%, 100%)" +colorString.to.hwb([50, 3, 15]) // "hwb(50, 3%, 15%)" + +// all functions also support swizzling +colorString.to.rgb(0, [0, 255], 0.4) // "rgba(0, 0, 255, 0.4)" +colorString.to.rgb([0, 0], [255], 0.4) // "rgba(0, 0, 255, 0.4)" +colorString.to.rgb([0], 0, [255, 0.4]) // "rgba(0, 0, 255, 0.4)" +``` diff --git a/backend/node_modules/color-string/index.js b/backend/node_modules/color-string/index.js new file mode 100644 index 00000000..dd5d2b7b --- /dev/null +++ b/backend/node_modules/color-string/index.js @@ -0,0 +1,242 @@ +/* MIT license */ +var colorNames = require('color-name'); +var swizzle = require('simple-swizzle'); +var hasOwnProperty = Object.hasOwnProperty; + +var reverseNames = Object.create(null); + +// create a list of reverse color names +for (var name in colorNames) { + if (hasOwnProperty.call(colorNames, name)) { + reverseNames[colorNames[name]] = name; + } +} + +var cs = module.exports = { + to: {}, + get: {} +}; + +cs.get = function (string) { + var prefix = string.substring(0, 3).toLowerCase(); + var val; + var model; + switch (prefix) { + case 'hsl': + val = cs.get.hsl(string); + model = 'hsl'; + break; + case 'hwb': + val = cs.get.hwb(string); + model = 'hwb'; + break; + default: + val = cs.get.rgb(string); + model = 'rgb'; + break; + } + + if (!val) { + return null; + } + + return {model: model, value: val}; +}; + +cs.get.rgb = function (string) { + if (!string) { + return null; + } + + var abbr = /^#([a-f0-9]{3,4})$/i; + var hex = /^#([a-f0-9]{6})([a-f0-9]{2})?$/i; + var rgba = /^rgba?\(\s*([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)\s*(?:[,|\/]\s*([+-]?[\d\.]+)(%?)\s*)?\)$/; + var per = /^rgba?\(\s*([+-]?[\d\.]+)\%\s*,?\s*([+-]?[\d\.]+)\%\s*,?\s*([+-]?[\d\.]+)\%\s*(?:[,|\/]\s*([+-]?[\d\.]+)(%?)\s*)?\)$/; + var keyword = /^(\w+)$/; + + var rgb = [0, 0, 0, 1]; + var match; + var i; + var hexAlpha; + + if (match = string.match(hex)) { + hexAlpha = match[2]; + match = match[1]; + + for (i = 0; i < 3; i++) { + // https://jsperf.com/slice-vs-substr-vs-substring-methods-long-string/19 + var i2 = i * 2; + rgb[i] = parseInt(match.slice(i2, i2 + 2), 16); + } + + if (hexAlpha) { + rgb[3] = parseInt(hexAlpha, 16) / 255; + } + } else if (match = string.match(abbr)) { + match = match[1]; + hexAlpha = match[3]; + + for (i = 0; i < 3; i++) { + rgb[i] = parseInt(match[i] + match[i], 16); + } + + if (hexAlpha) { + rgb[3] = parseInt(hexAlpha + hexAlpha, 16) / 255; + } + } else if (match = string.match(rgba)) { + for (i = 0; i < 3; i++) { + rgb[i] = parseInt(match[i + 1], 0); + } + + if (match[4]) { + if (match[5]) { + rgb[3] = parseFloat(match[4]) * 0.01; + } else { + rgb[3] = parseFloat(match[4]); + } + } + } else if (match = string.match(per)) { + for (i = 0; i < 3; i++) { + rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55); + } + + if (match[4]) { + if (match[5]) { + rgb[3] = parseFloat(match[4]) * 0.01; + } else { + rgb[3] = parseFloat(match[4]); + } + } + } else if (match = string.match(keyword)) { + if (match[1] === 'transparent') { + return [0, 0, 0, 0]; + } + + if (!hasOwnProperty.call(colorNames, match[1])) { + return null; + } + + rgb = colorNames[match[1]]; + rgb[3] = 1; + + return rgb; + } else { + return null; + } + + for (i = 0; i < 3; i++) { + rgb[i] = clamp(rgb[i], 0, 255); + } + rgb[3] = clamp(rgb[3], 0, 1); + + return rgb; +}; + +cs.get.hsl = function (string) { + if (!string) { + return null; + } + + var hsl = /^hsla?\(\s*([+-]?(?:\d{0,3}\.)?\d+)(?:deg)?\s*,?\s*([+-]?[\d\.]+)%\s*,?\s*([+-]?[\d\.]+)%\s*(?:[,|\/]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; + var match = string.match(hsl); + + if (match) { + var alpha = parseFloat(match[4]); + var h = ((parseFloat(match[1]) % 360) + 360) % 360; + var s = clamp(parseFloat(match[2]), 0, 100); + var l = clamp(parseFloat(match[3]), 0, 100); + var a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1); + + return [h, s, l, a]; + } + + return null; +}; + +cs.get.hwb = function (string) { + if (!string) { + return null; + } + + var hwb = /^hwb\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; + var match = string.match(hwb); + + if (match) { + var alpha = parseFloat(match[4]); + var h = ((parseFloat(match[1]) % 360) + 360) % 360; + var w = clamp(parseFloat(match[2]), 0, 100); + var b = clamp(parseFloat(match[3]), 0, 100); + var a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1); + return [h, w, b, a]; + } + + return null; +}; + +cs.to.hex = function () { + var rgba = swizzle(arguments); + + return ( + '#' + + hexDouble(rgba[0]) + + hexDouble(rgba[1]) + + hexDouble(rgba[2]) + + (rgba[3] < 1 + ? (hexDouble(Math.round(rgba[3] * 255))) + : '') + ); +}; + +cs.to.rgb = function () { + var rgba = swizzle(arguments); + + return rgba.length < 4 || rgba[3] === 1 + ? 'rgb(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ')' + : 'rgba(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ', ' + rgba[3] + ')'; +}; + +cs.to.rgb.percent = function () { + var rgba = swizzle(arguments); + + var r = Math.round(rgba[0] / 255 * 100); + var g = Math.round(rgba[1] / 255 * 100); + var b = Math.round(rgba[2] / 255 * 100); + + return rgba.length < 4 || rgba[3] === 1 + ? 'rgb(' + r + '%, ' + g + '%, ' + b + '%)' + : 'rgba(' + r + '%, ' + g + '%, ' + b + '%, ' + rgba[3] + ')'; +}; + +cs.to.hsl = function () { + var hsla = swizzle(arguments); + return hsla.length < 4 || hsla[3] === 1 + ? 'hsl(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%)' + : 'hsla(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%, ' + hsla[3] + ')'; +}; + +// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax +// (hwb have alpha optional & 1 is default value) +cs.to.hwb = function () { + var hwba = swizzle(arguments); + + var a = ''; + if (hwba.length >= 4 && hwba[3] !== 1) { + a = ', ' + hwba[3]; + } + + return 'hwb(' + hwba[0] + ', ' + hwba[1] + '%, ' + hwba[2] + '%' + a + ')'; +}; + +cs.to.keyword = function (rgb) { + return reverseNames[rgb.slice(0, 3)]; +}; + +// helpers +function clamp(num, min, max) { + return Math.min(Math.max(min, num), max); +} + +function hexDouble(num) { + var str = Math.round(num).toString(16).toUpperCase(); + return (str.length < 2) ? '0' + str : str; +} diff --git a/backend/node_modules/color-string/package.json b/backend/node_modules/color-string/package.json new file mode 100644 index 00000000..f34ee980 --- /dev/null +++ b/backend/node_modules/color-string/package.json @@ -0,0 +1,39 @@ +{ + "name": "color-string", + "description": "Parser and generator for CSS color strings", + "version": "1.9.1", + "author": "Heather Arthur ", + "contributors": [ + "Maxime Thirouin", + "Dyma Ywanov ", + "Josh Junon" + ], + "repository": "Qix-/color-string", + "scripts": { + "pretest": "xo", + "test": "node test/basic.js" + }, + "license": "MIT", + "files": [ + "index.js" + ], + "xo": { + "rules": { + "no-cond-assign": 0, + "operator-linebreak": 0 + } + }, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + }, + "devDependencies": { + "xo": "^0.12.1" + }, + "keywords": [ + "color", + "colour", + "rgb", + "css" + ] +} diff --git a/backend/node_modules/color/LICENSE b/backend/node_modules/color/LICENSE new file mode 100644 index 00000000..68c864ee --- /dev/null +++ b/backend/node_modules/color/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2012 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/backend/node_modules/color/README.md b/backend/node_modules/color/README.md new file mode 100644 index 00000000..674a7318 --- /dev/null +++ b/backend/node_modules/color/README.md @@ -0,0 +1,123 @@ +# color + +> JavaScript library for immutable color conversion and manipulation with support for CSS color strings. + +```js +const color = Color('#7743CE').alpha(0.5).lighten(0.5); +console.log(color.hsl().string()); // 'hsla(262, 59%, 81%, 0.5)' + +console.log(color.cmyk().round().array()); // [ 16, 25, 0, 8, 0.5 ] + +console.log(color.ansi256().object()); // { ansi256: 183, alpha: 0.5 } +``` + +## Install +```console +$ npm install color +``` + +## Usage +```js +const Color = require('color'); +``` + +### Constructors +```js +const color = Color('rgb(255, 255, 255)') +const color = Color({r: 255, g: 255, b: 255}) +const color = Color.rgb(255, 255, 255) +const color = Color.rgb([255, 255, 255]) +``` + +Set the values for individual channels with `alpha`, `red`, `green`, `blue`, `hue`, `saturationl` (hsl), `saturationv` (hsv), `lightness`, `whiteness`, `blackness`, `cyan`, `magenta`, `yellow`, `black` + +String constructors are handled by [color-string](https://www.npmjs.com/package/color-string) + +### Getters +```js +color.hsl(); +``` +Convert a color to a different space (`hsl()`, `cmyk()`, etc.). + +```js +color.object(); // {r: 255, g: 255, b: 255} +``` +Get a hash of the color value. Reflects the color's current model (see above). + +```js +color.rgb().array() // [255, 255, 255] +``` +Get an array of the values with `array()`. Reflects the color's current model (see above). + +```js +color.rgbNumber() // 16777215 (0xffffff) +``` +Get the rgb number value. + +```js +color.hex() // #ffffff +``` +Get the hex value. (**NOTE:** `.hex()` does not return alpha values; use `.hexa()` for an RGBA representation) + +```js +color.red() // 255 +``` +Get the value for an individual channel. + +### CSS Strings +```js +color.hsl().string() // 'hsl(320, 50%, 100%)' +``` + +Calling `.string()` with a number rounds the numbers to that decimal place. It defaults to 1. + +### Luminosity +```js +color.luminosity(); // 0.412 +``` +The [WCAG luminosity](http://www.w3.org/TR/WCAG20/#relativeluminancedef) of the color. 0 is black, 1 is white. + +```js +color.contrast(Color("blue")) // 12 +``` +The [WCAG contrast ratio](http://www.w3.org/TR/WCAG20/#contrast-ratiodef) to another color, from 1 (same color) to 21 (contrast b/w white and black). + +```js +color.isLight(); // true +color.isDark(); // false +``` +Get whether the color is "light" or "dark", useful for deciding text color. + +### Manipulation +```js +color.negate() // rgb(0, 100, 255) -> rgb(255, 155, 0) + +color.lighten(0.5) // hsl(100, 50%, 50%) -> hsl(100, 50%, 75%) +color.lighten(0.5) // hsl(100, 50%, 0) -> hsl(100, 50%, 0) +color.darken(0.5) // hsl(100, 50%, 50%) -> hsl(100, 50%, 25%) +color.darken(0.5) // hsl(100, 50%, 0) -> hsl(100, 50%, 0) + +color.lightness(50) // hsl(100, 50%, 10%) -> hsl(100, 50%, 50%) + +color.saturate(0.5) // hsl(100, 50%, 50%) -> hsl(100, 75%, 50%) +color.desaturate(0.5) // hsl(100, 50%, 50%) -> hsl(100, 25%, 50%) +color.grayscale() // #5CBF54 -> #969696 + +color.whiten(0.5) // hwb(100, 50%, 50%) -> hwb(100, 75%, 50%) +color.blacken(0.5) // hwb(100, 50%, 50%) -> hwb(100, 50%, 75%) + +color.fade(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 0.4) +color.opaquer(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 1.0) + +color.rotate(180) // hsl(60, 20%, 20%) -> hsl(240, 20%, 20%) +color.rotate(-90) // hsl(60, 20%, 20%) -> hsl(330, 20%, 20%) + +color.mix(Color("yellow")) // cyan -> rgb(128, 255, 128) +color.mix(Color("yellow"), 0.3) // cyan -> rgb(77, 255, 179) + +// chaining +color.green(100).grayscale().lighten(0.6) +``` + +## Propers +The API was inspired by [color-js](https://github.com/brehaut/color-js). Manipulation functions by CSS tools like Sass, LESS, and Stylus. diff --git a/backend/node_modules/color/index.js b/backend/node_modules/color/index.js new file mode 100644 index 00000000..ddb0b5df --- /dev/null +++ b/backend/node_modules/color/index.js @@ -0,0 +1,496 @@ +const colorString = require('color-string'); +const convert = require('color-convert'); + +const skippedModels = [ + // To be honest, I don't really feel like keyword belongs in color convert, but eh. + 'keyword', + + // Gray conflicts with some method names, and has its own method defined. + 'gray', + + // Shouldn't really be in color-convert either... + 'hex', +]; + +const hashedModelKeys = {}; +for (const model of Object.keys(convert)) { + hashedModelKeys[[...convert[model].labels].sort().join('')] = model; +} + +const limiters = {}; + +function Color(object, model) { + if (!(this instanceof Color)) { + return new Color(object, model); + } + + if (model && model in skippedModels) { + model = null; + } + + if (model && !(model in convert)) { + throw new Error('Unknown model: ' + model); + } + + let i; + let channels; + + if (object == null) { // eslint-disable-line no-eq-null,eqeqeq + this.model = 'rgb'; + this.color = [0, 0, 0]; + this.valpha = 1; + } else if (object instanceof Color) { + this.model = object.model; + this.color = [...object.color]; + this.valpha = object.valpha; + } else if (typeof object === 'string') { + const result = colorString.get(object); + if (result === null) { + throw new Error('Unable to parse color from string: ' + object); + } + + this.model = result.model; + channels = convert[this.model].channels; + this.color = result.value.slice(0, channels); + this.valpha = typeof result.value[channels] === 'number' ? result.value[channels] : 1; + } else if (object.length > 0) { + this.model = model || 'rgb'; + channels = convert[this.model].channels; + const newArray = Array.prototype.slice.call(object, 0, channels); + this.color = zeroArray(newArray, channels); + this.valpha = typeof object[channels] === 'number' ? object[channels] : 1; + } else if (typeof object === 'number') { + // This is always RGB - can be converted later on. + this.model = 'rgb'; + this.color = [ + (object >> 16) & 0xFF, + (object >> 8) & 0xFF, + object & 0xFF, + ]; + this.valpha = 1; + } else { + this.valpha = 1; + + const keys = Object.keys(object); + if ('alpha' in object) { + keys.splice(keys.indexOf('alpha'), 1); + this.valpha = typeof object.alpha === 'number' ? object.alpha : 0; + } + + const hashedKeys = keys.sort().join(''); + if (!(hashedKeys in hashedModelKeys)) { + throw new Error('Unable to parse color from object: ' + JSON.stringify(object)); + } + + this.model = hashedModelKeys[hashedKeys]; + + const {labels} = convert[this.model]; + const color = []; + for (i = 0; i < labels.length; i++) { + color.push(object[labels[i]]); + } + + this.color = zeroArray(color); + } + + // Perform limitations (clamping, etc.) + if (limiters[this.model]) { + channels = convert[this.model].channels; + for (i = 0; i < channels; i++) { + const limit = limiters[this.model][i]; + if (limit) { + this.color[i] = limit(this.color[i]); + } + } + } + + this.valpha = Math.max(0, Math.min(1, this.valpha)); + + if (Object.freeze) { + Object.freeze(this); + } +} + +Color.prototype = { + toString() { + return this.string(); + }, + + toJSON() { + return this[this.model](); + }, + + string(places) { + let self = this.model in colorString.to ? this : this.rgb(); + self = self.round(typeof places === 'number' ? places : 1); + const args = self.valpha === 1 ? self.color : [...self.color, this.valpha]; + return colorString.to[self.model](args); + }, + + percentString(places) { + const self = this.rgb().round(typeof places === 'number' ? places : 1); + const args = self.valpha === 1 ? self.color : [...self.color, this.valpha]; + return colorString.to.rgb.percent(args); + }, + + array() { + return this.valpha === 1 ? [...this.color] : [...this.color, this.valpha]; + }, + + object() { + const result = {}; + const {channels} = convert[this.model]; + const {labels} = convert[this.model]; + + for (let i = 0; i < channels; i++) { + result[labels[i]] = this.color[i]; + } + + if (this.valpha !== 1) { + result.alpha = this.valpha; + } + + return result; + }, + + unitArray() { + const rgb = this.rgb().color; + rgb[0] /= 255; + rgb[1] /= 255; + rgb[2] /= 255; + + if (this.valpha !== 1) { + rgb.push(this.valpha); + } + + return rgb; + }, + + unitObject() { + const rgb = this.rgb().object(); + rgb.r /= 255; + rgb.g /= 255; + rgb.b /= 255; + + if (this.valpha !== 1) { + rgb.alpha = this.valpha; + } + + return rgb; + }, + + round(places) { + places = Math.max(places || 0, 0); + return new Color([...this.color.map(roundToPlace(places)), this.valpha], this.model); + }, + + alpha(value) { + if (value !== undefined) { + return new Color([...this.color, Math.max(0, Math.min(1, value))], this.model); + } + + return this.valpha; + }, + + // Rgb + red: getset('rgb', 0, maxfn(255)), + green: getset('rgb', 1, maxfn(255)), + blue: getset('rgb', 2, maxfn(255)), + + hue: getset(['hsl', 'hsv', 'hsl', 'hwb', 'hcg'], 0, value => ((value % 360) + 360) % 360), + + saturationl: getset('hsl', 1, maxfn(100)), + lightness: getset('hsl', 2, maxfn(100)), + + saturationv: getset('hsv', 1, maxfn(100)), + value: getset('hsv', 2, maxfn(100)), + + chroma: getset('hcg', 1, maxfn(100)), + gray: getset('hcg', 2, maxfn(100)), + + white: getset('hwb', 1, maxfn(100)), + wblack: getset('hwb', 2, maxfn(100)), + + cyan: getset('cmyk', 0, maxfn(100)), + magenta: getset('cmyk', 1, maxfn(100)), + yellow: getset('cmyk', 2, maxfn(100)), + black: getset('cmyk', 3, maxfn(100)), + + x: getset('xyz', 0, maxfn(95.047)), + y: getset('xyz', 1, maxfn(100)), + z: getset('xyz', 2, maxfn(108.833)), + + l: getset('lab', 0, maxfn(100)), + a: getset('lab', 1), + b: getset('lab', 2), + + keyword(value) { + if (value !== undefined) { + return new Color(value); + } + + return convert[this.model].keyword(this.color); + }, + + hex(value) { + if (value !== undefined) { + return new Color(value); + } + + return colorString.to.hex(this.rgb().round().color); + }, + + hexa(value) { + if (value !== undefined) { + return new Color(value); + } + + const rgbArray = this.rgb().round().color; + + let alphaHex = Math.round(this.valpha * 255).toString(16).toUpperCase(); + if (alphaHex.length === 1) { + alphaHex = '0' + alphaHex; + } + + return colorString.to.hex(rgbArray) + alphaHex; + }, + + rgbNumber() { + const rgb = this.rgb().color; + return ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | (rgb[2] & 0xFF); + }, + + luminosity() { + // http://www.w3.org/TR/WCAG20/#relativeluminancedef + const rgb = this.rgb().color; + + const lum = []; + for (const [i, element] of rgb.entries()) { + const chan = element / 255; + lum[i] = (chan <= 0.04045) ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4; + } + + return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; + }, + + contrast(color2) { + // http://www.w3.org/TR/WCAG20/#contrast-ratiodef + const lum1 = this.luminosity(); + const lum2 = color2.luminosity(); + + if (lum1 > lum2) { + return (lum1 + 0.05) / (lum2 + 0.05); + } + + return (lum2 + 0.05) / (lum1 + 0.05); + }, + + level(color2) { + // https://www.w3.org/TR/WCAG/#contrast-enhanced + const contrastRatio = this.contrast(color2); + if (contrastRatio >= 7) { + return 'AAA'; + } + + return (contrastRatio >= 4.5) ? 'AA' : ''; + }, + + isDark() { + // YIQ equation from http://24ways.org/2010/calculating-color-contrast + const rgb = this.rgb().color; + const yiq = (rgb[0] * 2126 + rgb[1] * 7152 + rgb[2] * 722) / 10000; + return yiq < 128; + }, + + isLight() { + return !this.isDark(); + }, + + negate() { + const rgb = this.rgb(); + for (let i = 0; i < 3; i++) { + rgb.color[i] = 255 - rgb.color[i]; + } + + return rgb; + }, + + lighten(ratio) { + const hsl = this.hsl(); + hsl.color[2] += hsl.color[2] * ratio; + return hsl; + }, + + darken(ratio) { + const hsl = this.hsl(); + hsl.color[2] -= hsl.color[2] * ratio; + return hsl; + }, + + saturate(ratio) { + const hsl = this.hsl(); + hsl.color[1] += hsl.color[1] * ratio; + return hsl; + }, + + desaturate(ratio) { + const hsl = this.hsl(); + hsl.color[1] -= hsl.color[1] * ratio; + return hsl; + }, + + whiten(ratio) { + const hwb = this.hwb(); + hwb.color[1] += hwb.color[1] * ratio; + return hwb; + }, + + blacken(ratio) { + const hwb = this.hwb(); + hwb.color[2] += hwb.color[2] * ratio; + return hwb; + }, + + grayscale() { + // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale + const rgb = this.rgb().color; + const value = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; + return Color.rgb(value, value, value); + }, + + fade(ratio) { + return this.alpha(this.valpha - (this.valpha * ratio)); + }, + + opaquer(ratio) { + return this.alpha(this.valpha + (this.valpha * ratio)); + }, + + rotate(degrees) { + const hsl = this.hsl(); + let hue = hsl.color[0]; + hue = (hue + degrees) % 360; + hue = hue < 0 ? 360 + hue : hue; + hsl.color[0] = hue; + return hsl; + }, + + mix(mixinColor, weight) { + // Ported from sass implementation in C + // https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209 + if (!mixinColor || !mixinColor.rgb) { + throw new Error('Argument to "mix" was not a Color instance, but rather an instance of ' + typeof mixinColor); + } + + const color1 = mixinColor.rgb(); + const color2 = this.rgb(); + const p = weight === undefined ? 0.5 : weight; + + const w = 2 * p - 1; + const a = color1.alpha() - color2.alpha(); + + const w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2; + const w2 = 1 - w1; + + return Color.rgb( + w1 * color1.red() + w2 * color2.red(), + w1 * color1.green() + w2 * color2.green(), + w1 * color1.blue() + w2 * color2.blue(), + color1.alpha() * p + color2.alpha() * (1 - p)); + }, +}; + +// Model conversion methods and static constructors +for (const model of Object.keys(convert)) { + if (skippedModels.includes(model)) { + continue; + } + + const {channels} = convert[model]; + + // Conversion methods + Color.prototype[model] = function (...args) { + if (this.model === model) { + return new Color(this); + } + + if (args.length > 0) { + return new Color(args, model); + } + + return new Color([...assertArray(convert[this.model][model].raw(this.color)), this.valpha], model); + }; + + // 'static' construction methods + Color[model] = function (...args) { + let color = args[0]; + if (typeof color === 'number') { + color = zeroArray(args, channels); + } + + return new Color(color, model); + }; +} + +function roundTo(number, places) { + return Number(number.toFixed(places)); +} + +function roundToPlace(places) { + return function (number) { + return roundTo(number, places); + }; +} + +function getset(model, channel, modifier) { + model = Array.isArray(model) ? model : [model]; + + for (const m of model) { + (limiters[m] || (limiters[m] = []))[channel] = modifier; + } + + model = model[0]; + + return function (value) { + let result; + + if (value !== undefined) { + if (modifier) { + value = modifier(value); + } + + result = this[model](); + result.color[channel] = value; + return result; + } + + result = this[model]().color[channel]; + if (modifier) { + result = modifier(result); + } + + return result; + }; +} + +function maxfn(max) { + return function (v) { + return Math.max(0, Math.min(max, v)); + }; +} + +function assertArray(value) { + return Array.isArray(value) ? value : [value]; +} + +function zeroArray(array, length) { + for (let i = 0; i < length; i++) { + if (typeof array[i] !== 'number') { + array[i] = 0; + } + } + + return array; +} + +module.exports = Color; diff --git a/backend/node_modules/color/package.json b/backend/node_modules/color/package.json new file mode 100644 index 00000000..4cdb6e31 --- /dev/null +++ b/backend/node_modules/color/package.json @@ -0,0 +1,47 @@ +{ + "name": "color", + "version": "4.2.3", + "description": "Color conversion and manipulation with CSS string support", + "sideEffects": false, + "keywords": [ + "color", + "colour", + "css" + ], + "authors": [ + "Josh Junon ", + "Heather Arthur ", + "Maxime Thirouin" + ], + "license": "MIT", + "repository": "Qix-/color", + "xo": { + "rules": { + "no-cond-assign": 0, + "new-cap": 0, + "unicorn/prefer-module": 0, + "no-mixed-operators": 0, + "complexity": 0, + "unicorn/numeric-separators-style": 0 + } + }, + "files": [ + "LICENSE", + "index.js" + ], + "scripts": { + "pretest": "xo", + "test": "mocha" + }, + "engines": { + "node": ">=12.5.0" + }, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "devDependencies": { + "mocha": "9.0.2", + "xo": "0.42.0" + } +} diff --git a/backend/node_modules/decompress-response/index.d.ts b/backend/node_modules/decompress-response/index.d.ts new file mode 100644 index 00000000..c0be175f --- /dev/null +++ b/backend/node_modules/decompress-response/index.d.ts @@ -0,0 +1,22 @@ +/// +import {IncomingMessage} from 'http'; + +/** +Decompress a HTTP response if needed. + +@param response - The HTTP incoming stream with compressed data. +@returns The decompressed HTTP response stream. + +@example +``` +import {http} from 'http'; +import decompressResponse = require('decompress-response'); + +http.get('https://sindresorhus.com', response => { + response = decompressResponse(response); +}); +``` +*/ +declare function decompressResponse(response: IncomingMessage): IncomingMessage; + +export = decompressResponse; diff --git a/backend/node_modules/decompress-response/index.js b/backend/node_modules/decompress-response/index.js new file mode 100644 index 00000000..c8610362 --- /dev/null +++ b/backend/node_modules/decompress-response/index.js @@ -0,0 +1,58 @@ +'use strict'; +const {Transform, PassThrough} = require('stream'); +const zlib = require('zlib'); +const mimicResponse = require('mimic-response'); + +module.exports = response => { + const contentEncoding = (response.headers['content-encoding'] || '').toLowerCase(); + + if (!['gzip', 'deflate', 'br'].includes(contentEncoding)) { + return response; + } + + // TODO: Remove this when targeting Node.js 12. + const isBrotli = contentEncoding === 'br'; + if (isBrotli && typeof zlib.createBrotliDecompress !== 'function') { + response.destroy(new Error('Brotli is not supported on Node.js < 12')); + return response; + } + + let isEmpty = true; + + const checker = new Transform({ + transform(data, _encoding, callback) { + isEmpty = false; + + callback(null, data); + }, + + flush(callback) { + callback(); + } + }); + + const finalStream = new PassThrough({ + autoDestroy: false, + destroy(error, callback) { + response.destroy(); + + callback(error); + } + }); + + const decompressStream = isBrotli ? zlib.createBrotliDecompress() : zlib.createUnzip(); + + decompressStream.once('error', error => { + if (isEmpty && !response.readable) { + finalStream.end(); + return; + } + + finalStream.destroy(error); + }); + + mimicResponse(response, finalStream); + response.pipe(checker).pipe(decompressStream).pipe(finalStream); + + return finalStream; +}; diff --git a/backend/node_modules/decompress-response/license b/backend/node_modules/decompress-response/license new file mode 100644 index 00000000..fa7ceba3 --- /dev/null +++ b/backend/node_modules/decompress-response/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/decompress-response/package.json b/backend/node_modules/decompress-response/package.json new file mode 100644 index 00000000..57df8896 --- /dev/null +++ b/backend/node_modules/decompress-response/package.json @@ -0,0 +1,56 @@ +{ + "name": "decompress-response", + "version": "6.0.0", + "description": "Decompress a HTTP response if needed", + "license": "MIT", + "repository": "sindresorhus/decompress-response", + "funding": "https://github.com/sponsors/sindresorhus", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "https://sindresorhus.com" + }, + "engines": { + "node": ">=10" + }, + "scripts": { + "test": "xo && ava && tsd" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "decompress", + "response", + "http", + "https", + "zlib", + "gzip", + "zip", + "deflate", + "unzip", + "ungzip", + "incoming", + "message", + "stream", + "compressed", + "brotli" + ], + "dependencies": { + "mimic-response": "^3.1.0" + }, + "devDependencies": { + "@types/node": "^14.0.1", + "ava": "^2.2.0", + "get-stream": "^5.0.0", + "pify": "^5.0.0", + "tsd": "^0.11.0", + "xo": "^0.30.0" + }, + "xo": { + "rules": { + "@typescript-eslint/prefer-readonly-parameter-types": "off" + } + } +} diff --git a/backend/node_modules/decompress-response/readme.md b/backend/node_modules/decompress-response/readme.md new file mode 100644 index 00000000..58523ef3 --- /dev/null +++ b/backend/node_modules/decompress-response/readme.md @@ -0,0 +1,48 @@ +# decompress-response [![Build Status](https://travis-ci.com/sindresorhus/decompress-response.svg?branch=master)](https://travis-ci.com/sindresorhus/decompress-response) + +> Decompress a HTTP response if needed + +Decompresses the [response](https://nodejs.org/api/http.html#http_class_http_incomingmessage) from [`http.request`](https://nodejs.org/api/http.html#http_http_request_options_callback) if it's gzipped, deflated or compressed with Brotli, otherwise just passes it through. + +Used by [`got`](https://github.com/sindresorhus/got). + +## Install + +``` +$ npm install decompress-response +``` + +## Usage + +```js +const http = require('http'); +const decompressResponse = require('decompress-response'); + +http.get('https://sindresorhus.com', response => { + response = decompressResponse(response); +}); +``` + +## API + +### decompressResponse(response) + +Returns the decompressed HTTP response stream. + +#### response + +Type: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) + +The HTTP incoming stream with compressed data. + +--- + +
+ + Get professional support for this package with a Tidelift subscription + +
+ + Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. +
+
diff --git a/backend/node_modules/deep-extend/CHANGELOG.md b/backend/node_modules/deep-extend/CHANGELOG.md new file mode 100644 index 00000000..dd13ec13 --- /dev/null +++ b/backend/node_modules/deep-extend/CHANGELOG.md @@ -0,0 +1,46 @@ +Changelog +========= + +v0.6.0 +------ + +- Updated "devDependencies" versions to fix vulnerability alerts +- Dropped support of io.js and node.js v0.12.x and lower since new versions of + "devDependencies" couldn't work with those old node.js versions + (minimal supported version of node.js now is v4.0.0) + +v0.5.1 +------ + +- Fix prototype pollution vulnerability (thanks to @mwakerman for the PR) +- Avoid using deprecated Buffer API (thanks to @ChALkeR for the PR) + +v0.5.0 +------ + +- Auto-testing provided by Travis CI; +- Support older Node.JS versions (`v0.11.x` and `v0.10.x`); +- Removed tests files from npm package. + +v0.4.2 +------ + +- Fix for `null` as an argument. + +v0.4.1 +------ + +- Removed test code from npm package + ([see pull request #21](https://github.com/unclechu/node-deep-extend/pull/21)); +- Increased minimal version of Node from `0.4.0` to `0.12.0` + (because can't run tests on lesser version anyway). + +v0.4.0 +------ + +- **WARNING!** Broken backward compatibility with `v0.3.x`; +- Fixed bug with extending arrays instead of cloning; +- Deep cloning for arrays; +- Check for own property; +- Fixed some documentation issues; +- Strict JS mode. diff --git a/backend/node_modules/deep-extend/LICENSE b/backend/node_modules/deep-extend/LICENSE new file mode 100644 index 00000000..5c58916f --- /dev/null +++ b/backend/node_modules/deep-extend/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013-2018, Viacheslav Lotsmanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/deep-extend/README.md b/backend/node_modules/deep-extend/README.md new file mode 100644 index 00000000..67c7fc08 --- /dev/null +++ b/backend/node_modules/deep-extend/README.md @@ -0,0 +1,91 @@ +Deep Extend +=========== + +Recursive object extending. + +[![Build Status](https://api.travis-ci.org/unclechu/node-deep-extend.svg?branch=master)](https://travis-ci.org/unclechu/node-deep-extend) + +[![NPM](https://nodei.co/npm/deep-extend.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/deep-extend/) + +Install +------- + +```bash +$ npm install deep-extend +``` + +Usage +----- + +```javascript +var deepExtend = require('deep-extend'); +var obj1 = { + a: 1, + b: 2, + d: { + a: 1, + b: [], + c: { test1: 123, test2: 321 } + }, + f: 5, + g: 123, + i: 321, + j: [1, 2] +}; +var obj2 = { + b: 3, + c: 5, + d: { + b: { first: 'one', second: 'two' }, + c: { test2: 222 } + }, + e: { one: 1, two: 2 }, + f: [], + g: (void 0), + h: /abc/g, + i: null, + j: [3, 4] +}; + +deepExtend(obj1, obj2); + +console.log(obj1); +/* +{ a: 1, + b: 3, + d: + { a: 1, + b: { first: 'one', second: 'two' }, + c: { test1: 123, test2: 222 } }, + f: [], + g: undefined, + c: 5, + e: { one: 1, two: 2 }, + h: /abc/g, + i: null, + j: [3, 4] } +*/ +``` + +Unit testing +------------ + +```bash +$ npm test +``` + +Changelog +--------- + +[CHANGELOG.md](./CHANGELOG.md) + +Any issues? +----------- + +Please, report about issues +[here](https://github.com/unclechu/node-deep-extend/issues). + +License +------- + +[MIT](./LICENSE) diff --git a/backend/node_modules/deep-extend/index.js b/backend/node_modules/deep-extend/index.js new file mode 100644 index 00000000..762d81e9 --- /dev/null +++ b/backend/node_modules/deep-extend/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/deep-extend'); diff --git a/backend/node_modules/deep-extend/lib/deep-extend.js b/backend/node_modules/deep-extend/lib/deep-extend.js new file mode 100644 index 00000000..651fd8d3 --- /dev/null +++ b/backend/node_modules/deep-extend/lib/deep-extend.js @@ -0,0 +1,150 @@ +/*! + * @description Recursive object extending + * @author Viacheslav Lotsmanov + * @license MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2013-2018 Viacheslav Lotsmanov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +'use strict'; + +function isSpecificValue(val) { + return ( + val instanceof Buffer + || val instanceof Date + || val instanceof RegExp + ) ? true : false; +} + +function cloneSpecificValue(val) { + if (val instanceof Buffer) { + var x = Buffer.alloc + ? Buffer.alloc(val.length) + : new Buffer(val.length); + val.copy(x); + return x; + } else if (val instanceof Date) { + return new Date(val.getTime()); + } else if (val instanceof RegExp) { + return new RegExp(val); + } else { + throw new Error('Unexpected situation'); + } +} + +/** + * Recursive cloning array. + */ +function deepCloneArray(arr) { + var clone = []; + arr.forEach(function (item, index) { + if (typeof item === 'object' && item !== null) { + if (Array.isArray(item)) { + clone[index] = deepCloneArray(item); + } else if (isSpecificValue(item)) { + clone[index] = cloneSpecificValue(item); + } else { + clone[index] = deepExtend({}, item); + } + } else { + clone[index] = item; + } + }); + return clone; +} + +function safeGetProperty(object, property) { + return property === '__proto__' ? undefined : object[property]; +} + +/** + * Extening object that entered in first argument. + * + * Returns extended object or false if have no target object or incorrect type. + * + * If you wish to clone source object (without modify it), just use empty new + * object as first argument, like this: + * deepExtend({}, yourObj_1, [yourObj_N]); + */ +var deepExtend = module.exports = function (/*obj_1, [obj_2], [obj_N]*/) { + if (arguments.length < 1 || typeof arguments[0] !== 'object') { + return false; + } + + if (arguments.length < 2) { + return arguments[0]; + } + + var target = arguments[0]; + + // convert arguments to array and cut off target object + var args = Array.prototype.slice.call(arguments, 1); + + var val, src, clone; + + args.forEach(function (obj) { + // skip argument if isn't an object, is null, or is an array + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { + return; + } + + Object.keys(obj).forEach(function (key) { + src = safeGetProperty(target, key); // source value + val = safeGetProperty(obj, key); // new value + + // recursion prevention + if (val === target) { + return; + + /** + * if new value isn't object then just overwrite by new value + * instead of extending. + */ + } else if (typeof val !== 'object' || val === null) { + target[key] = val; + return; + + // just clone arrays (and recursive clone objects inside) + } else if (Array.isArray(val)) { + target[key] = deepCloneArray(val); + return; + + // custom cloning and overwrite for specific objects + } else if (isSpecificValue(val)) { + target[key] = cloneSpecificValue(val); + return; + + // overwrite by new value if source isn't object or array + } else if (typeof src !== 'object' || src === null || Array.isArray(src)) { + target[key] = deepExtend({}, val); + return; + + // source value and new value is objects both, extending... + } else { + target[key] = deepExtend(src, val); + return; + } + }); + }); + + return target; +}; diff --git a/backend/node_modules/deep-extend/package.json b/backend/node_modules/deep-extend/package.json new file mode 100644 index 00000000..5f2195ff --- /dev/null +++ b/backend/node_modules/deep-extend/package.json @@ -0,0 +1,62 @@ +{ + "name": "deep-extend", + "description": "Recursive object extending", + "license": "MIT", + "version": "0.6.0", + "homepage": "https://github.com/unclechu/node-deep-extend", + "keywords": [ + "deep-extend", + "extend", + "deep", + "recursive", + "xtend", + "clone", + "merge", + "json" + ], + "licenses": [ + { + "type": "MIT", + "url": "https://raw.githubusercontent.com/unclechu/node-deep-extend/master/LICENSE" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/unclechu/node-deep-extend.git" + }, + "author": "Viacheslav Lotsmanov ", + "bugs": "https://github.com/unclechu/node-deep-extend/issues", + "contributors": [ + { + "name": "Romain Prieto", + "url": "https://github.com/rprieto" + }, + { + "name": "Max Maximov", + "url": "https://github.com/maxmaximov" + }, + { + "name": "Marshall Bowers", + "url": "https://github.com/maxdeviant" + }, + { + "name": "Misha Wakerman", + "url": "https://github.com/mwakerman" + } + ], + "main": "lib/deep-extend.js", + "engines": { + "node": ">=4.0.0" + }, + "scripts": { + "test": "./node_modules/.bin/mocha" + }, + "devDependencies": { + "mocha": "5.2.0", + "should": "13.2.1" + }, + "files": [ + "index.js", + "lib/" + ] +} diff --git a/backend/node_modules/expand-template/.travis.yml b/backend/node_modules/expand-template/.travis.yml new file mode 100644 index 00000000..1335a770 --- /dev/null +++ b/backend/node_modules/expand-template/.travis.yml @@ -0,0 +1,6 @@ +language: node_js + +node_js: + - 6 + - 8 + - 10 diff --git a/backend/node_modules/expand-template/LICENSE b/backend/node_modules/expand-template/LICENSE new file mode 100644 index 00000000..814aef41 --- /dev/null +++ b/backend/node_modules/expand-template/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Lars-Magnus Skog + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/backend/node_modules/expand-template/README.md b/backend/node_modules/expand-template/README.md new file mode 100644 index 00000000..b98aa480 --- /dev/null +++ b/backend/node_modules/expand-template/README.md @@ -0,0 +1,43 @@ +# expand-template + +> Expand placeholders in a template string. + +[![npm](https://img.shields.io/npm/v/expand-template.svg)](https://www.npmjs.com/package/expand-template) +![Node version](https://img.shields.io/node/v/expand-template.svg) +[![Build Status](https://travis-ci.org/ralphtheninja/expand-template.svg?branch=master)](https://travis-ci.org/ralphtheninja/expand-template) +[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) + +## Install + +``` +$ npm i expand-template -S +``` + +## Usage + +Default functionality expands templates using `{}` as separators for string placeholders. + +```js +var expand = require('expand-template')() +var template = '{foo}/{foo}/{bar}/{bar}' +console.log(expand(template, { + foo: 'BAR', + bar: 'FOO' +})) +// -> BAR/BAR/FOO/FOO +``` + +Custom separators: + +```js +var expand = require('expand-template')({ sep: '[]' }) +var template = '[foo]/[foo]/[bar]/[bar]' +console.log(expand(template, { + foo: 'BAR', + bar: 'FOO' +})) +// -> BAR/BAR/FOO/FOO +``` + +## License +All code, unless stated otherwise, is dual-licensed under [`WTFPL`](http://www.wtfpl.net/txt/copying/) and [`MIT`](https://opensource.org/licenses/MIT). diff --git a/backend/node_modules/expand-template/index.js b/backend/node_modules/expand-template/index.js new file mode 100644 index 00000000..e182837c --- /dev/null +++ b/backend/node_modules/expand-template/index.js @@ -0,0 +1,26 @@ +module.exports = function (opts) { + var sep = opts ? opts.sep : '{}' + var len = sep.length + + var whitespace = '\\s*' + var left = escape(sep.substring(0, len / 2)) + whitespace + var right = whitespace + escape(sep.substring(len / 2, len)) + + return function (template, values) { + Object.keys(values).forEach(function (key) { + var value = String(values[key]).replace(/\$/g, '$$$$') + template = template.replace(regExp(key), value) + }) + return template + } + + function escape (s) { + return [].map.call(s, function (char) { + return '\\' + char + }).join('') + } + + function regExp (key) { + return new RegExp(left + key + right, 'g') + } +} diff --git a/backend/node_modules/expand-template/package.json b/backend/node_modules/expand-template/package.json new file mode 100644 index 00000000..9a09656c --- /dev/null +++ b/backend/node_modules/expand-template/package.json @@ -0,0 +1,29 @@ +{ + "name": "expand-template", + "version": "2.0.3", + "description": "Expand placeholders in a template string", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/ralphtheninja/expand-template.git" + }, + "homepage": "https://github.com/ralphtheninja/expand-template", + "scripts": { + "test": "tape test.js && standard" + }, + "keywords": [ + "template", + "expand", + "replace" + ], + "author": "LM ", + "license": "(MIT OR WTFPL)", + "dependencies": {}, + "devDependencies": { + "standard": "^12.0.0", + "tape": "^4.2.2" + }, + "engines": { + "node": ">=6" + } +} diff --git a/backend/node_modules/expand-template/test.js b/backend/node_modules/expand-template/test.js new file mode 100644 index 00000000..ba6ed871 --- /dev/null +++ b/backend/node_modules/expand-template/test.js @@ -0,0 +1,67 @@ +var test = require('tape') +var Expand = require('./') + +test('default expands {} placeholders', function (t) { + var expand = Expand() + t.equal(typeof expand, 'function', 'is a function') + t.equal(expand('{foo}/{bar}', { + foo: 'BAR', bar: 'FOO' + }), 'BAR/FOO') + t.equal(expand('{foo}{foo}{foo}', { + foo: 'FOO' + }), 'FOOFOOFOO', 'expands one placeholder many times') + t.end() +}) + +test('support for custom separators', function (t) { + var expand = Expand({ sep: '[]' }) + t.equal(expand('[foo]/[bar]', { + foo: 'BAR', bar: 'FOO' + }), 'BAR/FOO') + t.equal(expand('[foo][foo][foo]', { + foo: 'FOO' + }), 'FOOFOOFOO', 'expands one placeholder many times') + t.end() +}) + +test('support for longer custom separators', function (t) { + var expand = Expand({ sep: '[[]]' }) + t.equal(expand('[[foo]]/[[bar]]', { + foo: 'BAR', bar: 'FOO' + }), 'BAR/FOO') + t.equal(expand('[[foo]][[foo]][[foo]]', { + foo: 'FOO' + }), 'FOOFOOFOO', 'expands one placeholder many times') + t.end() +}) + +test('whitespace-insensitive', function (t) { + var expand = Expand({ sep: '[]' }) + t.equal(expand('[ foo ]/[ bar ]', { + foo: 'BAR', bar: 'FOO' + }), 'BAR/FOO') + t.equal(expand('[ foo ][ foo ][ foo]', { + foo: 'FOO' + }), 'FOOFOOFOO', 'expands one placeholder many times') + t.end() +}) + +test('dollar escape', function (t) { + var expand = Expand() + t.equal(expand('before {foo} after', { + foo: '$' + }), 'before $ after') + t.equal(expand('before {foo} after', { + foo: '$&' + }), 'before $& after') + t.equal(expand('before {foo} after', { + foo: '$`' + }), 'before $` after') + t.equal(expand('before {foo} after', { + foo: '$\'' + }), 'before $\' after') + t.equal(expand('before {foo} after', { + foo: '$0' + }), 'before $0 after') + t.end() +}) diff --git a/backend/node_modules/fs-constants/LICENSE b/backend/node_modules/fs-constants/LICENSE new file mode 100644 index 00000000..cb757e5d --- /dev/null +++ b/backend/node_modules/fs-constants/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/backend/node_modules/fs-constants/README.md b/backend/node_modules/fs-constants/README.md new file mode 100644 index 00000000..62b33742 --- /dev/null +++ b/backend/node_modules/fs-constants/README.md @@ -0,0 +1,26 @@ +# fs-constants + +Small module that allows you to get the fs constants across +Node and the browser. + +``` +npm install fs-constants +``` + +Previously you would use `require('constants')` for this in node but that has been +deprecated and changed to `require('fs').constants` which does not browserify. + +This module uses `require('constants')` in the browser and `require('fs').constants` in node to work around this + + +## Usage + +``` js +var constants = require('fs-constants') + +console.log('constants:', constants) +``` + +## License + +MIT diff --git a/backend/node_modules/fs-constants/browser.js b/backend/node_modules/fs-constants/browser.js new file mode 100644 index 00000000..3c87638d --- /dev/null +++ b/backend/node_modules/fs-constants/browser.js @@ -0,0 +1 @@ +module.exports = require('constants') diff --git a/backend/node_modules/fs-constants/index.js b/backend/node_modules/fs-constants/index.js new file mode 100644 index 00000000..2a3aadf3 --- /dev/null +++ b/backend/node_modules/fs-constants/index.js @@ -0,0 +1 @@ +module.exports = require('fs').constants || require('constants') diff --git a/backend/node_modules/fs-constants/package.json b/backend/node_modules/fs-constants/package.json new file mode 100644 index 00000000..6f2b8f24 --- /dev/null +++ b/backend/node_modules/fs-constants/package.json @@ -0,0 +1,19 @@ +{ + "name": "fs-constants", + "version": "1.0.0", + "description": "Require constants across node and the browser", + "main": "index.js", + "browser": "browser.js", + "dependencies": {}, + "devDependencies": {}, + "repository": { + "type": "git", + "url": "https://github.com/mafintosh/fs-constants.git" + }, + "author": "Mathias Buus (@mafintosh)", + "license": "MIT", + "bugs": { + "url": "https://github.com/mafintosh/fs-constants/issues" + }, + "homepage": "https://github.com/mafintosh/fs-constants" +} diff --git a/backend/node_modules/github-from-package/.travis.yml b/backend/node_modules/github-from-package/.travis.yml new file mode 100644 index 00000000..895dbd36 --- /dev/null +++ b/backend/node_modules/github-from-package/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - 0.6 + - 0.8 diff --git a/backend/node_modules/github-from-package/LICENSE b/backend/node_modules/github-from-package/LICENSE new file mode 100644 index 00000000..ee27ba4b --- /dev/null +++ b/backend/node_modules/github-from-package/LICENSE @@ -0,0 +1,18 @@ +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/github-from-package/example/package.json b/backend/node_modules/github-from-package/example/package.json new file mode 100644 index 00000000..03494f48 --- /dev/null +++ b/backend/node_modules/github-from-package/example/package.json @@ -0,0 +1,8 @@ +{ + "name": "beep-boop", + "version": "1.2.3", + "repository" : { + "type" : "git", + "url": "git@github.com:substack/beep-boop.git" + } +} diff --git a/backend/node_modules/github-from-package/example/url.js b/backend/node_modules/github-from-package/example/url.js new file mode 100644 index 00000000..138fb8a6 --- /dev/null +++ b/backend/node_modules/github-from-package/example/url.js @@ -0,0 +1,3 @@ +var github = require('../'); +var url = github(require('./package.json')); +console.log(url); diff --git a/backend/node_modules/github-from-package/index.js b/backend/node_modules/github-from-package/index.js new file mode 100644 index 00000000..3d1d657b --- /dev/null +++ b/backend/node_modules/github-from-package/index.js @@ -0,0 +1,17 @@ +module.exports = function (pkg) { + var m; + if (m = match(JSON.stringify(pkg.repository))) { + return m; + } + else if (m = match(JSON.stringify(pkg))) { + return m; + } + return undefined; +}; + +function match (str) { + var m = /\bgithub.com[:\/]([^\/"]+)\/([^\/"]+)/.exec(str); + if (m) { + return 'https://github.com/' + m[1] + '/' + m[2].replace(/\.git$/, ''); + } +} diff --git a/backend/node_modules/github-from-package/package.json b/backend/node_modules/github-from-package/package.json new file mode 100644 index 00000000..a3e240fe --- /dev/null +++ b/backend/node_modules/github-from-package/package.json @@ -0,0 +1,30 @@ +{ + "name" : "github-from-package", + "version" : "0.0.0", + "description" : "return the github url from a package.json file", + "main" : "index.js", + "devDependencies" : { + "tap" : "~0.3.0", + "tape" : "~0.1.5" + }, + "scripts" : { + "test" : "tap test/*.js" + }, + "repository" : { + "type" : "git", + "url" : "git://github.com/substack/github-from-package.git" + }, + "homepage" : "https://github.com/substack/github-from-package", + "keywords" : [ + "github", + "package.json", + "npm", + "repository" + ], + "author" : { + "name" : "James Halliday", + "email" : "mail@substack.net", + "url" : "http://substack.net" + }, + "license" : "MIT" +} diff --git a/backend/node_modules/github-from-package/readme.markdown b/backend/node_modules/github-from-package/readme.markdown new file mode 100644 index 00000000..5ba397da --- /dev/null +++ b/backend/node_modules/github-from-package/readme.markdown @@ -0,0 +1,53 @@ +# github-from-package + +return the github url from a package.json file + +[![build status](https://secure.travis-ci.org/substack/github-from-package.png)](http://travis-ci.org/substack/github-from-package) + +# example + +For the `./package.json` file: + +``` json +{ + "name": "beep-boop", + "version": "1.2.3", + "repository" : { + "type" : "git", + "url": "git@github.com:substack/beep-boop.git" + } +} +``` + +``` js +var github = require('github-from-package'); +var url = github(require('./package.json')); +console.log(url); +``` + +``` +https://github.com/substack/beep-boop +``` + +# methods + +``` js +var github = require('github-from-package') +``` + +## var url = github(pkg) + +Return the most likely github url from the package.json contents `pkg`. If no +github url can be determined, return `undefined`. + +# install + +With [npm](https://npmjs.org) do: + +``` +npm install github-from-package +``` + +# license + +MIT diff --git a/backend/node_modules/github-from-package/test/a.json b/backend/node_modules/github-from-package/test/a.json new file mode 100644 index 00000000..03494f48 --- /dev/null +++ b/backend/node_modules/github-from-package/test/a.json @@ -0,0 +1,8 @@ +{ + "name": "beep-boop", + "version": "1.2.3", + "repository" : { + "type" : "git", + "url": "git@github.com:substack/beep-boop.git" + } +} diff --git a/backend/node_modules/github-from-package/test/b.json b/backend/node_modules/github-from-package/test/b.json new file mode 100644 index 00000000..02093257 --- /dev/null +++ b/backend/node_modules/github-from-package/test/b.json @@ -0,0 +1,5 @@ +{ + "name": "beep-boop", + "version": "1.2.3", + "repository" : "git@github.com:substack/beep-boop.git" +} diff --git a/backend/node_modules/github-from-package/test/c.json b/backend/node_modules/github-from-package/test/c.json new file mode 100644 index 00000000..65f6ddad --- /dev/null +++ b/backend/node_modules/github-from-package/test/c.json @@ -0,0 +1,5 @@ +{ + "name": "beep-boop", + "version": "1.2.3", + "repository" : "https://github.com/substack/beep-boop.git" +} diff --git a/backend/node_modules/github-from-package/test/d.json b/backend/node_modules/github-from-package/test/d.json new file mode 100644 index 00000000..c61f3cd3 --- /dev/null +++ b/backend/node_modules/github-from-package/test/d.json @@ -0,0 +1,7 @@ +{ + "name": "beep-boop", + "version": "1.2.3", + "repository" : { + "url": "https://github.com/substack/beep-boop" + } +} diff --git a/backend/node_modules/github-from-package/test/e.json b/backend/node_modules/github-from-package/test/e.json new file mode 100644 index 00000000..770b4384 --- /dev/null +++ b/backend/node_modules/github-from-package/test/e.json @@ -0,0 +1,5 @@ +{ + "name": "beep-boop", + "version": "1.2.3", + "homepage": "https://github.com/substack/beep-boop/issues" +} diff --git a/backend/node_modules/github-from-package/test/url.js b/backend/node_modules/github-from-package/test/url.js new file mode 100644 index 00000000..d5a0a667 --- /dev/null +++ b/backend/node_modules/github-from-package/test/url.js @@ -0,0 +1,19 @@ +var test = require('tape'); +var github = require('../'); +var packages = { + a : require('./a.json'), + b : require('./b.json'), + c : require('./c.json'), + d : require('./d.json'), + e : require('./e.json') +}; + +test(function (t) { + t.plan(5); + var url = 'https://github.com/substack/beep-boop'; + t.equal(url, github(packages.a), 'a.json comparison'); + t.equal(url, github(packages.b), 'b.json comparison'); + t.equal(url, github(packages.c), 'c.json comparison'); + t.equal(url, github(packages.d), 'd.json comparison'); + t.equal(url, github(packages.e), 'e.json comparison'); +}); diff --git a/backend/node_modules/ini/LICENSE b/backend/node_modules/ini/LICENSE new file mode 100644 index 00000000..19129e31 --- /dev/null +++ b/backend/node_modules/ini/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/backend/node_modules/ini/README.md b/backend/node_modules/ini/README.md new file mode 100644 index 00000000..33df2582 --- /dev/null +++ b/backend/node_modules/ini/README.md @@ -0,0 +1,102 @@ +An ini format parser and serializer for node. + +Sections are treated as nested objects. Items before the first +heading are saved on the object directly. + +## Usage + +Consider an ini-file `config.ini` that looks like this: + + ; this comment is being ignored + scope = global + + [database] + user = dbuser + password = dbpassword + database = use_this_database + + [paths.default] + datadir = /var/lib/data + array[] = first value + array[] = second value + array[] = third value + +You can read, manipulate and write the ini-file like so: + + var fs = require('fs') + , ini = require('ini') + + var config = ini.parse(fs.readFileSync('./config.ini', 'utf-8')) + + config.scope = 'local' + config.database.database = 'use_another_database' + config.paths.default.tmpdir = '/tmp' + delete config.paths.default.datadir + config.paths.default.array.push('fourth value') + + fs.writeFileSync('./config_modified.ini', ini.stringify(config, { section: 'section' })) + +This will result in a file called `config_modified.ini` being written +to the filesystem with the following content: + + [section] + scope=local + [section.database] + user=dbuser + password=dbpassword + database=use_another_database + [section.paths.default] + tmpdir=/tmp + array[]=first value + array[]=second value + array[]=third value + array[]=fourth value + + +## API + +### decode(inistring) + +Decode the ini-style formatted `inistring` into a nested object. + +### parse(inistring) + +Alias for `decode(inistring)` + +### encode(object, [options]) + +Encode the object `object` into an ini-style formatted string. If the +optional parameter `section` is given, then all top-level properties +of the object are put into this section and the `section`-string is +prepended to all sub-sections, see the usage example above. + +The `options` object may contain the following: + +* `section` A string which will be the first `section` in the encoded + ini data. Defaults to none. +* `whitespace` Boolean to specify whether to put whitespace around the + `=` character. By default, whitespace is omitted, to be friendly to + some persnickety old parsers that don't tolerate it well. But some + find that it's more human-readable and pretty with the whitespace. + +For backwards compatibility reasons, if a `string` options is passed +in, then it is assumed to be the `section` value. + +### stringify(object, [options]) + +Alias for `encode(object, [options])` + +### safe(val) + +Escapes the string `val` such that it is safe to be used as a key or +value in an ini-file. Basically escapes quotes. For example + + ini.safe('"unsafe string"') + +would result in + + "\"unsafe string\"" + +### unsafe(val) + +Unescapes the string `val` diff --git a/backend/node_modules/ini/ini.js b/backend/node_modules/ini/ini.js new file mode 100644 index 00000000..b576f08d --- /dev/null +++ b/backend/node_modules/ini/ini.js @@ -0,0 +1,206 @@ +exports.parse = exports.decode = decode + +exports.stringify = exports.encode = encode + +exports.safe = safe +exports.unsafe = unsafe + +var eol = typeof process !== 'undefined' && + process.platform === 'win32' ? '\r\n' : '\n' + +function encode (obj, opt) { + var children = [] + var out = '' + + if (typeof opt === 'string') { + opt = { + section: opt, + whitespace: false, + } + } else { + opt = opt || {} + opt.whitespace = opt.whitespace === true + } + + var separator = opt.whitespace ? ' = ' : '=' + + Object.keys(obj).forEach(function (k, _, __) { + var val = obj[k] + if (val && Array.isArray(val)) { + val.forEach(function (item) { + out += safe(k + '[]') + separator + safe(item) + '\n' + }) + } else if (val && typeof val === 'object') + children.push(k) + else + out += safe(k) + separator + safe(val) + eol + }) + + if (opt.section && out.length) + out = '[' + safe(opt.section) + ']' + eol + out + + children.forEach(function (k, _, __) { + var nk = dotSplit(k).join('\\.') + var section = (opt.section ? opt.section + '.' : '') + nk + var child = encode(obj[k], { + section: section, + whitespace: opt.whitespace, + }) + if (out.length && child.length) + out += eol + + out += child + }) + + return out +} + +function dotSplit (str) { + return str.replace(/\1/g, '\u0002LITERAL\\1LITERAL\u0002') + .replace(/\\\./g, '\u0001') + .split(/\./).map(function (part) { + return part.replace(/\1/g, '\\.') + .replace(/\2LITERAL\\1LITERAL\2/g, '\u0001') + }) +} + +function decode (str) { + var out = {} + var p = out + var section = null + // section |key = value + var re = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i + var lines = str.split(/[\r\n]+/g) + + lines.forEach(function (line, _, __) { + if (!line || line.match(/^\s*[;#]/)) + return + var match = line.match(re) + if (!match) + return + if (match[1] !== undefined) { + section = unsafe(match[1]) + if (section === '__proto__') { + // not allowed + // keep parsing the section, but don't attach it. + p = {} + return + } + p = out[section] = out[section] || {} + return + } + var key = unsafe(match[2]) + if (key === '__proto__') + return + var value = match[3] ? unsafe(match[4]) : true + switch (value) { + case 'true': + case 'false': + case 'null': value = JSON.parse(value) + } + + // Convert keys with '[]' suffix to an array + if (key.length > 2 && key.slice(-2) === '[]') { + key = key.substring(0, key.length - 2) + if (key === '__proto__') + return + if (!p[key]) + p[key] = [] + else if (!Array.isArray(p[key])) + p[key] = [p[key]] + } + + // safeguard against resetting a previously defined + // array by accidentally forgetting the brackets + if (Array.isArray(p[key])) + p[key].push(value) + else + p[key] = value + }) + + // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} + // use a filter to return the keys that have to be deleted. + Object.keys(out).filter(function (k, _, __) { + if (!out[k] || + typeof out[k] !== 'object' || + Array.isArray(out[k])) + return false + + // see if the parent section is also an object. + // if so, add it to that, and mark this one for deletion + var parts = dotSplit(k) + var p = out + var l = parts.pop() + var nl = l.replace(/\\\./g, '.') + parts.forEach(function (part, _, __) { + if (part === '__proto__') + return + if (!p[part] || typeof p[part] !== 'object') + p[part] = {} + p = p[part] + }) + if (p === out && nl === l) + return false + + p[nl] = out[k] + return true + }).forEach(function (del, _, __) { + delete out[del] + }) + + return out +} + +function isQuoted (val) { + return (val.charAt(0) === '"' && val.slice(-1) === '"') || + (val.charAt(0) === "'" && val.slice(-1) === "'") +} + +function safe (val) { + return (typeof val !== 'string' || + val.match(/[=\r\n]/) || + val.match(/^\[/) || + (val.length > 1 && + isQuoted(val)) || + val !== val.trim()) + ? JSON.stringify(val) + : val.replace(/;/g, '\\;').replace(/#/g, '\\#') +} + +function unsafe (val, doUnesc) { + val = (val || '').trim() + if (isQuoted(val)) { + // remove the single quotes before calling JSON.parse + if (val.charAt(0) === "'") + val = val.substr(1, val.length - 2) + + try { + val = JSON.parse(val) + } catch (_) {} + } else { + // walk the val to find the first not-escaped ; character + var esc = false + var unesc = '' + for (var i = 0, l = val.length; i < l; i++) { + var c = val.charAt(i) + if (esc) { + if ('\\;#'.indexOf(c) !== -1) + unesc += c + else + unesc += '\\' + c + + esc = false + } else if (';#'.indexOf(c) !== -1) + break + else if (c === '\\') + esc = true + else + unesc += c + } + if (esc) + unesc += '\\' + + return unesc.trim() + } + return val +} diff --git a/backend/node_modules/ini/package.json b/backend/node_modules/ini/package.json new file mode 100644 index 00000000..c830a355 --- /dev/null +++ b/backend/node_modules/ini/package.json @@ -0,0 +1,33 @@ +{ + "author": "Isaac Z. Schlueter (http://blog.izs.me/)", + "name": "ini", + "description": "An ini encoder/decoder for node", + "version": "1.3.8", + "repository": { + "type": "git", + "url": "git://github.com/isaacs/ini.git" + }, + "main": "ini.js", + "scripts": { + "eslint": "eslint", + "lint": "npm run eslint -- ini.js test/*.js", + "lintfix": "npm run lint -- --fix", + "test": "tap", + "posttest": "npm run lint", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags" + }, + "devDependencies": { + "eslint": "^7.9.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "tap": "14" + }, + "license": "ISC", + "files": [ + "ini.js" + ] +} diff --git a/backend/node_modules/mimic-response/index.d.ts b/backend/node_modules/mimic-response/index.d.ts new file mode 100644 index 00000000..65a51e98 --- /dev/null +++ b/backend/node_modules/mimic-response/index.d.ts @@ -0,0 +1,17 @@ +import {IncomingMessage} from 'http'; + +/** +Mimic a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage) + +Makes `toStream` include the properties from `fromStream`. + +@param fromStream - The stream to copy the properties from. +@param toStream - The stream to copy the properties to. +@return The same object as `toStream`. +*/ +declare function mimicResponse( + fromStream: IncomingMessage, // eslint-disable-line @typescript-eslint/prefer-readonly-parameter-types + toStream: T, +): T & IncomingMessage; + +export = mimicResponse; diff --git a/backend/node_modules/mimic-response/index.js b/backend/node_modules/mimic-response/index.js new file mode 100644 index 00000000..404e1111 --- /dev/null +++ b/backend/node_modules/mimic-response/index.js @@ -0,0 +1,77 @@ +'use strict'; + +// We define these manually to ensure they're always copied +// even if they would move up the prototype chain +// https://nodejs.org/api/http.html#http_class_http_incomingmessage +const knownProperties = [ + 'aborted', + 'complete', + 'headers', + 'httpVersion', + 'httpVersionMinor', + 'httpVersionMajor', + 'method', + 'rawHeaders', + 'rawTrailers', + 'setTimeout', + 'socket', + 'statusCode', + 'statusMessage', + 'trailers', + 'url' +]; + +module.exports = (fromStream, toStream) => { + if (toStream._readableState.autoDestroy) { + throw new Error('The second stream must have the `autoDestroy` option set to `false`'); + } + + const fromProperties = new Set(Object.keys(fromStream).concat(knownProperties)); + + const properties = {}; + + for (const property of fromProperties) { + // Don't overwrite existing properties. + if (property in toStream) { + continue; + } + + properties[property] = { + get() { + const value = fromStream[property]; + const isFunction = typeof value === 'function'; + + return isFunction ? value.bind(fromStream) : value; + }, + set(value) { + fromStream[property] = value; + }, + enumerable: true, + configurable: false + }; + } + + Object.defineProperties(toStream, properties); + + fromStream.once('aborted', () => { + toStream.destroy(); + + toStream.emit('aborted'); + }); + + fromStream.once('close', () => { + if (fromStream.complete) { + if (toStream.readable) { + toStream.once('end', () => { + toStream.emit('close'); + }); + } else { + toStream.emit('close'); + } + } else { + toStream.emit('close'); + } + }); + + return toStream; +}; diff --git a/backend/node_modules/mimic-response/license b/backend/node_modules/mimic-response/license new file mode 100644 index 00000000..fa7ceba3 --- /dev/null +++ b/backend/node_modules/mimic-response/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/mimic-response/package.json b/backend/node_modules/mimic-response/package.json new file mode 100644 index 00000000..d478b0f3 --- /dev/null +++ b/backend/node_modules/mimic-response/package.json @@ -0,0 +1,42 @@ +{ + "name": "mimic-response", + "version": "3.1.0", + "description": "Mimic a Node.js HTTP response stream", + "license": "MIT", + "repository": "sindresorhus/mimic-response", + "funding": "https://github.com/sponsors/sindresorhus", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "https://sindresorhus.com" + }, + "engines": { + "node": ">=10" + }, + "scripts": { + "test": "xo && ava && tsd" + }, + "files": [ + "index.d.ts", + "index.js" + ], + "keywords": [ + "mimic", + "response", + "stream", + "http", + "https", + "request", + "get", + "core" + ], + "devDependencies": { + "@types/node": "^14.0.1", + "ava": "^2.4.0", + "create-test-server": "^2.4.0", + "p-event": "^4.1.0", + "pify": "^5.0.0", + "tsd": "^0.11.0", + "xo": "^0.30.0" + } +} diff --git a/backend/node_modules/mimic-response/readme.md b/backend/node_modules/mimic-response/readme.md new file mode 100644 index 00000000..e968620a --- /dev/null +++ b/backend/node_modules/mimic-response/readme.md @@ -0,0 +1,78 @@ +# mimic-response [![Build Status](https://travis-ci.com/sindresorhus/mimic-response.svg?branch=master)](https://travis-ci.com/sindresorhus/mimic-response) + +> Mimic a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage) + +## Install + +``` +$ npm install mimic-response +``` + +## Usage + +```js +const stream = require('stream'); +const mimicResponse = require('mimic-response'); + +const responseStream = getHttpResponseStream(); +const myStream = new stream.PassThrough(); + +mimicResponse(responseStream, myStream); + +console.log(myStream.statusCode); +//=> 200 +``` + +## API + +### mimicResponse(from, to) + +**Note #1:** The `from.destroy(error)` function is not proxied. You have to call it manually: + +```js +const stream = require('stream'); +const mimicResponse = require('mimic-response'); + +const responseStream = getHttpResponseStream(); + +const myStream = new stream.PassThrough({ + destroy(error, callback) { + responseStream.destroy(); + + callback(error); + } +}); + +myStream.destroy(); +``` + +Please note that `myStream` and `responseStream` never throws. The error is passed to the request instead. + +#### from + +Type: `Stream` + +[Node.js HTTP response stream.](https://nodejs.org/api/http.html#http_class_http_incomingmessage) + +#### to + +Type: `Stream` + +Any stream. + +## Related + +- [mimic-fn](https://github.com/sindresorhus/mimic-fn) - Make a function mimic another one +- [clone-response](https://github.com/lukechilds/clone-response) - Clone a Node.js response stream + +--- + +
+ + Get professional support for this package with a Tidelift subscription + +
+ + Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. +
+
diff --git a/backend/node_modules/minimist/.eslintrc b/backend/node_modules/minimist/.eslintrc new file mode 100644 index 00000000..bd1a5e04 --- /dev/null +++ b/backend/node_modules/minimist/.eslintrc @@ -0,0 +1,29 @@ +{ + "root": true, + + "extends": "@ljharb/eslint-config/node/0.4", + + "rules": { + "array-element-newline": 0, + "complexity": 0, + "func-style": [2, "declaration"], + "max-lines-per-function": 0, + "max-nested-callbacks": 1, + "max-statements-per-line": 1, + "max-statements": 0, + "multiline-comment-style": 0, + "no-continue": 1, + "no-param-reassign": 1, + "no-restricted-syntax": 1, + "object-curly-newline": 0, + }, + + "overrides": [ + { + "files": "test/**", + "rules": { + "camelcase": 0, + }, + }, + ] +} diff --git a/backend/node_modules/minimist/.github/FUNDING.yml b/backend/node_modules/minimist/.github/FUNDING.yml new file mode 100644 index 00000000..a9366222 --- /dev/null +++ b/backend/node_modules/minimist/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/minimist +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/backend/node_modules/minimist/.nycrc b/backend/node_modules/minimist/.nycrc new file mode 100644 index 00000000..55c3d293 --- /dev/null +++ b/backend/node_modules/minimist/.nycrc @@ -0,0 +1,14 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "lines": 86, + "statements": 85.93, + "functions": 82.43, + "branches": 76.06, + "exclude": [ + "coverage", + "example", + "test" + ] +} diff --git a/backend/node_modules/minimist/CHANGELOG.md b/backend/node_modules/minimist/CHANGELOG.md new file mode 100644 index 00000000..c9a1e15e --- /dev/null +++ b/backend/node_modules/minimist/CHANGELOG.md @@ -0,0 +1,298 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.2.8](https://github.com/minimistjs/minimist/compare/v1.2.7...v1.2.8) - 2023-02-09 + +### Merged + +- [Fix] Fix long option followed by single dash [`#17`](https://github.com/minimistjs/minimist/pull/17) +- [Tests] Remove duplicate test [`#12`](https://github.com/minimistjs/minimist/pull/12) +- [Fix] opt.string works with multiple aliases [`#10`](https://github.com/minimistjs/minimist/pull/10) + +### Fixed + +- [Fix] Fix long option followed by single dash (#17) [`#15`](https://github.com/minimistjs/minimist/issues/15) +- [Tests] Remove duplicate test (#12) [`#8`](https://github.com/minimistjs/minimist/issues/8) +- [Fix] Fix long option followed by single dash [`#15`](https://github.com/minimistjs/minimist/issues/15) +- [Fix] opt.string works with multiple aliases (#10) [`#9`](https://github.com/minimistjs/minimist/issues/9) +- [Fix] Fix handling of short option with non-trivial equals [`#5`](https://github.com/minimistjs/minimist/issues/5) +- [Tests] Remove duplicate test [`#8`](https://github.com/minimistjs/minimist/issues/8) +- [Fix] opt.string works with multiple aliases [`#9`](https://github.com/minimistjs/minimist/issues/9) + +### Commits + +- Merge tag 'v0.2.3' [`a026794`](https://github.com/minimistjs/minimist/commit/a0267947c7870fc5847cf2d437fbe33f392767da) +- [eslint] fix indentation and whitespace [`5368ca4`](https://github.com/minimistjs/minimist/commit/5368ca4147e974138a54cc0dc4cea8f756546b70) +- [eslint] fix indentation and whitespace [`e5f5067`](https://github.com/minimistjs/minimist/commit/e5f5067259ceeaf0b098d14bec910f87e58708c7) +- [eslint] more cleanup [`62fde7d`](https://github.com/minimistjs/minimist/commit/62fde7d935f83417fb046741531a9e2346a36976) +- [eslint] more cleanup [`36ac5d0`](https://github.com/minimistjs/minimist/commit/36ac5d0d95e4947d074e5737d94814034ca335d1) +- [meta] add `auto-changelog` [`73923d2`](https://github.com/minimistjs/minimist/commit/73923d223553fca08b1ba77e3fbc2a492862ae4c) +- [actions] add reusable workflows [`d80727d`](https://github.com/minimistjs/minimist/commit/d80727df77bfa9e631044d7f16368d8f09242c91) +- [eslint] add eslint; rules to enable later are warnings [`48bc06a`](https://github.com/minimistjs/minimist/commit/48bc06a1b41f00e9cdf183db34f7a51ba70e98d4) +- [eslint] fix indentation [`34b0f1c`](https://github.com/minimistjs/minimist/commit/34b0f1ccaa45183c3c4f06a91f9b405180a6f982) +- [readme] rename and add badges [`5df0fe4`](https://github.com/minimistjs/minimist/commit/5df0fe49211bd09a3636f8686a7cb3012c3e98f0) +- [Dev Deps] switch from `covert` to `nyc` [`a48b128`](https://github.com/minimistjs/minimist/commit/a48b128fdb8d427dfb20a15273f83e38d97bef07) +- [Dev Deps] update `covert`, `tape`; remove unnecessary `tap` [`f0fb958`](https://github.com/minimistjs/minimist/commit/f0fb958e9a1fe980cdffc436a211b0bda58f621b) +- [meta] create FUNDING.yml; add `funding` in package.json [`3639e0c`](https://github.com/minimistjs/minimist/commit/3639e0c819359a366387e425ab6eabf4c78d3caa) +- [meta] use `npmignore` to autogenerate an npmignore file [`be2e038`](https://github.com/minimistjs/minimist/commit/be2e038c342d8333b32f0fde67a0026b79c8150e) +- Only apps should have lockfiles [`282b570`](https://github.com/minimistjs/minimist/commit/282b570e7489d01b03f2d6d3dabf79cd3e5f84cf) +- isConstructorOrProto adapted from PR [`ef9153f`](https://github.com/minimistjs/minimist/commit/ef9153fc52b6cea0744b2239921c5dcae4697f11) +- [Dev Deps] update `@ljharb/eslint-config`, `aud` [`098873c`](https://github.com/minimistjs/minimist/commit/098873c213cdb7c92e55ae1ef5aa1af3a8192a79) +- [Dev Deps] update `@ljharb/eslint-config`, `aud` [`3124ed3`](https://github.com/minimistjs/minimist/commit/3124ed3e46306301ebb3c834874ce0241555c2c4) +- [meta] add `safe-publish-latest` [`4b927de`](https://github.com/minimistjs/minimist/commit/4b927de696d561c636b4f43bf49d4597cb36d6d6) +- [Tests] add `aud` in `posttest` [`b32d9bd`](https://github.com/minimistjs/minimist/commit/b32d9bd0ab340f4e9f8c3a97ff2a4424f25fab8c) +- [meta] update repo URLs [`f9fdfc0`](https://github.com/minimistjs/minimist/commit/f9fdfc032c54884d9a9996a390c63cd0719bbe1a) +- [actions] Avoid 0.6 tests due to build failures [`ba92fe6`](https://github.com/minimistjs/minimist/commit/ba92fe6ebbdc0431cca9a2ea8f27beb492f5e4ec) +- [Dev Deps] update `tape` [`950eaa7`](https://github.com/minimistjs/minimist/commit/950eaa74f112e04d23e9c606c67472c46739b473) +- [Dev Deps] add missing `npmignore` dev dep [`3226afa`](https://github.com/minimistjs/minimist/commit/3226afaf09e9d127ca369742437fe6e88f752d6b) +- Merge tag 'v0.2.2' [`980d7ac`](https://github.com/minimistjs/minimist/commit/980d7ac61a0b4bd552711251ac107d506b23e41f) + +## [v1.2.7](https://github.com/minimistjs/minimist/compare/v1.2.6...v1.2.7) - 2022-10-10 + +### Commits + +- [meta] add `auto-changelog` [`0ebf4eb`](https://github.com/minimistjs/minimist/commit/0ebf4ebcd5f7787a5524d31a849ef41316b83c3c) +- [actions] add reusable workflows [`e115b63`](https://github.com/minimistjs/minimist/commit/e115b63fa9d3909f33b00a2db647ff79068388de) +- [eslint] add eslint; rules to enable later are warnings [`f58745b`](https://github.com/minimistjs/minimist/commit/f58745b9bb84348e1be72af7dbba5840c7c13013) +- [Dev Deps] switch from `covert` to `nyc` [`ab03356`](https://github.com/minimistjs/minimist/commit/ab033567b9c8b31117cb026dc7f1e592ce455c65) +- [readme] rename and add badges [`236f4a0`](https://github.com/minimistjs/minimist/commit/236f4a07e4ebe5ee44f1496ec6974991ab293ffd) +- [meta] create FUNDING.yml; add `funding` in package.json [`783a49b`](https://github.com/minimistjs/minimist/commit/783a49bfd47e8335d3098a8cac75662cf71eb32a) +- [meta] use `npmignore` to autogenerate an npmignore file [`f81ece6`](https://github.com/minimistjs/minimist/commit/f81ece6aaec2fa14e69ff4f1e0407a8c4e2635a2) +- Only apps should have lockfiles [`56cad44`](https://github.com/minimistjs/minimist/commit/56cad44c7f879b9bb5ec18fcc349308024a89bfc) +- [Dev Deps] update `covert`, `tape`; remove unnecessary `tap` [`49c5f9f`](https://github.com/minimistjs/minimist/commit/49c5f9fb7e6a92db9eb340cc679de92fb3aacded) +- [Tests] add `aud` in `posttest` [`228ae93`](https://github.com/minimistjs/minimist/commit/228ae938f3cd9db9dfd8bd7458b076a7b2aef280) +- [meta] add `safe-publish-latest` [`01fc23f`](https://github.com/minimistjs/minimist/commit/01fc23f5104f85c75059972e01dd33796ab529ff) +- [meta] update repo URLs [`6b164c7`](https://github.com/minimistjs/minimist/commit/6b164c7d68e0b6bf32f894699effdfb7c63041dd) + +## [v1.2.6](https://github.com/minimistjs/minimist/compare/v1.2.5...v1.2.6) - 2022-03-21 + +### Commits + +- test from prototype pollution PR [`bc8ecee`](https://github.com/minimistjs/minimist/commit/bc8ecee43875261f4f17eb20b1243d3ed15e70eb) +- isConstructorOrProto adapted from PR [`c2b9819`](https://github.com/minimistjs/minimist/commit/c2b981977fa834b223b408cfb860f933c9811e4d) +- security notice for additional prototype pollution issue [`ef88b93`](https://github.com/minimistjs/minimist/commit/ef88b9325f77b5ee643ccfc97e2ebda577e4c4e2) + +## [v1.2.5](https://github.com/minimistjs/minimist/compare/v1.2.4...v1.2.5) - 2020-03-12 + +## [v1.2.4](https://github.com/minimistjs/minimist/compare/v1.2.3...v1.2.4) - 2020-03-11 + +### Commits + +- security notice [`4cf1354`](https://github.com/minimistjs/minimist/commit/4cf1354839cb972e38496d35e12f806eea92c11f) +- additional test for constructor prototype pollution [`1043d21`](https://github.com/minimistjs/minimist/commit/1043d212c3caaf871966e710f52cfdf02f9eea4b) + +## [v1.2.3](https://github.com/minimistjs/minimist/compare/v1.2.2...v1.2.3) - 2020-03-10 + +### Commits + +- more failing proto pollution tests [`13c01a5`](https://github.com/minimistjs/minimist/commit/13c01a5327736903704984b7f65616b8476850cc) +- even more aggressive checks for protocol pollution [`38a4d1c`](https://github.com/minimistjs/minimist/commit/38a4d1caead72ef99e824bb420a2528eec03d9ab) + +## [v1.2.2](https://github.com/minimistjs/minimist/compare/v1.2.1...v1.2.2) - 2020-03-10 + +### Commits + +- failing test for protocol pollution [`0efed03`](https://github.com/minimistjs/minimist/commit/0efed0340ec8433638758f7ca0c77cb20a0bfbab) +- cleanup [`67d3722`](https://github.com/minimistjs/minimist/commit/67d3722413448d00a62963d2d30c34656a92d7e2) +- console.dir -> console.log [`47acf72`](https://github.com/minimistjs/minimist/commit/47acf72c715a630bf9ea013867f47f1dd69dfc54) +- don't assign onto __proto__ [`63e7ed0`](https://github.com/minimistjs/minimist/commit/63e7ed05aa4b1889ec2f3b196426db4500cbda94) + +## [v1.2.1](https://github.com/minimistjs/minimist/compare/v1.2.0...v1.2.1) - 2020-03-10 + +### Merged + +- move the `opts['--']` example back where it belongs [`#63`](https://github.com/minimistjs/minimist/pull/63) + +### Commits + +- add test [`6be5dae`](https://github.com/minimistjs/minimist/commit/6be5dae35a32a987bcf4137fcd6c19c5200ee909) +- fix bad boolean regexp [`ac3fc79`](https://github.com/minimistjs/minimist/commit/ac3fc796e63b95128fdbdf67ea7fad71bd59aa76) + +## [v1.2.0](https://github.com/minimistjs/minimist/compare/v1.1.3...v1.2.0) - 2015-08-24 + +### Commits + +- failing -k=v short test [`63416b8`](https://github.com/minimistjs/minimist/commit/63416b8cd1d0d70e4714564cce465a36e4dd26d7) +- kv short fix [`6bbe145`](https://github.com/minimistjs/minimist/commit/6bbe14529166245e86424f220a2321442fe88dc3) +- failing kv short test [`f72ab7f`](https://github.com/minimistjs/minimist/commit/f72ab7f4572adc52902c9b6873cc969192f01b10) +- fixed kv test [`f5a48c3`](https://github.com/minimistjs/minimist/commit/f5a48c3e50e40ca54f00c8e84de4b4d6e9897fa8) +- enforce space between arg key and value [`86b321a`](https://github.com/minimistjs/minimist/commit/86b321affe648a8e016c095a4f0efa9d9074f502) + +## [v1.1.3](https://github.com/minimistjs/minimist/compare/v1.1.2...v1.1.3) - 2015-08-06 + +### Commits + +- add failing test - boolean alias array [`0fa3c5b`](https://github.com/minimistjs/minimist/commit/0fa3c5b3dd98551ddecf5392831b4c21211743fc) +- fix boolean values with multiple aliases [`9c0a6e7`](https://github.com/minimistjs/minimist/commit/9c0a6e7de25a273b11bbf9a7464f0bd833779795) + +## [v1.1.2](https://github.com/minimistjs/minimist/compare/v1.1.1...v1.1.2) - 2015-07-22 + +### Commits + +- Convert boolean arguments to boolean values [`8f3dc27`](https://github.com/minimistjs/minimist/commit/8f3dc27cf833f1d54671b6d0bcb55c2fe19672a9) +- use non-ancient npm, node 0.12 and iojs [`61ed1d0`](https://github.com/minimistjs/minimist/commit/61ed1d034b9ec7282764ce76f3992b1a0b4906ae) +- an older npm for 0.8 [`25cf778`](https://github.com/minimistjs/minimist/commit/25cf778b1220e7838a526832ad6972f75244054f) + +## [v1.1.1](https://github.com/minimistjs/minimist/compare/v1.1.0...v1.1.1) - 2015-03-10 + +### Commits + +- check that they type of a value is a boolean, not just that it is currently set to a boolean [`6863198`](https://github.com/minimistjs/minimist/commit/6863198e36139830ff1f20ffdceaddd93f2c1db9) +- upgrade tape, fix type issues from old tape version [`806712d`](https://github.com/minimistjs/minimist/commit/806712df91604ed02b8e39aa372b84aea659ee34) +- test for setting a boolean to a null default [`8c444fe`](https://github.com/minimistjs/minimist/commit/8c444fe89384ded7d441c120915ea60620b01dd3) +- if the previous value was a boolean, without an default (or with an alias) don't make an array either [`e5f419a`](https://github.com/minimistjs/minimist/commit/e5f419a3b5b3bc3f9e5ac71b7040621af70ed2dd) + +## [v1.1.0](https://github.com/minimistjs/minimist/compare/v1.0.0...v1.1.0) - 2014-08-10 + +### Commits + +- add support for handling "unknown" options not registered with the parser. [`6f3cc5d`](https://github.com/minimistjs/minimist/commit/6f3cc5d4e84524932a6ef2ce3592acc67cdd4383) +- reformat package.json [`02ed371`](https://github.com/minimistjs/minimist/commit/02ed37115194d3697ff358e8e25e5e66bab1d9f8) +- coverage script [`e5531ba`](https://github.com/minimistjs/minimist/commit/e5531ba0479da3b8138d3d8cac545d84ccb1c8df) +- extra fn to get 100% coverage again [`a6972da`](https://github.com/minimistjs/minimist/commit/a6972da89e56bf77642f8ec05a13b6558db93498) + +## [v1.0.0](https://github.com/minimistjs/minimist/compare/v0.2.3...v1.0.0) - 2014-08-10 + +### Commits + +- added stopEarly option [`471c7e4`](https://github.com/minimistjs/minimist/commit/471c7e4a7e910fc7ad8f9df850a186daf32c64e9) +- fix list [`fef6ae7`](https://github.com/minimistjs/minimist/commit/fef6ae79c38b9dc1c49569abb7cd04eb965eac5e) + +## [v0.2.3](https://github.com/minimistjs/minimist/compare/v0.2.2...v0.2.3) - 2023-02-09 + +### Merged + +- [Fix] Fix long option followed by single dash [`#17`](https://github.com/minimistjs/minimist/pull/17) +- [Tests] Remove duplicate test [`#12`](https://github.com/minimistjs/minimist/pull/12) +- [Fix] opt.string works with multiple aliases [`#10`](https://github.com/minimistjs/minimist/pull/10) + +### Fixed + +- [Fix] Fix long option followed by single dash (#17) [`#15`](https://github.com/minimistjs/minimist/issues/15) +- [Tests] Remove duplicate test (#12) [`#8`](https://github.com/minimistjs/minimist/issues/8) +- [Fix] opt.string works with multiple aliases (#10) [`#9`](https://github.com/minimistjs/minimist/issues/9) + +### Commits + +- [eslint] fix indentation and whitespace [`e5f5067`](https://github.com/minimistjs/minimist/commit/e5f5067259ceeaf0b098d14bec910f87e58708c7) +- [eslint] more cleanup [`36ac5d0`](https://github.com/minimistjs/minimist/commit/36ac5d0d95e4947d074e5737d94814034ca335d1) +- [eslint] fix indentation [`34b0f1c`](https://github.com/minimistjs/minimist/commit/34b0f1ccaa45183c3c4f06a91f9b405180a6f982) +- isConstructorOrProto adapted from PR [`ef9153f`](https://github.com/minimistjs/minimist/commit/ef9153fc52b6cea0744b2239921c5dcae4697f11) +- [Dev Deps] update `@ljharb/eslint-config`, `aud` [`098873c`](https://github.com/minimistjs/minimist/commit/098873c213cdb7c92e55ae1ef5aa1af3a8192a79) +- [Dev Deps] add missing `npmignore` dev dep [`3226afa`](https://github.com/minimistjs/minimist/commit/3226afaf09e9d127ca369742437fe6e88f752d6b) + +## [v0.2.2](https://github.com/minimistjs/minimist/compare/v0.2.1...v0.2.2) - 2022-10-10 + +### Commits + +- [meta] add `auto-changelog` [`73923d2`](https://github.com/minimistjs/minimist/commit/73923d223553fca08b1ba77e3fbc2a492862ae4c) +- [actions] add reusable workflows [`d80727d`](https://github.com/minimistjs/minimist/commit/d80727df77bfa9e631044d7f16368d8f09242c91) +- [eslint] add eslint; rules to enable later are warnings [`48bc06a`](https://github.com/minimistjs/minimist/commit/48bc06a1b41f00e9cdf183db34f7a51ba70e98d4) +- [readme] rename and add badges [`5df0fe4`](https://github.com/minimistjs/minimist/commit/5df0fe49211bd09a3636f8686a7cb3012c3e98f0) +- [Dev Deps] switch from `covert` to `nyc` [`a48b128`](https://github.com/minimistjs/minimist/commit/a48b128fdb8d427dfb20a15273f83e38d97bef07) +- [Dev Deps] update `covert`, `tape`; remove unnecessary `tap` [`f0fb958`](https://github.com/minimistjs/minimist/commit/f0fb958e9a1fe980cdffc436a211b0bda58f621b) +- [meta] create FUNDING.yml; add `funding` in package.json [`3639e0c`](https://github.com/minimistjs/minimist/commit/3639e0c819359a366387e425ab6eabf4c78d3caa) +- [meta] use `npmignore` to autogenerate an npmignore file [`be2e038`](https://github.com/minimistjs/minimist/commit/be2e038c342d8333b32f0fde67a0026b79c8150e) +- Only apps should have lockfiles [`282b570`](https://github.com/minimistjs/minimist/commit/282b570e7489d01b03f2d6d3dabf79cd3e5f84cf) +- [meta] add `safe-publish-latest` [`4b927de`](https://github.com/minimistjs/minimist/commit/4b927de696d561c636b4f43bf49d4597cb36d6d6) +- [Tests] add `aud` in `posttest` [`b32d9bd`](https://github.com/minimistjs/minimist/commit/b32d9bd0ab340f4e9f8c3a97ff2a4424f25fab8c) +- [meta] update repo URLs [`f9fdfc0`](https://github.com/minimistjs/minimist/commit/f9fdfc032c54884d9a9996a390c63cd0719bbe1a) + +## [v0.2.1](https://github.com/minimistjs/minimist/compare/v0.2.0...v0.2.1) - 2020-03-12 + +## [v0.2.0](https://github.com/minimistjs/minimist/compare/v0.1.0...v0.2.0) - 2014-06-19 + +### Commits + +- support all-boolean mode [`450a97f`](https://github.com/minimistjs/minimist/commit/450a97f6e2bc85c7a4a13185c19a818d9a5ebe69) + +## [v0.1.0](https://github.com/minimistjs/minimist/compare/v0.0.10...v0.1.0) - 2014-05-12 + +### Commits + +- Provide a mechanism to segregate -- arguments [`ce4a1e6`](https://github.com/minimistjs/minimist/commit/ce4a1e63a7e8d5ab88d2a3768adefa6af98a445a) +- documented argv['--'] [`14db0e6`](https://github.com/minimistjs/minimist/commit/14db0e6dbc6d2b9e472adaa54dad7004b364634f) +- Adding a test-case for notFlags segregation [`715c1e3`](https://github.com/minimistjs/minimist/commit/715c1e3714be223f998f6c537af6b505f0236c16) + +## [v0.0.10](https://github.com/minimistjs/minimist/compare/v0.0.9...v0.0.10) - 2014-05-11 + +### Commits + +- dedicated boolean test [`46e448f`](https://github.com/minimistjs/minimist/commit/46e448f9f513cfeb2bcc8b688b9b47ba1e515c2b) +- dedicated num test [`9bf2d36`](https://github.com/minimistjs/minimist/commit/9bf2d36f1d3b8795be90b8f7de0a937f098aa394) +- aliased values treated as strings [`1ab743b`](https://github.com/minimistjs/minimist/commit/1ab743bad4484d69f1259bed42f9531de01119de) +- cover the case of already numbers, at 100% coverage [`b2bb044`](https://github.com/minimistjs/minimist/commit/b2bb04436599d77a2ce029e8e555e25b3aa55d13) +- another test for higher coverage [`3662624`](https://github.com/minimistjs/minimist/commit/3662624be976d5489d486a856849c048d13be903) + +## [v0.0.9](https://github.com/minimistjs/minimist/compare/v0.0.8...v0.0.9) - 2014-05-08 + +### Commits + +- Eliminate `longest` fn. [`824f642`](https://github.com/minimistjs/minimist/commit/824f642038d1b02ede68b6261d1d65163390929a) + +## [v0.0.8](https://github.com/minimistjs/minimist/compare/v0.0.7...v0.0.8) - 2014-02-20 + +### Commits + +- return '' if flag is string and empty [`fa63ed4`](https://github.com/minimistjs/minimist/commit/fa63ed4651a4ef4eefddce34188e0d98d745a263) +- handle joined single letters [`66c248f`](https://github.com/minimistjs/minimist/commit/66c248f0241d4d421d193b022e9e365f11178534) + +## [v0.0.7](https://github.com/minimistjs/minimist/compare/v0.0.6...v0.0.7) - 2014-02-08 + +### Commits + +- another swap of .test for .match [`d1da408`](https://github.com/minimistjs/minimist/commit/d1da40819acbe846d89a5c02721211e3c1260dde) + +## [v0.0.6](https://github.com/minimistjs/minimist/compare/v0.0.5...v0.0.6) - 2014-02-08 + +### Commits + +- use .test() instead of .match() to not crash on non-string values in the arguments array [`7e0d1ad`](https://github.com/minimistjs/minimist/commit/7e0d1add8c9e5b9b20a4d3d0f9a94d824c578da1) + +## [v0.0.5](https://github.com/minimistjs/minimist/compare/v0.0.4...v0.0.5) - 2013-09-18 + +### Commits + +- Improve '--' handling. [`b11822c`](https://github.com/minimistjs/minimist/commit/b11822c09cc9d2460f30384d12afc0b953c037a4) + +## [v0.0.4](https://github.com/minimistjs/minimist/compare/v0.0.3...v0.0.4) - 2013-09-17 + +## [v0.0.3](https://github.com/minimistjs/minimist/compare/v0.0.2...v0.0.3) - 2013-09-12 + +### Commits + +- failing test for single dash preceeding a double dash [`b465514`](https://github.com/minimistjs/minimist/commit/b465514b82c9ae28972d714facd951deb2ad762b) +- fix for the dot test [`6a095f1`](https://github.com/minimistjs/minimist/commit/6a095f1d364c8fab2d6753d2291a0649315d297a) + +## [v0.0.2](https://github.com/minimistjs/minimist/compare/v0.0.1...v0.0.2) - 2013-08-28 + +### Commits + +- allow dotted aliases & defaults [`321c33e`](https://github.com/minimistjs/minimist/commit/321c33e755485faaeb44eeb1c05d33b2e0a5a7c4) +- use a better version of ff [`e40f611`](https://github.com/minimistjs/minimist/commit/e40f61114cf7be6f7947f7b3eed345853a67dbbb) + +## [v0.0.1](https://github.com/minimistjs/minimist/compare/v0.0.0...v0.0.1) - 2013-06-25 + +### Commits + +- remove trailing commas [`6ff0fa0`](https://github.com/minimistjs/minimist/commit/6ff0fa055064f15dbe06d50b89d5173a6796e1db) + +## v0.0.0 - 2013-06-25 + +### Commits + +- half of the parse test ported [`3079326`](https://github.com/minimistjs/minimist/commit/307932601325087de6cf94188eb798ffc4f3088a) +- stripped down code and a passing test from optimist [`7cced88`](https://github.com/minimistjs/minimist/commit/7cced88d82e399d1a03ed23eb667f04d3f320d10) +- ported parse tests completely over [`9448754`](https://github.com/minimistjs/minimist/commit/944875452e0820df6830b1408c26a0f7d3e1db04) +- docs, package.json [`a5bf46a`](https://github.com/minimistjs/minimist/commit/a5bf46ac9bb3bd114a9c340276c62c1091e538d5) +- move more short tests into short.js [`503edb5`](https://github.com/minimistjs/minimist/commit/503edb5c41d89c0d40831ee517154fc13b0f18b9) +- default bool test was wrong, not the code [`1b9f5db`](https://github.com/minimistjs/minimist/commit/1b9f5db4741b49962846081b68518de824992097) +- passing long tests ripped out of parse.js [`7972c4a`](https://github.com/minimistjs/minimist/commit/7972c4aff1f4803079e1668006658e2a761a0428) +- badges [`84c0370`](https://github.com/minimistjs/minimist/commit/84c037063664d42878aace715fe6572ce01b6f3b) +- all the tests now ported, some failures [`64239ed`](https://github.com/minimistjs/minimist/commit/64239edfe92c711c4eb0da254fcdfad2a5fdb605) +- failing short test [`f8a5341`](https://github.com/minimistjs/minimist/commit/f8a534112dd1138d2fad722def56a848480c446f) +- fixed the numeric test [`6b034f3`](https://github.com/minimistjs/minimist/commit/6b034f37c79342c60083ed97fd222e16928aac51) diff --git a/backend/node_modules/minimist/LICENSE b/backend/node_modules/minimist/LICENSE new file mode 100644 index 00000000..ee27ba4b --- /dev/null +++ b/backend/node_modules/minimist/LICENSE @@ -0,0 +1,18 @@ +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/minimist/README.md b/backend/node_modules/minimist/README.md new file mode 100644 index 00000000..74da3234 --- /dev/null +++ b/backend/node_modules/minimist/README.md @@ -0,0 +1,121 @@ +# minimist [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + +parse argument options + +This module is the guts of optimist's argument parser without all the +fanciful decoration. + +# example + +``` js +var argv = require('minimist')(process.argv.slice(2)); +console.log(argv); +``` + +``` +$ node example/parse.js -a beep -b boop +{ _: [], a: 'beep', b: 'boop' } +``` + +``` +$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz +{ + _: ['foo', 'bar', 'baz'], + x: 3, + y: 4, + n: 5, + a: true, + b: true, + c: true, + beep: 'boop' +} +``` + +# security + +Previous versions had a prototype pollution bug that could cause privilege +escalation in some circumstances when handling untrusted user input. + +Please use version 1.2.6 or later: + +* https://security.snyk.io/vuln/SNYK-JS-MINIMIST-2429795 (version <=1.2.5) +* https://snyk.io/vuln/SNYK-JS-MINIMIST-559764 (version <=1.2.3) + +# methods + +``` js +var parseArgs = require('minimist') +``` + +## var argv = parseArgs(args, opts={}) + +Return an argument object `argv` populated with the array arguments from `args`. + +`argv._` contains all the arguments that didn't have an option associated with +them. + +Numeric-looking arguments will be returned as numbers unless `opts.string` or +`opts.boolean` is set for that argument name. + +Any arguments after `'--'` will not be parsed and will end up in `argv._`. + +options can be: + +* `opts.string` - a string or array of strings argument names to always treat as +strings +* `opts.boolean` - a boolean, string or array of strings to always treat as +booleans. if `true` will treat all double hyphenated arguments without equal signs +as boolean (e.g. affects `--foo`, not `-f` or `--foo=bar`) +* `opts.alias` - an object mapping string names to strings or arrays of string +argument names to use as aliases +* `opts.default` - an object mapping string argument names to default values +* `opts.stopEarly` - when true, populate `argv._` with everything after the +first non-option +* `opts['--']` - when true, populate `argv._` with everything before the `--` +and `argv['--']` with everything after the `--`. Here's an example: + + ``` + > require('./')('one two three -- four five --six'.split(' '), { '--': true }) + { + _: ['one', 'two', 'three'], + '--': ['four', 'five', '--six'] + } + ``` + + Note that with `opts['--']` set, parsing for arguments still stops after the + `--`. + +* `opts.unknown` - a function which is invoked with a command line parameter not +defined in the `opts` configuration object. If the function returns `false`, the +unknown option is not added to `argv`. + +# install + +With [npm](https://npmjs.org) do: + +``` +npm install minimist +``` + +# license + +MIT + +[package-url]: https://npmjs.org/package/minimist +[npm-version-svg]: https://versionbadg.es/minimistjs/minimist.svg +[npm-badge-png]: https://nodei.co/npm/minimist.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/minimist.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/minimist.svg +[downloads-url]: https://npm-stat.com/charts.html?package=minimist +[codecov-image]: https://codecov.io/gh/minimistjs/minimist/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/minimistjs/minimist/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/minimistjs/minimist +[actions-url]: https://github.com/minimistjs/minimist/actions diff --git a/backend/node_modules/minimist/example/parse.js b/backend/node_modules/minimist/example/parse.js new file mode 100644 index 00000000..9d90ffb2 --- /dev/null +++ b/backend/node_modules/minimist/example/parse.js @@ -0,0 +1,4 @@ +'use strict'; + +var argv = require('../')(process.argv.slice(2)); +console.log(argv); diff --git a/backend/node_modules/minimist/index.js b/backend/node_modules/minimist/index.js new file mode 100644 index 00000000..f020f394 --- /dev/null +++ b/backend/node_modules/minimist/index.js @@ -0,0 +1,263 @@ +'use strict'; + +function hasKey(obj, keys) { + var o = obj; + keys.slice(0, -1).forEach(function (key) { + o = o[key] || {}; + }); + + var key = keys[keys.length - 1]; + return key in o; +} + +function isNumber(x) { + if (typeof x === 'number') { return true; } + if ((/^0x[0-9a-f]+$/i).test(x)) { return true; } + return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x); +} + +function isConstructorOrProto(obj, key) { + return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__'; +} + +module.exports = function (args, opts) { + if (!opts) { opts = {}; } + + var flags = { + bools: {}, + strings: {}, + unknownFn: null, + }; + + if (typeof opts.unknown === 'function') { + flags.unknownFn = opts.unknown; + } + + if (typeof opts.boolean === 'boolean' && opts.boolean) { + flags.allBools = true; + } else { + [].concat(opts.boolean).filter(Boolean).forEach(function (key) { + flags.bools[key] = true; + }); + } + + var aliases = {}; + + function aliasIsBoolean(key) { + return aliases[key].some(function (x) { + return flags.bools[x]; + }); + } + + Object.keys(opts.alias || {}).forEach(function (key) { + aliases[key] = [].concat(opts.alias[key]); + aliases[key].forEach(function (x) { + aliases[x] = [key].concat(aliases[key].filter(function (y) { + return x !== y; + })); + }); + }); + + [].concat(opts.string).filter(Boolean).forEach(function (key) { + flags.strings[key] = true; + if (aliases[key]) { + [].concat(aliases[key]).forEach(function (k) { + flags.strings[k] = true; + }); + } + }); + + var defaults = opts.default || {}; + + var argv = { _: [] }; + + function argDefined(key, arg) { + return (flags.allBools && (/^--[^=]+$/).test(arg)) + || flags.strings[key] + || flags.bools[key] + || aliases[key]; + } + + function setKey(obj, keys, value) { + var o = obj; + for (var i = 0; i < keys.length - 1; i++) { + var key = keys[i]; + if (isConstructorOrProto(o, key)) { return; } + if (o[key] === undefined) { o[key] = {}; } + if ( + o[key] === Object.prototype + || o[key] === Number.prototype + || o[key] === String.prototype + ) { + o[key] = {}; + } + if (o[key] === Array.prototype) { o[key] = []; } + o = o[key]; + } + + var lastKey = keys[keys.length - 1]; + if (isConstructorOrProto(o, lastKey)) { return; } + if ( + o === Object.prototype + || o === Number.prototype + || o === String.prototype + ) { + o = {}; + } + if (o === Array.prototype) { o = []; } + if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === 'boolean') { + o[lastKey] = value; + } else if (Array.isArray(o[lastKey])) { + o[lastKey].push(value); + } else { + o[lastKey] = [o[lastKey], value]; + } + } + + function setArg(key, val, arg) { + if (arg && flags.unknownFn && !argDefined(key, arg)) { + if (flags.unknownFn(arg) === false) { return; } + } + + var value = !flags.strings[key] && isNumber(val) + ? Number(val) + : val; + setKey(argv, key.split('.'), value); + + (aliases[key] || []).forEach(function (x) { + setKey(argv, x.split('.'), value); + }); + } + + Object.keys(flags.bools).forEach(function (key) { + setArg(key, defaults[key] === undefined ? false : defaults[key]); + }); + + var notFlags = []; + + if (args.indexOf('--') !== -1) { + notFlags = args.slice(args.indexOf('--') + 1); + args = args.slice(0, args.indexOf('--')); + } + + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + var key; + var next; + + if ((/^--.+=/).test(arg)) { + // Using [\s\S] instead of . because js doesn't support the + // 'dotall' regex modifier. See: + // http://stackoverflow.com/a/1068308/13216 + var m = arg.match(/^--([^=]+)=([\s\S]*)$/); + key = m[1]; + var value = m[2]; + if (flags.bools[key]) { + value = value !== 'false'; + } + setArg(key, value, arg); + } else if ((/^--no-.+/).test(arg)) { + key = arg.match(/^--no-(.+)/)[1]; + setArg(key, false, arg); + } else if ((/^--.+/).test(arg)) { + key = arg.match(/^--(.+)/)[1]; + next = args[i + 1]; + if ( + next !== undefined + && !(/^(-|--)[^-]/).test(next) + && !flags.bools[key] + && !flags.allBools + && (aliases[key] ? !aliasIsBoolean(key) : true) + ) { + setArg(key, next, arg); + i += 1; + } else if ((/^(true|false)$/).test(next)) { + setArg(key, next === 'true', arg); + i += 1; + } else { + setArg(key, flags.strings[key] ? '' : true, arg); + } + } else if ((/^-[^-]+/).test(arg)) { + var letters = arg.slice(1, -1).split(''); + + var broken = false; + for (var j = 0; j < letters.length; j++) { + next = arg.slice(j + 2); + + if (next === '-') { + setArg(letters[j], next, arg); + continue; + } + + if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') { + setArg(letters[j], next.slice(1), arg); + broken = true; + break; + } + + if ( + (/[A-Za-z]/).test(letters[j]) + && (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next) + ) { + setArg(letters[j], next, arg); + broken = true; + break; + } + + if (letters[j + 1] && letters[j + 1].match(/\W/)) { + setArg(letters[j], arg.slice(j + 2), arg); + broken = true; + break; + } else { + setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg); + } + } + + key = arg.slice(-1)[0]; + if (!broken && key !== '-') { + if ( + args[i + 1] + && !(/^(-|--)[^-]/).test(args[i + 1]) + && !flags.bools[key] + && (aliases[key] ? !aliasIsBoolean(key) : true) + ) { + setArg(key, args[i + 1], arg); + i += 1; + } else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) { + setArg(key, args[i + 1] === 'true', arg); + i += 1; + } else { + setArg(key, flags.strings[key] ? '' : true, arg); + } + } + } else { + if (!flags.unknownFn || flags.unknownFn(arg) !== false) { + argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg)); + } + if (opts.stopEarly) { + argv._.push.apply(argv._, args.slice(i + 1)); + break; + } + } + } + + Object.keys(defaults).forEach(function (k) { + if (!hasKey(argv, k.split('.'))) { + setKey(argv, k.split('.'), defaults[k]); + + (aliases[k] || []).forEach(function (x) { + setKey(argv, x.split('.'), defaults[k]); + }); + } + }); + + if (opts['--']) { + argv['--'] = notFlags.slice(); + } else { + notFlags.forEach(function (k) { + argv._.push(k); + }); + } + + return argv; +}; diff --git a/backend/node_modules/minimist/package.json b/backend/node_modules/minimist/package.json new file mode 100644 index 00000000..c10a3344 --- /dev/null +++ b/backend/node_modules/minimist/package.json @@ -0,0 +1,75 @@ +{ + "name": "minimist", + "version": "1.2.8", + "description": "parse argument options", + "main": "index.js", + "devDependencies": { + "@ljharb/eslint-config": "^21.0.1", + "aud": "^2.0.2", + "auto-changelog": "^2.4.0", + "eslint": "=8.8.0", + "in-publish": "^2.0.1", + "npmignore": "^0.3.0", + "nyc": "^10.3.2", + "safe-publish-latest": "^2.0.0", + "tape": "^5.6.3" + }, + "scripts": { + "prepack": "npmignore --auto --commentLines=auto", + "prepublishOnly": "safe-publish-latest", + "prepublish": "not-in-publish || npm run prepublishOnly", + "lint": "eslint --ext=js,mjs .", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "aud --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "testling": { + "files": "test/*.js", + "browsers": [ + "ie/6..latest", + "ff/5", + "firefox/latest", + "chrome/10", + "chrome/latest", + "safari/5.1", + "safari/latest", + "opera/12" + ] + }, + "repository": { + "type": "git", + "url": "git://github.com/minimistjs/minimist.git" + }, + "homepage": "https://github.com/minimistjs/minimist", + "keywords": [ + "argv", + "getopt", + "parser", + "optimist" + ], + "author": { + "name": "James Halliday", + "email": "mail@substack.net", + "url": "http://substack.net" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "license": "MIT", + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + } +} diff --git a/backend/node_modules/minimist/test/all_bool.js b/backend/node_modules/minimist/test/all_bool.js new file mode 100644 index 00000000..befa0c99 --- /dev/null +++ b/backend/node_modules/minimist/test/all_bool.js @@ -0,0 +1,34 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('flag boolean true (default all --args to boolean)', function (t) { + var argv = parse(['moo', '--honk', 'cow'], { + boolean: true, + }); + + t.deepEqual(argv, { + honk: true, + _: ['moo', 'cow'], + }); + + t.deepEqual(typeof argv.honk, 'boolean'); + t.end(); +}); + +test('flag boolean true only affects double hyphen arguments without equals signs', function (t) { + var argv = parse(['moo', '--honk', 'cow', '-p', '55', '--tacos=good'], { + boolean: true, + }); + + t.deepEqual(argv, { + honk: true, + tacos: 'good', + p: 55, + _: ['moo', 'cow'], + }); + + t.deepEqual(typeof argv.honk, 'boolean'); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/bool.js b/backend/node_modules/minimist/test/bool.js new file mode 100644 index 00000000..e58d47e4 --- /dev/null +++ b/backend/node_modules/minimist/test/bool.js @@ -0,0 +1,177 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('flag boolean default false', function (t) { + var argv = parse(['moo'], { + boolean: ['t', 'verbose'], + default: { verbose: false, t: false }, + }); + + t.deepEqual(argv, { + verbose: false, + t: false, + _: ['moo'], + }); + + t.deepEqual(typeof argv.verbose, 'boolean'); + t.deepEqual(typeof argv.t, 'boolean'); + t.end(); + +}); + +test('boolean groups', function (t) { + var argv = parse(['-x', '-z', 'one', 'two', 'three'], { + boolean: ['x', 'y', 'z'], + }); + + t.deepEqual(argv, { + x: true, + y: false, + z: true, + _: ['one', 'two', 'three'], + }); + + t.deepEqual(typeof argv.x, 'boolean'); + t.deepEqual(typeof argv.y, 'boolean'); + t.deepEqual(typeof argv.z, 'boolean'); + t.end(); +}); +test('boolean and alias with chainable api', function (t) { + var aliased = ['-h', 'derp']; + var regular = ['--herp', 'derp']; + var aliasedArgv = parse(aliased, { + boolean: 'herp', + alias: { h: 'herp' }, + }); + var propertyArgv = parse(regular, { + boolean: 'herp', + alias: { h: 'herp' }, + }); + var expected = { + herp: true, + h: true, + _: ['derp'], + }; + + t.same(aliasedArgv, expected); + t.same(propertyArgv, expected); + t.end(); +}); + +test('boolean and alias with options hash', function (t) { + var aliased = ['-h', 'derp']; + var regular = ['--herp', 'derp']; + var opts = { + alias: { h: 'herp' }, + boolean: 'herp', + }; + var aliasedArgv = parse(aliased, opts); + var propertyArgv = parse(regular, opts); + var expected = { + herp: true, + h: true, + _: ['derp'], + }; + t.same(aliasedArgv, expected); + t.same(propertyArgv, expected); + t.end(); +}); + +test('boolean and alias array with options hash', function (t) { + var aliased = ['-h', 'derp']; + var regular = ['--herp', 'derp']; + var alt = ['--harp', 'derp']; + var opts = { + alias: { h: ['herp', 'harp'] }, + boolean: 'h', + }; + var aliasedArgv = parse(aliased, opts); + var propertyArgv = parse(regular, opts); + var altPropertyArgv = parse(alt, opts); + var expected = { + harp: true, + herp: true, + h: true, + _: ['derp'], + }; + t.same(aliasedArgv, expected); + t.same(propertyArgv, expected); + t.same(altPropertyArgv, expected); + t.end(); +}); + +test('boolean and alias using explicit true', function (t) { + var aliased = ['-h', 'true']; + var regular = ['--herp', 'true']; + var opts = { + alias: { h: 'herp' }, + boolean: 'h', + }; + var aliasedArgv = parse(aliased, opts); + var propertyArgv = parse(regular, opts); + var expected = { + herp: true, + h: true, + _: [], + }; + + t.same(aliasedArgv, expected); + t.same(propertyArgv, expected); + t.end(); +}); + +// regression, see https://github.com/substack/node-optimist/issues/71 +test('boolean and --x=true', function (t) { + var parsed = parse(['--boool', '--other=true'], { + boolean: 'boool', + }); + + t.same(parsed.boool, true); + t.same(parsed.other, 'true'); + + parsed = parse(['--boool', '--other=false'], { + boolean: 'boool', + }); + + t.same(parsed.boool, true); + t.same(parsed.other, 'false'); + t.end(); +}); + +test('boolean --boool=true', function (t) { + var parsed = parse(['--boool=true'], { + default: { + boool: false, + }, + boolean: ['boool'], + }); + + t.same(parsed.boool, true); + t.end(); +}); + +test('boolean --boool=false', function (t) { + var parsed = parse(['--boool=false'], { + default: { + boool: true, + }, + boolean: ['boool'], + }); + + t.same(parsed.boool, false); + t.end(); +}); + +test('boolean using something similar to true', function (t) { + var opts = { boolean: 'h' }; + var result = parse(['-h', 'true.txt'], opts); + var expected = { + h: true, + _: ['true.txt'], + }; + + t.same(result, expected); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/dash.js b/backend/node_modules/minimist/test/dash.js new file mode 100644 index 00000000..70788177 --- /dev/null +++ b/backend/node_modules/minimist/test/dash.js @@ -0,0 +1,43 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('-', function (t) { + t.plan(6); + t.deepEqual(parse(['-n', '-']), { n: '-', _: [] }); + t.deepEqual(parse(['--nnn', '-']), { nnn: '-', _: [] }); + t.deepEqual(parse(['-']), { _: ['-'] }); + t.deepEqual(parse(['-f-']), { f: '-', _: [] }); + t.deepEqual( + parse(['-b', '-'], { boolean: 'b' }), + { b: true, _: ['-'] } + ); + t.deepEqual( + parse(['-s', '-'], { string: 's' }), + { s: '-', _: [] } + ); +}); + +test('-a -- b', function (t) { + t.plan(2); + t.deepEqual(parse(['-a', '--', 'b']), { a: true, _: ['b'] }); + t.deepEqual(parse(['--a', '--', 'b']), { a: true, _: ['b'] }); +}); + +test('move arguments after the -- into their own `--` array', function (t) { + t.plan(1); + t.deepEqual( + parse(['--name', 'John', 'before', '--', 'after'], { '--': true }), + { name: 'John', _: ['before'], '--': ['after'] } + ); +}); + +test('--- option value', function (t) { + // A multi-dash value is largely an edge case, but check the behaviour is as expected, + // and in particular the same for short option and long option (as made consistent in Jan 2023). + t.plan(2); + t.deepEqual(parse(['-n', '---']), { n: '---', _: [] }); + t.deepEqual(parse(['--nnn', '---']), { nnn: '---', _: [] }); +}); + diff --git a/backend/node_modules/minimist/test/default_bool.js b/backend/node_modules/minimist/test/default_bool.js new file mode 100644 index 00000000..4e9f6250 --- /dev/null +++ b/backend/node_modules/minimist/test/default_bool.js @@ -0,0 +1,37 @@ +'use strict'; + +var test = require('tape'); +var parse = require('../'); + +test('boolean default true', function (t) { + var argv = parse([], { + boolean: 'sometrue', + default: { sometrue: true }, + }); + t.equal(argv.sometrue, true); + t.end(); +}); + +test('boolean default false', function (t) { + var argv = parse([], { + boolean: 'somefalse', + default: { somefalse: false }, + }); + t.equal(argv.somefalse, false); + t.end(); +}); + +test('boolean default to null', function (t) { + var argv = parse([], { + boolean: 'maybe', + default: { maybe: null }, + }); + t.equal(argv.maybe, null); + + var argvLong = parse(['--maybe'], { + boolean: 'maybe', + default: { maybe: null }, + }); + t.equal(argvLong.maybe, true); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/dotted.js b/backend/node_modules/minimist/test/dotted.js new file mode 100644 index 00000000..126ff033 --- /dev/null +++ b/backend/node_modules/minimist/test/dotted.js @@ -0,0 +1,24 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('dotted alias', function (t) { + var argv = parse(['--a.b', '22'], { default: { 'a.b': 11 }, alias: { 'a.b': 'aa.bb' } }); + t.equal(argv.a.b, 22); + t.equal(argv.aa.bb, 22); + t.end(); +}); + +test('dotted default', function (t) { + var argv = parse('', { default: { 'a.b': 11 }, alias: { 'a.b': 'aa.bb' } }); + t.equal(argv.a.b, 11); + t.equal(argv.aa.bb, 11); + t.end(); +}); + +test('dotted default with no alias', function (t) { + var argv = parse('', { default: { 'a.b': 11 } }); + t.equal(argv.a.b, 11); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/kv_short.js b/backend/node_modules/minimist/test/kv_short.js new file mode 100644 index 00000000..6d1b53a7 --- /dev/null +++ b/backend/node_modules/minimist/test/kv_short.js @@ -0,0 +1,32 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('short -k=v', function (t) { + t.plan(1); + + var argv = parse(['-b=123']); + t.deepEqual(argv, { b: 123, _: [] }); +}); + +test('multi short -k=v', function (t) { + t.plan(1); + + var argv = parse(['-a=whatever', '-b=robots']); + t.deepEqual(argv, { a: 'whatever', b: 'robots', _: [] }); +}); + +test('short with embedded equals -k=a=b', function (t) { + t.plan(1); + + var argv = parse(['-k=a=b']); + t.deepEqual(argv, { k: 'a=b', _: [] }); +}); + +test('short with later equals like -ab=c', function (t) { + t.plan(1); + + var argv = parse(['-ab=c']); + t.deepEqual(argv, { a: true, b: 'c', _: [] }); +}); diff --git a/backend/node_modules/minimist/test/long.js b/backend/node_modules/minimist/test/long.js new file mode 100644 index 00000000..9fef51f1 --- /dev/null +++ b/backend/node_modules/minimist/test/long.js @@ -0,0 +1,33 @@ +'use strict'; + +var test = require('tape'); +var parse = require('../'); + +test('long opts', function (t) { + t.deepEqual( + parse(['--bool']), + { bool: true, _: [] }, + 'long boolean' + ); + t.deepEqual( + parse(['--pow', 'xixxle']), + { pow: 'xixxle', _: [] }, + 'long capture sp' + ); + t.deepEqual( + parse(['--pow=xixxle']), + { pow: 'xixxle', _: [] }, + 'long capture eq' + ); + t.deepEqual( + parse(['--host', 'localhost', '--port', '555']), + { host: 'localhost', port: 555, _: [] }, + 'long captures sp' + ); + t.deepEqual( + parse(['--host=localhost', '--port=555']), + { host: 'localhost', port: 555, _: [] }, + 'long captures eq' + ); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/num.js b/backend/node_modules/minimist/test/num.js new file mode 100644 index 00000000..074393ec --- /dev/null +++ b/backend/node_modules/minimist/test/num.js @@ -0,0 +1,38 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('nums', function (t) { + var argv = parse([ + '-x', '1234', + '-y', '5.67', + '-z', '1e7', + '-w', '10f', + '--hex', '0xdeadbeef', + '789', + ]); + t.deepEqual(argv, { + x: 1234, + y: 5.67, + z: 1e7, + w: '10f', + hex: 0xdeadbeef, + _: [789], + }); + t.deepEqual(typeof argv.x, 'number'); + t.deepEqual(typeof argv.y, 'number'); + t.deepEqual(typeof argv.z, 'number'); + t.deepEqual(typeof argv.w, 'string'); + t.deepEqual(typeof argv.hex, 'number'); + t.deepEqual(typeof argv._[0], 'number'); + t.end(); +}); + +test('already a number', function (t) { + var argv = parse(['-x', 1234, 789]); + t.deepEqual(argv, { x: 1234, _: [789] }); + t.deepEqual(typeof argv.x, 'number'); + t.deepEqual(typeof argv._[0], 'number'); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/parse.js b/backend/node_modules/minimist/test/parse.js new file mode 100644 index 00000000..65d9d909 --- /dev/null +++ b/backend/node_modules/minimist/test/parse.js @@ -0,0 +1,209 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('parse args', function (t) { + t.deepEqual( + parse(['--no-moo']), + { moo: false, _: [] }, + 'no' + ); + t.deepEqual( + parse(['-v', 'a', '-v', 'b', '-v', 'c']), + { v: ['a', 'b', 'c'], _: [] }, + 'multi' + ); + t.end(); +}); + +test('comprehensive', function (t) { + t.deepEqual( + parse([ + '--name=meowmers', 'bare', '-cats', 'woo', + '-h', 'awesome', '--multi=quux', + '--key', 'value', + '-b', '--bool', '--no-meep', '--multi=baz', + '--', '--not-a-flag', 'eek', + ]), + { + c: true, + a: true, + t: true, + s: 'woo', + h: 'awesome', + b: true, + bool: true, + key: 'value', + multi: ['quux', 'baz'], + meep: false, + name: 'meowmers', + _: ['bare', '--not-a-flag', 'eek'], + } + ); + t.end(); +}); + +test('flag boolean', function (t) { + var argv = parse(['-t', 'moo'], { boolean: 't' }); + t.deepEqual(argv, { t: true, _: ['moo'] }); + t.deepEqual(typeof argv.t, 'boolean'); + t.end(); +}); + +test('flag boolean value', function (t) { + var argv = parse(['--verbose', 'false', 'moo', '-t', 'true'], { + boolean: ['t', 'verbose'], + default: { verbose: true }, + }); + + t.deepEqual(argv, { + verbose: false, + t: true, + _: ['moo'], + }); + + t.deepEqual(typeof argv.verbose, 'boolean'); + t.deepEqual(typeof argv.t, 'boolean'); + t.end(); +}); + +test('newlines in params', function (t) { + var args = parse(['-s', 'X\nX']); + t.deepEqual(args, { _: [], s: 'X\nX' }); + + // reproduce in bash: + // VALUE="new + // line" + // node program.js --s="$VALUE" + args = parse(['--s=X\nX']); + t.deepEqual(args, { _: [], s: 'X\nX' }); + t.end(); +}); + +test('strings', function (t) { + var s = parse(['-s', '0001234'], { string: 's' }).s; + t.equal(s, '0001234'); + t.equal(typeof s, 'string'); + + var x = parse(['-x', '56'], { string: 'x' }).x; + t.equal(x, '56'); + t.equal(typeof x, 'string'); + t.end(); +}); + +test('stringArgs', function (t) { + var s = parse([' ', ' '], { string: '_' })._; + t.same(s.length, 2); + t.same(typeof s[0], 'string'); + t.same(s[0], ' '); + t.same(typeof s[1], 'string'); + t.same(s[1], ' '); + t.end(); +}); + +test('empty strings', function (t) { + var s = parse(['-s'], { string: 's' }).s; + t.equal(s, ''); + t.equal(typeof s, 'string'); + + var str = parse(['--str'], { string: 'str' }).str; + t.equal(str, ''); + t.equal(typeof str, 'string'); + + var letters = parse(['-art'], { + string: ['a', 't'], + }); + + t.equal(letters.a, ''); + t.equal(letters.r, true); + t.equal(letters.t, ''); + + t.end(); +}); + +test('string and alias', function (t) { + var x = parse(['--str', '000123'], { + string: 's', + alias: { s: 'str' }, + }); + + t.equal(x.str, '000123'); + t.equal(typeof x.str, 'string'); + t.equal(x.s, '000123'); + t.equal(typeof x.s, 'string'); + + var y = parse(['-s', '000123'], { + string: 'str', + alias: { str: 's' }, + }); + + t.equal(y.str, '000123'); + t.equal(typeof y.str, 'string'); + t.equal(y.s, '000123'); + t.equal(typeof y.s, 'string'); + + var z = parse(['-s123'], { + alias: { str: ['s', 'S'] }, + string: ['str'], + }); + + t.deepEqual( + z, + { _: [], s: '123', S: '123', str: '123' }, + 'opt.string works with multiple aliases' + ); + t.end(); +}); + +test('slashBreak', function (t) { + t.same( + parse(['-I/foo/bar/baz']), + { I: '/foo/bar/baz', _: [] } + ); + t.same( + parse(['-xyz/foo/bar/baz']), + { x: true, y: true, z: '/foo/bar/baz', _: [] } + ); + t.end(); +}); + +test('alias', function (t) { + var argv = parse(['-f', '11', '--zoom', '55'], { + alias: { z: 'zoom' }, + }); + t.equal(argv.zoom, 55); + t.equal(argv.z, argv.zoom); + t.equal(argv.f, 11); + t.end(); +}); + +test('multiAlias', function (t) { + var argv = parse(['-f', '11', '--zoom', '55'], { + alias: { z: ['zm', 'zoom'] }, + }); + t.equal(argv.zoom, 55); + t.equal(argv.z, argv.zoom); + t.equal(argv.z, argv.zm); + t.equal(argv.f, 11); + t.end(); +}); + +test('nested dotted objects', function (t) { + var argv = parse([ + '--foo.bar', '3', '--foo.baz', '4', + '--foo.quux.quibble', '5', '--foo.quux.o_O', + '--beep.boop', + ]); + + t.same(argv.foo, { + bar: 3, + baz: 4, + quux: { + quibble: 5, + o_O: true, + }, + }); + t.same(argv.beep, { boop: true }); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/parse_modified.js b/backend/node_modules/minimist/test/parse_modified.js new file mode 100644 index 00000000..32965d13 --- /dev/null +++ b/backend/node_modules/minimist/test/parse_modified.js @@ -0,0 +1,11 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('parse with modifier functions', function (t) { + t.plan(1); + + var argv = parse(['-b', '123'], { boolean: 'b' }); + t.deepEqual(argv, { b: true, _: [123] }); +}); diff --git a/backend/node_modules/minimist/test/proto.js b/backend/node_modules/minimist/test/proto.js new file mode 100644 index 00000000..6e629dd3 --- /dev/null +++ b/backend/node_modules/minimist/test/proto.js @@ -0,0 +1,64 @@ +'use strict'; + +/* eslint no-proto: 0 */ + +var parse = require('../'); +var test = require('tape'); + +test('proto pollution', function (t) { + var argv = parse(['--__proto__.x', '123']); + t.equal({}.x, undefined); + t.equal(argv.__proto__.x, undefined); + t.equal(argv.x, undefined); + t.end(); +}); + +test('proto pollution (array)', function (t) { + var argv = parse(['--x', '4', '--x', '5', '--x.__proto__.z', '789']); + t.equal({}.z, undefined); + t.deepEqual(argv.x, [4, 5]); + t.equal(argv.x.z, undefined); + t.equal(argv.x.__proto__.z, undefined); + t.end(); +}); + +test('proto pollution (number)', function (t) { + var argv = parse(['--x', '5', '--x.__proto__.z', '100']); + t.equal({}.z, undefined); + t.equal((4).z, undefined); + t.equal(argv.x, 5); + t.equal(argv.x.z, undefined); + t.end(); +}); + +test('proto pollution (string)', function (t) { + var argv = parse(['--x', 'abc', '--x.__proto__.z', 'def']); + t.equal({}.z, undefined); + t.equal('...'.z, undefined); + t.equal(argv.x, 'abc'); + t.equal(argv.x.z, undefined); + t.end(); +}); + +test('proto pollution (constructor)', function (t) { + var argv = parse(['--constructor.prototype.y', '123']); + t.equal({}.y, undefined); + t.equal(argv.y, undefined); + t.end(); +}); + +test('proto pollution (constructor function)', function (t) { + var argv = parse(['--_.concat.constructor.prototype.y', '123']); + function fnToBeTested() {} + t.equal(fnToBeTested.y, undefined); + t.equal(argv.y, undefined); + t.end(); +}); + +// powered by snyk - https://github.com/backstage/backstage/issues/10343 +test('proto pollution (constructor function) snyk', function (t) { + var argv = parse('--_.constructor.constructor.prototype.foo bar'.split(' ')); + t.equal(function () {}.foo, undefined); + t.equal(argv.y, undefined); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/short.js b/backend/node_modules/minimist/test/short.js new file mode 100644 index 00000000..4a7b8438 --- /dev/null +++ b/backend/node_modules/minimist/test/short.js @@ -0,0 +1,69 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('numeric short args', function (t) { + t.plan(2); + t.deepEqual(parse(['-n123']), { n: 123, _: [] }); + t.deepEqual( + parse(['-123', '456']), + { 1: true, 2: true, 3: 456, _: [] } + ); +}); + +test('short', function (t) { + t.deepEqual( + parse(['-b']), + { b: true, _: [] }, + 'short boolean' + ); + t.deepEqual( + parse(['foo', 'bar', 'baz']), + { _: ['foo', 'bar', 'baz'] }, + 'bare' + ); + t.deepEqual( + parse(['-cats']), + { c: true, a: true, t: true, s: true, _: [] }, + 'group' + ); + t.deepEqual( + parse(['-cats', 'meow']), + { c: true, a: true, t: true, s: 'meow', _: [] }, + 'short group next' + ); + t.deepEqual( + parse(['-h', 'localhost']), + { h: 'localhost', _: [] }, + 'short capture' + ); + t.deepEqual( + parse(['-h', 'localhost', '-p', '555']), + { h: 'localhost', p: 555, _: [] }, + 'short captures' + ); + t.end(); +}); + +test('mixed short bool and capture', function (t) { + t.same( + parse(['-h', 'localhost', '-fp', '555', 'script.js']), + { + f: true, p: 555, h: 'localhost', + _: ['script.js'], + } + ); + t.end(); +}); + +test('short and long', function (t) { + t.deepEqual( + parse(['-h', 'localhost', '-fp', '555', 'script.js']), + { + f: true, p: 555, h: 'localhost', + _: ['script.js'], + } + ); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/stop_early.js b/backend/node_modules/minimist/test/stop_early.js new file mode 100644 index 00000000..52a6a919 --- /dev/null +++ b/backend/node_modules/minimist/test/stop_early.js @@ -0,0 +1,17 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('stops parsing on the first non-option when stopEarly is set', function (t) { + var argv = parse(['--aaa', 'bbb', 'ccc', '--ddd'], { + stopEarly: true, + }); + + t.deepEqual(argv, { + aaa: 'bbb', + _: ['ccc', '--ddd'], + }); + + t.end(); +}); diff --git a/backend/node_modules/minimist/test/unknown.js b/backend/node_modules/minimist/test/unknown.js new file mode 100644 index 00000000..4f2e0ca4 --- /dev/null +++ b/backend/node_modules/minimist/test/unknown.js @@ -0,0 +1,104 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('boolean and alias is not unknown', function (t) { + var unknown = []; + function unknownFn(arg) { + unknown.push(arg); + return false; + } + var aliased = ['-h', 'true', '--derp', 'true']; + var regular = ['--herp', 'true', '-d', 'true']; + var opts = { + alias: { h: 'herp' }, + boolean: 'h', + unknown: unknownFn, + }; + parse(aliased, opts); + parse(regular, opts); + + t.same(unknown, ['--derp', '-d']); + t.end(); +}); + +test('flag boolean true any double hyphen argument is not unknown', function (t) { + var unknown = []; + function unknownFn(arg) { + unknown.push(arg); + return false; + } + var argv = parse(['--honk', '--tacos=good', 'cow', '-p', '55'], { + boolean: true, + unknown: unknownFn, + }); + t.same(unknown, ['--tacos=good', 'cow', '-p']); + t.same(argv, { + honk: true, + _: [], + }); + t.end(); +}); + +test('string and alias is not unknown', function (t) { + var unknown = []; + function unknownFn(arg) { + unknown.push(arg); + return false; + } + var aliased = ['-h', 'hello', '--derp', 'goodbye']; + var regular = ['--herp', 'hello', '-d', 'moon']; + var opts = { + alias: { h: 'herp' }, + string: 'h', + unknown: unknownFn, + }; + parse(aliased, opts); + parse(regular, opts); + + t.same(unknown, ['--derp', '-d']); + t.end(); +}); + +test('default and alias is not unknown', function (t) { + var unknown = []; + function unknownFn(arg) { + unknown.push(arg); + return false; + } + var aliased = ['-h', 'hello']; + var regular = ['--herp', 'hello']; + var opts = { + default: { h: 'bar' }, + alias: { h: 'herp' }, + unknown: unknownFn, + }; + parse(aliased, opts); + parse(regular, opts); + + t.same(unknown, []); + t.end(); + unknownFn(); // exercise fn for 100% coverage +}); + +test('value following -- is not unknown', function (t) { + var unknown = []; + function unknownFn(arg) { + unknown.push(arg); + return false; + } + var aliased = ['--bad', '--', 'good', 'arg']; + var opts = { + '--': true, + unknown: unknownFn, + }; + var argv = parse(aliased, opts); + + t.same(unknown, ['--bad']); + t.same(argv, { + '--': ['good', 'arg'], + _: [], + }); + t.end(); +}); diff --git a/backend/node_modules/minimist/test/whitespace.js b/backend/node_modules/minimist/test/whitespace.js new file mode 100644 index 00000000..4fdaf1d3 --- /dev/null +++ b/backend/node_modules/minimist/test/whitespace.js @@ -0,0 +1,10 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +test('whitespace should be whitespace', function (t) { + t.plan(1); + var x = parse(['-x', '\t']).x; + t.equal(x, '\t'); +}); diff --git a/backend/node_modules/napi-build-utils/.github/workflows/run-npm-tests.yml b/backend/node_modules/napi-build-utils/.github/workflows/run-npm-tests.yml new file mode 100644 index 00000000..3298868e --- /dev/null +++ b/backend/node_modules/napi-build-utils/.github/workflows/run-npm-tests.yml @@ -0,0 +1,31 @@ +name: Run npm Tests + +on: + workflow_dispatch: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [22, 23] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test \ No newline at end of file diff --git a/backend/node_modules/napi-build-utils/LICENSE b/backend/node_modules/napi-build-utils/LICENSE new file mode 100644 index 00000000..8e0248a4 --- /dev/null +++ b/backend/node_modules/napi-build-utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 inspiredware + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/node_modules/napi-build-utils/README.md b/backend/node_modules/napi-build-utils/README.md new file mode 100644 index 00000000..7c29b680 --- /dev/null +++ b/backend/node_modules/napi-build-utils/README.md @@ -0,0 +1,52 @@ +# napi-build-utils + +[![npm](https://img.shields.io/npm/v/napi-build-utils.svg)](https://www.npmjs.com/package/napi-build-utils) +![Node version](https://img.shields.io/node/v/prebuild.svg) +![Build Status](https://github.com/inspiredware/napi-build-utils/actions/workflows/run-npm-tests.yml/badge.svg) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A set of utilities to assist developers of tools that build [Node-API](https://nodejs.org/api/n-api.html#n_api_n_api) native add-ons. + +## Background + +This module is targeted to developers creating tools that build Node-API native add-ons. + +It implements a set of functions that aid in determining the Node-API version supported by the currently running Node instance and the set of Node-API versions against which the Node-API native add-on is designed to be built. Other functions determine whether a particular Node-API version can be built and can issue console warnings for unsupported Node-API versions. + +Unlike the modules this code is designed to facilitate building, this module is written entirely in JavaScript. + +## Quick start + +```bash +npm install napi-build-utils +``` + +The module exports a set of functions documented [here](./index.md). For example: + +```javascript +var napiBuildUtils = require('napi-build-utils'); +var napiVersion = napiBuildUtils.getNapiVersion(); // Node-API version supported by Node, or undefined. +``` + +## Declaring supported Node-API versions + +Native modules that are designed to work with [Node-API](https://nodejs.org/api/n-api.html#n_api_n_api) must explicitly declare the Node-API version(s) against which they are coded to build. This is accomplished by including a `binary.napi_versions` property in the module's `package.json` file. For example: + +```json +"binary": { + "napi_versions": [2,3] +} +``` + +In the absence of a need to compile against a specific Node-API version, the value `3` is a good choice as this is the Node-API version that was supported when Node-API left experimental status. + +Modules that are built against a specific Node-API version will continue to operate indefinitely, even as later versions of Node-API are introduced. + +## History + +**v2.0.0** This version was introduced to address a limitation when the Node-API version reached `10` in NodeJS `v23.6.0`. There was no change in the API, but a SemVer bump to `2.0.0` was made out of an abundance of caution. + +## Support + +If you run into problems or limitations, please file an issue and we'll take a look. Pull requests are also welcome. diff --git a/backend/node_modules/napi-build-utils/index.js b/backend/node_modules/napi-build-utils/index.js new file mode 100644 index 00000000..d143d5d0 --- /dev/null +++ b/backend/node_modules/napi-build-utils/index.js @@ -0,0 +1,214 @@ +'use strict' +// Copyright (c) 2018 inspiredware + +var path = require('path') +var pkg = require(path.resolve('package.json')) + +var versionArray = process.version + .substr(1) + .replace(/-.*$/, '') + .split('.') + .map(function (item) { + return +item + }) + +/** + * + * A set of utilities to assist developers of tools that build + * [N-API](https://nodejs.org/api/n-api.html#n_api_n_api) native add-ons. + * + * The main repository can be found + * [here](https://github.com/inspiredware/napi-build-utils#napi-build-utils). + * + * @module napi-build-utils + */ + +/** + * Implements a consistent name of `napi` for N-API runtimes. + * + * @param {string} runtime The runtime string. + * @returns {boolean} + */ +exports.isNapiRuntime = function (runtime) { + return runtime === 'napi' +} + +/** + * Determines whether the specified N-API version is supported + * by both the currently running Node instance and the package. + * + * @param {string} napiVersion The N-API version to check. + * @returns {boolean} + */ +exports.isSupportedVersion = function (napiVersion) { + var version = parseInt(napiVersion, 10) + return version <= exports.getNapiVersion() && exports.packageSupportsVersion(version) +} + +/** + * Determines whether the specified N-API version is supported by the package. + * The N-API version must be present in the `package.json` + * `binary.napi_versions` array. + * + * @param {number} napiVersion The N-API version to check. + * @returns {boolean} + * @private + */ +exports.packageSupportsVersion = function (napiVersion) { + if (pkg.binary && pkg.binary.napi_versions && + pkg.binary.napi_versions instanceof Array) { // integer array + for (var i = 0; i < pkg.binary.napi_versions.length; i++) { + if (pkg.binary.napi_versions[i] === napiVersion) return true + }; + }; + return false +} + +/** + * Issues a warning to the supplied log if the N-API version is not supported + * by the current Node instance or if the N-API version is not supported + * by the package. + * + * @param {string} napiVersion The N-API version to check. + * @param {Object} log The log object to which the warnings are to be issued. + * Must implement the `warn` method. + */ +exports.logUnsupportedVersion = function (napiVersion, log) { + if (!exports.isSupportedVersion(napiVersion)) { + if (exports.packageSupportsVersion(napiVersion)) { + log.warn('This Node instance does not support N-API version ' + napiVersion) + } else { + log.warn('This package does not support N-API version ' + napiVersion) + } + } +} + +/** + * Issues warnings to the supplied log for those N-API versions not supported + * by the N-API runtime or the package. + * + * Note that this function is specific to the + * [`prebuild`](https://github.com/prebuild/prebuild#prebuild) package. + * + * `target` is the list of targets to be built and is determined in one of + * three ways from the command line arguments: + * (1) `--target` specifies a specific target to build. + * (2) `--all` specifies all N-API versions supported by the package. + * (3) Neither of these specifies to build the single "best version available." + * + * `prebuild` is an array of objects in the form `{runtime: 'napi', target: '2'}`. + * The array contains the list of N-API versions that are supported by both the + * package being built and the currently running Node instance. + * + * The objective of this function is to issue a warning for those items that appear + * in the `target` argument but not in the `prebuild` argument. + * If a specific target is supported by the package (`packageSupportsVersion`) but + * but note in `prebuild`, the assumption is that the target is not supported by + * Node. + * + * @param {(Array|string)} target The N-API version(s) to check. Target is + * @param {Array} prebuild A config object created by the `prebuild` package. + * @param {Object} log The log object to which the warnings are to be issued. + * Must implement the `warn` method. + * @private + */ +exports.logMissingNapiVersions = function (target, prebuild, log) { + if (exports.getNapiBuildVersions()) { + var targets = [].concat(target) + targets.forEach(function (napiVersion) { + if (!prebuildExists(prebuild, napiVersion)) { + if (exports.packageSupportsVersion(parseInt(napiVersion, 10))) { + log.warn('This Node instance does not support N-API version ' + napiVersion) + } else { + log.warn('This package does not support N-API version ' + napiVersion) + } + } + }) + } else { + log.error('Builds with runtime \'napi\' require a binary.napi_versions ' + + 'property on the package.json file') + } +} + +/** + * Determines whether the specified N-API version exists in the prebuild + * configuration object. + * + * Note that this function is specific to the `prebuild` and `prebuild-install` + * packages. + * + * @param {Object} prebuild A config object created by the `prebuild` package. + * @param {string} napiVersion The N-APi version to be checked. + * @return {boolean} + * @private + */ +var prebuildExists = function (prebuild, napiVersion) { + if (prebuild) { + for (var i = 0; i < prebuild.length; i++) { + if (prebuild[i].target === napiVersion) return true + } + } + return false +} + +/** + * Returns the best N-API version to build given the highest N-API + * version supported by the current Node instance and the N-API versions + * supported by the package, or undefined if a suitable N-API version + * cannot be determined. + * + * The best build version is the greatest N-API version supported by + * the package that is less than or equal to the highest N-API version + * supported by the current Node instance. + * + * @returns {number|undefined} + */ +exports.getBestNapiBuildVersion = function () { + var bestNapiBuildVersion = 0 + var napiBuildVersions = exports.getNapiBuildVersions(pkg) // array of integer strings + if (napiBuildVersions) { + var ourNapiVersion = exports.getNapiVersion() + napiBuildVersions.forEach(function (napiBuildVersionStr) { + var napiBuildVersion = parseInt(napiBuildVersionStr, 10) + if (napiBuildVersion > bestNapiBuildVersion && + napiBuildVersion <= ourNapiVersion) { + bestNapiBuildVersion = napiBuildVersion + } + }) + } + return bestNapiBuildVersion === 0 ? undefined : bestNapiBuildVersion +} + +/** + * Returns an array of N-API versions supported by the package. + * + * @returns {Array|undefined} + */ +exports.getNapiBuildVersions = function () { + var napiBuildVersions = [] + // remove duplicates, convert to text + if (pkg.binary && pkg.binary.napi_versions) { + pkg.binary.napi_versions.forEach(function (napiVersion) { + var duplicated = napiBuildVersions.indexOf('' + napiVersion) !== -1 + if (!duplicated) { + napiBuildVersions.push('' + napiVersion) + } + }) + } + return napiBuildVersions.length ? napiBuildVersions : undefined +} + +/** + * Returns the highest N-API version supported by the current node instance + * or undefined if N-API is not supported. + * + * @returns {string|undefined} + */ +exports.getNapiVersion = function () { + var version = process.versions.napi // integer string, can be undefined + if (!version) { // this code should never need to be updated + if (versionArray[0] === 9 && versionArray[1] >= 3) version = '2' // 9.3.0+ + else if (versionArray[0] === 8) version = '1' // 8.0.0+ + } + return version +} diff --git a/backend/node_modules/napi-build-utils/index.md b/backend/node_modules/napi-build-utils/index.md new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/napi-build-utils/package.json b/backend/node_modules/napi-build-utils/package.json new file mode 100644 index 00000000..ad15183c --- /dev/null +++ b/backend/node_modules/napi-build-utils/package.json @@ -0,0 +1,42 @@ +{ + "name": "napi-build-utils", + "version": "2.0.0", + "description": "A set of utilities to assist developers of tools that build N-API native add-ons", + "main": "index.js", + "scripts": { + "doc": "jsdoc2md index.js >index.md", + "test": "mocha test/ && npm run lint", + "lint": "standard", + "prepublishOnly": "npm run test" + }, + "keywords": [ + "n-api", + "prebuild", + "prebuild-install" + ], + "author": "Jim Schlight", + "license": "MIT", + "homepage": "https://github.com/inspiredware/napi-build-utils#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/inspiredware/napi-build-utils.git" + }, + "bugs": { + "url": "https://github.com/inspiredware/napi-build-utils/issues" + }, + "devDependencies": { + "chai": "^4.1.2", + "jsdoc-to-markdown": "^4.0.1", + "mocha": "^5.2.0", + "standard": "^12.0.1" + }, + "binary": { + "note": "napi-build-tools is not an N-API module. This entry is for unit testing.", + "napi_versions": [ + 2, + 2, + 3, + 10 + ] + } +} diff --git a/backend/node_modules/node-abi/LICENSE b/backend/node_modules/node-abi/LICENSE new file mode 100644 index 00000000..5513de0d --- /dev/null +++ b/backend/node_modules/node-abi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Lukas Geiger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/node_modules/node-abi/README.md b/backend/node_modules/node-abi/README.md new file mode 100644 index 00000000..680a0d0b --- /dev/null +++ b/backend/node_modules/node-abi/README.md @@ -0,0 +1,54 @@ +# Node.js ABI + +[![Build Status](https://github.com/electron/node-abi/actions/workflows/test.yml/badge.svg)](https://github.com/electron/node-abi/actions/workflows/test.yml) +[![Auto-update ABI JSON file](https://github.com/electron/node-abi/actions/workflows/update-abi.yml/badge.svg)](https://github.com/electron/node-abi/actions/workflows/update-abi.yml) +[![Snyk badge](https://snyk.io/test/github/electron/node-abi/badge.svg)](https://snyk.io/test/github/electron/node-abi) +[![npm version](http://img.shields.io/npm/v/node-abi.svg)](https://npmjs.org/package/node-abi) + +Get the Node ABI (application binary interface) for a given target and runtime, and vice versa. + +## Installation + +```shell +npm install node-abi +``` + +## Usage + +```javascript +const nodeAbi = require('node-abi') + +nodeAbi.getAbi('7.2.0', 'node') +// '51' +nodeAbi.getAbi('1.4.10', 'electron') +// '50' +nodeAbi.getTarget('51', 'node') +// '7.2.0' +nodeAbi.getTarget('50', 'electron') +// '1.4.15' + +nodeAbi.allTargets +// [ +// { runtime: 'node', target: '0.10.48', abi: '11', lts: false }, +// { runtime: 'node', target: '0.12.17', abi: '14', lts: false }, +// { runtime: 'node', target: '4.6.1', abi: '46', lts: true }, +// { runtime: 'node', target: '5.12.0', abi: '47', lts: false }, +// { runtime: 'node', target: '6.9.4', abi: '48', lts: true }, +// { runtime: 'node', target: '7.4.0', abi: '51', lts: false }, +// { runtime: 'electron', target: '1.0.2', abi: '47', lts: false }, +// { runtime: 'electron', target: '1.2.8', abi: '48', lts: false }, +// { runtime: 'electron', target: '1.3.13', abi: '49', lts: false }, +// { runtime: 'electron', target: '1.4.15', abi: '50', lts: false } +// ] +nodeAbi.deprecatedTargets +nodeAbi.supportedTargets +nodeAbi.additionalTargets +nodeAbi.futureTargets +// ... +``` + +## References + +- https://github.com/lgeiger/electron-abi +- https://nodejs.org/en/download/releases/ +- https://github.com/nodejs/Release diff --git a/backend/node_modules/node-abi/abi_registry.json b/backend/node_modules/node-abi/abi_registry.json new file mode 100644 index 00000000..b8a266ca --- /dev/null +++ b/backend/node_modules/node-abi/abi_registry.json @@ -0,0 +1,425 @@ +[ + { + "runtime": "node", + "target": "11.0.0", + "lts": false, + "future": false, + "abi": "67" + }, + { + "runtime": "node", + "target": "12.0.0", + "lts": [ + "2019-10-21", + "2020-11-30" + ], + "future": false, + "abi": "72" + }, + { + "runtime": "node", + "target": "13.0.0", + "lts": false, + "future": false, + "abi": "79" + }, + { + "runtime": "node", + "target": "14.0.0", + "lts": [ + "2020-10-27", + "2021-10-19" + ], + "future": false, + "abi": "83" + }, + { + "runtime": "node", + "target": "15.0.0", + "lts": false, + "future": false, + "abi": "88" + }, + { + "runtime": "node", + "target": "16.0.0", + "lts": [ + "2021-10-26", + "2022-10-18" + ], + "future": false, + "abi": "93" + }, + { + "runtime": "node", + "target": "17.0.0", + "lts": false, + "future": false, + "abi": "102" + }, + { + "runtime": "node", + "target": "18.0.0", + "lts": [ + "2022-10-25", + "2023-10-18" + ], + "future": false, + "abi": "108" + }, + { + "runtime": "node", + "target": "19.0.0", + "lts": false, + "future": false, + "abi": "111" + }, + { + "runtime": "node", + "target": "20.0.0", + "lts": [ + "2023-10-24", + "2024-10-22" + ], + "future": false, + "abi": "115" + }, + { + "runtime": "node", + "target": "21.0.0", + "lts": false, + "future": false, + "abi": "120" + }, + { + "runtime": "node", + "target": "22.0.0", + "lts": [ + "2024-10-29", + "2025-10-21" + ], + "future": false, + "abi": "127" + }, + { + "runtime": "node", + "target": "23.0.0", + "lts": false, + "future": false, + "abi": "131" + }, + { + "runtime": "node", + "target": "24.0.0", + "lts": [ + "2025-10-28", + "2026-10-20" + ], + "future": false, + "abi": "137" + }, + { + "runtime": "node", + "target": "25.0.0", + "lts": false, + "future": false, + "abi": "141" + }, + { + "runtime": "node", + "target": "26.0.0", + "lts": [ + "2026-10-28", + "2027-10-20" + ], + "future": true, + "abi": "144" + }, + { + "abi": "70", + "future": false, + "lts": false, + "runtime": "electron", + "target": "5.0.0-beta.9" + }, + { + "abi": "73", + "future": false, + "lts": false, + "runtime": "electron", + "target": "6.0.0-beta.1" + }, + { + "abi": "75", + "future": false, + "lts": false, + "runtime": "electron", + "target": "7.0.0-beta.1" + }, + { + "abi": "76", + "future": false, + "lts": false, + "runtime": "electron", + "target": "9.0.0-beta.1" + }, + { + "abi": "76", + "future": false, + "lts": false, + "runtime": "electron", + "target": "8.0.0-beta.1" + }, + { + "abi": "80", + "future": false, + "lts": false, + "runtime": "electron", + "target": "9.0.0-beta.2" + }, + { + "abi": "82", + "future": false, + "lts": false, + "runtime": "electron", + "target": "11.0.0-beta.1" + }, + { + "abi": "82", + "future": false, + "lts": false, + "runtime": "electron", + "target": "10.0.0-beta.1" + }, + { + "abi": "85", + "future": false, + "lts": false, + "runtime": "electron", + "target": "11.0.0-beta.11" + }, + { + "abi": "87", + "future": false, + "lts": false, + "runtime": "electron", + "target": "12.0.0-beta.1" + }, + { + "abi": "89", + "future": false, + "lts": false, + "runtime": "electron", + "target": "15.0.0-alpha.1" + }, + { + "abi": "89", + "future": false, + "lts": false, + "runtime": "electron", + "target": "14.0.0-beta.1" + }, + { + "abi": "89", + "future": false, + "lts": false, + "runtime": "electron", + "target": "13.0.0-beta.2" + }, + { + "abi": "97", + "future": false, + "lts": false, + "runtime": "electron", + "target": "14.0.2" + }, + { + "abi": "98", + "future": false, + "lts": false, + "runtime": "electron", + "target": "15.0.0-beta.7" + }, + { + "abi": "99", + "future": false, + "lts": false, + "runtime": "electron", + "target": "16.0.0-alpha.1" + }, + { + "abi": "101", + "future": false, + "lts": false, + "runtime": "electron", + "target": "17.0.0-alpha.1" + }, + { + "abi": "103", + "future": false, + "lts": false, + "runtime": "electron", + "target": "18.0.0-alpha.1" + }, + { + "abi": "106", + "future": false, + "lts": false, + "runtime": "electron", + "target": "19.0.0-alpha.1" + }, + { + "abi": "107", + "future": false, + "lts": false, + "runtime": "electron", + "target": "20.0.0-alpha.1" + }, + { + "abi": "109", + "future": false, + "lts": false, + "runtime": "electron", + "target": "21.0.0-alpha.1" + }, + { + "abi": "110", + "future": false, + "lts": false, + "runtime": "electron", + "target": "22.0.0-alpha.1" + }, + { + "abi": "113", + "future": false, + "lts": false, + "runtime": "electron", + "target": "23.0.0-alpha.1" + }, + { + "abi": "114", + "future": false, + "lts": false, + "runtime": "electron", + "target": "24.0.0-alpha.1" + }, + { + "abi": "116", + "future": false, + "lts": false, + "runtime": "electron", + "target": "26.0.0-alpha.1" + }, + { + "abi": "116", + "future": false, + "lts": false, + "runtime": "electron", + "target": "25.0.0-alpha.1" + }, + { + "abi": "118", + "future": false, + "lts": false, + "runtime": "electron", + "target": "27.0.0-alpha.1" + }, + { + "abi": "119", + "future": false, + "lts": false, + "runtime": "electron", + "target": "28.0.0-alpha.1" + }, + { + "abi": "121", + "future": false, + "lts": false, + "runtime": "electron", + "target": "29.0.0-alpha.1" + }, + { + "abi": "123", + "future": false, + "lts": false, + "runtime": "electron", + "target": "31.0.0-alpha.1" + }, + { + "abi": "123", + "future": false, + "lts": false, + "runtime": "electron", + "target": "30.0.0-alpha.1" + }, + { + "abi": "125", + "future": false, + "lts": false, + "runtime": "electron", + "target": "31.0.0-beta.7" + }, + { + "abi": "128", + "future": false, + "lts": false, + "runtime": "electron", + "target": "32.0.0-alpha.1" + }, + { + "abi": "130", + "future": false, + "lts": false, + "runtime": "electron", + "target": "33.0.0-alpha.1" + }, + { + "abi": "132", + "future": false, + "lts": false, + "runtime": "electron", + "target": "34.0.0-alpha.1" + }, + { + "abi": "133", + "future": false, + "lts": false, + "runtime": "electron", + "target": "35.0.0-alpha.1" + }, + { + "abi": "135", + "future": false, + "lts": false, + "runtime": "electron", + "target": "36.0.0-alpha.1" + }, + { + "abi": "136", + "future": false, + "lts": false, + "runtime": "electron", + "target": "37.0.0-alpha.1" + }, + { + "abi": "139", + "future": false, + "lts": false, + "runtime": "electron", + "target": "38.0.0-alpha.1" + }, + { + "abi": "140", + "future": false, + "lts": false, + "runtime": "electron", + "target": "39.0.0-alpha.1" + }, + { + "abi": "143", + "future": true, + "lts": false, + "runtime": "electron", + "target": "40.0.0-alpha.2" + } +] \ No newline at end of file diff --git a/backend/node_modules/node-abi/index.js b/backend/node_modules/node-abi/index.js new file mode 100644 index 00000000..f6561353 --- /dev/null +++ b/backend/node_modules/node-abi/index.js @@ -0,0 +1,179 @@ +var semver = require('semver') + +function getNextTarget (runtime, targets) { + if (targets == null) targets = allTargets + var latest = targets.filter(function (t) { return t.runtime === runtime }).slice(-1)[0] + var increment = runtime === 'electron' ? 'minor' : 'major' + var next = semver.inc(latest.target, increment) + // Electron releases appear in the registry in their beta form, sometimes there is + // no active beta line. During this time we need to double bump + if (runtime === 'electron' && semver.parse(latest.target).prerelease.length) { + next = semver.inc(next, 'major') + } + return next +} + +function getAbi (target, runtime) { + if (target === String(Number(target))) return target + if (target) target = target.replace(/^v/, '') + if (!runtime) runtime = 'node' + + if (runtime === 'node') { + if (!target) return process.versions.modules + if (target === process.versions.node) return process.versions.modules + } + + var abi + var lastTarget + + for (var i = 0; i < allTargets.length; i++) { + var t = allTargets[i] + if (t.runtime !== runtime) continue + if (semver.lte(t.target, target) && (!lastTarget || semver.gte(t.target, lastTarget))) { + abi = t.abi + lastTarget = t.target + } + } + + if (abi && semver.lt(target, getNextTarget(runtime))) return abi + throw new Error('Could not detect abi for version ' + target + ' and runtime ' + runtime + '. Updating "node-abi" might help solve this issue if it is a new release of ' + runtime) +} + +function getTarget (abi, runtime) { + if (abi && abi !== String(Number(abi))) return abi + if (!runtime) runtime = 'node' + + if (runtime === 'node' && !abi) return process.versions.node + + var match = allTargets + .filter(function (t) { + return t.abi === abi && t.runtime === runtime + }) + .map(function (t) { + return t.target + }) + if (match.length) { + var betaSeparatorIndex = match[0].indexOf("-") + return betaSeparatorIndex > -1 + ? match[0].substring(0, betaSeparatorIndex) + : match[0] + } + + throw new Error('Could not detect target for abi ' + abi + ' and runtime ' + runtime) +} + +function sortByTargetFn (a, b) { + var abiComp = Number(a.abi) - Number(b.abi) + if (abiComp !== 0) return abiComp + if (a.target < b.target) return -1 + if (a.target > b.target) return 1 + return 0 +} + +function loadGeneratedTargets () { + var registry = require('./abi_registry.json') + var targets = { + supported: [], + additional: [], + future: [] + } + + registry.forEach(function (item) { + var target = { + runtime: item.runtime, + target: item.target, + abi: item.abi + } + if (item.lts) { + var startDate = new Date(Date.parse(item.lts[0])) + var endDate = new Date(Date.parse(item.lts[1])) + var currentDate = new Date() + target.lts = startDate < currentDate && currentDate < endDate + } else { + target.lts = false + } + + if (target.runtime === 'node-webkit') { + targets.additional.push(target) + } else if (item.future) { + targets.future.push(target) + } else { + targets.supported.push(target) + } + }) + + targets.supported.sort(sortByTargetFn) + targets.additional.sort(sortByTargetFn) + targets.future.sort(sortByTargetFn) + + return targets +} + +var generatedTargets = loadGeneratedTargets() + +var supportedTargets = [ + {runtime: 'node', target: '5.0.0', abi: '47', lts: false}, + {runtime: 'node', target: '6.0.0', abi: '48', lts: false}, + {runtime: 'node', target: '7.0.0', abi: '51', lts: false}, + {runtime: 'node', target: '8.0.0', abi: '57', lts: false}, + {runtime: 'node', target: '9.0.0', abi: '59', lts: false}, + {runtime: 'node', target: '10.0.0', abi: '64', lts: new Date(2018, 10, 1) < new Date() && new Date() < new Date(2020, 4, 31)}, + {runtime: 'electron', target: '0.36.0', abi: '47', lts: false}, + {runtime: 'electron', target: '1.1.0', abi: '48', lts: false}, + {runtime: 'electron', target: '1.3.0', abi: '49', lts: false}, + {runtime: 'electron', target: '1.4.0', abi: '50', lts: false}, + {runtime: 'electron', target: '1.5.0', abi: '51', lts: false}, + {runtime: 'electron', target: '1.6.0', abi: '53', lts: false}, + {runtime: 'electron', target: '1.7.0', abi: '54', lts: false}, + {runtime: 'electron', target: '1.8.0', abi: '57', lts: false}, + {runtime: 'electron', target: '2.0.0', abi: '57', lts: false}, + {runtime: 'electron', target: '3.0.0', abi: '64', lts: false}, + {runtime: 'electron', target: '4.0.0', abi: '64', lts: false}, + {runtime: 'electron', target: '4.0.4', abi: '69', lts: false} +] + +supportedTargets.push.apply(supportedTargets, generatedTargets.supported) + +var additionalTargets = [ + {runtime: 'node-webkit', target: '0.13.0', abi: '47', lts: false}, + {runtime: 'node-webkit', target: '0.15.0', abi: '48', lts: false}, + {runtime: 'node-webkit', target: '0.18.3', abi: '51', lts: false}, + {runtime: 'node-webkit', target: '0.23.0', abi: '57', lts: false}, + {runtime: 'node-webkit', target: '0.26.5', abi: '59', lts: false} +] + +additionalTargets.push.apply(additionalTargets, generatedTargets.additional) + +var deprecatedTargets = [ + {runtime: 'node', target: '0.2.0', abi: '1', lts: false}, + {runtime: 'node', target: '0.9.1', abi: '0x000A', lts: false}, + {runtime: 'node', target: '0.9.9', abi: '0x000B', lts: false}, + {runtime: 'node', target: '0.10.4', abi: '11', lts: false}, + {runtime: 'node', target: '0.11.0', abi: '0x000C', lts: false}, + {runtime: 'node', target: '0.11.8', abi: '13', lts: false}, + {runtime: 'node', target: '0.11.11', abi: '14', lts: false}, + {runtime: 'node', target: '1.0.0', abi: '42', lts: false}, + {runtime: 'node', target: '1.1.0', abi: '43', lts: false}, + {runtime: 'node', target: '2.0.0', abi: '44', lts: false}, + {runtime: 'node', target: '3.0.0', abi: '45', lts: false}, + {runtime: 'node', target: '4.0.0', abi: '46', lts: false}, + {runtime: 'electron', target: '0.30.0', abi: '44', lts: false}, + {runtime: 'electron', target: '0.31.0', abi: '45', lts: false}, + {runtime: 'electron', target: '0.33.0', abi: '46', lts: false} +] + +var futureTargets = generatedTargets.future + +var allTargets = deprecatedTargets + .concat(supportedTargets) + .concat(additionalTargets) + .concat(futureTargets) + +exports.getAbi = getAbi +exports.getTarget = getTarget +exports.deprecatedTargets = deprecatedTargets +exports.supportedTargets = supportedTargets +exports.additionalTargets = additionalTargets +exports.futureTargets = futureTargets +exports.allTargets = allTargets +exports._getNextTarget = getNextTarget diff --git a/backend/node_modules/node-abi/package.json b/backend/node_modules/node-abi/package.json new file mode 100644 index 00000000..8bc35890 --- /dev/null +++ b/backend/node_modules/node-abi/package.json @@ -0,0 +1,45 @@ +{ + "name": "node-abi", + "version": "3.85.0", + "description": "Get the Node ABI for a given target and runtime, and vice versa.", + "main": "index.js", + "scripts": { + "test": "tape test/index.js", + "update-abi-registry": "node --unhandled-rejections=strict scripts/update-abi-registry.js" + }, + "files": [ + "abi_registry.json" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/electron/node-abi.git" + }, + "keywords": [ + "node", + "electron", + "node_module_version", + "abi", + "v8" + ], + "author": "Lukas Geiger", + "license": "MIT", + "bugs": { + "url": "https://github.com/electron/node-abi/issues" + }, + "homepage": "https://github.com/electron/node-abi#readme", + "devDependencies": { + "@semantic-release/npm": "13.0.0-alpha.15", + "semantic-release": "^24.2.7", + "tape": "^5.3.1" + }, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + }, + "publishConfig": { + "provenance": true + }, + "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f" +} diff --git a/backend/node_modules/prebuild-install/CHANGELOG.md b/backend/node_modules/prebuild-install/CHANGELOG.md new file mode 100644 index 00000000..03cd97aa --- /dev/null +++ b/backend/node_modules/prebuild-install/CHANGELOG.md @@ -0,0 +1,131 @@ +# Changelog + +## [7.1.3] - 2025-01-22 + +### Fixed + +- Bump napi-build-utils from 1 to 2 ([#204](https://github.com/prebuild/prebuild-install/issues/204)) ([`1bf4a15`](https://github.com/prebuild/prebuild-install/commit/1bf4a15)) (Bailey Pearson) + +## [7.1.2] - 2024-02-29 + +### Fixed + +- Support environments where MD5 is prohibited ([#191](https://github.com/prebuild/prebuild-install/issues/191)) ([`9140468`](https://github.com/prebuild/prebuild-install/commit/9140468)) (Tomasz Szuba) + +## [7.1.1] - 2022-06-07 + +### Fixed + +- Replace use of npmlog dependency with console.error ([#182](https://github.com/prebuild/prebuild-install/issues/182)) ([`4e2284c`](https://github.com/prebuild/prebuild-install/commit/4e2284c)) (Lovell Fuller) +- Ensure script output can be captured by tests ([#181](https://github.com/prebuild/prebuild-install/issues/181)) ([`d1853cb`](https://github.com/prebuild/prebuild-install/commit/d1853cb)) (Lovell Fuller) + +## [7.1.0] - 2022-04-20 + +### Changed + +- Allow setting libc to glibc on non-glibc platform ([#176](https://github.com/prebuild/prebuild-install/issues/176)) ([`f729abb`](https://github.com/prebuild/prebuild-install/commit/f729abb)) (Joona Heinikoski) + +## [7.0.1] - 2022-01-28 + +### Changed + +- Upgrade to the latest version of `detect-libc` ([#166](https://github.com/prebuild/prebuild-install/issues/166)) ([`f71c6b9`](https://github.com/prebuild/prebuild-install/commit/f71c6b9)) (Lovell Fuller) + +## [7.0.0] - 2021-11-12 + +### Changed + +- **Breaking:** bump `node-abi` so that Electron 14+ gets correct ABI ([#161](https://github.com/prebuild/prebuild-install/issues/161)) ([`477f347`](https://github.com/prebuild/prebuild-install/commit/477f347)) (csett86). Drops support of Node.js < 10. +- Bump `simple-get` ([`7468c14`](https://github.com/prebuild/prebuild-install/commit/7468c14)) (Vincent Weevers). + +## [6.1.4] - 2021-08-11 + +### Fixed + +- Move auth token to header instead of query param ([#160](https://github.com/prebuild/prebuild-install/issues/160)) ([`b3fad76`](https://github.com/prebuild/prebuild-install/commit/b3fad76)) (nicolai-nordic) +- Remove `_` prefix as it isn't allowed by npm config ([#153](https://github.com/prebuild/prebuild-install/issues/153)) ([`a964e5b`](https://github.com/prebuild/prebuild-install/commit/a964e5b)) (Tom Boothman) +- Make `rc.path` absolute ([#158](https://github.com/prebuild/prebuild-install/issues/158)) ([`57bcc06`](https://github.com/prebuild/prebuild-install/commit/57bcc06)) (George Waters). + +## [6.1.3] - 2021-06-03 + +### Changed + +- Inline no longer maintained `noop-logger` ([#155](https://github.com/prebuild/prebuild-install/issues/155)) ([`e08d75a`](https://github.com/prebuild/prebuild-install/commit/e08d75a)) (Alexandru Dima) +- Point users towards `prebuildify` in README ([#150](https://github.com/prebuild/prebuild-install/issues/150)) ([`5ee1a2f`](https://github.com/prebuild/prebuild-install/commit/5ee1a2f)) (Vincent Weevers) + +## [6.1.2] - 2021-04-24 + +### Fixed + +- Support URL-safe strings in scoped packages ([#148](https://github.com/prebuild/prebuild-install/issues/148)) ([`db36c7a`](https://github.com/prebuild/prebuild-install/commit/db36c7a)) (Marco) + +## [6.1.1] - 2021-04-04 + +### Fixed + +- Support `force` & `buildFromSource` options in yarn ([#140](https://github.com/prebuild/prebuild-install/issues/140)) ([`8cb1ced`](https://github.com/prebuild/prebuild-install/commit/8cb1ced)) (João Moreno) +- Bump `node-abi` to prevent dedupe (closes [#135](https://github.com/prebuild/prebuild-install/issues/135)) ([`2950fb2`](https://github.com/prebuild/prebuild-install/commit/2950fb2)) (Vincent Weevers) + +## [6.1.0] - 2021-04-03 + +### Added + +- Restore local prebuilds feature ([#137](https://github.com/prebuild/prebuild-install/issues/137)) ([`dc4e5ea`](https://github.com/prebuild/prebuild-install/commit/dc4e5ea)) (Wes Roberts). Previously removed in [#81](https://github.com/prebuild/prebuild-install/issues/81) / [`a069253`](https://github.com/prebuild/prebuild-install/commit/a06925378d38ca821bfa93aa4c1fdedc253b2420). + +## [6.0.1] - 2021-02-14 + +### Fixed + +- Fixes empty `--tag-prefix` ([#143](https://github.com/prebuild/prebuild-install/issues/143)) ([**@mathiask88**](https://github.com/mathiask88)) + +## [6.0.0] - 2020-10-23 + +### Changed + +- **Breaking:** don't skip downloads in standalone mode ([`b6f3b36`](https://github.com/prebuild/prebuild-install/commit/b6f3b36)) ([**@vweevers**](https://github.com/vweevers)) + +### Added + +- Document cross platform options ([`e5c9a5a`](https://github.com/prebuild/prebuild-install/commit/e5c9a5a)) ([**@fishbone1**](https://github.com/fishbone1)) + +### Removed + +- **Breaking:** remove `--compile` and `--prebuild` options ([`94f2492`](https://github.com/prebuild/prebuild-install/commit/94f2492)) ([**@vweevers**](https://github.com/vweevers)) + +### Fixed + +- Support npm 7 ([`8acccac`](https://github.com/prebuild/prebuild-install/commit/8acccac), [`08eaf6d`](https://github.com/prebuild/prebuild-install/commit/08eaf6d), [`22175b8`](https://github.com/prebuild/prebuild-install/commit/22175b8)) ([**@vweevers**](https://github.com/vweevers)) + +## [5.3.6] - 2020-10-20 + +### Changed + +- Replace `mkdirp` dependency with `mkdirp-classic` ([**@ralphtheninja**](https://github.com/ralphtheninja)) + +[7.1.3]: https://github.com/prebuild/prebuild-install/releases/tag/v7.1.3 + +[7.1.2]: https://github.com/prebuild/prebuild-install/releases/tag/v7.1.2 + +[7.1.1]: https://github.com/prebuild/prebuild-install/releases/tag/v7.1.1 + +[7.1.0]: https://github.com/prebuild/prebuild-install/releases/tag/v7.1.0 + +[7.0.1]: https://github.com/prebuild/prebuild-install/releases/tag/v7.0.1 + +[7.0.0]: https://github.com/prebuild/prebuild-install/releases/tag/v7.0.0 + +[6.1.4]: https://github.com/prebuild/prebuild-install/releases/tag/v6.1.4 + +[6.1.3]: https://github.com/prebuild/prebuild-install/releases/tag/v6.1.3 + +[6.1.2]: https://github.com/prebuild/prebuild-install/releases/tag/v6.1.2 + +[6.1.1]: https://github.com/prebuild/prebuild-install/releases/tag/v6.1.1 + +[6.1.0]: https://github.com/prebuild/prebuild-install/releases/tag/v6.1.0 + +[6.0.1]: https://github.com/prebuild/prebuild-install/releases/tag/v6.0.1 + +[6.0.0]: https://github.com/prebuild/prebuild-install/releases/tag/v6.0.0 + +[5.3.6]: https://github.com/prebuild/prebuild-install/releases/tag/v5.3.6 diff --git a/backend/node_modules/prebuild-install/CONTRIBUTING.md b/backend/node_modules/prebuild-install/CONTRIBUTING.md new file mode 100644 index 00000000..07860da8 --- /dev/null +++ b/backend/node_modules/prebuild-install/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing to prebuild + +- no commits direct to master +- all commits as pull requests (one or several per PR) +- each commit solves one identifiable problem +- never merge one's own PRs, another contributor does this diff --git a/backend/node_modules/prebuild-install/LICENSE b/backend/node_modules/prebuild-install/LICENSE new file mode 100644 index 00000000..66a4d2a1 --- /dev/null +++ b/backend/node_modules/prebuild-install/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/backend/node_modules/prebuild-install/README.md b/backend/node_modules/prebuild-install/README.md new file mode 100644 index 00000000..d5aafa9b --- /dev/null +++ b/backend/node_modules/prebuild-install/README.md @@ -0,0 +1,163 @@ +# prebuild-install + +> **A command line tool to easily install prebuilt binaries for multiple versions of Node.js & Electron on a specific platform.** +> By default it downloads prebuilt binaries from a GitHub release. + +[![npm](https://img.shields.io/npm/v/prebuild-install.svg)](https://www.npmjs.com/package/prebuild-install) +![Node version](https://img.shields.io/node/v/prebuild-install.svg) +[![Test](https://img.shields.io/github/workflow/status/prebuild/prebuild-install/Test?label=test)](https://github.com/prebuild/prebuild-install/actions/workflows/test.yml) +[![Standard](https://img.shields.io/badge/standard-informational?logo=javascript\&logoColor=fff)](https://standardjs.com) +[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org) + +## Note + +**Instead of [`prebuild`](https://github.com/prebuild/prebuild) paired with [`prebuild-install`](https://github.com/prebuild/prebuild-install), we recommend [`prebuildify`](https://github.com/prebuild/prebuildify) paired with [`node-gyp-build`](https://github.com/prebuild/node-gyp-build).** + +With `prebuildify`, all prebuilt binaries are shipped inside the package that is published to npm, which means there's no need for a separate download step like you find in `prebuild`. The irony of this approach is that it is faster to download all prebuilt binaries for every platform when they are bundled than it is to download a single prebuilt binary as an install script. + +Upsides: + +1. No extra download step, making it more reliable and faster to install. +2. Supports changing runtime versions locally and using the same install between Node.js and Electron. Reinstalling or rebuilding is not necessary, as all prebuilt binaries are in the npm tarball and the correct one is simply picked on runtime. +3. The `node-gyp-build` runtime dependency is dependency-free and will remain so out of principle, because introducing dependencies would negate the shorter install time. +4. Prebuilt binaries work even if npm install scripts are disabled. +5. The npm package checksum covers prebuilt binaries too. + +Downsides: + +1. The installed npm package is larger on disk. Using [Node-API](https://nodejs.org/api/n-api.html) alleviates this because Node-API binaries are runtime-agnostic and forward-compatible. +2. Publishing is mildly more complicated, because `npm publish` must be done after compiling and fetching prebuilt binaries (typically in CI). + +## Usage + +Use [`prebuild`](https://github.com/prebuild/prebuild) to create and upload prebuilt binaries. Then change your package.json install script to: + +```json +{ + "scripts": { + "install": "prebuild-install || node-gyp rebuild" + } +} +``` + +When a consumer then installs your package with npm thus triggering the above install script, `prebuild-install` will download a suitable prebuilt binary, or exit with a non-zero exit code if there is none, which triggers `node-gyp rebuild` in order to build from source. + +Options (see below) can be passed to `prebuild-install` like so: + +```json +{ + "scripts": { + "install": "prebuild-install -r napi || node-gyp rebuild" + } +} +``` + +### Help + +``` +prebuild-install [options] + + --download -d [url] (download prebuilds, no url means github) + --target -t version (version to install for) + --runtime -r runtime (Node runtime [node, napi or electron] to build or install for, default is node) + --path -p path (make a prebuild-install here) + --token -T gh-token (github token for private repos) + --arch arch (target CPU architecture, see Node OS module docs, default is current arch) + --platform platform (target platform, see Node OS module docs, default is current platform) + --tag-prefix (github tag prefix, default is "v") + --build-from-source (skip prebuild download) + --verbose (log verbosely) + --libc (use provided libc rather than system default) + --debug (set Debug or Release configuration) + --version (print prebuild-install version and exit) +``` + +When `prebuild-install` is run via an `npm` script, options `--build-from-source`, `--debug`, `--download`, `--target`, `--runtime`, `--arch` `--platform` and `--libc` may be passed through via arguments given to the `npm` command. + +Alternatively you can set environment variables `npm_config_build_from_source=true`, `npm_config_platform`, `npm_config_arch`, `npm_config_target` `npm_config_runtime` and `npm_config_libc`. + +### Libc + +On non-glibc Linux platforms, the Libc name is appended to platform name. For example, musl-based environments are called `linuxmusl`. If `--libc=glibc` is passed as option, glibc is discarded and platform is called as just `linux`. This can be used for example to build cross-platform packages on Alpine Linux. + +### Private Repositories + +`prebuild-install` supports downloading prebuilds from private GitHub repositories using the `-T `: + +``` +$ prebuild-install -T +``` + +If you don't want to use the token on cli you can put it in `~/.prebuild-installrc`: + +``` +token= +``` + +Alternatively you can specify it in the `prebuild-install_token` environment variable. + +Note that using a GitHub token uses the API to resolve the correct release meaning that you are subject to the ([GitHub Rate Limit](https://developer.github.com/v3/rate_limit/)). + +### Create GitHub Token + +To create a token: + +- Go to [this page](https://github.com/settings/tokens) +- Click the `Generate new token` button +- Give the token a name and click the `Generate token` button, see below + +![prebuild-token](https://cloud.githubusercontent.com/assets/13285808/20844584/d0b85268-b8c0-11e6-8b08-2b19522165a9.png) + +The default scopes should be fine. + +### Custom binaries + +The end user can override binary download location through environment variables in their .npmrc file. +The variable needs to meet the mask `% your package name %_binary_host` or `% your package name %_binary_host_mirror`. For example: + +``` +leveldown_binary_host=http://overriden-host.com/overriden-path +``` + +Note that the package version subpath and file name will still be appended. +So if you are installing `leveldown@1.2.3` the resulting url will be: + +``` +http://overriden-host.com/overriden-path/v1.2.3/leveldown-v1.2.3-node-v57-win32-x64.tar.gz +``` + +#### Local prebuilds + +If you want to use prebuilds from your local filesystem, you can use the `% your package name %_local_prebuilds` .npmrc variable to set a path to the folder containing prebuilds. For example: + +``` +leveldown_local_prebuilds=/path/to/prebuilds +``` + +This option will look directly in that folder for bundles created with `prebuild`, for example: + +``` +/path/to/prebuilds/leveldown-v1.2.3-node-v57-win32-x64.tar.gz +``` + +Non-absolute paths resolve relative to the directory of the package invoking prebuild-install, e.g. for nested dependencies. + +### Cache + +All prebuilt binaries are cached to minimize traffic. So first `prebuild-install` picks binaries from the cache and if no binary could be found, it will be downloaded. Depending on the environment, the cache folder is determined in the following order: + +- `${npm_config_cache}/_prebuilds` +- `${APP_DATA}/npm-cache/_prebuilds` +- `${HOME}/.npm/_prebuilds` + +## Install + +With [npm](https://npmjs.org) do: + +``` +npm install prebuild-install +``` + +## License + +[MIT](./LICENSE) diff --git a/backend/node_modules/prebuild-install/asset.js b/backend/node_modules/prebuild-install/asset.js new file mode 100644 index 00000000..7a58e8b2 --- /dev/null +++ b/backend/node_modules/prebuild-install/asset.js @@ -0,0 +1,44 @@ +const get = require('simple-get') +const util = require('./util') +const proxy = require('./proxy') + +function findAssetId (opts, cb) { + const downloadUrl = util.getDownloadUrl(opts) + const apiUrl = util.getApiUrl(opts) + const log = opts.log || util.noopLogger + + log.http('request', 'GET ' + apiUrl) + const reqOpts = proxy({ + url: apiUrl, + json: true, + headers: { + 'User-Agent': 'simple-get', + Authorization: 'token ' + opts.token + } + }, opts) + + const req = get.concat(reqOpts, function (err, res, data) { + if (err) return cb(err) + log.http(res.statusCode, apiUrl) + if (res.statusCode !== 200) return cb(err) + + // Find asset id in release + for (const release of data) { + if (release.tag_name === opts['tag-prefix'] + opts.pkg.version) { + for (const asset of release.assets) { + if (asset.browser_download_url === downloadUrl) { + return cb(null, asset.id) + } + } + } + } + + cb(new Error('Could not find GitHub release for version')) + }) + + req.setTimeout(30 * 1000, function () { + req.abort() + }) +} + +module.exports = findAssetId diff --git a/backend/node_modules/prebuild-install/bin.js b/backend/node_modules/prebuild-install/bin.js new file mode 100755 index 00000000..e5260cce --- /dev/null +++ b/backend/node_modules/prebuild-install/bin.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +const path = require('path') +const fs = require('fs') +const napi = require('napi-build-utils') + +const pkg = require(path.resolve('package.json')) +const rc = require('./rc')(pkg) +const log = require('./log')(rc, process.env) +const download = require('./download') +const asset = require('./asset') +const util = require('./util') + +const prebuildClientVersion = require('./package.json').version +if (rc.version) { + console.log(prebuildClientVersion) + process.exit(0) +} + +if (rc.path) process.chdir(rc.path) + +if (rc.runtime === 'electron' && rc.target[0] === '4' && rc.abi === '64') { + log.error(`Electron version ${rc.target} found - skipping prebuild-install work due to known ABI issue`) + log.error('More information about this issue can be found at https://github.com/lgeiger/node-abi/issues/54') + process.exit(1) +} + +if (!fs.existsSync('package.json')) { + log.error('setup', 'No package.json found. Aborting...') + process.exit(1) +} + +if (rc.help) { + console.error(fs.readFileSync(path.join(__dirname, 'help.txt'), 'utf-8')) + process.exit(0) +} + +log.info('begin', 'Prebuild-install version', prebuildClientVersion) + +const opts = Object.assign({}, rc, { pkg: pkg, log: log }) + +if (napi.isNapiRuntime(rc.runtime)) napi.logUnsupportedVersion(rc.target, log) + +const origin = util.packageOrigin(process.env, pkg) + +if (opts.force) { + log.warn('install', 'prebuilt binaries enforced with --force!') + log.warn('install', 'prebuilt binaries may be out of date!') +} else if (origin && origin.length > 4 && origin.substr(0, 4) === 'git+') { + log.info('install', 'installing from git repository, skipping download.') + process.exit(1) +} else if (opts.buildFromSource) { + log.info('install', '--build-from-source specified, not attempting download.') + process.exit(1) +} + +const startDownload = function (downloadUrl) { + download(downloadUrl, opts, function (err) { + if (err) { + log.warn('install', err.message) + return process.exit(1) + } + log.info('install', 'Successfully installed prebuilt binary!') + }) +} + +if (opts.token) { + asset(opts, function (err, assetId) { + if (err) { + log.warn('install', err.message) + return process.exit(1) + } + + startDownload(util.getAssetUrl(opts, assetId)) + }) +} else { + startDownload(util.getDownloadUrl(opts)) +} diff --git a/backend/node_modules/prebuild-install/download.js b/backend/node_modules/prebuild-install/download.js new file mode 100644 index 00000000..26f04b05 --- /dev/null +++ b/backend/node_modules/prebuild-install/download.js @@ -0,0 +1,142 @@ +const path = require('path') +const fs = require('fs') +const get = require('simple-get') +const pump = require('pump') +const tfs = require('tar-fs') +const zlib = require('zlib') +const util = require('./util') +const error = require('./error') +const proxy = require('./proxy') +const mkdirp = require('mkdirp-classic') + +function downloadPrebuild (downloadUrl, opts, cb) { + let cachedPrebuild = util.cachedPrebuild(downloadUrl) + const localPrebuild = util.localPrebuild(downloadUrl, opts) + const tempFile = util.tempFile(cachedPrebuild) + const log = opts.log || util.noopLogger + + if (opts.nolocal) return download() + + log.info('looking for local prebuild @', localPrebuild) + fs.access(localPrebuild, fs.R_OK | fs.W_OK, function (err) { + if (err && err.code === 'ENOENT') { + return download() + } + + log.info('found local prebuild') + cachedPrebuild = localPrebuild + unpack() + }) + + function download () { + ensureNpmCacheDir(function (err) { + if (err) return onerror(err) + + log.info('looking for cached prebuild @', cachedPrebuild) + fs.access(cachedPrebuild, fs.R_OK | fs.W_OK, function (err) { + if (!(err && err.code === 'ENOENT')) { + log.info('found cached prebuild') + return unpack() + } + + log.http('request', 'GET ' + downloadUrl) + const reqOpts = proxy({ url: downloadUrl }, opts) + + if (opts.token) { + reqOpts.headers = { + 'User-Agent': 'simple-get', + Accept: 'application/octet-stream', + Authorization: 'token ' + opts.token + } + } + + const req = get(reqOpts, function (err, res) { + if (err) return onerror(err) + log.http(res.statusCode, downloadUrl) + if (res.statusCode !== 200) return onerror() + mkdirp(util.prebuildCache(), function () { + log.info('downloading to @', tempFile) + pump(res, fs.createWriteStream(tempFile), function (err) { + if (err) return onerror(err) + fs.rename(tempFile, cachedPrebuild, function (err) { + if (err) return cb(err) + log.info('renaming to @', cachedPrebuild) + unpack() + }) + }) + }) + }) + + req.setTimeout(30 * 1000, function () { + req.abort() + }) + }) + + function onerror (err) { + fs.unlink(tempFile, function () { + cb(err || error.noPrebuilts(opts)) + }) + } + }) + } + + function unpack () { + let binaryName + + const updateName = opts.updateName || function (entry) { + if (/\.node$/i.test(entry.name)) binaryName = entry.name + } + + log.info('unpacking @', cachedPrebuild) + + const options = { + readable: true, + writable: true, + hardlinkAsFilesFallback: true + } + const extract = tfs.extract(opts.path, options).on('entry', updateName) + + pump(fs.createReadStream(cachedPrebuild), zlib.createGunzip(), extract, + function (err) { + if (err) return cb(err) + + let resolved + if (binaryName) { + try { + resolved = path.resolve(opts.path || '.', binaryName) + } catch (err) { + return cb(err) + } + log.info('unpack', 'resolved to ' + resolved) + + if (opts.runtime === 'node' && opts.platform === process.platform && opts.abi === process.versions.modules && opts.arch === process.arch) { + try { + require(resolved) + } catch (err) { + return cb(err) + } + log.info('unpack', 'required ' + resolved + ' successfully') + } + } + + cb(null, resolved) + }) + } + + function ensureNpmCacheDir (cb) { + const cacheFolder = util.npmCache() + fs.access(cacheFolder, fs.R_OK | fs.W_OK, function (err) { + if (err && err.code === 'ENOENT') { + return makeNpmCacheDir() + } + cb(err) + }) + + function makeNpmCacheDir () { + log.info('npm cache directory missing, creating it...') + mkdirp(cacheFolder, cb) + } + } +} + +module.exports = downloadPrebuild diff --git a/backend/node_modules/prebuild-install/error.js b/backend/node_modules/prebuild-install/error.js new file mode 100644 index 00000000..c266c189 --- /dev/null +++ b/backend/node_modules/prebuild-install/error.js @@ -0,0 +1,14 @@ +exports.noPrebuilts = function (opts) { + return new Error([ + 'No prebuilt binaries found', + '(target=' + opts.target, + 'runtime=' + opts.runtime, + 'arch=' + opts.arch, + 'libc=' + opts.libc, + 'platform=' + opts.platform + ')' + ].join(' ')) +} + +exports.invalidArchive = function () { + return new Error('Missing .node file in archive') +} diff --git a/backend/node_modules/prebuild-install/help.txt b/backend/node_modules/prebuild-install/help.txt new file mode 100644 index 00000000..0dd316e3 --- /dev/null +++ b/backend/node_modules/prebuild-install/help.txt @@ -0,0 +1,16 @@ +prebuild-install [options] + + --download -d [url] (download prebuilds, no url means github) + --target -t version (version to install for) + --runtime -r runtime (Node runtime [node or electron] to build or install for, default is node) + --path -p path (make a prebuild-install here) + --token -T gh-token (github token for private repos) + --arch arch (target CPU architecture, see Node OS module docs, default is current arch) + --platform platform (target platform, see Node OS module docs, default is current platform) + --tag-prefix (github tag prefix, default is "v") + --force (always use prebuilt binaries when available) + --build-from-source (skip prebuild download) + --verbose (log verbosely) + --libc (use provided libc rather than system default) + --debug (set Debug or Release configuration) + --version (print prebuild-install version and exit) diff --git a/backend/node_modules/prebuild-install/index.js b/backend/node_modules/prebuild-install/index.js new file mode 100644 index 00000000..b5fc28a7 --- /dev/null +++ b/backend/node_modules/prebuild-install/index.js @@ -0,0 +1 @@ +exports.download = require('./download') diff --git a/backend/node_modules/prebuild-install/log.js b/backend/node_modules/prebuild-install/log.js new file mode 100644 index 00000000..b5ecc01b --- /dev/null +++ b/backend/node_modules/prebuild-install/log.js @@ -0,0 +1,33 @@ +const levels = { + silent: 0, + error: 1, + warn: 2, + notice: 3, + http: 4, + timing: 5, + info: 6, + verbose: 7, + silly: 8 +} + +module.exports = function (rc, env) { + const level = rc.verbose + ? 'verbose' + : env.npm_config_loglevel || 'notice' + + const logAtLevel = function (messageLevel) { + return function (...args) { + if (levels[messageLevel] <= levels[level]) { + console.error(`prebuild-install ${messageLevel} ${args.join(' ')}`) + } + } + } + + return { + error: logAtLevel('error'), + warn: logAtLevel('warn'), + http: logAtLevel('http'), + info: logAtLevel('info'), + level + } +} diff --git a/backend/node_modules/prebuild-install/node_modules/chownr/LICENSE b/backend/node_modules/prebuild-install/node_modules/chownr/LICENSE new file mode 100644 index 00000000..19129e31 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/chownr/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/backend/node_modules/prebuild-install/node_modules/chownr/README.md b/backend/node_modules/prebuild-install/node_modules/chownr/README.md new file mode 100644 index 00000000..70e9a54a --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/chownr/README.md @@ -0,0 +1,3 @@ +Like `chown -R`. + +Takes the same arguments as `fs.chown()` diff --git a/backend/node_modules/prebuild-install/node_modules/chownr/chownr.js b/backend/node_modules/prebuild-install/node_modules/chownr/chownr.js new file mode 100644 index 00000000..0d409321 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/chownr/chownr.js @@ -0,0 +1,167 @@ +'use strict' +const fs = require('fs') +const path = require('path') + +/* istanbul ignore next */ +const LCHOWN = fs.lchown ? 'lchown' : 'chown' +/* istanbul ignore next */ +const LCHOWNSYNC = fs.lchownSync ? 'lchownSync' : 'chownSync' + +/* istanbul ignore next */ +const needEISDIRHandled = fs.lchown && + !process.version.match(/v1[1-9]+\./) && + !process.version.match(/v10\.[6-9]/) + +const lchownSync = (path, uid, gid) => { + try { + return fs[LCHOWNSYNC](path, uid, gid) + } catch (er) { + if (er.code !== 'ENOENT') + throw er + } +} + +/* istanbul ignore next */ +const chownSync = (path, uid, gid) => { + try { + return fs.chownSync(path, uid, gid) + } catch (er) { + if (er.code !== 'ENOENT') + throw er + } +} + +/* istanbul ignore next */ +const handleEISDIR = + needEISDIRHandled ? (path, uid, gid, cb) => er => { + // Node prior to v10 had a very questionable implementation of + // fs.lchown, which would always try to call fs.open on a directory + // Fall back to fs.chown in those cases. + if (!er || er.code !== 'EISDIR') + cb(er) + else + fs.chown(path, uid, gid, cb) + } + : (_, __, ___, cb) => cb + +/* istanbul ignore next */ +const handleEISDirSync = + needEISDIRHandled ? (path, uid, gid) => { + try { + return lchownSync(path, uid, gid) + } catch (er) { + if (er.code !== 'EISDIR') + throw er + chownSync(path, uid, gid) + } + } + : (path, uid, gid) => lchownSync(path, uid, gid) + +// fs.readdir could only accept an options object as of node v6 +const nodeVersion = process.version +let readdir = (path, options, cb) => fs.readdir(path, options, cb) +let readdirSync = (path, options) => fs.readdirSync(path, options) +/* istanbul ignore next */ +if (/^v4\./.test(nodeVersion)) + readdir = (path, options, cb) => fs.readdir(path, cb) + +const chown = (cpath, uid, gid, cb) => { + fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, er => { + // Skip ENOENT error + cb(er && er.code !== 'ENOENT' ? er : null) + })) +} + +const chownrKid = (p, child, uid, gid, cb) => { + if (typeof child === 'string') + return fs.lstat(path.resolve(p, child), (er, stats) => { + // Skip ENOENT error + if (er) + return cb(er.code !== 'ENOENT' ? er : null) + stats.name = child + chownrKid(p, stats, uid, gid, cb) + }) + + if (child.isDirectory()) { + chownr(path.resolve(p, child.name), uid, gid, er => { + if (er) + return cb(er) + const cpath = path.resolve(p, child.name) + chown(cpath, uid, gid, cb) + }) + } else { + const cpath = path.resolve(p, child.name) + chown(cpath, uid, gid, cb) + } +} + + +const chownr = (p, uid, gid, cb) => { + readdir(p, { withFileTypes: true }, (er, children) => { + // any error other than ENOTDIR or ENOTSUP means it's not readable, + // or doesn't exist. give up. + if (er) { + if (er.code === 'ENOENT') + return cb() + else if (er.code !== 'ENOTDIR' && er.code !== 'ENOTSUP') + return cb(er) + } + if (er || !children.length) + return chown(p, uid, gid, cb) + + let len = children.length + let errState = null + const then = er => { + if (errState) + return + if (er) + return cb(errState = er) + if (-- len === 0) + return chown(p, uid, gid, cb) + } + + children.forEach(child => chownrKid(p, child, uid, gid, then)) + }) +} + +const chownrKidSync = (p, child, uid, gid) => { + if (typeof child === 'string') { + try { + const stats = fs.lstatSync(path.resolve(p, child)) + stats.name = child + child = stats + } catch (er) { + if (er.code === 'ENOENT') + return + else + throw er + } + } + + if (child.isDirectory()) + chownrSync(path.resolve(p, child.name), uid, gid) + + handleEISDirSync(path.resolve(p, child.name), uid, gid) +} + +const chownrSync = (p, uid, gid) => { + let children + try { + children = readdirSync(p, { withFileTypes: true }) + } catch (er) { + if (er.code === 'ENOENT') + return + else if (er.code === 'ENOTDIR' || er.code === 'ENOTSUP') + return handleEISDirSync(p, uid, gid) + else + throw er + } + + if (children && children.length) + children.forEach(child => chownrKidSync(p, child, uid, gid)) + + return handleEISDirSync(p, uid, gid) +} + +module.exports = chownr +chownr.sync = chownrSync diff --git a/backend/node_modules/prebuild-install/node_modules/chownr/package.json b/backend/node_modules/prebuild-install/node_modules/chownr/package.json new file mode 100644 index 00000000..c273a7d1 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/chownr/package.json @@ -0,0 +1,29 @@ +{ + "author": "Isaac Z. Schlueter (http://blog.izs.me/)", + "name": "chownr", + "description": "like `chown -R`", + "version": "1.1.4", + "repository": { + "type": "git", + "url": "git://github.com/isaacs/chownr.git" + }, + "main": "chownr.js", + "files": [ + "chownr.js" + ], + "devDependencies": { + "mkdirp": "0.3", + "rimraf": "^2.7.1", + "tap": "^14.10.6" + }, + "tap": { + "check-coverage": true + }, + "scripts": { + "test": "tap", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags" + }, + "license": "ISC" +} diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/.travis.yml b/backend/node_modules/prebuild-install/node_modules/tar-fs/.travis.yml new file mode 100644 index 00000000..977f7a61 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - 8 + - 10 + - 12 + - 14 diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/LICENSE b/backend/node_modules/prebuild-install/node_modules/tar-fs/LICENSE new file mode 100644 index 00000000..757562ec --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/README.md b/backend/node_modules/prebuild-install/node_modules/tar-fs/README.md new file mode 100644 index 00000000..c6d35cfa --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/README.md @@ -0,0 +1,165 @@ +# tar-fs + +filesystem bindings for [tar-stream](https://github.com/mafintosh/tar-stream). + +``` +npm install tar-fs +``` + +[![build status](https://secure.travis-ci.org/mafintosh/tar-fs.png)](http://travis-ci.org/mafintosh/tar-fs) + +## Usage + +tar-fs allows you to pack directories into tarballs and extract tarballs into directories. + +It doesn't gunzip for you, so if you want to extract a `.tar.gz` with this you'll need to use something like [gunzip-maybe](https://github.com/mafintosh/gunzip-maybe) in addition to this. + +``` js +var tar = require('tar-fs') +var fs = require('fs') + +// packing a directory +tar.pack('./my-directory').pipe(fs.createWriteStream('my-tarball.tar')) + +// extracting a directory +fs.createReadStream('my-other-tarball.tar').pipe(tar.extract('./my-other-directory')) +``` + +To ignore various files when packing or extracting add a ignore function to the options. `ignore` +is also an alias for `filter`. Additionally you get `header` if you use ignore while extracting. +That way you could also filter by metadata. + +``` js +var pack = tar.pack('./my-directory', { + ignore: function(name) { + return path.extname(name) === '.bin' // ignore .bin files when packing + } +}) + +var extract = tar.extract('./my-other-directory', { + ignore: function(name) { + return path.extname(name) === '.bin' // ignore .bin files inside the tarball when extracing + } +}) + +var extractFilesDirs = tar.extract('./my-other-other-directory', { + ignore: function(_, header) { + // pass files & directories, ignore e.g. symlinks + return header.type !== 'file' && header.type !== 'directory' + } +}) +``` + +You can also specify which entries to pack using the `entries` option + +```js +var pack = tar.pack('./my-directory', { + entries: ['file1', 'subdir/file2'] // only the specific entries will be packed +}) +``` + +If you want to modify the headers when packing/extracting add a map function to the options + +``` js +var pack = tar.pack('./my-directory', { + map: function(header) { + header.name = 'prefixed/'+header.name + return header + } +}) + +var extract = tar.extract('./my-directory', { + map: function(header) { + header.name = 'another-prefix/'+header.name + return header + } +}) +``` + +Similarly you can use `mapStream` incase you wanna modify the input/output file streams + +``` js +var pack = tar.pack('./my-directory', { + mapStream: function(fileStream, header) { + // NOTE: the returned stream HAS to have the same length as the input stream. + // If not make sure to update the size in the header passed in here. + if (path.extname(header.name) === '.js') { + return fileStream.pipe(someTransform) + } + return fileStream; + } +}) + +var extract = tar.extract('./my-directory', { + mapStream: function(fileStream, header) { + if (path.extname(header.name) === '.js') { + return fileStream.pipe(someTransform) + } + return fileStream; + } +}) +``` + +Set `options.fmode` and `options.dmode` to ensure that files/directories extracted have the corresponding modes + +``` js +var extract = tar.extract('./my-directory', { + dmode: parseInt(555, 8), // all dirs should be readable + fmode: parseInt(444, 8) // all files should be readable +}) +``` + +It can be useful to use `dmode` and `fmode` if you are packing/unpacking tarballs between *nix/windows to ensure that all files/directories unpacked are readable. + +Alternatively you can set `options.readable` and/or `options.writable` to set the dmode and fmode to readable/writable. + +``` js +var extract = tar.extract('./my-directory', { + readable: true, // all dirs and files should be readable + writable: true, // all dirs and files should be writable +}) +``` + +Set `options.strict` to `false` if you want to ignore errors due to unsupported entry types (like device files) + +To dereference symlinks (pack the contents of the symlink instead of the link itself) set `options.dereference` to `true`. + +## Copy a directory + +Copying a directory with permissions and mtime intact is as simple as + +``` js +tar.pack('source-directory').pipe(tar.extract('dest-directory')) +``` + +## Interaction with [`tar-stream`](https://github.com/mafintosh/tar-stream) + +Use `finalize: false` and the `finish` hook to +leave the pack stream open for further entries (see +[`tar-stream#pack`](https://github.com/mafintosh/tar-stream#packing)), +and use `pack` to pass an existing pack stream. + +``` js +var mypack = tar.pack('./my-directory', { + finalize: false, + finish: function(sameAsMypack) { + mypack.entry({name: 'generated-file.txt'}, "hello") + tar.pack('./other-directory', { + pack: sameAsMypack + }) + } +}) +``` + + +## Performance + +Packing and extracting a 6.1 GB with 2496 directories and 2398 files yields the following results on my Macbook Air. +[See the benchmark here](https://gist.github.com/mafintosh/8102201) + +* tar-fs: 34.261 seconds +* [node-tar](https://github.com/isaacs/node-tar): 366.123 seconds (or 10x slower) + +## License + +MIT diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/index.js b/backend/node_modules/prebuild-install/node_modules/tar-fs/index.js new file mode 100644 index 00000000..4797bc93 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/index.js @@ -0,0 +1,363 @@ +var chownr = require('chownr') +var tar = require('tar-stream') +var pump = require('pump') +var mkdirp = require('mkdirp-classic') +var fs = require('fs') +var path = require('path') +var os = require('os') + +var win32 = os.platform() === 'win32' + +var noop = function () {} + +var echo = function (name) { + return name +} + +var normalize = !win32 ? echo : function (name) { + return name.replace(/\\/g, '/').replace(/[:?<>|]/g, '_') +} + +var statAll = function (fs, stat, cwd, ignore, entries, sort) { + var queue = entries || ['.'] + + return function loop (callback) { + if (!queue.length) return callback() + var next = queue.shift() + var nextAbs = path.join(cwd, next) + + stat.call(fs, nextAbs, function (err, stat) { + if (err) return callback(err) + + if (!stat.isDirectory()) return callback(null, next, stat) + + fs.readdir(nextAbs, function (err, files) { + if (err) return callback(err) + + if (sort) files.sort() + for (var i = 0; i < files.length; i++) { + if (!ignore(path.join(cwd, next, files[i]))) queue.push(path.join(next, files[i])) + } + + callback(null, next, stat) + }) + }) + } +} + +var strip = function (map, level) { + return function (header) { + header.name = header.name.split('/').slice(level).join('/') + + var linkname = header.linkname + if (linkname && (header.type === 'link' || path.isAbsolute(linkname))) { + header.linkname = linkname.split('/').slice(level).join('/') + } + + return map(header) + } +} + +exports.pack = function (cwd, opts) { + if (!cwd) cwd = '.' + if (!opts) opts = {} + + var xfs = opts.fs || fs + var ignore = opts.ignore || opts.filter || noop + var map = opts.map || noop + var mapStream = opts.mapStream || echo + var statNext = statAll(xfs, opts.dereference ? xfs.stat : xfs.lstat, cwd, ignore, opts.entries, opts.sort) + var strict = opts.strict !== false + var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask() + var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0 + var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0 + var pack = opts.pack || tar.pack() + var finish = opts.finish || noop + + if (opts.strip) map = strip(map, opts.strip) + + if (opts.readable) { + dmode |= parseInt(555, 8) + fmode |= parseInt(444, 8) + } + if (opts.writable) { + dmode |= parseInt(333, 8) + fmode |= parseInt(222, 8) + } + + var onsymlink = function (filename, header) { + xfs.readlink(path.join(cwd, filename), function (err, linkname) { + if (err) return pack.destroy(err) + header.linkname = normalize(linkname) + pack.entry(header, onnextentry) + }) + } + + var onstat = function (err, filename, stat) { + if (err) return pack.destroy(err) + if (!filename) { + if (opts.finalize !== false) pack.finalize() + return finish(pack) + } + + if (stat.isSocket()) return onnextentry() // tar does not support sockets... + + var header = { + name: normalize(filename), + mode: (stat.mode | (stat.isDirectory() ? dmode : fmode)) & umask, + mtime: stat.mtime, + size: stat.size, + type: 'file', + uid: stat.uid, + gid: stat.gid + } + + if (stat.isDirectory()) { + header.size = 0 + header.type = 'directory' + header = map(header) || header + return pack.entry(header, onnextentry) + } + + if (stat.isSymbolicLink()) { + header.size = 0 + header.type = 'symlink' + header = map(header) || header + return onsymlink(filename, header) + } + + // TODO: add fifo etc... + + header = map(header) || header + + if (!stat.isFile()) { + if (strict) return pack.destroy(new Error('unsupported type for ' + filename)) + return onnextentry() + } + + var entry = pack.entry(header, onnextentry) + if (!entry) return + + var rs = mapStream(xfs.createReadStream(path.join(cwd, filename), { start: 0, end: header.size > 0 ? header.size - 1 : header.size }), header) + + rs.on('error', function (err) { // always forward errors on destroy + entry.destroy(err) + }) + + pump(rs, entry) + } + + var onnextentry = function (err) { + if (err) return pack.destroy(err) + statNext(onstat) + } + + onnextentry() + + return pack +} + +var head = function (list) { + return list.length ? list[list.length - 1] : null +} + +var processGetuid = function () { + return process.getuid ? process.getuid() : -1 +} + +var processUmask = function () { + return process.umask ? process.umask() : 0 +} + +exports.extract = function (cwd, opts) { + if (!cwd) cwd = '.' + if (!opts) opts = {} + + var xfs = opts.fs || fs + var ignore = opts.ignore || opts.filter || noop + var map = opts.map || noop + var mapStream = opts.mapStream || echo + var own = opts.chown !== false && !win32 && processGetuid() === 0 + var extract = opts.extract || tar.extract() + var stack = [] + var now = new Date() + var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask() + var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0 + var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0 + var strict = opts.strict !== false + + if (opts.strip) map = strip(map, opts.strip) + + if (opts.readable) { + dmode |= parseInt(555, 8) + fmode |= parseInt(444, 8) + } + if (opts.writable) { + dmode |= parseInt(333, 8) + fmode |= parseInt(222, 8) + } + + var utimesParent = function (name, cb) { // we just set the mtime on the parent dir again everytime we write an entry + var top + while ((top = head(stack)) && name.slice(0, top[0].length) !== top[0]) stack.pop() + if (!top) return cb() + xfs.utimes(top[0], now, top[1], cb) + } + + var utimes = function (name, header, cb) { + if (opts.utimes === false) return cb() + + if (header.type === 'directory') return xfs.utimes(name, now, header.mtime, cb) + if (header.type === 'symlink') return utimesParent(name, cb) // TODO: how to set mtime on link? + + xfs.utimes(name, now, header.mtime, function (err) { + if (err) return cb(err) + utimesParent(name, cb) + }) + } + + var chperm = function (name, header, cb) { + var link = header.type === 'symlink' + + /* eslint-disable node/no-deprecated-api */ + var chmod = link ? xfs.lchmod : xfs.chmod + var chown = link ? xfs.lchown : xfs.chown + /* eslint-enable node/no-deprecated-api */ + + if (!chmod) return cb() + + var mode = (header.mode | (header.type === 'directory' ? dmode : fmode)) & umask + + if (chown && own) chown.call(xfs, name, header.uid, header.gid, onchown) + else onchown(null) + + function onchown (err) { + if (err) return cb(err) + if (!chmod) return cb() + chmod.call(xfs, name, mode, cb) + } + } + + extract.on('entry', function (header, stream, next) { + header = map(header) || header + header.name = normalize(header.name) + var name = path.join(cwd, path.join('/', header.name)) + + if (ignore(name, header)) { + stream.resume() + return next() + } + + var stat = function (err) { + if (err) return next(err) + utimes(name, header, function (err) { + if (err) return next(err) + if (win32) return next() + chperm(name, header, next) + }) + } + + var onsymlink = function () { + if (win32) return next() // skip symlinks on win for now before it can be tested + xfs.unlink(name, function () { + var dst = path.resolve(path.dirname(name), header.linkname) + if (!inCwd(dst, cwd)) return next(new Error(name + ' is not a valid symlink')) + + xfs.symlink(header.linkname, name, stat) + }) + } + + var onlink = function () { + if (win32) return next() // skip links on win for now before it can be tested + xfs.unlink(name, function () { + var srcpath = path.join(cwd, path.join('/', header.linkname)) + + xfs.realpath(srcpath, function (err, dst) { + if (err || !inCwd(dst, cwd)) return next(new Error(name + ' is not a valid hardlink')) + + xfs.link(dst, name, function (err) { + if (err && err.code === 'EPERM' && opts.hardlinkAsFilesFallback) { + stream = xfs.createReadStream(srcpath) + return onfile() + } + + stat(err) + }) + }) + }) + } + + var onfile = function () { + var ws = xfs.createWriteStream(name) + var rs = mapStream(stream, header) + + ws.on('error', function (err) { // always forward errors on destroy + rs.destroy(err) + }) + + pump(rs, ws, function (err) { + if (err) return next(err) + ws.on('close', stat) + }) + } + + if (header.type === 'directory') { + stack.push([name, header.mtime]) + return mkdirfix(name, { + fs: xfs, own: own, uid: header.uid, gid: header.gid + }, stat) + } + + var dir = path.dirname(name) + + validate(xfs, dir, path.join(cwd, '.'), function (err, valid) { + if (err) return next(err) + if (!valid) return next(new Error(dir + ' is not a valid path')) + + mkdirfix(dir, { + fs: xfs, own: own, uid: header.uid, gid: header.gid + }, function (err) { + if (err) return next(err) + + switch (header.type) { + case 'file': return onfile() + case 'link': return onlink() + case 'symlink': return onsymlink() + } + + if (strict) return next(new Error('unsupported type for ' + name + ' (' + header.type + ')')) + + stream.resume() + next() + }) + }) + }) + + if (opts.finish) extract.on('finish', opts.finish) + + return extract +} + +function validate (fs, name, root, cb) { + if (name === root) return cb(null, true) + fs.lstat(name, function (err, st) { + if (err && err.code !== 'ENOENT') return cb(err) + if (err || st.isDirectory()) return validate(fs, path.join(name, '..'), root, cb) + cb(null, false) + }) +} + +function mkdirfix (name, opts, cb) { + mkdirp(name, { fs: opts.fs }, function (err, made) { + if (!err && made && opts.own) { + chownr(made, opts.uid, opts.gid, cb) + } else { + cb(err) + } + }) +} + +function inCwd (dst, cwd) { + cwd = path.resolve(cwd) + return cwd === dst || dst.startsWith(cwd + path.sep) +} diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/package.json b/backend/node_modules/prebuild-install/node_modules/tar-fs/package.json new file mode 100644 index 00000000..61365778 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/package.json @@ -0,0 +1,41 @@ +{ + "name": "tar-fs", + "version": "2.1.4", + "description": "filesystem bindings for tar-stream", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "keywords": [ + "tar", + "fs", + "file", + "tarball", + "directory", + "stream" + ], + "devDependencies": { + "rimraf": "^2.6.3", + "standard": "^13.0.1", + "tape": "^4.9.2" + }, + "scripts": { + "test": "standard && tape test/index.js" + }, + "bugs": { + "url": "https://github.com/mafintosh/tar-fs/issues" + }, + "homepage": "https://github.com/mafintosh/tar-fs", + "main": "index.js", + "directories": { + "test": "test" + }, + "author": "Mathias Buus", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/mafintosh/tar-fs.git" + } +} diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/a/hello.txt b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/a/hello.txt new file mode 100644 index 00000000..3b18e512 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/a/hello.txt @@ -0,0 +1 @@ +hello world diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/b/a/test.txt b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/b/a/test.txt new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/b/a/test.txt @@ -0,0 +1 @@ +test diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/file1 b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/file1 new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/file2 b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/file2 new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/sub-dir/file5 b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/sub-dir/file5 new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/sub-files/file3 b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/sub-files/file3 new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/sub-files/file4 b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/d/sub-files/file4 new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/e/directory/.ignore b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/e/directory/.ignore new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/e/file b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/e/file new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/invalid.tar b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/invalid.tar new file mode 100644 index 00000000..a645e9ce Binary files /dev/null and b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/fixtures/invalid.tar differ diff --git a/backend/node_modules/prebuild-install/node_modules/tar-fs/test/index.js b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/index.js new file mode 100644 index 00000000..f983b4cd --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-fs/test/index.js @@ -0,0 +1,346 @@ +var test = require('tape') +var rimraf = require('rimraf') +var tar = require('../index') +var tarStream = require('tar-stream') +var path = require('path') +var fs = require('fs') +var os = require('os') + +var win32 = os.platform() === 'win32' + +var mtime = function (st) { + return Math.floor(st.mtime.getTime() / 1000) +} + +test('copy a -> copy/a', function (t) { + t.plan(5) + + var a = path.join(__dirname, 'fixtures', 'a') + var b = path.join(__dirname, 'fixtures', 'copy', 'a') + + rimraf.sync(b) + tar.pack(a) + .pipe(tar.extract(b)) + .on('finish', function () { + var files = fs.readdirSync(b) + t.same(files.length, 1) + t.same(files[0], 'hello.txt') + var fileB = path.join(b, files[0]) + var fileA = path.join(a, files[0]) + t.same(fs.readFileSync(fileB, 'utf-8'), fs.readFileSync(fileA, 'utf-8')) + t.same(fs.statSync(fileB).mode, fs.statSync(fileA).mode) + t.same(mtime(fs.statSync(fileB)), mtime(fs.statSync(fileA))) + }) +}) + +test('copy b -> copy/b', function (t) { + t.plan(8) + + var a = path.join(__dirname, 'fixtures', 'b') + var b = path.join(__dirname, 'fixtures', 'copy', 'b') + + rimraf.sync(b) + tar.pack(a) + .pipe(tar.extract(b)) + .on('finish', function () { + var files = fs.readdirSync(b) + t.same(files.length, 1) + t.same(files[0], 'a') + var dirB = path.join(b, files[0]) + var dirA = path.join(a, files[0]) + t.same(fs.statSync(dirB).mode, fs.statSync(dirA).mode) + t.same(mtime(fs.statSync(dirB)), mtime(fs.statSync(dirA))) + t.ok(fs.statSync(dirB).isDirectory()) + var fileB = path.join(dirB, 'test.txt') + var fileA = path.join(dirA, 'test.txt') + t.same(fs.readFileSync(fileB, 'utf-8'), fs.readFileSync(fileA, 'utf-8')) + t.same(fs.statSync(fileB).mode, fs.statSync(fileA).mode) + t.same(mtime(fs.statSync(fileB)), mtime(fs.statSync(fileA))) + }) +}) + +test('symlink', function (t) { + if (win32) { // no symlink support on win32 currently. TODO: test if this can be enabled somehow + t.plan(1) + t.ok(true) + return + } + + t.plan(5) + + var a = path.join(__dirname, 'fixtures', 'c') + + rimraf.sync(path.join(a, 'link')) + fs.symlinkSync('.gitignore', path.join(a, 'link')) + + var b = path.join(__dirname, 'fixtures', 'copy', 'c') + + rimraf.sync(b) + tar.pack(a) + .pipe(tar.extract(b)) + .on('finish', function () { + var files = fs.readdirSync(b).sort() + t.same(files.length, 2) + t.same(files[0], '.gitignore') + t.same(files[1], 'link') + + var linkA = path.join(a, 'link') + var linkB = path.join(b, 'link') + + t.same(mtime(fs.lstatSync(linkB)), mtime(fs.lstatSync(linkA))) + t.same(fs.readlinkSync(linkB), fs.readlinkSync(linkA)) + }) +}) + +test('follow symlinks', function (t) { + if (win32) { // no symlink support on win32 currently. TODO: test if this can be enabled somehow + t.plan(1) + t.ok(true) + return + } + + t.plan(5) + + var a = path.join(__dirname, 'fixtures', 'c') + + rimraf.sync(path.join(a, 'link')) + fs.symlinkSync('.gitignore', path.join(a, 'link')) + + var b = path.join(__dirname, 'fixtures', 'copy', 'c-dereference') + + rimraf.sync(b) + tar.pack(a, { dereference: true }) + .pipe(tar.extract(b)) + .on('finish', function () { + var files = fs.readdirSync(b).sort() + t.same(files.length, 2) + t.same(files[0], '.gitignore') + t.same(files[1], 'link') + + var file1 = path.join(b, '.gitignore') + var file2 = path.join(b, 'link') + + t.same(mtime(fs.lstatSync(file1)), mtime(fs.lstatSync(file2))) + t.same(fs.readFileSync(file1), fs.readFileSync(file2)) + }) +}) + +test('strip', function (t) { + t.plan(2) + + var a = path.join(__dirname, 'fixtures', 'b') + var b = path.join(__dirname, 'fixtures', 'copy', 'b-strip') + + rimraf.sync(b) + + tar.pack(a) + .pipe(tar.extract(b, { strip: 1 })) + .on('finish', function () { + var files = fs.readdirSync(b).sort() + t.same(files.length, 1) + t.same(files[0], 'test.txt') + }) +}) + +test('strip + map', function (t) { + t.plan(2) + + var a = path.join(__dirname, 'fixtures', 'b') + var b = path.join(__dirname, 'fixtures', 'copy', 'b-strip') + + rimraf.sync(b) + + var uppercase = function (header) { + header.name = header.name.toUpperCase() + return header + } + + tar.pack(a) + .pipe(tar.extract(b, { strip: 1, map: uppercase })) + .on('finish', function () { + var files = fs.readdirSync(b).sort() + t.same(files.length, 1) + t.same(files[0], 'TEST.TXT') + }) +}) + +test('map + dir + permissions', function (t) { + t.plan(win32 ? 1 : 2) // skip chmod test, it's not working like unix + + var a = path.join(__dirname, 'fixtures', 'b') + var b = path.join(__dirname, 'fixtures', 'copy', 'a-perms') + + rimraf.sync(b) + + var aWithMode = function (header) { + if (header.name === 'a') { + header.mode = parseInt(700, 8) + } + return header + } + + tar.pack(a) + .pipe(tar.extract(b, { map: aWithMode })) + .on('finish', function () { + var files = fs.readdirSync(b).sort() + var stat = fs.statSync(path.join(b, 'a')) + t.same(files.length, 1) + if (!win32) { + t.same(stat.mode & parseInt(777, 8), parseInt(700, 8)) + } + }) +}) + +test('specific entries', function (t) { + t.plan(6) + + var a = path.join(__dirname, 'fixtures', 'd') + var b = path.join(__dirname, 'fixtures', 'copy', 'd-entries') + + var entries = ['file1', 'sub-files/file3', 'sub-dir'] + + rimraf.sync(b) + tar.pack(a, { entries: entries }) + .pipe(tar.extract(b)) + .on('finish', function () { + var files = fs.readdirSync(b) + t.same(files.length, 3) + t.notSame(files.indexOf('file1'), -1) + t.notSame(files.indexOf('sub-files'), -1) + t.notSame(files.indexOf('sub-dir'), -1) + var subFiles = fs.readdirSync(path.join(b, 'sub-files')) + t.same(subFiles, ['file3']) + var subDir = fs.readdirSync(path.join(b, 'sub-dir')) + t.same(subDir, ['file5']) + }) +}) + +test('check type while mapping header on packing', function (t) { + t.plan(3) + + var e = path.join(__dirname, 'fixtures', 'e') + + var checkHeaderType = function (header) { + if (header.name.indexOf('.') === -1) t.same(header.type, header.name) + } + + tar.pack(e, { map: checkHeaderType }) +}) + +test('finish callbacks', function (t) { + t.plan(3) + + var a = path.join(__dirname, 'fixtures', 'a') + var b = path.join(__dirname, 'fixtures', 'copy', 'a') + + rimraf.sync(b) + + var packEntries = 0 + var extractEntries = 0 + + var countPackEntry = function (header) { packEntries++ } + var countExtractEntry = function (header) { extractEntries++ } + + var pack + var onPackFinish = function (passedPack) { + t.equal(packEntries, 2, 'All entries have been packed') // 2 entries - the file and base directory + t.equal(passedPack, pack, 'The finish hook passes the pack') + } + + var onExtractFinish = function () { t.equal(extractEntries, 2) } + + pack = tar.pack(a, { map: countPackEntry, finish: onPackFinish }) + + pack.pipe(tar.extract(b, { map: countExtractEntry, finish: onExtractFinish })) + .on('finish', function () { + t.end() + }) +}) + +test('not finalizing the pack', function (t) { + t.plan(2) + + var a = path.join(__dirname, 'fixtures', 'a') + var b = path.join(__dirname, 'fixtures', 'b') + + var out = path.join(__dirname, 'fixtures', 'copy', 'merged-packs') + + rimraf.sync(out) + + var prefixer = function (prefix) { + return function (header) { + header.name = path.join(prefix, header.name) + return header + } + } + + tar.pack(a, { + map: prefixer('a-files'), + finalize: false, + finish: packB + }) + + function packB (pack) { + tar.pack(b, { pack: pack, map: prefixer('b-files') }) + .pipe(tar.extract(out)) + .on('finish', assertResults) + } + + function assertResults () { + var containers = fs.readdirSync(out) + t.deepEqual(containers, ['a-files', 'b-files']) + var aFiles = fs.readdirSync(path.join(out, 'a-files')) + t.deepEqual(aFiles, ['hello.txt']) + } +}) + +test('do not extract invalid tar', function (t) { + var a = path.join(__dirname, 'fixtures', 'invalid.tar') + + var out = path.join(__dirname, 'fixtures', 'invalid') + + rimraf.sync(out) + + fs.createReadStream(a) + .pipe(tar.extract(out)) + .on('error', function (err) { + t.ok(/is not a valid symlink/i.test(err.message)) + fs.stat(path.join(out, '../bar'), function (err) { + t.ok(err) + t.end() + }) + }) +}) + +test('no abs hardlink targets', function (t) { + var out = path.join(__dirname, 'fixtures', 'invalid') + var outside = path.join(__dirname, 'fixtures', 'outside') + + rimraf.sync(out) + + var s = tarStream.pack() + + fs.writeFileSync(outside, 'something') + + s.entry({ + type: 'link', + name: 'link', + linkname: outside + }) + + s.entry({ + name: 'link' + }, 'overwrite') + + s.finalize() + + s.pipe(tar.extract(out)) + .on('error', function (err) { + t.ok(err, 'had error') + fs.readFile(outside, 'utf-8', function (err, str) { + t.error(err, 'no error') + t.same(str, 'something') + t.end() + }) + }) +}) diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/LICENSE b/backend/node_modules/prebuild-install/node_modules/tar-stream/LICENSE new file mode 100644 index 00000000..757562ec --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/README.md b/backend/node_modules/prebuild-install/node_modules/tar-stream/README.md new file mode 100644 index 00000000..2679d9d0 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/README.md @@ -0,0 +1,168 @@ +# tar-stream + +tar-stream is a streaming tar parser and generator and nothing else. It is streams2 and operates purely using streams which means you can easily extract/parse tarballs without ever hitting the file system. + +Note that you still need to gunzip your data if you have a `.tar.gz`. We recommend using [gunzip-maybe](https://github.com/mafintosh/gunzip-maybe) in conjunction with this. + +``` +npm install tar-stream +``` + +[![build status](https://secure.travis-ci.org/mafintosh/tar-stream.png)](http://travis-ci.org/mafintosh/tar-stream) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) + +## Usage + +tar-stream exposes two streams, [pack](https://github.com/mafintosh/tar-stream#packing) which creates tarballs and [extract](https://github.com/mafintosh/tar-stream#extracting) which extracts tarballs. To [modify an existing tarball](https://github.com/mafintosh/tar-stream#modifying-existing-tarballs) use both. + + +It implementes USTAR with additional support for pax extended headers. It should be compatible with all popular tar distributions out there (gnutar, bsdtar etc) + +## Related + +If you want to pack/unpack directories on the file system check out [tar-fs](https://github.com/mafintosh/tar-fs) which provides file system bindings to this module. + +## Packing + +To create a pack stream use `tar.pack()` and call `pack.entry(header, [callback])` to add tar entries. + +``` js +var tar = require('tar-stream') +var pack = tar.pack() // pack is a streams2 stream + +// add a file called my-test.txt with the content "Hello World!" +pack.entry({ name: 'my-test.txt' }, 'Hello World!') + +// add a file called my-stream-test.txt from a stream +var entry = pack.entry({ name: 'my-stream-test.txt', size: 11 }, function(err) { + // the stream was added + // no more entries + pack.finalize() +}) + +entry.write('hello') +entry.write(' ') +entry.write('world') +entry.end() + +// pipe the pack stream somewhere +pack.pipe(process.stdout) +``` + +## Extracting + +To extract a stream use `tar.extract()` and listen for `extract.on('entry', (header, stream, next) )` + +``` js +var extract = tar.extract() + +extract.on('entry', function(header, stream, next) { + // header is the tar header + // stream is the content body (might be an empty stream) + // call next when you are done with this entry + + stream.on('end', function() { + next() // ready for next entry + }) + + stream.resume() // just auto drain the stream +}) + +extract.on('finish', function() { + // all entries read +}) + +pack.pipe(extract) +``` + +The tar archive is streamed sequentially, meaning you **must** drain each entry's stream as you get them or else the main extract stream will receive backpressure and stop reading. + +## Headers + +The header object using in `entry` should contain the following properties. +Most of these values can be found by stat'ing a file. + +``` js +{ + name: 'path/to/this/entry.txt', + size: 1314, // entry size. defaults to 0 + mode: 0o644, // entry mode. defaults to to 0o755 for dirs and 0o644 otherwise + mtime: new Date(), // last modified date for entry. defaults to now. + type: 'file', // type of entry. defaults to file. can be: + // file | link | symlink | directory | block-device + // character-device | fifo | contiguous-file + linkname: 'path', // linked file name + uid: 0, // uid of entry owner. defaults to 0 + gid: 0, // gid of entry owner. defaults to 0 + uname: 'maf', // uname of entry owner. defaults to null + gname: 'staff', // gname of entry owner. defaults to null + devmajor: 0, // device major version. defaults to 0 + devminor: 0 // device minor version. defaults to 0 +} +``` + +## Modifying existing tarballs + +Using tar-stream it is easy to rewrite paths / change modes etc in an existing tarball. + +``` js +var extract = tar.extract() +var pack = tar.pack() +var path = require('path') + +extract.on('entry', function(header, stream, callback) { + // let's prefix all names with 'tmp' + header.name = path.join('tmp', header.name) + // write the new entry to the pack stream + stream.pipe(pack.entry(header, callback)) +}) + +extract.on('finish', function() { + // all entries done - lets finalize it + pack.finalize() +}) + +// pipe the old tarball to the extractor +oldTarballStream.pipe(extract) + +// pipe the new tarball the another stream +pack.pipe(newTarballStream) +``` + +## Saving tarball to fs + + +``` js +var fs = require('fs') +var tar = require('tar-stream') + +var pack = tar.pack() // pack is a streams2 stream +var path = 'YourTarBall.tar' +var yourTarball = fs.createWriteStream(path) + +// add a file called YourFile.txt with the content "Hello World!" +pack.entry({name: 'YourFile.txt'}, 'Hello World!', function (err) { + if (err) throw err + pack.finalize() +}) + +// pipe the pack stream to your file +pack.pipe(yourTarball) + +yourTarball.on('close', function () { + console.log(path + ' has been written') + fs.stat(path, function(err, stats) { + if (err) throw err + console.log(stats) + console.log('Got file info successfully!') + }) +}) +``` + +## Performance + +[See tar-fs for a performance comparison with node-tar](https://github.com/mafintosh/tar-fs/blob/master/README.md#performance) + +# License + +MIT diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/extract.js b/backend/node_modules/prebuild-install/node_modules/tar-stream/extract.js new file mode 100644 index 00000000..11b13b7c --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/extract.js @@ -0,0 +1,257 @@ +var util = require('util') +var bl = require('bl') +var headers = require('./headers') + +var Writable = require('readable-stream').Writable +var PassThrough = require('readable-stream').PassThrough + +var noop = function () {} + +var overflow = function (size) { + size &= 511 + return size && 512 - size +} + +var emptyStream = function (self, offset) { + var s = new Source(self, offset) + s.end() + return s +} + +var mixinPax = function (header, pax) { + if (pax.path) header.name = pax.path + if (pax.linkpath) header.linkname = pax.linkpath + if (pax.size) header.size = parseInt(pax.size, 10) + header.pax = pax + return header +} + +var Source = function (self, offset) { + this._parent = self + this.offset = offset + PassThrough.call(this, { autoDestroy: false }) +} + +util.inherits(Source, PassThrough) + +Source.prototype.destroy = function (err) { + this._parent.destroy(err) +} + +var Extract = function (opts) { + if (!(this instanceof Extract)) return new Extract(opts) + Writable.call(this, opts) + + opts = opts || {} + + this._offset = 0 + this._buffer = bl() + this._missing = 0 + this._partial = false + this._onparse = noop + this._header = null + this._stream = null + this._overflow = null + this._cb = null + this._locked = false + this._destroyed = false + this._pax = null + this._paxGlobal = null + this._gnuLongPath = null + this._gnuLongLinkPath = null + + var self = this + var b = self._buffer + + var oncontinue = function () { + self._continue() + } + + var onunlock = function (err) { + self._locked = false + if (err) return self.destroy(err) + if (!self._stream) oncontinue() + } + + var onstreamend = function () { + self._stream = null + var drain = overflow(self._header.size) + if (drain) self._parse(drain, ondrain) + else self._parse(512, onheader) + if (!self._locked) oncontinue() + } + + var ondrain = function () { + self._buffer.consume(overflow(self._header.size)) + self._parse(512, onheader) + oncontinue() + } + + var onpaxglobalheader = function () { + var size = self._header.size + self._paxGlobal = headers.decodePax(b.slice(0, size)) + b.consume(size) + onstreamend() + } + + var onpaxheader = function () { + var size = self._header.size + self._pax = headers.decodePax(b.slice(0, size)) + if (self._paxGlobal) self._pax = Object.assign({}, self._paxGlobal, self._pax) + b.consume(size) + onstreamend() + } + + var ongnulongpath = function () { + var size = self._header.size + this._gnuLongPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding) + b.consume(size) + onstreamend() + } + + var ongnulonglinkpath = function () { + var size = self._header.size + this._gnuLongLinkPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding) + b.consume(size) + onstreamend() + } + + var onheader = function () { + var offset = self._offset + var header + try { + header = self._header = headers.decode(b.slice(0, 512), opts.filenameEncoding, opts.allowUnknownFormat) + } catch (err) { + self.emit('error', err) + } + b.consume(512) + + if (!header) { + self._parse(512, onheader) + oncontinue() + return + } + if (header.type === 'gnu-long-path') { + self._parse(header.size, ongnulongpath) + oncontinue() + return + } + if (header.type === 'gnu-long-link-path') { + self._parse(header.size, ongnulonglinkpath) + oncontinue() + return + } + if (header.type === 'pax-global-header') { + self._parse(header.size, onpaxglobalheader) + oncontinue() + return + } + if (header.type === 'pax-header') { + self._parse(header.size, onpaxheader) + oncontinue() + return + } + + if (self._gnuLongPath) { + header.name = self._gnuLongPath + self._gnuLongPath = null + } + + if (self._gnuLongLinkPath) { + header.linkname = self._gnuLongLinkPath + self._gnuLongLinkPath = null + } + + if (self._pax) { + self._header = header = mixinPax(header, self._pax) + self._pax = null + } + + self._locked = true + + if (!header.size || header.type === 'directory') { + self._parse(512, onheader) + self.emit('entry', header, emptyStream(self, offset), onunlock) + return + } + + self._stream = new Source(self, offset) + + self.emit('entry', header, self._stream, onunlock) + self._parse(header.size, onstreamend) + oncontinue() + } + + this._onheader = onheader + this._parse(512, onheader) +} + +util.inherits(Extract, Writable) + +Extract.prototype.destroy = function (err) { + if (this._destroyed) return + this._destroyed = true + + if (err) this.emit('error', err) + this.emit('close') + if (this._stream) this._stream.emit('close') +} + +Extract.prototype._parse = function (size, onparse) { + if (this._destroyed) return + this._offset += size + this._missing = size + if (onparse === this._onheader) this._partial = false + this._onparse = onparse +} + +Extract.prototype._continue = function () { + if (this._destroyed) return + var cb = this._cb + this._cb = noop + if (this._overflow) this._write(this._overflow, undefined, cb) + else cb() +} + +Extract.prototype._write = function (data, enc, cb) { + if (this._destroyed) return + + var s = this._stream + var b = this._buffer + var missing = this._missing + if (data.length) this._partial = true + + // we do not reach end-of-chunk now. just forward it + + if (data.length < missing) { + this._missing -= data.length + this._overflow = null + if (s) return s.write(data, cb) + b.append(data) + return cb() + } + + // end-of-chunk. the parser should call cb. + + this._cb = cb + this._missing = 0 + + var overflow = null + if (data.length > missing) { + overflow = data.slice(missing) + data = data.slice(0, missing) + } + + if (s) s.end(data) + else b.append(data) + + this._overflow = overflow + this._onparse() +} + +Extract.prototype._final = function (cb) { + if (this._partial) return this.destroy(new Error('Unexpected end of data')) + cb() +} + +module.exports = Extract diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/headers.js b/backend/node_modules/prebuild-install/node_modules/tar-stream/headers.js new file mode 100644 index 00000000..aba4ca49 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/headers.js @@ -0,0 +1,295 @@ +var alloc = Buffer.alloc + +var ZEROS = '0000000000000000000' +var SEVENS = '7777777777777777777' +var ZERO_OFFSET = '0'.charCodeAt(0) +var USTAR_MAGIC = Buffer.from('ustar\x00', 'binary') +var USTAR_VER = Buffer.from('00', 'binary') +var GNU_MAGIC = Buffer.from('ustar\x20', 'binary') +var GNU_VER = Buffer.from('\x20\x00', 'binary') +var MASK = parseInt('7777', 8) +var MAGIC_OFFSET = 257 +var VERSION_OFFSET = 263 + +var clamp = function (index, len, defaultValue) { + if (typeof index !== 'number') return defaultValue + index = ~~index // Coerce to integer. + if (index >= len) return len + if (index >= 0) return index + index += len + if (index >= 0) return index + return 0 +} + +var toType = function (flag) { + switch (flag) { + case 0: + return 'file' + case 1: + return 'link' + case 2: + return 'symlink' + case 3: + return 'character-device' + case 4: + return 'block-device' + case 5: + return 'directory' + case 6: + return 'fifo' + case 7: + return 'contiguous-file' + case 72: + return 'pax-header' + case 55: + return 'pax-global-header' + case 27: + return 'gnu-long-link-path' + case 28: + case 30: + return 'gnu-long-path' + } + + return null +} + +var toTypeflag = function (flag) { + switch (flag) { + case 'file': + return 0 + case 'link': + return 1 + case 'symlink': + return 2 + case 'character-device': + return 3 + case 'block-device': + return 4 + case 'directory': + return 5 + case 'fifo': + return 6 + case 'contiguous-file': + return 7 + case 'pax-header': + return 72 + } + + return 0 +} + +var indexOf = function (block, num, offset, end) { + for (; offset < end; offset++) { + if (block[offset] === num) return offset + } + return end +} + +var cksum = function (block) { + var sum = 8 * 32 + for (var i = 0; i < 148; i++) sum += block[i] + for (var j = 156; j < 512; j++) sum += block[j] + return sum +} + +var encodeOct = function (val, n) { + val = val.toString(8) + if (val.length > n) return SEVENS.slice(0, n) + ' ' + else return ZEROS.slice(0, n - val.length) + val + ' ' +} + +/* Copied from the node-tar repo and modified to meet + * tar-stream coding standard. + * + * Source: https://github.com/npm/node-tar/blob/51b6627a1f357d2eb433e7378e5f05e83b7aa6cd/lib/header.js#L349 + */ +function parse256 (buf) { + // first byte MUST be either 80 or FF + // 80 for positive, FF for 2's comp + var positive + if (buf[0] === 0x80) positive = true + else if (buf[0] === 0xFF) positive = false + else return null + + // build up a base-256 tuple from the least sig to the highest + var tuple = [] + for (var i = buf.length - 1; i > 0; i--) { + var byte = buf[i] + if (positive) tuple.push(byte) + else tuple.push(0xFF - byte) + } + + var sum = 0 + var l = tuple.length + for (i = 0; i < l; i++) { + sum += tuple[i] * Math.pow(256, i) + } + + return positive ? sum : -1 * sum +} + +var decodeOct = function (val, offset, length) { + val = val.slice(offset, offset + length) + offset = 0 + + // If prefixed with 0x80 then parse as a base-256 integer + if (val[offset] & 0x80) { + return parse256(val) + } else { + // Older versions of tar can prefix with spaces + while (offset < val.length && val[offset] === 32) offset++ + var end = clamp(indexOf(val, 32, offset, val.length), val.length, val.length) + while (offset < end && val[offset] === 0) offset++ + if (end === offset) return 0 + return parseInt(val.slice(offset, end).toString(), 8) + } +} + +var decodeStr = function (val, offset, length, encoding) { + return val.slice(offset, indexOf(val, 0, offset, offset + length)).toString(encoding) +} + +var addLength = function (str) { + var len = Buffer.byteLength(str) + var digits = Math.floor(Math.log(len) / Math.log(10)) + 1 + if (len + digits >= Math.pow(10, digits)) digits++ + + return (len + digits) + str +} + +exports.decodeLongPath = function (buf, encoding) { + return decodeStr(buf, 0, buf.length, encoding) +} + +exports.encodePax = function (opts) { // TODO: encode more stuff in pax + var result = '' + if (opts.name) result += addLength(' path=' + opts.name + '\n') + if (opts.linkname) result += addLength(' linkpath=' + opts.linkname + '\n') + var pax = opts.pax + if (pax) { + for (var key in pax) { + result += addLength(' ' + key + '=' + pax[key] + '\n') + } + } + return Buffer.from(result) +} + +exports.decodePax = function (buf) { + var result = {} + + while (buf.length) { + var i = 0 + while (i < buf.length && buf[i] !== 32) i++ + var len = parseInt(buf.slice(0, i).toString(), 10) + if (!len) return result + + var b = buf.slice(i + 1, len - 1).toString() + var keyIndex = b.indexOf('=') + if (keyIndex === -1) return result + result[b.slice(0, keyIndex)] = b.slice(keyIndex + 1) + + buf = buf.slice(len) + } + + return result +} + +exports.encode = function (opts) { + var buf = alloc(512) + var name = opts.name + var prefix = '' + + if (opts.typeflag === 5 && name[name.length - 1] !== '/') name += '/' + if (Buffer.byteLength(name) !== name.length) return null // utf-8 + + while (Buffer.byteLength(name) > 100) { + var i = name.indexOf('/') + if (i === -1) return null + prefix += prefix ? '/' + name.slice(0, i) : name.slice(0, i) + name = name.slice(i + 1) + } + + if (Buffer.byteLength(name) > 100 || Buffer.byteLength(prefix) > 155) return null + if (opts.linkname && Buffer.byteLength(opts.linkname) > 100) return null + + buf.write(name) + buf.write(encodeOct(opts.mode & MASK, 6), 100) + buf.write(encodeOct(opts.uid, 6), 108) + buf.write(encodeOct(opts.gid, 6), 116) + buf.write(encodeOct(opts.size, 11), 124) + buf.write(encodeOct((opts.mtime.getTime() / 1000) | 0, 11), 136) + + buf[156] = ZERO_OFFSET + toTypeflag(opts.type) + + if (opts.linkname) buf.write(opts.linkname, 157) + + USTAR_MAGIC.copy(buf, MAGIC_OFFSET) + USTAR_VER.copy(buf, VERSION_OFFSET) + if (opts.uname) buf.write(opts.uname, 265) + if (opts.gname) buf.write(opts.gname, 297) + buf.write(encodeOct(opts.devmajor || 0, 6), 329) + buf.write(encodeOct(opts.devminor || 0, 6), 337) + + if (prefix) buf.write(prefix, 345) + + buf.write(encodeOct(cksum(buf), 6), 148) + + return buf +} + +exports.decode = function (buf, filenameEncoding, allowUnknownFormat) { + var typeflag = buf[156] === 0 ? 0 : buf[156] - ZERO_OFFSET + + var name = decodeStr(buf, 0, 100, filenameEncoding) + var mode = decodeOct(buf, 100, 8) + var uid = decodeOct(buf, 108, 8) + var gid = decodeOct(buf, 116, 8) + var size = decodeOct(buf, 124, 12) + var mtime = decodeOct(buf, 136, 12) + var type = toType(typeflag) + var linkname = buf[157] === 0 ? null : decodeStr(buf, 157, 100, filenameEncoding) + var uname = decodeStr(buf, 265, 32) + var gname = decodeStr(buf, 297, 32) + var devmajor = decodeOct(buf, 329, 8) + var devminor = decodeOct(buf, 337, 8) + + var c = cksum(buf) + + // checksum is still initial value if header was null. + if (c === 8 * 32) return null + + // valid checksum + if (c !== decodeOct(buf, 148, 8)) throw new Error('Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?') + + if (USTAR_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0) { + // ustar (posix) format. + // prepend prefix, if present. + if (buf[345]) name = decodeStr(buf, 345, 155, filenameEncoding) + '/' + name + } else if (GNU_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0 && + GNU_VER.compare(buf, VERSION_OFFSET, VERSION_OFFSET + 2) === 0) { + // 'gnu'/'oldgnu' format. Similar to ustar, but has support for incremental and + // multi-volume tarballs. + } else { + if (!allowUnknownFormat) { + throw new Error('Invalid tar header: unknown format.') + } + } + + // to support old tar versions that use trailing / to indicate dirs + if (typeflag === 0 && name && name[name.length - 1] === '/') typeflag = 5 + + return { + name, + mode, + uid, + gid, + size, + mtime: new Date(1000 * mtime), + type, + linkname, + uname, + gname, + devmajor, + devminor + } +} diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/index.js b/backend/node_modules/prebuild-install/node_modules/tar-stream/index.js new file mode 100644 index 00000000..64817048 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/index.js @@ -0,0 +1,2 @@ +exports.extract = require('./extract') +exports.pack = require('./pack') diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/pack.js b/backend/node_modules/prebuild-install/node_modules/tar-stream/pack.js new file mode 100644 index 00000000..f1da3b73 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/pack.js @@ -0,0 +1,255 @@ +var constants = require('fs-constants') +var eos = require('end-of-stream') +var inherits = require('inherits') +var alloc = Buffer.alloc + +var Readable = require('readable-stream').Readable +var Writable = require('readable-stream').Writable +var StringDecoder = require('string_decoder').StringDecoder + +var headers = require('./headers') + +var DMODE = parseInt('755', 8) +var FMODE = parseInt('644', 8) + +var END_OF_TAR = alloc(1024) + +var noop = function () {} + +var overflow = function (self, size) { + size &= 511 + if (size) self.push(END_OF_TAR.slice(0, 512 - size)) +} + +function modeToType (mode) { + switch (mode & constants.S_IFMT) { + case constants.S_IFBLK: return 'block-device' + case constants.S_IFCHR: return 'character-device' + case constants.S_IFDIR: return 'directory' + case constants.S_IFIFO: return 'fifo' + case constants.S_IFLNK: return 'symlink' + } + + return 'file' +} + +var Sink = function (to) { + Writable.call(this) + this.written = 0 + this._to = to + this._destroyed = false +} + +inherits(Sink, Writable) + +Sink.prototype._write = function (data, enc, cb) { + this.written += data.length + if (this._to.push(data)) return cb() + this._to._drain = cb +} + +Sink.prototype.destroy = function () { + if (this._destroyed) return + this._destroyed = true + this.emit('close') +} + +var LinkSink = function () { + Writable.call(this) + this.linkname = '' + this._decoder = new StringDecoder('utf-8') + this._destroyed = false +} + +inherits(LinkSink, Writable) + +LinkSink.prototype._write = function (data, enc, cb) { + this.linkname += this._decoder.write(data) + cb() +} + +LinkSink.prototype.destroy = function () { + if (this._destroyed) return + this._destroyed = true + this.emit('close') +} + +var Void = function () { + Writable.call(this) + this._destroyed = false +} + +inherits(Void, Writable) + +Void.prototype._write = function (data, enc, cb) { + cb(new Error('No body allowed for this entry')) +} + +Void.prototype.destroy = function () { + if (this._destroyed) return + this._destroyed = true + this.emit('close') +} + +var Pack = function (opts) { + if (!(this instanceof Pack)) return new Pack(opts) + Readable.call(this, opts) + + this._drain = noop + this._finalized = false + this._finalizing = false + this._destroyed = false + this._stream = null +} + +inherits(Pack, Readable) + +Pack.prototype.entry = function (header, buffer, callback) { + if (this._stream) throw new Error('already piping an entry') + if (this._finalized || this._destroyed) return + + if (typeof buffer === 'function') { + callback = buffer + buffer = null + } + + if (!callback) callback = noop + + var self = this + + if (!header.size || header.type === 'symlink') header.size = 0 + if (!header.type) header.type = modeToType(header.mode) + if (!header.mode) header.mode = header.type === 'directory' ? DMODE : FMODE + if (!header.uid) header.uid = 0 + if (!header.gid) header.gid = 0 + if (!header.mtime) header.mtime = new Date() + + if (typeof buffer === 'string') buffer = Buffer.from(buffer) + if (Buffer.isBuffer(buffer)) { + header.size = buffer.length + this._encode(header) + var ok = this.push(buffer) + overflow(self, header.size) + if (ok) process.nextTick(callback) + else this._drain = callback + return new Void() + } + + if (header.type === 'symlink' && !header.linkname) { + var linkSink = new LinkSink() + eos(linkSink, function (err) { + if (err) { // stream was closed + self.destroy() + return callback(err) + } + + header.linkname = linkSink.linkname + self._encode(header) + callback() + }) + + return linkSink + } + + this._encode(header) + + if (header.type !== 'file' && header.type !== 'contiguous-file') { + process.nextTick(callback) + return new Void() + } + + var sink = new Sink(this) + + this._stream = sink + + eos(sink, function (err) { + self._stream = null + + if (err) { // stream was closed + self.destroy() + return callback(err) + } + + if (sink.written !== header.size) { // corrupting tar + self.destroy() + return callback(new Error('size mismatch')) + } + + overflow(self, header.size) + if (self._finalizing) self.finalize() + callback() + }) + + return sink +} + +Pack.prototype.finalize = function () { + if (this._stream) { + this._finalizing = true + return + } + + if (this._finalized) return + this._finalized = true + this.push(END_OF_TAR) + this.push(null) +} + +Pack.prototype.destroy = function (err) { + if (this._destroyed) return + this._destroyed = true + + if (err) this.emit('error', err) + this.emit('close') + if (this._stream && this._stream.destroy) this._stream.destroy() +} + +Pack.prototype._encode = function (header) { + if (!header.pax) { + var buf = headers.encode(header) + if (buf) { + this.push(buf) + return + } + } + this._encodePax(header) +} + +Pack.prototype._encodePax = function (header) { + var paxHeader = headers.encodePax({ + name: header.name, + linkname: header.linkname, + pax: header.pax + }) + + var newHeader = { + name: 'PaxHeader', + mode: header.mode, + uid: header.uid, + gid: header.gid, + size: paxHeader.length, + mtime: header.mtime, + type: 'pax-header', + linkname: header.linkname && 'PaxHeader', + uname: header.uname, + gname: header.gname, + devmajor: header.devmajor, + devminor: header.devminor + } + + this.push(headers.encode(newHeader)) + this.push(paxHeader) + overflow(this, paxHeader.length) + + newHeader.size = header.size + newHeader.type = header.type + this.push(headers.encode(newHeader)) +} + +Pack.prototype._read = function (n) { + var drain = this._drain + this._drain = noop + drain() +} + +module.exports = Pack diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/package.json b/backend/node_modules/prebuild-install/node_modules/tar-stream/package.json new file mode 100644 index 00000000..d717dfcc --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/package.json @@ -0,0 +1,58 @@ +{ + "name": "tar-stream", + "version": "2.2.0", + "description": "tar-stream is a streaming tar parser and generator and nothing else. It is streams2 and operates purely using streams which means you can easily extract/parse tarballs without ever hitting the file system.", + "author": "Mathias Buus ", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "devDependencies": { + "concat-stream": "^2.0.0", + "standard": "^12.0.1", + "tape": "^4.9.2" + }, + "scripts": { + "test": "standard && tape test/extract.js test/pack.js", + "test-all": "standard && tape test/*.js" + }, + "keywords": [ + "tar", + "tarball", + "parse", + "parser", + "generate", + "generator", + "stream", + "stream2", + "streams", + "streams2", + "streaming", + "pack", + "extract", + "modify" + ], + "bugs": { + "url": "https://github.com/mafintosh/tar-stream/issues" + }, + "homepage": "https://github.com/mafintosh/tar-stream", + "main": "index.js", + "files": [ + "*.js", + "LICENSE" + ], + "directories": { + "test": "test" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/mafintosh/tar-stream.git" + }, + "engines": { + "node": ">=6" + } +} diff --git a/backend/node_modules/prebuild-install/node_modules/tar-stream/sandbox.js b/backend/node_modules/prebuild-install/node_modules/tar-stream/sandbox.js new file mode 100644 index 00000000..9b82d401 --- /dev/null +++ b/backend/node_modules/prebuild-install/node_modules/tar-stream/sandbox.js @@ -0,0 +1,11 @@ +const tar = require('tar-stream') +const fs = require('fs') +const path = require('path') +const pipeline = require('pump') // eequire('stream').pipeline + +fs.createReadStream('test.tar') + .pipe(tar.extract()) + .on('entry', function (header, stream, done) { + console.log(header.name) + pipeline(stream, fs.createWriteStream(path.join('/tmp', header.name)), done) + }) diff --git a/backend/node_modules/prebuild-install/package.json b/backend/node_modules/prebuild-install/package.json new file mode 100644 index 00000000..316b8c2c --- /dev/null +++ b/backend/node_modules/prebuild-install/package.json @@ -0,0 +1,67 @@ +{ + "name": "prebuild-install", + "version": "7.1.3", + "description": "A command line tool to easily install prebuilt binaries for multiple version of node/iojs on a specific platform", + "scripts": { + "test": "standard && hallmark && tape test/*-test.js", + "hallmark": "hallmark --fix" + }, + "keywords": [ + "prebuilt", + "binaries", + "native", + "addon", + "module", + "c", + "c++", + "bindings", + "devops", + "napi" + ], + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "devDependencies": { + "a-native-module": "^1.0.0", + "hallmark": "^4.0.0", + "nock": "^10.0.6", + "rimraf": "^2.5.2", + "standard": "^16.0.4", + "tape": "^5.3.1", + "tempy": "0.2.1" + }, + "bin": "./bin.js", + "repository": { + "type": "git", + "url": "https://github.com/prebuild/prebuild-install.git" + }, + "author": "Mathias Buus (@mafintosh)", + "contributors": [ + "Julian Gruber (https://github.com/juliangruber)", + "Brett Lawson (https://github.com/brett19)", + "Pieter Hintjens (https://github.com/hintjens)", + "Lars-Magnus Skog (https://github.com/ralphtheninja)", + "Jesús Leganés Combarro (https://github.com/piranna)", + "Mathias Küsel (https://github.com/mathiask88)", + "Lukas Geiger (https://github.com/lgeiger)" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/prebuild/prebuild-install/issues" + }, + "homepage": "https://github.com/prebuild/prebuild-install", + "engines": { + "node": ">=10" + } +} \ No newline at end of file diff --git a/backend/node_modules/prebuild-install/proxy.js b/backend/node_modules/prebuild-install/proxy.js new file mode 100644 index 00000000..40d3aea3 --- /dev/null +++ b/backend/node_modules/prebuild-install/proxy.js @@ -0,0 +1,35 @@ +const url = require('url') +const tunnel = require('tunnel-agent') +const util = require('./util') + +function applyProxy (reqOpts, opts) { + const log = opts.log || util.noopLogger + + const proxy = opts['https-proxy'] || opts.proxy + + if (proxy) { + // eslint-disable-next-line node/no-deprecated-api + const parsedDownloadUrl = url.parse(reqOpts.url) + // eslint-disable-next-line node/no-deprecated-api + const parsedProxy = url.parse(proxy) + const uriProtocol = (parsedDownloadUrl.protocol === 'https:' ? 'https' : 'http') + const proxyProtocol = (parsedProxy.protocol === 'https:' ? 'Https' : 'Http') + const tunnelFnName = [uriProtocol, proxyProtocol].join('Over') + reqOpts.agent = tunnel[tunnelFnName]({ + proxy: { + host: parsedProxy.hostname, + port: +parsedProxy.port, + proxyAuth: parsedProxy.auth + } + }) + log.http('request', 'Proxy setup detected (Host: ' + + parsedProxy.hostname + ', Port: ' + + parsedProxy.port + ', Authentication: ' + + (parsedProxy.auth ? 'Yes' : 'No') + ')' + + ' Tunneling with ' + tunnelFnName) + } + + return reqOpts +} + +module.exports = applyProxy diff --git a/backend/node_modules/prebuild-install/rc.js b/backend/node_modules/prebuild-install/rc.js new file mode 100644 index 00000000..de0ea7a7 --- /dev/null +++ b/backend/node_modules/prebuild-install/rc.js @@ -0,0 +1,64 @@ +const path = require('path') +const minimist = require('minimist') +const getAbi = require('node-abi').getAbi +const detectLibc = require('detect-libc') +const napi = require('napi-build-utils') + +const env = process.env + +const libc = env.LIBC || process.env.npm_config_libc || + (detectLibc.isNonGlibcLinuxSync() && detectLibc.familySync()) || '' + +// Get the configuration +module.exports = function (pkg) { + const pkgConf = pkg.config || {} + const buildFromSource = env.npm_config_build_from_source + + const rc = require('rc')('prebuild-install', { + target: pkgConf.target || env.npm_config_target || process.versions.node, + runtime: pkgConf.runtime || env.npm_config_runtime || 'node', + arch: pkgConf.arch || env.npm_config_arch || process.arch, + libc: libc, + platform: env.npm_config_platform || process.platform, + debug: env.npm_config_debug === 'true', + force: false, + verbose: env.npm_config_verbose === 'true', + buildFromSource: buildFromSource === pkg.name || buildFromSource === 'true', + path: '.', + proxy: env.npm_config_proxy || env.http_proxy || env.HTTP_PROXY, + 'https-proxy': env.npm_config_https_proxy || env.https_proxy || env.HTTPS_PROXY, + 'local-address': env.npm_config_local_address, + 'local-prebuilds': 'prebuilds', + 'tag-prefix': 'v', + download: env.npm_config_download + }, minimist(process.argv, { + alias: { + target: 't', + runtime: 'r', + help: 'h', + arch: 'a', + path: 'p', + version: 'v', + download: 'd', + buildFromSource: 'build-from-source', + token: 'T' + } + })) + + rc.path = path.resolve(rc.path === true ? '.' : rc.path || '.') + + if (napi.isNapiRuntime(rc.runtime) && rc.target === process.versions.node) { + rc.target = napi.getBestNapiBuildVersion() + } + + rc.abi = napi.isNapiRuntime(rc.runtime) ? rc.target : getAbi(rc.target, rc.runtime) + + rc.libc = rc.platform !== 'linux' || rc.libc === detectLibc.GLIBC ? '' : rc.libc + + return rc +} + +// Print the configuration values when executed standalone for testing purposses +if (!module.parent) { + console.log(JSON.stringify(module.exports({}), null, 2)) +} diff --git a/backend/node_modules/prebuild-install/util.js b/backend/node_modules/prebuild-install/util.js new file mode 100644 index 00000000..d7cc515d --- /dev/null +++ b/backend/node_modules/prebuild-install/util.js @@ -0,0 +1,143 @@ +const path = require('path') +const github = require('github-from-package') +const home = require('os').homedir +const crypto = require('crypto') +const expandTemplate = require('expand-template')() + +function getDownloadUrl (opts) { + const pkgName = opts.pkg.name.replace(/^@[a-zA-Z0-9_\-.~]+\//, '') + return expandTemplate(urlTemplate(opts), { + name: pkgName, + package_name: pkgName, + version: opts.pkg.version, + major: opts.pkg.version.split('.')[0], + minor: opts.pkg.version.split('.')[1], + patch: opts.pkg.version.split('.')[2], + prerelease: opts.pkg.version.split('-')[1], + build: opts.pkg.version.split('+')[1], + abi: opts.abi || process.versions.modules, + node_abi: process.versions.modules, + runtime: opts.runtime || 'node', + platform: opts.platform, + arch: opts.arch, + libc: opts.libc || '', + configuration: (opts.debug ? 'Debug' : 'Release'), + module_name: opts.pkg.binary && opts.pkg.binary.module_name, + tag_prefix: opts['tag-prefix'] + }) +} + +function getApiUrl (opts) { + return github(opts.pkg).replace('github.com', 'api.github.com/repos') + '/releases' +} + +function getAssetUrl (opts, assetId) { + return getApiUrl(opts) + '/assets/' + assetId +} + +function urlTemplate (opts) { + if (typeof opts.download === 'string') { + return opts.download + } + + const packageName = '{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz' + const hostMirrorUrl = getHostMirrorUrl(opts) + + if (hostMirrorUrl) { + return hostMirrorUrl + '/{tag_prefix}{version}/' + packageName + } + + if (opts.pkg.binary && opts.pkg.binary.host) { + return [ + opts.pkg.binary.host, + opts.pkg.binary.remote_path, + opts.pkg.binary.package_name || packageName + ].map(function (path) { + return trimSlashes(path) + }).filter(Boolean).join('/') + } + + return github(opts.pkg) + '/releases/download/{tag_prefix}{version}/' + packageName +} + +function getEnvPrefix (pkgName) { + return 'npm_config_' + (pkgName || '').replace(/[^a-zA-Z0-9]/g, '_').replace(/^_/, '') +} + +function getHostMirrorUrl (opts) { + const propName = getEnvPrefix(opts.pkg.name) + '_binary_host' + return process.env[propName] || process.env[propName + '_mirror'] +} + +function trimSlashes (str) { + if (str) return str.replace(/^\.\/|^\/|\/$/g, '') +} + +function cachedPrebuild (url) { + const digest = crypto.createHash('sha512').update(url).digest('hex').slice(0, 6) + return path.join(prebuildCache(), digest + '-' + path.basename(url).replace(/[^a-zA-Z0-9.]+/g, '-')) +} + +function npmCache () { + const env = process.env + return env.npm_config_cache || (env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(home(), '.npm')) +} + +function prebuildCache () { + return path.join(npmCache(), '_prebuilds') +} + +function tempFile (cached) { + return cached + '.' + process.pid + '-' + Math.random().toString(16).slice(2) + '.tmp' +} + +function packageOrigin (env, pkg) { + // npm <= 6: metadata is stored on disk in node_modules + if (pkg._from) { + return pkg._from + } + + // npm 7: metadata is exposed to environment by arborist + if (env.npm_package_from) { + // NOTE: seems undefined atm (npm 7.0.2) + return env.npm_package_from + } + + if (env.npm_package_resolved) { + // NOTE: not sure about the difference with _from, but it's all we have + return env.npm_package_resolved + } +} + +function localPrebuild (url, opts) { + const propName = getEnvPrefix(opts.pkg.name) + '_local_prebuilds' + const prefix = process.env[propName] || opts['local-prebuilds'] || 'prebuilds' + return path.join(prefix, path.basename(url)) +} + +const noopLogger = { + http: function () {}, + silly: function () {}, + debug: function () {}, + info: function () {}, + warn: function () {}, + error: function () {}, + critical: function () {}, + alert: function () {}, + emergency: function () {}, + notice: function () {}, + verbose: function () {}, + fatal: function () {} +} + +exports.getDownloadUrl = getDownloadUrl +exports.getApiUrl = getApiUrl +exports.getAssetUrl = getAssetUrl +exports.urlTemplate = urlTemplate +exports.cachedPrebuild = cachedPrebuild +exports.localPrebuild = localPrebuild +exports.prebuildCache = prebuildCache +exports.npmCache = npmCache +exports.tempFile = tempFile +exports.packageOrigin = packageOrigin +exports.noopLogger = noopLogger diff --git a/backend/node_modules/@img/sharp-linux-x64/README.md b/backend/node_modules/rc/LICENSE.APACHE2 similarity index 68% rename from backend/node_modules/@img/sharp-linux-x64/README.md rename to backend/node_modules/rc/LICENSE.APACHE2 index cae31de5..6366c047 100644 --- a/backend/node_modules/@img/sharp-linux-x64/README.md +++ b/backend/node_modules/rc/LICENSE.APACHE2 @@ -1,15 +1,12 @@ -# `@img/sharp-linux-x64` +Apache License, Version 2.0 -Prebuilt sharp for use with Linux (glibc) x64. - -## Licensing - -Copyright 2013 Lovell Fuller and others. +Copyright (c) 2011 Dominic Tarr Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/backend/node_modules/rc/LICENSE.BSD b/backend/node_modules/rc/LICENSE.BSD new file mode 100644 index 00000000..96bb796a --- /dev/null +++ b/backend/node_modules/rc/LICENSE.BSD @@ -0,0 +1,26 @@ +Copyright (c) 2013, Dominic Tarr +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. diff --git a/backend/node_modules/rc/LICENSE.MIT b/backend/node_modules/rc/LICENSE.MIT new file mode 100644 index 00000000..6eafbd73 --- /dev/null +++ b/backend/node_modules/rc/LICENSE.MIT @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/rc/README.md b/backend/node_modules/rc/README.md new file mode 100644 index 00000000..e6522e26 --- /dev/null +++ b/backend/node_modules/rc/README.md @@ -0,0 +1,227 @@ +# rc + +The non-configurable configuration loader for lazy people. + +## Usage + +The only option is to pass rc the name of your app, and your default configuration. + +```javascript +var conf = require('rc')(appname, { + //defaults go here. + port: 2468, + + //defaults which are objects will be merged, not replaced + views: { + engine: 'jade' + } +}); +``` + +`rc` will return your configuration options merged with the defaults you specify. +If you pass in a predefined defaults object, it will be mutated: + +```javascript +var conf = {}; +require('rc')(appname, conf); +``` + +If `rc` finds any config files for your app, the returned config object will have +a `configs` array containing their paths: + +```javascript +var appCfg = require('rc')(appname, conf); +appCfg.configs[0] // /etc/appnamerc +appCfg.configs[1] // /home/dominictarr/.config/appname +appCfg.config // same as appCfg.configs[appCfg.configs.length - 1] +``` + +## Standards + +Given your application name (`appname`), rc will look in all the obvious places for configuration. + + * command line arguments, parsed by minimist _(e.g. `--foo baz`, also nested: `--foo.bar=baz`)_ + * environment variables prefixed with `${appname}_` + * or use "\_\_" to indicate nested properties
_(e.g. `appname_foo__bar__baz` => `foo.bar.baz`)_ + * if you passed an option `--config file` then from that file + * a local `.${appname}rc` or the first found looking in `./ ../ ../../ ../../../` etc. + * `$HOME/.${appname}rc` + * `$HOME/.${appname}/config` + * `$HOME/.config/${appname}` + * `$HOME/.config/${appname}/config` + * `/etc/${appname}rc` + * `/etc/${appname}/config` + * the defaults object you passed in. + +All configuration sources that were found will be flattened into one object, +so that sources **earlier** in this list override later ones. + + +## Configuration File Formats + +Configuration files (e.g. `.appnamerc`) may be in either [json](http://json.org/example) or [ini](http://en.wikipedia.org/wiki/INI_file) format. **No** file extension (`.json` or `.ini`) should be used. The example configurations below are equivalent: + + +#### Formatted as `ini` + +``` +; You can include comments in `ini` format if you want. + +dependsOn=0.10.0 + + +; `rc` has built-in support for ini sections, see? + +[commands] + www = ./commands/www + console = ./commands/repl + + +; You can even do nested sections + +[generators.options] + engine = ejs + +[generators.modules] + new = generate-new + engine = generate-backend + +``` + +#### Formatted as `json` + +```javascript +{ + // You can even comment your JSON, if you want + "dependsOn": "0.10.0", + "commands": { + "www": "./commands/www", + "console": "./commands/repl" + }, + "generators": { + "options": { + "engine": "ejs" + }, + "modules": { + "new": "generate-new", + "backend": "generate-backend" + } + } +} +``` + +Comments are stripped from JSON config via [strip-json-comments](https://github.com/sindresorhus/strip-json-comments). + +> Since ini, and env variables do not have a standard for types, your application needs be prepared for strings. + +To ensure that string representations of booleans and numbers are always converted into their proper types (especially useful if you intend to do strict `===` comparisons), consider using a module such as [parse-strings-in-object](https://github.com/anselanza/parse-strings-in-object) to wrap the config object returned from rc. + + +## Simple example demonstrating precedence +Assume you have an application like this (notice the hard-coded defaults passed to rc): +``` +const conf = require('rc')('myapp', { + port: 12345, + mode: 'test' +}); + +console.log(JSON.stringify(conf, null, 2)); +``` +You also have a file `config.json`, with these contents: +``` +{ + "port": 9000, + "foo": "from config json", + "something": "else" +} +``` +And a file `.myapprc` in the same folder, with these contents: +``` +{ + "port": "3001", + "foo": "bar" +} +``` +Here is the expected output from various commands: + +`node .` +``` +{ + "port": "3001", + "mode": "test", + "foo": "bar", + "_": [], + "configs": [ + "/Users/stephen/repos/conftest/.myapprc" + ], + "config": "/Users/stephen/repos/conftest/.myapprc" +} +``` +*Default `mode` from hard-coded object is retained, but port is overridden by `.myapprc` file (automatically found based on appname match), and `foo` is added.* + + +`node . --foo baz` +``` +{ + "port": "3001", + "mode": "test", + "foo": "baz", + "_": [], + "configs": [ + "/Users/stephen/repos/conftest/.myapprc" + ], + "config": "/Users/stephen/repos/conftest/.myapprc" +} +``` +*Same result as above but `foo` is overridden because command-line arguments take precedence over `.myapprc` file.* + +`node . --foo barbar --config config.json` +``` +{ + "port": 9000, + "mode": "test", + "foo": "barbar", + "something": "else", + "_": [], + "config": "config.json", + "configs": [ + "/Users/stephen/repos/conftest/.myapprc", + "config.json" + ] +} +``` +*Now the `port` comes from the `config.json` file specified (overriding the value from `.myapprc`), and `foo` value is overriden by command-line despite also being specified in the `config.json` file.* + + + +## Advanced Usage + +#### Pass in your own `argv` + +You may pass in your own `argv` as the third argument to `rc`. This is in case you want to [use your own command-line opts parser](https://github.com/dominictarr/rc/pull/12). + +```javascript +require('rc')(appname, defaults, customArgvParser); +``` + +## Pass in your own parser + +If you have a special need to use a non-standard parser, +you can do so by passing in the parser as the 4th argument. +(leave the 3rd as null to get the default args parser) + +```javascript +require('rc')(appname, defaults, null, parser); +``` + +This may also be used to force a more strict format, +such as strict, valid JSON only. + +## Note on Performance + +`rc` is running `fs.statSync`-- so make sure you don't use it in a hot code path (e.g. a request handler) + + +## License + +Multi-licensed under the two-clause BSD License, MIT License, or Apache License, version 2.0 diff --git a/backend/node_modules/rc/browser.js b/backend/node_modules/rc/browser.js new file mode 100644 index 00000000..8c230c5c --- /dev/null +++ b/backend/node_modules/rc/browser.js @@ -0,0 +1,7 @@ + +// when this is loaded into the browser, +// just use the defaults... + +module.exports = function (name, defaults) { + return defaults +} diff --git a/backend/node_modules/rc/cli.js b/backend/node_modules/rc/cli.js new file mode 100755 index 00000000..ab05b607 --- /dev/null +++ b/backend/node_modules/rc/cli.js @@ -0,0 +1,4 @@ +#! /usr/bin/env node +var rc = require('./index') + +console.log(JSON.stringify(rc(process.argv[2]), false, 2)) diff --git a/backend/node_modules/rc/index.js b/backend/node_modules/rc/index.js new file mode 100755 index 00000000..65eb47af --- /dev/null +++ b/backend/node_modules/rc/index.js @@ -0,0 +1,53 @@ +var cc = require('./lib/utils') +var join = require('path').join +var deepExtend = require('deep-extend') +var etc = '/etc' +var win = process.platform === "win32" +var home = win + ? process.env.USERPROFILE + : process.env.HOME + +module.exports = function (name, defaults, argv, parse) { + if('string' !== typeof name) + throw new Error('rc(name): name *must* be string') + if(!argv) + argv = require('minimist')(process.argv.slice(2)) + defaults = ( + 'string' === typeof defaults + ? cc.json(defaults) : defaults + ) || {} + + parse = parse || cc.parse + + var env = cc.env(name + '_') + + var configs = [defaults] + var configFiles = [] + function addConfigFile (file) { + if (configFiles.indexOf(file) >= 0) return + var fileConfig = cc.file(file) + if (fileConfig) { + configs.push(parse(fileConfig)) + configFiles.push(file) + } + } + + // which files do we look at? + if (!win) + [join(etc, name, 'config'), + join(etc, name + 'rc')].forEach(addConfigFile) + if (home) + [join(home, '.config', name, 'config'), + join(home, '.config', name), + join(home, '.' + name, 'config'), + join(home, '.' + name + 'rc')].forEach(addConfigFile) + addConfigFile(cc.find('.'+name+'rc')) + if (env.config) addConfigFile(env.config) + if (argv.config) addConfigFile(argv.config) + + return deepExtend.apply(null, configs.concat([ + env, + argv, + configFiles.length ? {configs: configFiles, config: configFiles[configFiles.length - 1]} : undefined, + ])) +} diff --git a/backend/node_modules/rc/lib/utils.js b/backend/node_modules/rc/lib/utils.js new file mode 100644 index 00000000..8b3beffa --- /dev/null +++ b/backend/node_modules/rc/lib/utils.js @@ -0,0 +1,104 @@ +'use strict'; +var fs = require('fs') +var ini = require('ini') +var path = require('path') +var stripJsonComments = require('strip-json-comments') + +var parse = exports.parse = function (content) { + + //if it ends in .json or starts with { then it must be json. + //must be done this way, because ini accepts everything. + //can't just try and parse it and let it throw if it's not ini. + //everything is ini. even json with a syntax error. + + if(/^\s*{/.test(content)) + return JSON.parse(stripJsonComments(content)) + return ini.parse(content) + +} + +var file = exports.file = function () { + var args = [].slice.call(arguments).filter(function (arg) { return arg != null }) + + //path.join breaks if it's a not a string, so just skip this. + for(var i in args) + if('string' !== typeof args[i]) + return + + var file = path.join.apply(null, args) + var content + try { + return fs.readFileSync(file,'utf-8') + } catch (err) { + return + } +} + +var json = exports.json = function () { + var content = file.apply(null, arguments) + return content ? parse(content) : null +} + +var env = exports.env = function (prefix, env) { + env = env || process.env + var obj = {} + var l = prefix.length + for(var k in env) { + if(k.toLowerCase().indexOf(prefix.toLowerCase()) === 0) { + + var keypath = k.substring(l).split('__') + + // Trim empty strings from keypath array + var _emptyStringIndex + while ((_emptyStringIndex=keypath.indexOf('')) > -1) { + keypath.splice(_emptyStringIndex, 1) + } + + var cursor = obj + keypath.forEach(function _buildSubObj(_subkey,i){ + + // (check for _subkey first so we ignore empty strings) + // (check for cursor to avoid assignment to primitive objects) + if (!_subkey || typeof cursor !== 'object') + return + + // If this is the last key, just stuff the value in there + // Assigns actual value from env variable to final key + // (unless it's just an empty string- in that case use the last valid key) + if (i === keypath.length-1) + cursor[_subkey] = env[k] + + + // Build sub-object if nothing already exists at the keypath + if (cursor[_subkey] === undefined) + cursor[_subkey] = {} + + // Increment cursor used to track the object at the current depth + cursor = cursor[_subkey] + + }) + + } + + } + + return obj +} + +var find = exports.find = function () { + var rel = path.join.apply(null, [].slice.call(arguments)) + + function find(start, rel) { + var file = path.join(start, rel) + try { + fs.statSync(file) + return file + } catch (err) { + if(path.dirname(start) !== start) // root + return find(path.dirname(start), rel) + } + } + return find(process.cwd(), rel) +} + + diff --git a/backend/node_modules/rc/package.json b/backend/node_modules/rc/package.json new file mode 100644 index 00000000..887238fa --- /dev/null +++ b/backend/node_modules/rc/package.json @@ -0,0 +1,29 @@ +{ + "name": "rc", + "version": "1.2.8", + "description": "hardwired configuration loader", + "main": "index.js", + "browser": "browser.js", + "scripts": { + "test": "set -e; node test/test.js; node test/ini.js; node test/nested-env-vars.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/dominictarr/rc.git" + }, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "keywords": [ + "config", + "rc", + "unix", + "defaults" + ], + "bin": "./cli.js", + "author": "Dominic Tarr (dominictarr.com)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } +} diff --git a/backend/node_modules/rc/test/ini.js b/backend/node_modules/rc/test/ini.js new file mode 100644 index 00000000..e6857f8b --- /dev/null +++ b/backend/node_modules/rc/test/ini.js @@ -0,0 +1,16 @@ +var cc =require('../lib/utils') +var INI = require('ini') +var assert = require('assert') + +function test(obj) { + + var _json, _ini + var json = cc.parse (_json = JSON.stringify(obj)) + var ini = cc.parse (_ini = INI.stringify(obj)) + console.log(_ini, _json) + assert.deepEqual(json, ini) +} + + +test({hello: true}) + diff --git a/backend/node_modules/rc/test/nested-env-vars.js b/backend/node_modules/rc/test/nested-env-vars.js new file mode 100644 index 00000000..0ecd1763 --- /dev/null +++ b/backend/node_modules/rc/test/nested-env-vars.js @@ -0,0 +1,50 @@ + +var seed = Math.random(); +var n = 'rc'+ seed; +var N = 'RC'+ seed; +var assert = require('assert') + + +// Basic usage +process.env[n+'_someOpt__a'] = 42 +process.env[n+'_someOpt__x__'] = 99 +process.env[n+'_someOpt__a__b'] = 186 +process.env[n+'_someOpt__a__b__c'] = 243 +process.env[n+'_someOpt__x__y'] = 1862 +process.env[n+'_someOpt__z'] = 186577 + +// Should ignore empty strings from orphaned '__' +process.env[n+'_someOpt__z__x__'] = 18629 +process.env[n+'_someOpt__w__w__'] = 18629 + +// Leading '__' should ignore everything up to 'z' +process.env[n+'___z__i__'] = 9999 + +// should ignore case for config name section. +process.env[N+'_test_upperCase'] = 187 + +function testPrefix(prefix) { + var config = require('../')(prefix, { + option: true + }) + + console.log('\n\n------ nested-env-vars ------\n',{prefix: prefix}, '\n', config); + + assert.equal(config.option, true) + assert.equal(config.someOpt.a, 42) + assert.equal(config.someOpt.x, 99) + // Should not override `a` once it's been set + assert.equal(config.someOpt.a/*.b*/, 42) + // Should not override `x` once it's been set + assert.equal(config.someOpt.x/*.y*/, 99) + assert.equal(config.someOpt.z, 186577) + // Should not override `z` once it's been set + assert.equal(config.someOpt.z/*.x*/, 186577) + assert.equal(config.someOpt.w.w, 18629) + assert.equal(config.z.i, 9999) + + assert.equal(config.test_upperCase, 187) +} + +testPrefix(n); +testPrefix(N); diff --git a/backend/node_modules/rc/test/test.js b/backend/node_modules/rc/test/test.js new file mode 100644 index 00000000..4f633518 --- /dev/null +++ b/backend/node_modules/rc/test/test.js @@ -0,0 +1,59 @@ + +var n = 'rc'+Math.random() +var assert = require('assert') + +process.env[n+'_envOption'] = 42 + +var config = require('../')(n, { + option: true +}) + +console.log(config) + +assert.equal(config.option, true) +assert.equal(config.envOption, 42) + +var customArgv = require('../')(n, { + option: true +}, { // nopt-like argv + option: false, + envOption: 24, + argv: { + remain: [], + cooked: ['--no-option', '--envOption', '24'], + original: ['--no-option', '--envOption=24'] + } +}) + +console.log(customArgv) + +assert.equal(customArgv.option, false) +assert.equal(customArgv.envOption, 24) + +var fs = require('fs') +var path = require('path') +var jsonrc = path.resolve('.' + n + 'rc'); + +fs.writeFileSync(jsonrc, [ + '{', + '// json overrides default', + '"option": false,', + '/* env overrides json */', + '"envOption": 24', + '}' +].join('\n')); + +var commentedJSON = require('../')(n, { + option: true +}) + +fs.unlinkSync(jsonrc); + +console.log(commentedJSON) + +assert.equal(commentedJSON.option, false) +assert.equal(commentedJSON.envOption, 42) + +assert.equal(commentedJSON.config, jsonrc) +assert.equal(commentedJSON.configs.length, 1) +assert.equal(commentedJSON.configs[0], jsonrc) diff --git a/backend/node_modules/sharp/README.md b/backend/node_modules/sharp/README.md index 47da52e8..ba0db79c 100644 --- a/backend/node_modules/sharp/README.md +++ b/backend/node_modules/sharp/README.md @@ -1,15 +1,11 @@ # sharp -sharp logo +sharp logo -The typical use case for this high speed Node-API module +The typical use case for this high speed Node.js module is to convert large images in common formats to smaller, web-friendly JPEG, PNG, WebP, GIF and AVIF images of varying dimensions. -It can be used with all JavaScript runtimes -that provide support for Node-API v9, including -Node.js (^18.17.0 or >= 20.3.0), Deno and Bun. - Resizing an image is typically 4x-5x faster than using the quickest ImageMagick and GraphicsMagick settings due to its use of [libvips](https://github.com/libvips/libvips). @@ -20,7 +16,7 @@ Lanczos resampling ensures quality is not sacrificed for speed. As well as image resizing, operations such as rotation, extraction, compositing and gamma correction are available. -Most modern macOS, Windows and Linux systems +Most modern macOS, Windows and Linux systems running Node.js >= 14.15.0 do not require any additional install or runtime dependencies. ## Documentation diff --git a/backend/node_modules/sharp/binding.gyp b/backend/node_modules/sharp/binding.gyp new file mode 100644 index 00000000..436ead95 --- /dev/null +++ b/backend/node_modules/sharp/binding.gyp @@ -0,0 +1,236 @@ +{ + 'variables': { + 'vips_version': '= v8, the user must own the directory "npm install" is run in'); + } + libvips.log('Please see https://sharp.pixelplumbing.com/install for required dependencies'); + process.exit(1); +}; + +const handleError = function (err) { + if (installationForced) { + libvips.log(`Installation warning: ${err.message}`); + } else { + throw err; + } +}; + +const verifyIntegrity = function (platformAndArch) { + const expected = libvips.integrity(platformAndArch); + if (installationForced || !expected) { + libvips.log(`Integrity check skipped for ${platformAndArch}`); + return new stream.PassThrough(); + } + const hash = createHash('sha512'); + return new stream.Transform({ + transform: function (chunk, _encoding, done) { + hash.update(chunk); + done(null, chunk); + }, + flush: function (done) { + const digest = `sha512-${hash.digest('base64')}`; + if (expected !== digest) { + try { + libvips.removeVendoredLibvips(); + } catch (err) { + libvips.log(err.message); + } + libvips.log(`Integrity expected: ${expected}`); + libvips.log(`Integrity received: ${digest}`); + done(new Error(`Integrity check failed for ${platformAndArch}`)); + } else { + libvips.log(`Integrity check passed for ${platformAndArch}`); + done(); + } + } + }); +}; + +const extractTarball = function (tarPath, platformAndArch) { + const versionedVendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platformAndArch); + libvips.mkdirSync(versionedVendorPath); + + const ignoreVendorInclude = hasSharpPrebuild.includes(platformAndArch) && !process.env.npm_config_build_from_source; + const ignore = function (name) { + return ignoreVendorInclude && name.includes('include/'); + }; + + stream.pipeline( + fs.createReadStream(tarPath), + verifyIntegrity(platformAndArch), + new zlib.BrotliDecompress(), + tarFs.extract(versionedVendorPath, { ignore }), + function (err) { + if (err) { + if (/unexpected end of file/.test(err.message)) { + fail(new Error(`Please delete ${tarPath} as it is not a valid tarball`)); + } + fail(err); + } + } + ); +}; + +try { + const useGlobalLibvips = libvips.useGlobalLibvips(); + + if (useGlobalLibvips) { + const globalLibvipsVersion = libvips.globalLibvipsVersion(); + libvips.log(`Detected globally-installed libvips v${globalLibvipsVersion}`); + libvips.log('Building from source via node-gyp'); + process.exit(1); + } else if (libvips.hasVendoredLibvips()) { + libvips.log(`Using existing vendored libvips v${minimumLibvipsVersion}`); + } else { + // Is this arch/platform supported? + const arch = process.env.npm_config_arch || process.arch; + const platformAndArch = platform(); + if (arch === 'ia32' && !platformAndArch.startsWith('win32')) { + throw new Error(`Intel Architecture 32-bit systems require manual installation of libvips >= ${minimumLibvipsVersion}`); + } + if (platformAndArch === 'freebsd-x64' || platformAndArch === 'openbsd-x64' || platformAndArch === 'sunos-x64') { + throw new Error(`BSD/SunOS systems require manual installation of libvips >= ${minimumLibvipsVersion}`); + } + // Linux libc version check + const libcVersionRaw = detectLibc.versionSync(); + if (libcVersionRaw) { + const libcFamily = detectLibc.familySync(); + const libcVersion = semverCoerce(libcVersionRaw).version; + if (libcFamily === detectLibc.GLIBC && minimumGlibcVersionByArch[arch]) { + if (semverLessThan(libcVersion, semverCoerce(minimumGlibcVersionByArch[arch]).version)) { + handleError(new Error(`Use with glibc ${libcVersionRaw} requires manual installation of libvips >= ${minimumLibvipsVersion}`)); + } + } + if (libcFamily === detectLibc.MUSL) { + if (semverLessThan(libcVersion, '1.1.24')) { + handleError(new Error(`Use with musl ${libcVersionRaw} requires manual installation of libvips >= ${minimumLibvipsVersion}`)); + } + } + } + // Node.js minimum version check + const supportedNodeVersion = process.env.npm_package_engines_node || require('../package.json').engines.node; + if (!semverSatisfies(process.versions.node, supportedNodeVersion)) { + handleError(new Error(`Expected Node.js version ${supportedNodeVersion} but found ${process.versions.node}`)); + } + // Download to per-process temporary file + const tarFilename = ['libvips', minimumLibvipsVersionLabelled, platformAndArch].join('-') + '.tar.br'; + const tarPathCache = path.join(libvips.cachePath(), tarFilename); + if (fs.existsSync(tarPathCache)) { + libvips.log(`Using cached ${tarPathCache}`); + extractTarball(tarPathCache, platformAndArch); + } else if (localLibvipsDir) { + // If localLibvipsDir is given try to use binaries from local directory + const tarPathLocal = path.join(path.resolve(localLibvipsDir), `v${minimumLibvipsVersionLabelled}`, tarFilename); + libvips.log(`Using local libvips from ${tarPathLocal}`); + extractTarball(tarPathLocal, platformAndArch); + } else { + const url = distBaseUrl + tarFilename; + libvips.log(`Downloading ${url}`); + simpleGet({ url: url, agent: agent(libvips.log) }, function (err, response) { + if (err) { + fail(err); + } else if (response.statusCode === 404) { + fail(new Error(`Prebuilt libvips ${minimumLibvipsVersion} binaries are not yet available for ${platformAndArch}`)); + } else if (response.statusCode !== 200) { + fail(new Error(`Status ${response.statusCode} ${response.statusMessage}`)); + } else { + const tarPathTemp = path.join(os.tmpdir(), `${process.pid}-${tarFilename}`); + const tmpFileStream = fs.createWriteStream(tarPathTemp); + response + .on('error', function (err) { + tmpFileStream.destroy(err); + }) + .on('close', function () { + if (!response.complete) { + tmpFileStream.destroy(new Error('Download incomplete (connection was terminated)')); + } + }) + .pipe(tmpFileStream); + tmpFileStream + .on('error', function (err) { + // Clean up temporary file + try { + fs.unlinkSync(tarPathTemp); + } catch (e) {} + fail(err); + }) + .on('close', function () { + try { + // Attempt to rename + fs.renameSync(tarPathTemp, tarPathCache); + } catch (err) { + // Fall back to copy and unlink + fs.copyFileSync(tarPathTemp, tarPathCache); + fs.unlinkSync(tarPathTemp); + } + extractTarball(tarPathCache, platformAndArch); + }); + } + }); + } + } +} catch (err) { + fail(err); +} diff --git a/backend/node_modules/sharp/lib/agent.js b/backend/node_modules/sharp/lib/agent.js new file mode 100644 index 00000000..74b6f47e --- /dev/null +++ b/backend/node_modules/sharp/lib/agent.js @@ -0,0 +1,44 @@ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; + +const url = require('url'); +const tunnelAgent = require('tunnel-agent'); + +const is = require('./is'); + +const proxies = [ + 'HTTPS_PROXY', + 'https_proxy', + 'HTTP_PROXY', + 'http_proxy', + 'npm_config_https_proxy', + 'npm_config_proxy' +]; + +function env (key) { + return process.env[key]; +} + +module.exports = function (log) { + try { + const proxy = new url.URL(proxies.map(env).find(is.string)); + const tunnel = proxy.protocol === 'https:' + ? tunnelAgent.httpsOverHttps + : tunnelAgent.httpsOverHttp; + const proxyAuth = proxy.username && proxy.password + ? `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}` + : null; + log(`Via proxy ${proxy.protocol}//${proxy.hostname}:${proxy.port} ${proxyAuth ? 'with' : 'no'} credentials`); + return tunnel({ + proxy: { + port: Number(proxy.port), + host: proxy.hostname, + proxyAuth + } + }); + } catch (err) { + return null; + } +}; diff --git a/backend/node_modules/sharp/lib/channel.js b/backend/node_modules/sharp/lib/channel.js index 3c6c0b43..977ea386 100644 --- a/backend/node_modules/sharp/lib/channel.js +++ b/backend/node_modules/sharp/lib/channel.js @@ -1,7 +1,7 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; const is = require('./is'); @@ -16,9 +16,9 @@ const bool = { }; /** - * Remove alpha channels, if any. This is a no-op if the image does not have an alpha channel. + * Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel. * - * See also {@link /api-operation/#flatten flatten}. + * See also {@link /api-operation#flatten|flatten}. * * @example * sharp('rgba.png') @@ -74,8 +74,6 @@ function ensureAlpha (alpha) { /** * Extract a single channel from a multi-channel image. * - * The output colourspace will be either `b-w` (8-bit) or `grey16` (16-bit). - * * @example * // green.jpg is a greyscale image containing the green channel of the input * await sharp(input) @@ -160,10 +158,9 @@ function bandbool (boolOp) { /** * Decorate the Sharp prototype with channel-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Object.assign(Sharp.prototype, { // Public instance functions removeAlpha, diff --git a/backend/node_modules/sharp/lib/colour.js b/backend/node_modules/sharp/lib/colour.js index e61c248a..a7761d29 100644 --- a/backend/node_modules/sharp/lib/colour.js +++ b/backend/node_modules/sharp/lib/colour.js @@ -1,9 +1,9 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -const color = require('@img/colour'); +'use strict'; + +const color = require('color'); const is = require('./is'); /** @@ -19,7 +19,7 @@ const colourspace = { }; /** - * Tint the image using the provided colour. + * Tint the image using the provided chroma while preserving the image luminance. * An alpha channel may be present and will be unchanged by the operation. * * @example @@ -27,21 +27,23 @@ const colourspace = { * .tint({ r: 255, g: 240, b: 16 }) * .toBuffer(); * - * @param {string|Object} tint - Parsed by the [color](https://www.npmjs.org/package/color) module. + * @param {string|Object} rgb - parsed by the [color](https://www.npmjs.org/package/color) module to extract chroma values. * @returns {Sharp} * @throws {Error} Invalid parameter */ -function tint (tint) { - this._setBackgroundColourOption('tint', tint); +function tint (rgb) { + const colour = color(rgb); + this.options.tintA = colour.a(); + this.options.tintB = colour.b(); return this; } /** * Convert to 8-bit greyscale; 256 shades of grey. * This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results. - * By default the output image will be web-friendly sRGB and contain three (identical) colour channels. + * By default the output image will be web-friendly sRGB and contain three (identical) color channels. * This may be overridden by other sharp operations such as `toColourspace('b-w')`, - * which will produce an output image containing one colour channel. + * which will produce an output image containing one color channel. * An alpha channel may be present, and will be unchanged by the operation. * * @example @@ -69,7 +71,9 @@ function grayscale (grayscale) { * * The input image will be converted to the provided colourspace at the start of the pipeline. * All operations will use this colourspace before converting to the output colourspace, - * as defined by {@link #tocolourspace toColourspace}. + * as defined by {@link #tocolourspace|toColourspace}. + * + * This feature is experimental and has not yet been fully-tested with all operations. * * @since 0.29.0 * @@ -80,7 +84,7 @@ function grayscale (grayscale) { * .toColourspace('srgb') * .toFile('16bpc-pipeline-to-8bpc-output.png') * - * @param {string} [colourspace] - pipeline colourspace e.g. `rgb16`, `scrgb`, `lab`, `grey16` [...](https://www.libvips.org/API/current/enum.Interpretation.html) + * @param {string} [colourspace] - pipeline colourspace e.g. `rgb16`, `scrgb`, `lab`, `grey16` [...](https://github.com/libvips/libvips/blob/41cff4e9d0838498487a00623462204eb10ee5b8/libvips/iofuncs/enumtypes.c#L774) * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -88,7 +92,7 @@ function pipelineColourspace (colourspace) { if (!is.string(colourspace)) { throw is.invalidParameterError('colourspace', 'string', colourspace); } - this.options.colourspacePipeline = colourspace; + this.options.colourspaceInput = colourspace; return this; } @@ -112,7 +116,7 @@ function pipelineColorspace (colorspace) { * .toColourspace('rgb16') * .toFile('16-bpp.png') * - * @param {string} [colourspace] - output colourspace e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://www.libvips.org/API/current/enum.Interpretation.html) + * @param {string} [colourspace] - output colourspace e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://github.com/libvips/libvips/blob/3c0bfdf74ce1dc37a6429bed47fa76f16e2cd70a/libvips/iofuncs/enumtypes.c#L777-L794) * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -134,29 +138,6 @@ function toColorspace (colorspace) { return this.toColourspace(colorspace); } -/** - * Create a RGBA colour array from a given value. - * @private - * @param {string|Object} value - * @throws {Error} Invalid value - */ -function _getBackgroundColourOption (value) { - if ( - is.object(value) || - (is.string(value) && value.length >= 3 && value.length <= 200) - ) { - const colour = color(value); - return [ - colour.red(), - colour.green(), - colour.blue(), - Math.round(colour.alpha() * 255) - ]; - } else { - throw is.invalidParameterError('background', 'object or string', value); - } -} - /** * Update a colour attribute of the this.options Object. * @private @@ -166,16 +147,25 @@ function _getBackgroundColourOption (value) { */ function _setBackgroundColourOption (key, value) { if (is.defined(value)) { - this.options[key] = _getBackgroundColourOption(value); + if (is.object(value) || is.string(value)) { + const colour = color(value); + this.options[key] = [ + colour.red(), + colour.green(), + colour.blue(), + Math.round(colour.alpha() * 255) + ]; + } else { + throw is.invalidParameterError('background', 'object or string', value); + } } } /** * Decorate the Sharp prototype with colour-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Object.assign(Sharp.prototype, { // Public tint, @@ -186,7 +176,6 @@ module.exports = (Sharp) => { toColourspace, toColorspace, // Private - _getBackgroundColourOption, _setBackgroundColourOption }); // Class attributes diff --git a/backend/node_modules/sharp/lib/composite.js b/backend/node_modules/sharp/lib/composite.js index 1c3e5e62..28e83788 100644 --- a/backend/node_modules/sharp/lib/composite.js +++ b/backend/node_modules/sharp/lib/composite.js @@ -1,7 +1,7 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; const is = require('./is'); @@ -46,8 +46,8 @@ const blend = { * The images to composite must be the same size or smaller than the processed image. * If both `top` and `left` options are provided, they take precedence over `gravity`. * - * Other operations in the same processing pipeline (e.g. resize, rotate, flip, - * flop, extract) will always be applied to the input image before composition. + * Any resize, rotate or extract operations in the same processing pipeline + * will always be applied to the input image before composition. * * The `blend` option can be one of `clear`, `source`, `over`, `in`, `out`, `atop`, * `dest`, `dest-over`, `dest-in`, `dest-out`, `dest-atop`, @@ -56,7 +56,7 @@ const blend = { * `hard-light`, `soft-light`, `difference`, `exclusion`. * * More information about blend modes can be found at - * https://www.libvips.org/API/current/enum.BlendMode.html + * https://www.libvips.org/API/current/libvips-conversion.html#VipsBlendMode * and https://www.cairographics.org/operators/ * * @since 0.22.0 @@ -110,7 +110,6 @@ const blend = { * @param {number} [images[].input.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified. * @param {boolean} [images[].input.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for Pango markup features like `Red!`. * @param {number} [images[].input.text.spacing=0] - text line height in points. Will use the font line height if none is specified. - * @param {Boolean} [images[].autoOrient=false] - set to true to use EXIF orientation data, if present, to orient the image. * @param {String} [images[].blend='over'] - how to blend this image with the image below. * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay. * @param {Number} [images[].top] - the pixel offset from the top edge. @@ -123,8 +122,8 @@ const blend = { * @param {Number} [images[].raw.height] * @param {Number} [images[].raw.channels] * @param {boolean} [images[].animated=false] - Set to `true` to read all frames/pages of an animated image. - * @param {string} [images[].failOn='warning'] - @see {@link /api-constructor/ constructor parameters} - * @param {number|boolean} [images[].limitInputPixels=268402689] - @see {@link /api-constructor/ constructor parameters} + * @param {string} [images[].failOn='warning'] - @see {@link /api-constructor#parameters|constructor parameters} + * @param {number|boolean} [images[].limitInputPixels=268402689] - @see {@link /api-constructor#parameters|constructor parameters} * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -203,10 +202,9 @@ function composite (images) { /** * Decorate the Sharp prototype with composite-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Sharp.prototype.composite = composite; Sharp.blend = blend; }; diff --git a/backend/node_modules/sharp/lib/constructor.js b/backend/node_modules/sharp/lib/constructor.js index 9aac8105..1fbd6e34 100644 --- a/backend/node_modules/sharp/lib/constructor.js +++ b/backend/node_modules/sharp/lib/constructor.js @@ -1,21 +1,18 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -const util = require('node:util'); -const stream = require('node:stream'); +'use strict'; + +const util = require('util'); +const stream = require('stream'); const is = require('./is'); +require('./libvips').hasVendoredLibvips(); require('./sharp'); // Use NODE_DEBUG=sharp to enable libvips warnings const debuglog = util.debuglog('sharp'); -const queueListener = (queueLength) => { - Sharp.queue.emit('change', queueLength); -}; - /** * Constructor factory to create an instance of `sharp`, to which further methods are chained. * @@ -26,10 +23,6 @@ const queueListener = (queueLength) => { * * Implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class. * - * When loading more than one page/frame of an animated image, - * these are combined as a vertically-stacked "toilet roll" image - * where the overall height is the `pageHeight` multiplied by the number of `pages`. - * * @constructs Sharp * * @emits Sharp#info @@ -44,16 +37,14 @@ const queueListener = (queueLength) => { * }); * * @example - * // Read image data from remote URL, + * // Read image data from readableStream, * // resize to 300 pixels wide, * // emit an 'info' event with calculated dimensions * // and finally write image data to writableStream - * const { body } = fetch('https://...'); - * const readableStream = Readable.fromWeb(body); - * const transformer = sharp() + * var transformer = sharp() * .resize(300) - * .on('info', ({ height }) => { - * console.log(`Image height is ${height}`); + * .on('info', function(info) { + * console.log('Image height is ' + info.height); * }); * readableStream.pipe(transformer).pipe(writableStream); * @@ -125,38 +116,24 @@ const queueListener = (queueLength) => { * } * }).toFile('text_rgba.png'); * - * @example - * // Join four input images as a 2x2 grid with a 4 pixel gutter - * const data = await sharp( - * [image1, image2, image3, image4], - * { join: { across: 2, shim: 4 } } - * ).toBuffer(); - * - * @example - * // Generate a two-frame animated image from emoji - * const images = ['😀', '😛'].map(text => ({ - * text: { text, width: 64, height: 64, channels: 4, rgba: true } - * })); - * await sharp(images, { join: { animated: true } }).toFile('out.gif'); - * - * @param {(Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string|Array)} [input] - if present, can be + * @param {(Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string)} [input] - if present, can be * a Buffer / ArrayBuffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or * a TypedArray containing raw pixel image data, or * a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file. - * An array of inputs can be provided, and these will be joined together. * JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present. * @param {Object} [options] - if present, is an Object with optional attributes. - * @param {string} [options.failOn='warning'] - When to abort processing of invalid pixel data, one of (in order of sensitivity, least to most): 'none', 'truncated', 'error', 'warning'. Higher levels imply lower levels. Invalid metadata will always abort. + * @param {string} [options.failOn='warning'] - when to abort processing of invalid pixel data, one of (in order of sensitivity): 'none' (least), 'truncated', 'error' or 'warning' (most), higher levels imply lower levels, invalid metadata will always abort. * @param {number|boolean} [options.limitInputPixels=268402689] - Do not process input images where the number of pixels * (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted. * An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF). * @param {boolean} [options.unlimited=false] - Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF). - * @param {boolean} [options.autoOrient=false] - Set this to `true` to rotate/flip the image to match EXIF `Orientation`, if any. * @param {boolean} [options.sequentialRead=true] - Set this to `false` to use random access rather than sequential read. Some operations will do this automatically. * @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000. * @param {number} [options.ignoreIcc=false] - should the embedded ICC profile, if any, be ignored. * @param {number} [options.pages=1] - Number of pages to extract for multi-page input (GIF, WebP, TIFF), use -1 for all pages. * @param {number} [options.page=0] - Page number to start extracting from for multi-page input (GIF, WebP, TIFF), zero based. + * @param {number} [options.subifd=-1] - subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image. + * @param {number} [options.level=0] - level to extract from a multi-level input (OpenSlide), zero based. * @param {boolean} [options.animated=false] - Set to `true` to read all frames/pages of an animated image (GIF, WebP, TIFF), equivalent of setting `pages` to `-1`. * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. * @param {number} [options.raw.width] - integral number of pixels wide. @@ -164,17 +141,15 @@ const queueListener = (queueLength) => { * @param {number} [options.raw.channels] - integral number of channels, between 1 and 4. * @param {boolean} [options.raw.premultiplied] - specifies that the raw input has already been premultiplied, set to `true` * to avoid sharp premultiplying the image. (optional, default `false`) - * @param {number} [options.raw.pageHeight] - The pixel height of each page/frame for animated images, must be an integral factor of `raw.height`. * @param {Object} [options.create] - describes a new image to be created. * @param {number} [options.create.width] - integral number of pixels wide. * @param {number} [options.create.height] - integral number of pixels high. * @param {number} [options.create.channels] - integral number of channels, either 3 (RGB) or 4 (RGBA). * @param {string|Object} [options.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. - * @param {number} [options.create.pageHeight] - The pixel height of each page/frame for animated images, must be an integral factor of `create.height`. * @param {Object} [options.create.noise] - describes a noise to be created. * @param {string} [options.create.noise.type] - type of generated noise, currently only `gaussian` is supported. - * @param {number} [options.create.noise.mean=128] - Mean value of pixels in the generated noise. - * @param {number} [options.create.noise.sigma=30] - Standard deviation of pixel values in the generated noise. + * @param {number} [options.create.noise.mean] - mean of pixels in generated noise. + * @param {number} [options.create.noise.sigma] - standard deviation of pixels in generated noise. * @param {Object} [options.text] - describes a new text image to be created. * @param {string} [options.text.text] - text to render as a UTF-8 string. It can contain Pango markup, for example `LeMonde`. * @param {string} [options.text.font] - font name to render with. @@ -186,30 +161,11 @@ const queueListener = (queueLength) => { * @param {number} [options.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified. * @param {boolean} [options.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for pango markup features like `Red!`. * @param {number} [options.text.spacing=0] - text line height in points. Will use the font line height if none is specified. - * @param {string} [options.text.wrap='word'] - word wrapping style when width is provided, one of: 'word', 'char', 'word-char' (prefer word, fallback to char) or 'none'. - * @param {Object} [options.join] - describes how an array of input images should be joined. - * @param {number} [options.join.across=1] - number of images to join horizontally. - * @param {boolean} [options.join.animated=false] - set this to `true` to join the images as an animated image. - * @param {number} [options.join.shim=0] - number of pixels to insert between joined images. - * @param {string|Object} [options.join.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. - * @param {string} [options.join.halign='left'] - horizontal alignment style for images joined horizontally (`'left'`, `'centre'`, `'center'`, `'right'`). - * @param {string} [options.join.valign='top'] - vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`). - * @param {Object} [options.tiff] - Describes TIFF specific options. - * @param {number} [options.tiff.subifd=-1] - Sub Image File Directory to extract for OME-TIFF, defaults to main image. - * @param {Object} [options.svg] - Describes SVG specific options. - * @param {string} [options.svg.stylesheet] - Custom CSS for SVG input, applied with a User Origin during the CSS cascade. - * @param {boolean} [options.svg.highBitdepth=false] - Set to `true` to render SVG input at 32-bits per channel (128-bit) instead of 8-bits per channel (32-bit) RGBA. - * @param {Object} [options.pdf] - Describes PDF specific options. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. - * @param {string|Object} [options.pdf.background] - Background colour to use when PDF is partially transparent. Parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. - * @param {Object} [options.openSlide] - Describes OpenSlide specific options. Requires the use of a globally-installed libvips compiled with support for OpenSlide. - * @param {number} [options.openSlide.level=0] - Level to extract from a multi-level input, zero based. - * @param {Object} [options.jp2] - Describes JPEG 2000 specific options. Requires the use of a globally-installed libvips compiled with support for OpenJPEG. - * @param {boolean} [options.jp2.oneshot=false] - Set to `true` to decode tiled JPEG 2000 images in a single operation, improving compatibility. + * @param {string} [options.text.wrap='word'] - word wrapping style when width is provided, one of: 'word', 'char', 'charWord' (prefer char, fallback to word) or 'none'. * @returns {Sharp} * @throws {Error} Invalid parameters */ const Sharp = function (input, options) { - // biome-ignore lint/complexity/noArguments: constructor factory if (arguments.length === 1 && !is.defined(input)) { throw new Error('Invalid input'); } @@ -232,11 +188,11 @@ const Sharp = function (input, options) { canvas: 'crop', position: 0, resizeBackground: [0, 0, 0, 255], + useExifOrientation: false, angle: 0, rotationAngle: 0, rotationBackground: [0, 0, 0, 255], - rotateBefore: false, - orientBefore: false, + rotateBeforePreExtract: false, flip: false, flop: false, extendTop: 0, @@ -257,7 +213,8 @@ const Sharp = function (input, options) { kernel: 'lanczos3', fastShrinkOnLoad: true, // operations - tint: [-1, 0, 0, 0], + tintA: 128, + tintB: 128, flatten: false, flattenBackground: [0, 0, 0], unflatten: false, @@ -265,8 +222,6 @@ const Sharp = function (input, options) { negateAlpha: true, medianSize: 0, blurSigma: 0, - precision: 'integer', - minAmpl: 0.2, sharpenSigma: 0, sharpenM1: 1, sharpenM2: 2, @@ -276,10 +231,7 @@ const Sharp = function (input, options) { threshold: 0, thresholdGrayscale: true, trimBackground: [], - trimThreshold: -1, - trimLineArt: false, - dilateWidth: 0, - erodeWidth: 0, + trimThreshold: 0, gamma: 0, gammaOut: 0, greyscale: false, @@ -300,22 +252,18 @@ const Sharp = function (input, options) { removeAlpha: false, ensureAlpha: -1, colourspace: 'srgb', - colourspacePipeline: 'last', + colourspaceInput: 'last', composite: [], // output fileOut: '', formatOut: 'input', streamOut: false, - keepMetadata: 0, + withMetadata: false, withMetadataOrientation: -1, withMetadataDensity: 0, - withIccProfile: '', - withExif: {}, - withExifMerge: true, - withXmp: '', + withMetadataIcc: '', + withMetadataStrs: {}, resolveWithObject: false, - loop: -1, - delay: [], // output format jpegQuality: 80, jpegProgressive: false, @@ -343,7 +291,6 @@ const Sharp = function (input, options) { webpLossless: false, webpNearLossless: false, webpSmartSubsample: false, - webpSmartDeblock: false, webpPreset: 'default', webpEffort: 4, webpMinSize: false, @@ -353,15 +300,12 @@ const Sharp = function (input, options) { gifDither: 1, gifInterFrameMaxError: 0, gifInterPaletteMaxError: 3, - gifKeepDuplicateFrames: false, gifReuse: true, gifProgressive: false, tiffQuality: 80, tiffCompression: 'jpeg', - tiffBigtiff: false, tiffPredictor: 'horizontal', tiffPyramid: false, - tiffMiniswhite: false, tiffBitdepth: 8, tiffTile: false, tiffTileHeight: 256, @@ -374,7 +318,6 @@ const Sharp = function (input, options) { heifCompression: 'av1', heifEffort: 4, heifChromaSubsampling: '4:4:4', - heifBitdepth: 8, jxlDistance: 1, jxlDecodingTier: 0, jxlEffort: 7, @@ -395,14 +338,15 @@ const Sharp = function (input, options) { timeoutSeconds: 0, linearA: [], linearB: [], - pdfBackground: [255, 255, 255, 255], // Function to notify of libvips warnings debuglog: warning => { this.emit('warning', warning); debuglog(warning); }, // Function to notify of queue length changes - queueListener + queueListener: function (queueLength) { + Sharp.queue.emit('change', queueLength); + } }; this.options.input = this._createInputDescriptor(input, options, { allowStream: true }); return this; @@ -474,16 +418,13 @@ Object.setPrototypeOf(Sharp, stream.Duplex); function clone () { // Clone existing options const clone = this.constructor.call(); - const { debuglog, queueListener, ...options } = this.options; - clone.options = structuredClone(options); - clone.options.debuglog = debuglog; - clone.options.queueListener = queueListener; + clone.options = Object.assign({}, this.options); // Pass 'finish' event to clone for Stream-based input if (this._isStreamInput()) { this.on('finish', () => { // Clone inherits input data this._flattenBufferIn(); - clone.options.input.buffer = this.options.input.buffer; + clone.options.bufferIn = this.options.bufferIn; clone.emit('finish'); }); } @@ -493,7 +434,6 @@ Object.assign(Sharp.prototype, { clone }); /** * Export constructor. - * @module Sharp * @private */ module.exports = Sharp; diff --git a/backend/node_modules/sharp/lib/index.d.ts b/backend/node_modules/sharp/lib/index.d.ts index 89ff39e7..cf504a99 100644 --- a/backend/node_modules/sharp/lib/index.d.ts +++ b/backend/node_modules/sharp/lib/index.d.ts @@ -27,7 +27,7 @@ /// -import type { Duplex } from 'node:stream'; +import { Duplex } from 'stream'; //#region Constructor functions @@ -40,7 +40,19 @@ import type { Duplex } from 'node:stream'; */ declare function sharp(options?: sharp.SharpOptions): sharp.Sharp; declare function sharp( - input?: sharp.SharpInput | Array, + input?: + | Buffer + | ArrayBuffer + | Uint8Array + | Uint8ClampedArray + | Int8Array + | Uint16Array + | Int16Array + | Uint32Array + | Int32Array + | Float32Array + | Float64Array + | string, options?: sharp.SharpOptions, ): sharp.Sharp; @@ -50,35 +62,39 @@ declare namespace sharp { /** An Object containing the version numbers of sharp, libvips and its dependencies. */ const versions: { - aom?: string | undefined; - archive?: string | undefined; + vips: string; cairo?: string | undefined; - cgif?: string | undefined; + croco?: string | undefined; exif?: string | undefined; expat?: string | undefined; ffi?: string | undefined; fontconfig?: string | undefined; freetype?: string | undefined; - fribidi?: string | undefined; + gdkpixbuf?: string | undefined; + gif?: string | undefined; glib?: string | undefined; + gsf?: string | undefined; harfbuzz?: string | undefined; - heif?: string | undefined; - highway?: string | undefined; - imagequant?: string | undefined; + jpeg?: string | undefined; lcms?: string | undefined; - mozjpeg?: string | undefined; + orc?: string | undefined; pango?: string | undefined; pixman?: string | undefined; png?: string | undefined; - "proxy-libintl"?: string | undefined; - rsvg?: string | undefined; - sharp: string; - spng?: string | undefined; + sharp?: string | undefined; + svg?: string | undefined; tiff?: string | undefined; - vips: string; webp?: string | undefined; + avif?: string | undefined; + heif?: string | undefined; xml?: string | undefined; - "zlib-ng"?: string | undefined; + zlib?: string | undefined; + }; + + /** An Object containing the platform and architecture of the current and installed vendored binaries. */ + const vendor: { + current: string; + installed: string[]; }; /** An Object containing the available interpolators and their proper values */ @@ -116,7 +132,7 @@ declare namespace sharp { function counters(): SharpCounters; /** - * Get and set use of SIMD vector unit instructions. Requires libvips to have been compiled with highway support. + * Get and set use of SIMD vector unit instructions. Requires libvips to have been compiled with liborc support. * Improves the performance of resize, blur and sharpen operations by taking advantage of the SIMD vector unit of the CPU, e.g. Intel SSE and ARM NEON. * @param enable enable or disable use of SIMD vector unit instructions * @returns true if usage of SIMD vector unit instructions is enabled @@ -229,19 +245,19 @@ declare namespace sharp { //#region Color functions /** - * Tint the image using the provided colour. + * Tint the image using the provided chroma while preserving the image luminance. * An alpha channel may be present and will be unchanged by the operation. - * @param tint Parsed by the color module. + * @param rgb Parsed by the color module to extract chroma values. * @returns A sharp instance that can be used to chain operations */ - tint(tint: Colour | Color): Sharp; + tint(rgb: Color): Sharp; /** * Convert to 8-bit greyscale; 256 shades of grey. * This is a linear operation. * If the input image is in a non-linear colour space such as sRGB, use gamma() with greyscale() for the best results. - * By default the output image will be web-friendly sRGB and contain three (identical) colour channels. - * This may be overridden by other sharp operations such as toColourspace('b-w'), which will produce an output image containing one colour channel. + * By default the output image will be web-friendly sRGB and contain three (identical) color channels. + * This may be overridden by other sharp operations such as toColourspace('b-w'), which will produce an output image containing one color channel. * An alpha channel may be present, and will be unchanged by the operation. * @param greyscale true to enable and false to disable (defaults to true) * @returns A sharp instance that can be used to chain operations @@ -331,12 +347,6 @@ declare namespace sharp { */ metadata(): Promise; - /** - * Keep all metadata (EXIF, ICC, XMP, IPTC) from the input image in the output image. - * @returns A sharp instance that can be used to chain operations - */ - keepMetadata(): Sharp; - /** * Access to pixel-derived image statistics for every channel in the image. * @returns A sharp instance that can be used to chain operations @@ -354,72 +364,24 @@ declare namespace sharp { //#region Operation functions /** - * Rotate the output image by either an explicit angle - * or auto-orient based on the EXIF `Orientation` tag. + * Rotate the output image by either an explicit angle or auto-orient based on the EXIF Orientation tag. * - * If an angle is provided, it is converted to a valid positive degree rotation. - * For example, `-450` will produce a 270 degree rotation. + * If an angle is provided, it is converted to a valid positive degree rotation. For example, -450 will produce a 270deg rotation. * - * When rotating by an angle other than a multiple of 90, - * the background colour can be provided with the `background` option. + * When rotating by an angle other than a multiple of 90, the background colour can be provided with the background option. * - * If no angle is provided, it is determined from the EXIF data. - * Mirroring is supported and may infer the use of a flip operation. + * If no angle is provided, it is determined from the EXIF data. Mirroring is supported and may infer the use of a flip operation. * - * The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any. + * The use of rotate implies the removal of the EXIF Orientation tag, if any. * - * Only one rotation can occur per pipeline (aside from an initial call without - * arguments to orient via EXIF data). Previous calls to `rotate` in the same - * pipeline will be ignored. - * - * Multi-page images can only be rotated by 180 degrees. - * - * Method order is important when rotating, resizing and/or extracting regions, - * for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`. - * - * @example - * const pipeline = sharp() - * .rotate() - * .resize(null, 200) - * .toBuffer(function (err, outputBuffer, info) { - * // outputBuffer contains 200px high JPEG image data, - * // auto-rotated using EXIF Orientation tag - * // info.width and info.height contain the dimensions of the resized image - * }); - * readableStream.pipe(pipeline); - * - * @example - * const rotateThenResize = await sharp(input) - * .rotate(90) - * .resize({ width: 16, height: 8, fit: 'fill' }) - * .toBuffer(); - * const resizeThenRotate = await sharp(input) - * .resize({ width: 16, height: 8, fit: 'fill' }) - * .rotate(90) - * .toBuffer(); - * - * @param {number} [angle=auto] angle of rotation. - * @param {Object} [options] - if present, is an Object with optional attributes. - * @param {string|Object} [options.background="#000000"] parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. - * @returns {Sharp} + * Method order is important when both rotating and extracting regions, for example rotate(x).extract(y) will produce a different result to extract(y).rotate(x). + * @param angle angle of rotation. (optional, default auto) + * @param options if present, is an Object with optional attributes. * @throws {Error} Invalid parameters + * @returns A sharp instance that can be used to chain operations */ rotate(angle?: number, options?: RotateOptions): Sharp; - /** - * Alias for calling `rotate()` with no arguments, which orients the image based - * on EXIF orientsion. - * - * This operation is aliased to emphasize its purpose, helping to remove any - * confusion between rotation and orientation. - * - * @example - * const output = await sharp(input).autoOrient().toBuffer(); - * - * @returns {Sharp} - */ - autoOrient(): Sharp - /** * Flip the image about the vertical Y axis. This always occurs after rotation, if any. * The use of flip implies the removal of the EXIF Orientation tag, if any. @@ -439,7 +401,7 @@ declare namespace sharp { /** * Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any. * You must provide an array of length 4 or a 2x2 affine transformation matrix. - * By default, new pixels are filled with a black background. You can provide a background colour with the `background` option. + * By default, new pixels are filled with a black background. You can provide a background color with the `background` option. * A particular interpolator may also be specified. Set the `interpolator` option to an attribute of the `sharp.interpolators` Object e.g. `sharp.interpolators.nohalo`. * * In the case of a 2x2 matrix, the transform is: @@ -502,23 +464,7 @@ declare namespace sharp { * @throws {Error} Invalid parameters * @returns A sharp instance that can be used to chain operations */ - blur(sigma?: number | boolean | BlurOptions): Sharp; - - /** - * Expand foreground objects using the dilate morphological operator. - * @param {Number} [width=1] dilation width in pixels. - * @throws {Error} Invalid parameters - * @returns A sharp instance that can be used to chain operations - */ - dilate(width?: number): Sharp; - - /** - * Shrink foreground objects using the erode morphological operator. - * @param {Number} [width=1] erosion width in pixels. - * @throws {Error} Invalid parameters - * @returns A sharp instance that can be used to chain operations - */ - erode(width?: number): Sharp; + blur(sigma?: number | boolean): Sharp; /** * Merge alpha transparency channel, if any, with background. @@ -625,11 +571,11 @@ declare namespace sharp { /** * Recomb the image with the specified matrix. - * @param inputMatrix 3x3 Recombination matrix or 4x4 Recombination matrix + * @param inputMatrix 3x3 Recombination matrix * @throws {Error} Invalid parameters * @returns A sharp instance that can be used to chain operations */ - recomb(inputMatrix: Matrix3x3 | Matrix4x4): Sharp; + recomb(inputMatrix: Matrix3x3): Sharp; /** * Transforms the image using brightness, saturation, hue rotation and lightness. @@ -693,57 +639,6 @@ declare namespace sharp { */ toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: OutputInfo }>; - /** - * Keep all EXIF metadata from the input image in the output image. - * EXIF metadata is unsupported for TIFF output. - * @returns A sharp instance that can be used to chain operations - */ - keepExif(): Sharp; - - /** - * Set EXIF metadata in the output image, ignoring any EXIF in the input image. - * @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. - * @returns A sharp instance that can be used to chain operations - * @throws {Error} Invalid parameters - */ - withExif(exif: Exif): Sharp; - - /** - * Update EXIF metadata from the input image in the output image. - * @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. - * @returns A sharp instance that can be used to chain operations - * @throws {Error} Invalid parameters - */ - withExifMerge(exif: Exif): Sharp; - - /** - * Keep ICC profile from the input image in the output image where possible. - * @returns A sharp instance that can be used to chain operations - */ - keepIccProfile(): Sharp; - - /** - * Transform using an ICC profile and attach to the output image. - * @param {string} icc - Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk). - * @returns A sharp instance that can be used to chain operations - * @throws {Error} Invalid parameters - */ - withIccProfile(icc: string, options?: WithIccProfileOptions): Sharp; - - /** - * Keep all XMP metadata from the input image in the output image. - * @returns A sharp instance that can be used to chain operations - */ - keepXmp(): Sharp; - - /** - * Set XMP metadata in the output image. - * @param {string} xmp - String containing XMP metadata to be embedded in the output image. - * @returns A sharp instance that can be used to chain operations - * @throws {Error} Invalid parameters - */ - withXmp(xmp: string): Sharp; - /** * Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. * The default behaviour, when withMetadata is not used, is to strip all metadata and convert to the device-independent sRGB colour space. @@ -810,6 +705,7 @@ declare namespace sharp { /** * Use these AVIF options for output image. + * Whilst it is possible to create AVIF images smaller than 16x16 pixels, most web browsers do not display these properly. * @param options Output options. * @throws {Error} Invalid options * @returns A sharp instance that can be used to chain operations @@ -860,7 +756,6 @@ declare namespace sharp { | JxlOptions | GifOptions | Jp2Options - | RawOptions | TiffOptions, ): Sharp; @@ -868,6 +763,8 @@ declare namespace sharp { * Use tile-based deep zoom (image pyramid) output. * Set the format and options for tile images via the toFormat, jpeg, png or webp functions. * Use a .zip or .szi file extension with toFile to write to a compressed archive file format. + * + * Warning: multiple sharp instances concurrently producing tile output can expose a possible race condition in some versions of libgsf. * @param tile tile options * @throws {Error} Invalid options * @returns A sharp instance that can be used to chain operations @@ -957,36 +854,16 @@ declare namespace sharp { * Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel. * Images with an alpha channel will use the combined bounding box of alpha and non-alpha channels. * The info response Object will contain trimOffsetLeft and trimOffsetTop properties. - * @param options trim options + * @param trim The specific background colour to trim, the threshold for doing so or an Object with both. * @throws {Error} Invalid parameters * @returns A sharp instance that can be used to chain operations */ - trim(options?: TrimOptions): Sharp; + trim(trim?: string | number | TrimOptions): Sharp; //#endregion } - type SharpInput = Buffer - | ArrayBuffer - | Uint8Array - | Uint8ClampedArray - | Int8Array - | Uint16Array - | Int16Array - | Uint32Array - | Int32Array - | Float32Array - | Float64Array - | string; - interface SharpOptions { - /** - * Auto-orient based on the EXIF `Orientation` tag, if present. - * Mirroring is supported and may infer the use of a flip operation. - * - * Using this option will remove the EXIF `Orientation` tag, if any. - */ - autoOrient?: boolean | undefined; /** * When to abort processing of invalid pixel data, one of (in order of sensitivity): * 'none' (least), 'truncated', 'error' or 'warning' (most), highers level imply lower levels, invalid metadata will always abort. (optional, default 'warning') @@ -1018,21 +895,9 @@ declare namespace sharp { pages?: number | undefined; /** Page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based. (optional, default 0) */ page?: number | undefined; - /** TIFF specific input options */ - tiff?: TiffInputOptions | undefined; - /** SVG specific input options */ - svg?: SvgInputOptions | undefined; - /** PDF specific input options */ - pdf?: PdfInputOptions | undefined; - /** OpenSlide specific input options */ - openSlide?: OpenSlideInputOptions | undefined; - /** JPEG 2000 specific input options */ - jp2?: Jp2InputOptions | undefined; - /** Deprecated: use tiff.subifd instead */ + /** subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image. (optional, default -1) */ subifd?: number | undefined; - /** Deprecated: use pdf.background instead */ - pdfBackground?: Colour | Color | undefined; - /** Deprecated: use openSlide.level instead */ + /** Level to extract from a multi-level input (OpenSlide), zero based. (optional, default 0) */ level?: number | undefined; /** Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default false) */ animated?: boolean | undefined; @@ -1042,8 +907,6 @@ declare namespace sharp { create?: Create | undefined; /** Describes a new text image to be created. */ text?: CreateText | undefined; - /** Describes how array of input images should be joined. */ - join?: Join | undefined; } interface CacheOptions { @@ -1070,32 +933,25 @@ declare namespace sharp { interface Raw { width: number; height: number; - channels: Channels; + channels: 1 | 2 | 3 | 4; } interface CreateRaw extends Raw { /** Specifies that the raw input has already been premultiplied, set to true to avoid sharp premultiplying the image. (optional, default false) */ premultiplied?: boolean | undefined; - /** The height of each page/frame for animated images, must be an integral factor of the overall image height. */ - pageHeight?: number | undefined; } - type CreateChannels = 3 | 4; - interface Create { /** Number of pixels wide. */ width: number; /** Number of pixels high. */ height: number; - /** Number of bands, 3 for RGB, 4 for RGBA */ - channels: CreateChannels; + /** Number of bands e.g. 3 for RGB, 4 for RGBA */ + channels: Channels; /** Parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. */ - background: Colour | Color; + background: Color; /** Describes a noise to be created. */ noise?: Noise | undefined; - /** The height of each page/frame for animated images, must be an integral factor of the overall image height. */ - pageHeight?: number | undefined; - } interface CreateText { @@ -1125,118 +981,44 @@ declare namespace sharp { rgba?: boolean; /** Text line height in points. Will use the font line height if none is specified. (optional, default `0`) */ spacing?: number; - /** Word wrapping style when width is provided, one of: 'word', 'char', 'word-char' (prefer word, fallback to char) or 'none' */ + /** Word wrapping style when width is provided, one of: 'word', 'char', 'charWord' (prefer char, fallback to word) or 'none' */ wrap?: TextWrap; } - interface Join { - /** Number of images per row. */ - across?: number | undefined; - /** Treat input as frames of an animated image. */ - animated?: boolean | undefined; - /** Space between images, in pixels. */ - shim?: number | undefined; - /** Background colour. */ - background?: Colour | Color | undefined; - /** Horizontal alignment. */ - halign?: HorizontalAlignment | undefined; - /** Vertical alignment. */ - valign?: VerticalAlignment | undefined; - } - - interface TiffInputOptions { - /** Sub Image File Directory to extract, defaults to main image. Use -1 for all subifds. */ - subifd?: number | undefined; - } - - interface SvgInputOptions { - /** Custom CSS for SVG input, applied with a User Origin during the CSS cascade. */ - stylesheet?: string | undefined; - /** Set to `true` to render SVG input at 32-bits per channel (128-bit) instead of 8-bits per channel (32-bit) RGBA. */ - highBitdepth?: boolean | undefined; - } - - interface PdfInputOptions { - /** Background colour to use when PDF is partially transparent. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. */ - background?: Colour | Color | undefined; - } - - interface OpenSlideInputOptions { - /** Level to extract from a multi-level input, zero based. (optional, default 0) */ - level?: number | undefined; - } - - interface Jp2InputOptions { - /** Set to `true` to load JPEG 2000 images using [oneshot mode](https://github.com/libvips/libvips/issues/4205) */ - oneshot?: boolean | undefined; - } - - interface ExifDir { - [k: string]: string; - } - - interface Exif { - 'IFD0'?: ExifDir; - 'IFD1'?: ExifDir; - 'IFD2'?: ExifDir; - 'IFD3'?: ExifDir; - } - - type HeifCompression = 'av1' | 'hevc'; - - type Unit = 'inch' | 'cm'; - interface WriteableMetadata { - /** Number of pixels per inch (DPI) */ - density?: number | undefined; /** Value between 1 and 8, used to update the EXIF Orientation tag. */ orientation?: number | undefined; - /** - * Filesystem path to output ICC profile, defaults to sRGB. - * @deprecated Use `withIccProfile()` instead. - */ + /** Filesystem path to output ICC profile, defaults to sRGB. */ icc?: string | undefined; - /** - * Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. - * @deprecated Use `withExif()` or `withExifMerge()` instead. - */ - exif?: Exif | undefined; + /** Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. (optional, default {}) */ + exif?: Record | undefined; + /** Number of pixels per inch (DPI) */ + density?: number | undefined; } interface Metadata { /** Number value of the EXIF Orientation header, if present */ orientation?: number | undefined; /** Name of decoder used to decompress image data e.g. jpeg, png, webp, gif, svg */ - format: keyof FormatEnum; + format?: keyof FormatEnum | undefined; /** Total size of image in bytes, for Stream and Buffer input only */ size?: number | undefined; /** Number of pixels wide (EXIF orientation is not taken into consideration) */ - width: number; + width?: number | undefined; /** Number of pixels high (EXIF orientation is not taken into consideration) */ - height: number; - /** Any changed metadata after the image orientation is applied. */ - autoOrient: { - /** Number of pixels wide (EXIF orientation is taken into consideration) */ - width: number; - /** Number of pixels high (EXIF orientation is taken into consideration) */ - height: number; - }; + height?: number | undefined; /** Name of colour space interpretation */ - space: keyof ColourspaceEnum; + space?: keyof ColourspaceEnum | undefined; /** Number of bands e.g. 3 for sRGB, 4 for CMYK */ - channels: Channels; + channels?: Channels | undefined; /** Name of pixel depth format e.g. uchar, char, ushort, float ... */ - depth: keyof DepthEnum; + depth?: string | undefined; /** Number of pixels per inch (DPI), if present */ density?: number | undefined; /** String containing JPEG chroma subsampling, 4:2:0 or 4:4:4 for RGB, 4:2:0:4 or 4:4:4:4 for CMYK */ - chromaSubsampling?: string | undefined; + chromaSubsampling: string; /** Boolean indicating whether the image is interlaced using a progressive scan */ - isProgressive: boolean; - /** Boolean indicating whether the image is palette-based (GIF, PNG). */ - isPalette: boolean; - /** Number of bits per sample for each channel (GIF, PNG). */ - bitsPerSample?: number | undefined; + isProgressive?: boolean | undefined; /** Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP */ pages?: number | undefined; /** Number of pixels high each page in a multi-page image will be. */ @@ -1248,9 +1030,9 @@ declare namespace sharp { /** Number of the primary page in a HEIF image */ pagePrimary?: number | undefined; /** Boolean indicating the presence of an embedded ICC profile */ - hasProfile: boolean; + hasProfile?: boolean | undefined; /** Boolean indicating the presence of an alpha transparency channel */ - hasAlpha: boolean; + hasAlpha?: boolean | undefined; /** Buffer containing raw EXIF data, if present */ exif?: Buffer | undefined; /** Buffer containing raw ICC profile data, if present */ @@ -1259,24 +1041,20 @@ declare namespace sharp { iptc?: Buffer | undefined; /** Buffer containing raw XMP data, if present */ xmp?: Buffer | undefined; - /** String containing XMP data, if valid UTF-8 */ - xmpAsString?: string | undefined; /** Buffer containing raw TIFFTAG_PHOTOSHOP data, if present */ tifftagPhotoshop?: Buffer | undefined; /** The encoder used to compress an HEIF file, `av1` (AVIF) or `hevc` (HEIC) */ - compression?: HeifCompression | undefined; - /** Default background colour, if present, for PNG (bKGD) and GIF images */ - background?: { r: number; g: number; b: number } | { gray: number }; + compression?: 'av1' | 'hevc'; + /** Default background colour, if present, for PNG (bKGD) and GIF images, either an RGB Object or a single greyscale value */ + background?: { r: number; g: number; b: number } | number; /** Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide */ levels?: LevelMetadata[] | undefined; /** Number of Sub Image File Directories in an OME-TIFF image */ subifds?: number | undefined; /** The unit of resolution (density) */ - resolutionUnit?: Unit | undefined; + resolutionUnit?: 'inch' | 'cm' | undefined; /** String containing format for images loaded via *magick */ formatMagick?: string | undefined; - /** Array of keyword/text pairs representing PNG text blocks, if present. */ - comments?: CommentsMetadata[] | undefined; } interface LevelMetadata { @@ -1284,11 +1062,6 @@ declare namespace sharp { height: number; } - interface CommentsMetadata { - keyword: string; - text: string; - } - interface Stats { /** Array of channel statistics for each channel in the image. */ channels: ChannelStats[]; @@ -1330,11 +1103,6 @@ declare namespace sharp { force?: boolean | undefined; } - interface WithIccProfileOptions { - /** Should the ICC profile be included in the output image metadata? (optional, default true) */ - attach?: boolean | undefined; - } - interface JpegOptions extends OutputOptions { /** Quality, integer 1-100 (optional, default 80) */ quality?: number | undefined; @@ -1399,8 +1167,6 @@ declare namespace sharp { nearLossless?: boolean | undefined; /** Use high quality chroma subsampling (optional, default false) */ smartSubsample?: boolean | undefined; - /** Auto-adjust the deblocking filter, slow but can improve low contrast edges (optional, default false) */ - smartDeblock?: boolean | undefined; /** Level of CPU effort to reduce file size, integer 0-6 (optional, default 4) */ effort?: number | undefined; /** Prevent use of animation key frames to minimise file size (slow) (optional, default false) */ @@ -1420,23 +1186,19 @@ declare namespace sharp { effort?: number | undefined; /** set to '4:2:0' to use chroma subsampling, requires libvips v8.11.0 (optional, default '4:4:4') */ chromaSubsampling?: string | undefined; - /** Set bitdepth to 8, 10 or 12 bit (optional, default 8) */ - bitdepth?: 8 | 10 | 12 | undefined; } interface HeifOptions extends OutputOptions { /** quality, integer 1-100 (optional, default 50) */ quality?: number | undefined; /** compression format: av1, hevc (optional, default 'av1') */ - compression?: HeifCompression | undefined; + compression?: 'av1' | 'hevc' | undefined; /** use lossless compression (optional, default false) */ lossless?: boolean | undefined; /** Level of CPU effort to reduce file size, between 0 (fastest) and 9 (slowest) (optional, default 4) */ effort?: number | undefined; /** set to '4:2:0' to use chroma subsampling (optional, default '4:4:4') */ chromaSubsampling?: string | undefined; - /** Set bitdepth to 8, 10 or 12 bit (optional, default 8) */ - bitdepth?: 8 | 10 | 12 | undefined; } interface GifOptions extends OutputOptions, AnimationOptions { @@ -1453,11 +1215,9 @@ declare namespace sharp { /** Level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most) (optional, default 1.0) */ dither?: number | undefined; /** Maximum inter-frame error for transparency, between 0 (lossless) and 32 (optional, default 0) */ - interFrameMaxError?: number | undefined; + interFrameMaxError?: number; /** Maximum inter-palette error for palette reuse, between 0 and 256 (optional, default 3) */ - interPaletteMaxError?: number | undefined; - /** Keep duplicate frames in the output instead of combining them (optional, default false) */ - keepDuplicateFrames?: boolean | undefined; + interPaletteMaxError?: number; } interface TiffOptions extends OutputOptions { @@ -1465,8 +1225,6 @@ declare namespace sharp { quality?: number | undefined; /** Compression options: none, jpeg, deflate, packbits, ccittfax4, lzw, webp, zstd, jp2k (optional, default 'jpeg') */ compression?: string | undefined; - /** Use BigTIFF variant (has no effect when compression is none) (optional, default false) */ - bigtiff?: boolean | undefined; /** Compression predictor options: none, horizontal, float (optional, default 'horizontal') */ predictor?: string | undefined; /** Write an image pyramid (optional, default false) */ @@ -1483,10 +1241,8 @@ declare namespace sharp { yres?: number | undefined; /** Reduce bitdepth to 1, 2 or 4 bit (optional, default 8) */ bitdepth?: 1 | 2 | 4 | 8 | undefined; - /** Write 1-bit images as miniswhite (optional, default false) */ - miniswhite?: boolean | undefined; /** Resolution unit options: inch, cm (optional, default 'inch') */ - resolutionUnit?: Unit | undefined; + resolutionUnit?: 'inch' | 'cm' | undefined; } interface PngOptions extends OutputOptions { @@ -1512,23 +1268,12 @@ declare namespace sharp { interface RotateOptions { /** parsed by the color module to extract values for red, green, blue and alpha. (optional, default "#000000") */ - background?: Colour | Color | undefined; - } - - type Precision = 'integer' | 'float' | 'approximate'; - - interface BlurOptions { - /** A value between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2` */ - sigma: number; - /** A value between 0.001 and 1. A smaller value will generate a larger, more accurate mask. */ - minAmplitude?: number; - /** How accurate the operation should be, one of: integer, float, approximate. (optional, default "integer") */ - precision?: Precision | undefined; + background?: Color | undefined; } interface FlattenOptions { /** background colour, parsed by the color module, defaults to black. (optional, default {r:0,g:0,b:0}) */ - background?: Colour | Color | undefined; + background?: Color | undefined; } interface NegateOptions { @@ -1537,10 +1282,10 @@ declare namespace sharp { } interface NormaliseOptions { - /** Percentile below which luminance values will be underexposed. */ - lower?: number | undefined; - /** Percentile above which luminance values will be overexposed. */ - upper?: number | undefined; + /** Percentile below which luminance values will be underexposed. */ + lower?: number | undefined; + /** Percentile above which luminance values will be overexposed. */ + upper?: number | undefined; } interface ResizeOptions { @@ -1553,7 +1298,7 @@ declare namespace sharp { /** Position, gravity or strategy to use when fit is cover or contain. (optional, default 'centre') */ position?: number | string | undefined; /** Background colour when using a fit of contain, parsed by the color module, defaults to black without transparency. (optional, default {r:0,g:0,b:0,alpha:1}) */ - background?: Colour | Color | undefined; + background?: Color | undefined; /** The kernel to use for image reduction. (optional, default 'lanczos3') */ kernel?: keyof KernelEnum | undefined; /** Do not enlarge if the width or height are already less than the specified dimensions, equivalent to GraphicsMagick's > geometry option. (optional, default false) */ @@ -1577,7 +1322,7 @@ declare namespace sharp { interface Noise { /** type of generated noise, currently only gaussian is supported. */ - type: 'gaussian'; + type?: 'gaussian' | undefined; /** mean of pixels in generated noise. */ mean?: number | undefined; /** standard deviation of pixels in generated noise. */ @@ -1596,26 +1341,24 @@ declare namespace sharp { /** single pixel count to right edge (optional, default 0) */ right?: number | undefined; /** background colour, parsed by the color module, defaults to black without transparency. (optional, default {r:0,g:0,b:0,alpha:1}) */ - background?: Colour | Color | undefined; + background?: Color | undefined; /** how the extension is done, one of: "background", "copy", "repeat", "mirror" (optional, default `'background'`) */ extendWith?: ExtendWith | undefined; } interface TrimOptions { - /** Background colour, parsed by the color module, defaults to that of the top-left pixel. (optional) */ - background?: Colour | Color | undefined; - /** Allowed difference from the above colour, a positive number. (optional, default 10) */ + /** background colour, parsed by the color module, defaults to that of the top-left pixel. (optional) */ + background?: Color | undefined; + /** the allowed difference from the above colour, a positive number. (optional, default `10`) */ threshold?: number | undefined; - /** Does the input more closely resemble line art (e.g. vector) rather than being photographic? (optional, default false) */ - lineArt?: boolean | undefined; } interface RawOptions { - depth?: keyof DepthEnum; + depth?: 'char' | 'uchar' | 'short' | 'ushort' | 'int' | 'uint' | 'float' | 'complex' | 'double' | 'dpcomplex'; } - /** 1 for grayscale, 2 for grayscale + alpha, 3 for sRGB, 4 for CMYK or RGBA */ - type Channels = 1 | 2 | 3 | 4; + /** 3 for sRGB, 4 for CMYK */ + type Channels = 3 | 4; interface RGBA { r?: number | undefined; @@ -1624,8 +1367,7 @@ declare namespace sharp { alpha?: number | undefined; } - type Colour = string | RGBA; - type Color = Colour; + type Color = string | RGBA; interface Kernel { /** width of the kernel in pixels. */ @@ -1671,16 +1413,6 @@ declare namespace sharp { tile?: boolean | undefined; /** Set to true to avoid premultipling the image below. Equivalent to the --premultiplied vips option. */ premultiplied?: boolean | undefined; - /** number representing the DPI for vector overlay image. (optional, default 72)*/ - density?: number | undefined; - /** Set to true to read all frames/pages of an animated image. (optional, default false) */ - animated?: boolean | undefined; - /** see sharp() constructor, (optional, default 'warning') */ - failOn?: FailOnOptions | undefined; - /** see sharp() constructor, (optional, default 268402689) */ - limitInputPixels?: number | boolean | undefined; - /** see sharp() constructor, (optional, default false) */ - autoOrient?: boolean | undefined; } interface TileOptions { @@ -1752,7 +1484,7 @@ declare namespace sharp { size: number; width: number; height: number; - channels: Channels; + channels: 1 | 2 | 3 | 4; /** indicating if premultiplication was used */ premultiplied: boolean; /** Only defined when using a crop strategy */ @@ -1768,10 +1500,6 @@ declare namespace sharp { /** When using the attention crop strategy, the focal point of the cropped region */ attentionX?: number | undefined; attentionY?: number | undefined; - /** Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP */ - pages?: number | undefined; - /** Number of pixels high each page in a multi-page image will be. */ - pageHeight?: number | undefined; } interface AvailableFormatInfo { @@ -1791,12 +1519,9 @@ declare namespace sharp { interface KernelEnum { nearest: 'nearest'; cubic: 'cubic'; - linear: 'linear'; mitchell: 'mitchell'; lanczos2: 'lanczos2'; lanczos3: 'lanczos3'; - mks2013: 'mks2013'; - mks2021: 'mks2021'; } interface PresetEnum { @@ -1815,49 +1540,18 @@ declare namespace sharp { } interface ColourspaceEnum { - 'b-w': string; - cmc: string; - cmyk: string; - fourier: string; - grey16: string; - histogram: string; - hsv: string; - lab: string; - labq: string; - labs: string; - lch: string; - matrix: string; multiband: string; - rgb: string; - rgb16: string; - scrgb: string; + 'b-w': string; + bw: string; + cmyk: string; srgb: string; - xyz: string; - yxy: string; - } - - interface DepthEnum { - char: string; - complex: string; - double: string; - dpcomplex: string; - float: string; - int: string; - short: string; - uchar: string; - uint: string; - ushort: string; } type FailOnOptions = 'none' | 'truncated' | 'error' | 'warning'; type TextAlign = 'left' | 'centre' | 'center' | 'right'; - type TextWrap = 'word' | 'char' | 'word-char' | 'none'; - - type HorizontalAlignment = 'left' | 'centre' | 'center' | 'right'; - - type VerticalAlignment = 'top' | 'centre' | 'center' | 'bottom'; + type TextWrap = 'word' | 'char' | 'charWord' | 'none'; type TileContainer = 'fs' | 'zip'; @@ -1914,9 +1608,7 @@ declare namespace sharp { interface FormatEnum { avif: AvailableFormatInfo; - dcraw: AvailableFormatInfo; dz: AvailableFormatInfo; - exr: AvailableFormatInfo; fits: AvailableFormatInfo; gif: AvailableFormatInfo; heif: AvailableFormatInfo; @@ -1930,7 +1622,6 @@ declare namespace sharp { pdf: AvailableFormatInfo; png: AvailableFormatInfo; ppm: AvailableFormatInfo; - rad: AvailableFormatInfo; raw: AvailableFormatInfo; svg: AvailableFormatInfo; tiff: AvailableFormatInfo; @@ -1965,7 +1656,6 @@ declare namespace sharp { type Matrix2x2 = [[number, number], [number, number]]; type Matrix3x3 = [[number, number, number], [number, number, number], [number, number, number]]; - type Matrix4x4 = [[number, number, number, number], [number, number, number, number], [number, number, number, number], [number, number, number, number]]; } export = sharp; diff --git a/backend/node_modules/sharp/lib/index.js b/backend/node_modules/sharp/lib/index.js index b80191d7..8cfc08a8 100644 --- a/backend/node_modules/sharp/lib/index.js +++ b/backend/node_modules/sharp/lib/index.js @@ -1,7 +1,7 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; const Sharp = require('./constructor'); require('./input')(Sharp); diff --git a/backend/node_modules/sharp/lib/input.js b/backend/node_modules/sharp/lib/input.js index 728b7188..0fd4bad6 100644 --- a/backend/node_modules/sharp/lib/input.js +++ b/backend/node_modules/sharp/lib/input.js @@ -1,48 +1,32 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 +'use strict'; + +const color = require('color'); const is = require('./is'); const sharp = require('./sharp'); /** - * Justification alignment + * Justication alignment * @member * @private */ const align = { left: 'low', - top: 'low', - low: 'low', center: 'centre', centre: 'centre', - right: 'high', - bottom: 'high', - high: 'high' + right: 'high' }; -const inputStreamParameters = [ - // Limits and error handling - 'failOn', 'limitInputPixels', 'unlimited', - // Format-generic - 'animated', 'autoOrient', 'density', 'ignoreIcc', 'page', 'pages', 'sequentialRead', - // Format-specific - 'jp2', 'openSlide', 'pdf', 'raw', 'svg', 'tiff', - // Deprecated - 'failOnError', 'openSlideLevel', 'pdfBackground', 'tiffSubifd' -]; - /** * Extract input options, if any, from an object. * @private */ function _inputOptionsFromObject (obj) { - const params = inputStreamParameters - .filter(p => is.defined(obj[p])) - .map(p => ([p, obj[p]])); - return params.length - ? Object.fromEntries(params) + const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } = obj; + return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd].some(is.defined) + ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } : undefined; } @@ -52,9 +36,8 @@ function _inputOptionsFromObject (obj) { */ function _createInputDescriptor (input, inputOptions, containerOptions) { const inputDescriptor = { - autoOrient: false, failOn: 'warning', - limitInputPixels: 0x3FFF ** 2, + limitInputPixels: Math.pow(0x3FFF, 2), ignoreIcc: false, unlimited: false, sequentialRead: true @@ -88,18 +71,6 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { } else if (!is.defined(input) && !is.defined(inputOptions) && is.object(containerOptions) && containerOptions.allowStream) { // Stream without options inputDescriptor.buffer = []; - } else if (Array.isArray(input)) { - if (input.length > 1) { - // Join images together - if (!this.options.joining) { - this.options.joining = true; - this.options.join = input.map(i => this._createInputDescriptor(i)); - } else { - throw new Error('Recursive join is unsupported'); - } - } else { - throw new Error('Expected at least two images to join'); - } } else { throw new Error(`Unsupported input '${input}' of type ${typeof input}${ is.defined(inputOptions) ? ` when also providing options of type ${typeof inputOptions}` : '' @@ -122,14 +93,6 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw is.invalidParameterError('failOn', 'one of: none, truncated, error, warning', inputOptions.failOn); } } - // autoOrient - if (is.defined(inputOptions.autoOrient)) { - if (is.bool(inputOptions.autoOrient)) { - inputDescriptor.autoOrient = inputOptions.autoOrient; - } else { - throw is.invalidParameterError('autoOrient', 'boolean', inputOptions.autoOrient); - } - } // Density if (is.defined(inputOptions.density)) { if (is.inRange(inputOptions.density, 1, 100000)) { @@ -150,7 +113,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { if (is.defined(inputOptions.limitInputPixels)) { if (is.bool(inputOptions.limitInputPixels)) { inputDescriptor.limitInputPixels = inputOptions.limitInputPixels - ? 0x3FFF ** 2 + ? Math.pow(0x3FFF, 2) : 0; } else if (is.integer(inputOptions.limitInputPixels) && is.inRange(inputOptions.limitInputPixels, 0, Number.MAX_SAFE_INTEGER)) { inputDescriptor.limitInputPixels = inputOptions.limitInputPixels; @@ -185,6 +148,8 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { inputDescriptor.rawWidth = inputOptions.raw.width; inputDescriptor.rawHeight = inputOptions.raw.height; inputDescriptor.rawChannels = inputOptions.raw.channels; + inputDescriptor.rawPremultiplied = !!inputOptions.raw.premultiplied; + switch (input.constructor) { case Uint8Array: case Uint8ClampedArray: @@ -218,25 +183,6 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { } else { throw new Error('Expected width, height and channels for raw pixel input'); } - inputDescriptor.rawPremultiplied = false; - if (is.defined(inputOptions.raw.premultiplied)) { - if (is.bool(inputOptions.raw.premultiplied)) { - inputDescriptor.rawPremultiplied = inputOptions.raw.premultiplied; - } else { - throw is.invalidParameterError('raw.premultiplied', 'boolean', inputOptions.raw.premultiplied); - } - } - inputDescriptor.rawPageHeight = 0; - if (is.defined(inputOptions.raw.pageHeight)) { - if (is.integer(inputOptions.raw.pageHeight) && inputOptions.raw.pageHeight > 0 && inputOptions.raw.pageHeight <= inputOptions.raw.height) { - if (inputOptions.raw.height % inputOptions.raw.pageHeight !== 0) { - throw new Error(`Expected raw.height ${inputOptions.raw.height} to be a multiple of raw.pageHeight ${inputOptions.raw.pageHeight}`); - } - inputDescriptor.rawPageHeight = inputOptions.raw.pageHeight; - } else { - throw is.invalidParameterError('raw.pageHeight', 'positive integer', inputOptions.raw.pageHeight); - } - } } // Multi-page input (GIF, TIFF, PDF) if (is.defined(inputOptions.animated)) { @@ -260,68 +206,22 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw is.invalidParameterError('page', 'integer between 0 and 100000', inputOptions.page); } } - // OpenSlide specific options - if (is.object(inputOptions.openSlide) && is.defined(inputOptions.openSlide.level)) { - if (is.integer(inputOptions.openSlide.level) && is.inRange(inputOptions.openSlide.level, 0, 256)) { - inputDescriptor.openSlideLevel = inputOptions.openSlide.level; - } else { - throw is.invalidParameterError('openSlide.level', 'integer between 0 and 256', inputOptions.openSlide.level); - } - } else if (is.defined(inputOptions.level)) { - // Deprecated + // Multi-level input (OpenSlide) + if (is.defined(inputOptions.level)) { if (is.integer(inputOptions.level) && is.inRange(inputOptions.level, 0, 256)) { - inputDescriptor.openSlideLevel = inputOptions.level; + inputDescriptor.level = inputOptions.level; } else { throw is.invalidParameterError('level', 'integer between 0 and 256', inputOptions.level); } } - // TIFF specific options - if (is.object(inputOptions.tiff) && is.defined(inputOptions.tiff.subifd)) { - if (is.integer(inputOptions.tiff.subifd) && is.inRange(inputOptions.tiff.subifd, -1, 100000)) { - inputDescriptor.tiffSubifd = inputOptions.tiff.subifd; - } else { - throw is.invalidParameterError('tiff.subifd', 'integer between -1 and 100000', inputOptions.tiff.subifd); - } - } else if (is.defined(inputOptions.subifd)) { - // Deprecated + // Sub Image File Directory (TIFF) + if (is.defined(inputOptions.subifd)) { if (is.integer(inputOptions.subifd) && is.inRange(inputOptions.subifd, -1, 100000)) { - inputDescriptor.tiffSubifd = inputOptions.subifd; + inputDescriptor.subifd = inputOptions.subifd; } else { throw is.invalidParameterError('subifd', 'integer between -1 and 100000', inputOptions.subifd); } } - // SVG specific options - if (is.object(inputOptions.svg)) { - if (is.defined(inputOptions.svg.stylesheet)) { - if (is.string(inputOptions.svg.stylesheet)) { - inputDescriptor.svgStylesheet = inputOptions.svg.stylesheet; - } else { - throw is.invalidParameterError('svg.stylesheet', 'string', inputOptions.svg.stylesheet); - } - } - if (is.defined(inputOptions.svg.highBitdepth)) { - if (is.bool(inputOptions.svg.highBitdepth)) { - inputDescriptor.svgHighBitdepth = inputOptions.svg.highBitdepth; - } else { - throw is.invalidParameterError('svg.highBitdepth', 'boolean', inputOptions.svg.highBitdepth); - } - } - } - // PDF specific options - if (is.object(inputOptions.pdf) && is.defined(inputOptions.pdf.background)) { - inputDescriptor.pdfBackground = this._getBackgroundColourOption(inputOptions.pdf.background); - } else if (is.defined(inputOptions.pdfBackground)) { - // Deprecated - inputDescriptor.pdfBackground = this._getBackgroundColourOption(inputOptions.pdfBackground); - } - // JPEG 2000 specific options - if (is.object(inputOptions.jp2) && is.defined(inputOptions.jp2.oneshot)) { - if (is.bool(inputOptions.jp2.oneshot)) { - inputDescriptor.jp2Oneshot = inputOptions.jp2.oneshot; - } else { - throw is.invalidParameterError('jp2.oneshot', 'boolean', inputOptions.jp2.oneshot); - } - } // Create new image if (is.defined(inputOptions.create)) { if ( @@ -333,50 +233,39 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { inputDescriptor.createWidth = inputOptions.create.width; inputDescriptor.createHeight = inputOptions.create.height; inputDescriptor.createChannels = inputOptions.create.channels; - inputDescriptor.createPageHeight = 0; - if (is.defined(inputOptions.create.pageHeight)) { - if (is.integer(inputOptions.create.pageHeight) && inputOptions.create.pageHeight > 0 && inputOptions.create.pageHeight <= inputOptions.create.height) { - if (inputOptions.create.height % inputOptions.create.pageHeight !== 0) { - throw new Error(`Expected create.height ${inputOptions.create.height} to be a multiple of create.pageHeight ${inputOptions.create.pageHeight}`); - } - inputDescriptor.createPageHeight = inputOptions.create.pageHeight; - } else { - throw is.invalidParameterError('create.pageHeight', 'positive integer', inputOptions.create.pageHeight); - } - } // Noise if (is.defined(inputOptions.create.noise)) { if (!is.object(inputOptions.create.noise)) { throw new Error('Expected noise to be an object'); } - if (inputOptions.create.noise.type !== 'gaussian') { + if (!is.inArray(inputOptions.create.noise.type, ['gaussian'])) { throw new Error('Only gaussian noise is supported at the moment'); } - inputDescriptor.createNoiseType = inputOptions.create.noise.type; if (!is.inRange(inputOptions.create.channels, 1, 4)) { throw is.invalidParameterError('create.channels', 'number between 1 and 4', inputOptions.create.channels); } - inputDescriptor.createNoiseMean = 128; - if (is.defined(inputOptions.create.noise.mean)) { - if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) { - inputDescriptor.createNoiseMean = inputOptions.create.noise.mean; - } else { - throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean); - } + inputDescriptor.createNoiseType = inputOptions.create.noise.type; + if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) { + inputDescriptor.createNoiseMean = inputOptions.create.noise.mean; + } else { + throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean); } - inputDescriptor.createNoiseSigma = 30; - if (is.defined(inputOptions.create.noise.sigma)) { - if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) { - inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma; - } else { - throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma); - } + if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) { + inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma; + } else { + throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma); } } else if (is.defined(inputOptions.create.background)) { if (!is.inRange(inputOptions.create.channels, 3, 4)) { throw is.invalidParameterError('create.channels', 'number between 3 and 4', inputOptions.create.channels); } - inputDescriptor.createBackground = this._getBackgroundColourOption(inputOptions.create.background); + const background = color(inputOptions.create.background); + inputDescriptor.createBackground = [ + background.red(), + background.green(), + background.blue(), + Math.round(background.alpha() * 255) + ]; } else { throw new Error('Expected valid noise or background to create a new input image'); } @@ -407,17 +296,17 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { } } if (is.defined(inputOptions.text.width)) { - if (is.integer(inputOptions.text.width) && inputOptions.text.width > 0) { + if (is.number(inputOptions.text.width)) { inputDescriptor.textWidth = inputOptions.text.width; } else { - throw is.invalidParameterError('text.width', 'positive integer', inputOptions.text.width); + throw is.invalidParameterError('text.textWidth', 'number', inputOptions.text.width); } } if (is.defined(inputOptions.text.height)) { - if (is.integer(inputOptions.text.height) && inputOptions.text.height > 0) { + if (is.number(inputOptions.text.height)) { inputDescriptor.textHeight = inputOptions.text.height; } else { - throw is.invalidParameterError('text.height', 'positive integer', inputOptions.text.height); + throw is.invalidParameterError('text.height', 'number', inputOptions.text.height); } } if (is.defined(inputOptions.text.align)) { @@ -435,10 +324,10 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { } } if (is.defined(inputOptions.text.dpi)) { - if (is.integer(inputOptions.text.dpi) && is.inRange(inputOptions.text.dpi, 1, 1000000)) { + if (is.number(inputOptions.text.dpi) && is.inRange(inputOptions.text.dpi, 1, 100000)) { inputDescriptor.textDpi = inputOptions.text.dpi; } else { - throw is.invalidParameterError('text.dpi', 'integer between 1 and 1000000', inputOptions.text.dpi); + throw is.invalidParameterError('text.dpi', 'number between 1 and 100000', inputOptions.text.dpi); } } if (is.defined(inputOptions.text.rgba)) { @@ -449,17 +338,17 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { } } if (is.defined(inputOptions.text.spacing)) { - if (is.integer(inputOptions.text.spacing) && is.inRange(inputOptions.text.spacing, -1000000, 1000000)) { + if (is.number(inputOptions.text.spacing)) { inputDescriptor.textSpacing = inputOptions.text.spacing; } else { - throw is.invalidParameterError('text.spacing', 'integer between -1000000 and 1000000', inputOptions.text.spacing); + throw is.invalidParameterError('text.spacing', 'number', inputOptions.text.spacing); } } if (is.defined(inputOptions.text.wrap)) { - if (is.string(inputOptions.text.wrap) && is.inArray(inputOptions.text.wrap, ['word', 'char', 'word-char', 'none'])) { + if (is.string(inputOptions.text.wrap) && is.inArray(inputOptions.text.wrap, ['word', 'char', 'wordChar', 'none'])) { inputDescriptor.textWrap = inputOptions.text.wrap; } else { - throw is.invalidParameterError('text.wrap', 'one of: word, char, word-char, none', inputOptions.text.wrap); + throw is.invalidParameterError('text.wrap', 'one of: word, char, wordChar, none', inputOptions.text.wrap); } } delete inputDescriptor.buffer; @@ -467,53 +356,8 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw new Error('Expected a valid string to create an image with text.'); } } - // Join images together - if (is.defined(inputOptions.join)) { - if (is.defined(this.options.join)) { - if (is.defined(inputOptions.join.animated)) { - if (is.bool(inputOptions.join.animated)) { - inputDescriptor.joinAnimated = inputOptions.join.animated; - } else { - throw is.invalidParameterError('join.animated', 'boolean', inputOptions.join.animated); - } - } - if (is.defined(inputOptions.join.across)) { - if (is.integer(inputOptions.join.across) && is.inRange(inputOptions.join.across, 1, 1000000)) { - inputDescriptor.joinAcross = inputOptions.join.across; - } else { - throw is.invalidParameterError('join.across', 'integer between 1 and 100000', inputOptions.join.across); - } - } - if (is.defined(inputOptions.join.shim)) { - if (is.integer(inputOptions.join.shim) && is.inRange(inputOptions.join.shim, 0, 1000000)) { - inputDescriptor.joinShim = inputOptions.join.shim; - } else { - throw is.invalidParameterError('join.shim', 'integer between 0 and 100000', inputOptions.join.shim); - } - } - if (is.defined(inputOptions.join.background)) { - inputDescriptor.joinBackground = this._getBackgroundColourOption(inputOptions.join.background); - } - if (is.defined(inputOptions.join.halign)) { - if (is.string(inputOptions.join.halign) && is.string(this.constructor.align[inputOptions.join.halign])) { - inputDescriptor.joinHalign = this.constructor.align[inputOptions.join.halign]; - } else { - throw is.invalidParameterError('join.halign', 'valid alignment', inputOptions.join.halign); - } - } - if (is.defined(inputOptions.join.valign)) { - if (is.string(inputOptions.join.valign) && is.string(this.constructor.align[inputOptions.join.valign])) { - inputDescriptor.joinValign = this.constructor.align[inputOptions.join.valign]; - } else { - throw is.invalidParameterError('join.valign', 'valid alignment', inputOptions.join.valign); - } - } - } else { - throw new Error('Expected input to be an array of images to join'); - } - } } else if (is.defined(inputOptions)) { - throw new Error(`Invalid input options ${inputOptions}`); + throw new Error('Invalid input options ' + inputOptions); } return inputDescriptor; } @@ -525,8 +369,10 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { * @param {string} encoding - unused * @param {Function} callback */ -function _write (chunk, _encoding, callback) { +function _write (chunk, encoding, callback) { + /* istanbul ignore else */ if (Array.isArray(this.options.input.buffer)) { + /* istanbul ignore else */ if (is.buffer(chunk)) { if (this.options.input.buffer.length === 0) { this.on('finish', () => { @@ -570,7 +416,7 @@ function _isStreamInput () { * such as resize or rotate. * * Dimensions in the response will respect the `page` and `pages` properties of the - * {@link /api-constructor/ constructor parameters}. + * {@link /api-constructor#parameters|constructor parameters}. * * A `Promise` is returned when `callback` is not provided. * @@ -578,22 +424,21 @@ function _isStreamInput () { * - `size`: Total size of image in bytes, for Stream and Buffer input only * - `width`: Number of pixels wide (EXIF orientation is not taken into consideration, see example below) * - `height`: Number of pixels high (EXIF orientation is not taken into consideration, see example below) - * - `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://www.libvips.org/API/current/enum.Interpretation.html) + * - `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://www.libvips.org/API/current/VipsImage.html#VipsInterpretation) * - `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK - * - `depth`: Name of pixel depth format e.g. `uchar`, `char`, `ushort`, `float` [...](https://www.libvips.org/API/current/enum.BandFormat.html) + * - `depth`: Name of pixel depth format e.g. `uchar`, `char`, `ushort`, `float` [...](https://www.libvips.org/API/current/VipsImage.html#VipsBandFormat) * - `density`: Number of pixels per inch (DPI), if present * - `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK * - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan - * - `isPalette`: Boolean indicating whether the image is palette-based (GIF, PNG). - * - `bitsPerSample`: Number of bits per sample for each channel (GIF, PNG, HEIF). * - `pages`: Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP * - `pageHeight`: Number of pixels high each page in a multi-page image will be. + * - `paletteBitDepth`: Bit depth of palette-based image (GIF, PNG). * - `loop`: Number of times to loop an animated image, zero refers to a continuous loop. * - `delay`: Delay in ms between each page in an animated image, provided as an array of integers. * - `pagePrimary`: Number of the primary page in a HEIF image * - `levels`: Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide * - `subifds`: Number of Sub Image File Directories in an OME-TIFF image - * - `background`: Default background colour, if present, for PNG (bKGD) and GIF images + * - `background`: Default background colour, if present, for PNG (bKGD) and GIF images, either an RGB Object or a single greyscale value * - `compression`: The encoder used to compress an HEIF file, `av1` (AVIF) or `hevc` (HEIC) * - `resolutionUnit`: The unit of resolution (density), either `inch` or `cm`, if present * - `hasProfile`: Boolean indicating the presence of an embedded ICC profile @@ -603,10 +448,8 @@ function _isStreamInput () { * - `icc`: Buffer containing raw [ICC](https://www.npmjs.com/package/icc) profile data, if present * - `iptc`: Buffer containing raw IPTC data, if present * - `xmp`: Buffer containing raw XMP data, if present - * - `xmpAsString`: String containing XMP data, if valid UTF-8. * - `tifftagPhotoshop`: Buffer containing raw TIFFTAG_PHOTOSHOP data, if present * - `formatMagick`: String containing format for images loaded via *magick - * - `comments`: Array of keyword/text pairs representing PNG text blocks, if present. * * @example * const metadata = await sharp(input).metadata(); @@ -626,35 +469,28 @@ function _isStreamInput () { * }); * * @example - * // Get dimensions taking EXIF Orientation into account. - * const { autoOrient } = await sharp(input).metadata(); - * const { width, height } = autoOrient; + * // Based on EXIF rotation metadata, get the right-side-up width and height: + * + * const size = getNormalSize(await sharp(input).metadata()); + * + * function getNormalSize({ width, height, orientation }) { + * return (orientation || 0) >= 5 + * ? { width: height, height: width } + * : { width, height }; + * } * * @param {Function} [callback] - called with the arguments `(err, metadata)` * @returns {Promise|Sharp} */ function metadata (callback) { - const stack = Error(); if (is.fn(callback)) { if (this._isStreamInput()) { this.on('finish', () => { this._flattenBufferIn(); - sharp.metadata(this.options, (err, metadata) => { - if (err) { - callback(is.nativeError(err, stack)); - } else { - callback(null, metadata); - } - }); + sharp.metadata(this.options, callback); }); } else { - sharp.metadata(this.options, (err, metadata) => { - if (err) { - callback(is.nativeError(err, stack)); - } else { - callback(null, metadata); - } - }); + sharp.metadata(this.options, callback); } return this; } else { @@ -664,7 +500,7 @@ function metadata (callback) { this._flattenBufferIn(); sharp.metadata(this.options, (err, metadata) => { if (err) { - reject(is.nativeError(err, stack)); + reject(err); } else { resolve(metadata); } @@ -680,7 +516,7 @@ function metadata (callback) { return new Promise((resolve, reject) => { sharp.metadata(this.options, (err, metadata) => { if (err) { - reject(is.nativeError(err, stack)); + reject(err); } else { resolve(metadata); } @@ -736,27 +572,14 @@ function metadata (callback) { * @returns {Promise} */ function stats (callback) { - const stack = Error(); if (is.fn(callback)) { if (this._isStreamInput()) { this.on('finish', () => { this._flattenBufferIn(); - sharp.stats(this.options, (err, stats) => { - if (err) { - callback(is.nativeError(err, stack)); - } else { - callback(null, stats); - } - }); + sharp.stats(this.options, callback); }); } else { - sharp.stats(this.options, (err, stats) => { - if (err) { - callback(is.nativeError(err, stack)); - } else { - callback(null, stats); - } - }); + sharp.stats(this.options, callback); } return this; } else { @@ -766,7 +589,7 @@ function stats (callback) { this._flattenBufferIn(); sharp.stats(this.options, (err, stats) => { if (err) { - reject(is.nativeError(err, stack)); + reject(err); } else { resolve(stats); } @@ -777,7 +600,7 @@ function stats (callback) { return new Promise((resolve, reject) => { sharp.stats(this.options, (err, stats) => { if (err) { - reject(is.nativeError(err, stack)); + reject(err); } else { resolve(stats); } @@ -789,10 +612,9 @@ function stats (callback) { /** * Decorate the Sharp prototype with input-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Object.assign(Sharp.prototype, { // Private _inputOptionsFromObject, diff --git a/backend/node_modules/sharp/lib/is.js b/backend/node_modules/sharp/lib/is.js index 3ac9a1a3..364778f2 100644 --- a/backend/node_modules/sharp/lib/is.js +++ b/backend/node_modules/sharp/lib/is.js @@ -1,49 +1,61 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; /** * Is this value defined and not null? * @private */ -const defined = (val) => typeof val !== 'undefined' && val !== null; +const defined = function (val) { + return typeof val !== 'undefined' && val !== null; +}; /** * Is this value an object? * @private */ -const object = (val) => typeof val === 'object'; +const object = function (val) { + return typeof val === 'object'; +}; /** * Is this value a plain object? * @private */ -const plainObject = (val) => Object.prototype.toString.call(val) === '[object Object]'; +const plainObject = function (val) { + return Object.prototype.toString.call(val) === '[object Object]'; +}; /** * Is this value a function? * @private */ -const fn = (val) => typeof val === 'function'; +const fn = function (val) { + return typeof val === 'function'; +}; /** * Is this value a boolean? * @private */ -const bool = (val) => typeof val === 'boolean'; +const bool = function (val) { + return typeof val === 'boolean'; +}; /** * Is this value a Buffer object? * @private */ -const buffer = (val) => val instanceof Buffer; +const buffer = function (val) { + return val instanceof Buffer; +}; /** * Is this value a typed array object?. E.g. Uint8Array or Uint8ClampedArray? * @private */ -const typedArray = (val) => { +const typedArray = function (val) { if (defined(val)) { switch (val.constructor) { case Uint8Array: @@ -66,37 +78,49 @@ const typedArray = (val) => { * Is this value an ArrayBuffer object? * @private */ -const arrayBuffer = (val) => val instanceof ArrayBuffer; +const arrayBuffer = function (val) { + return val instanceof ArrayBuffer; +}; /** * Is this value a non-empty string? * @private */ -const string = (val) => typeof val === 'string' && val.length > 0; +const string = function (val) { + return typeof val === 'string' && val.length > 0; +}; /** * Is this value a real number? * @private */ -const number = (val) => typeof val === 'number' && !Number.isNaN(val); +const number = function (val) { + return typeof val === 'number' && !Number.isNaN(val); +}; /** * Is this value an integer? * @private */ -const integer = (val) => Number.isInteger(val); +const integer = function (val) { + return Number.isInteger(val); +}; /** * Is this value within an inclusive given range? * @private */ -const inRange = (val, min, max) => val >= min && val <= max; +const inRange = function (val, min, max) { + return val >= min && val <= max; +}; /** * Is this value within the elements of an array? * @private */ -const inArray = (val, list) => list.includes(val); +const inArray = function (val, list) { + return list.includes(val); +}; /** * Create an Error with a message relating to an invalid parameter. @@ -107,37 +131,25 @@ const inArray = (val, list) => list.includes(val); * @returns {Error} Containing the formatted message. * @private */ -const invalidParameterError = (name, expected, actual) => new Error( +const invalidParameterError = function (name, expected, actual) { + return new Error( `Expected ${expected} for ${name} but received ${actual} of type ${typeof actual}` ); - -/** - * Ensures an Error from C++ contains a JS stack. - * - * @param {Error} native - Error with message from C++. - * @param {Error} context - Error with stack from JS. - * @returns {Error} Error with message and stack. - * @private - */ -const nativeError = (native, context) => { - context.message = native.message; - return context; }; module.exports = { - defined, - object, - plainObject, - fn, - bool, - buffer, - typedArray, - arrayBuffer, - string, - number, - integer, - inRange, - inArray, - invalidParameterError, - nativeError + defined: defined, + object: object, + plainObject: plainObject, + fn: fn, + bool: bool, + buffer: buffer, + typedArray: typedArray, + arrayBuffer: arrayBuffer, + string: string, + number: number, + integer: integer, + inRange: inRange, + inArray: inArray, + invalidParameterError: invalidParameterError }; diff --git a/backend/node_modules/sharp/lib/libvips.js b/backend/node_modules/sharp/lib/libvips.js index 881dc5c1..ca001d95 100644 --- a/backend/node_modules/sharp/lib/libvips.js +++ b/backend/node_modules/sharp/lib/libvips.js @@ -1,34 +1,55 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -const { spawnSync } = require('node:child_process'); -const { createHash } = require('node:crypto'); +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const spawnSync = require('child_process').spawnSync; const semverCoerce = require('semver/functions/coerce'); const semverGreaterThanOrEqualTo = require('semver/functions/gte'); -const semverSatisfies = require('semver/functions/satisfies'); -const detectLibc = require('detect-libc'); -const { config, engines, optionalDependencies } = require('../package.json'); +const platform = require('./platform'); +const { config } = require('../package.json'); -/* node:coverage ignore next */ -const minimumLibvipsVersionLabelled = process.env.npm_package_config_libvips || config.libvips; +const env = process.env; +const minimumLibvipsVersionLabelled = env.npm_package_config_libvips || /* istanbul ignore next */ + config.libvips; const minimumLibvipsVersion = semverCoerce(minimumLibvipsVersionLabelled).version; -const prebuiltPlatforms = [ - 'darwin-arm64', 'darwin-x64', - 'linux-arm', 'linux-arm64', 'linux-ppc64', 'linux-riscv64', 'linux-s390x', 'linux-x64', - 'linuxmusl-arm64', 'linuxmusl-x64', - 'win32-arm64', 'win32-ia32', 'win32-x64' -]; - const spawnSyncOptions = { encoding: 'utf8', shell: true }; -const log = (item) => { +const vendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platform()); + +const mkdirSync = function (dirPath) { + try { + fs.mkdirSync(dirPath, { recursive: true }); + } catch (err) { + /* istanbul ignore next */ + if (err.code !== 'EEXIST') { + throw err; + } + } +}; + +const cachePath = function () { + const npmCachePath = env.npm_config_cache || /* istanbul ignore next */ + (env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(os.homedir(), '.npm')); + mkdirSync(npmCachePath); + const libvipsCachePath = path.join(npmCachePath, '_libvips'); + mkdirSync(libvipsCachePath); + return libvipsCachePath; +}; + +const integrity = function (platformAndArch) { + return env[`npm_package_config_integrity_${platformAndArch.replace('-', '_')}`] || config.integrity[platformAndArch]; +}; + +const log = function (item) { if (item instanceof Error) { console.error(`sharp: Installation error: ${item.message}`); } else { @@ -36,69 +57,8 @@ const log = (item) => { } }; -/* node:coverage ignore next */ -const runtimeLibc = () => detectLibc.isNonGlibcLinuxSync() ? detectLibc.familySync() : ''; - -const runtimePlatformArch = () => `${process.platform}${runtimeLibc()}-${process.arch}`; - -const buildPlatformArch = () => { - /* node:coverage ignore next 3 */ - if (isEmscripten()) { - return 'wasm32'; - } - const { npm_config_arch, npm_config_platform, npm_config_libc } = process.env; - const libc = typeof npm_config_libc === 'string' ? npm_config_libc : runtimeLibc(); - return `${npm_config_platform || process.platform}${libc}-${npm_config_arch || process.arch}`; -}; - -const buildSharpLibvipsIncludeDir = () => { - try { - return require(`@img/sharp-libvips-dev-${buildPlatformArch()}/include`); - } catch { - /* node:coverage ignore next 5 */ - try { - return require('@img/sharp-libvips-dev/include'); - } catch {} - } - return ''; -}; - -const buildSharpLibvipsCPlusPlusDir = () => { - /* node:coverage ignore next 4 */ - try { - return require('@img/sharp-libvips-dev/cplusplus'); - } catch {} - return ''; -}; - -const buildSharpLibvipsLibDir = () => { - try { - return require(`@img/sharp-libvips-dev-${buildPlatformArch()}/lib`); - } catch { - /* node:coverage ignore next 5 */ - try { - return require(`@img/sharp-libvips-${buildPlatformArch()}/lib`); - } catch {} - } - return ''; -}; - -/* node:coverage disable */ - -const isUnsupportedNodeRuntime = () => { - if (process.release?.name === 'node' && process.versions) { - if (!semverSatisfies(process.versions.node, engines.node)) { - return { found: process.versions.node, expected: engines.node }; - } - } -}; - -const isEmscripten = () => { - const { CC } = process.env; - return Boolean(CC?.endsWith('/emcc')); -}; - -const isRosetta = () => { +const isRosetta = function () { + /* istanbul ignore next */ if (process.platform === 'darwin' && process.arch === 'x64') { const translated = spawnSync('sysctl sysctl.proc_translated', spawnSyncOptions).stdout; return (translated || '').trim() === 'sysctl.proc_translated: 1'; @@ -106,56 +66,41 @@ const isRosetta = () => { return false; }; -/* node:coverage enable */ - -const sha512 = (s) => createHash('sha512').update(s).digest('hex'); - -const yarnLocator = () => { - try { - const identHash = sha512(`imgsharp-libvips-${buildPlatformArch()}`); - const npmVersion = semverCoerce(optionalDependencies[`@img/sharp-libvips-${buildPlatformArch()}`], { - includePrerelease: true - }).version; - return sha512(`${identHash}npm:${npmVersion}`).slice(0, 10); - } catch {} - return ''; -}; - -/* node:coverage disable */ - -const spawnRebuild = () => - spawnSync(`node-gyp rebuild --directory=src ${isEmscripten() ? '--nodedir=emscripten' : ''}`, { - ...spawnSyncOptions, - stdio: 'inherit' - }).status; - -const globalLibvipsVersion = () => { +const globalLibvipsVersion = function () { if (process.platform !== 'win32') { const globalLibvipsVersion = spawnSync('pkg-config --modversion vips-cpp', { ...spawnSyncOptions, env: { - ...process.env, + ...env, PKG_CONFIG_PATH: pkgConfigPath() } }).stdout; + /* istanbul ignore next */ return (globalLibvipsVersion || '').trim(); } else { return ''; } }; -/* node:coverage enable */ +const hasVendoredLibvips = function () { + return fs.existsSync(vendorPath); +}; -const pkgConfigPath = () => { +/* istanbul ignore next */ +const removeVendoredLibvips = function () { + fs.rmSync(vendorPath, { recursive: true, maxRetries: 3, force: true }); +}; + +/* istanbul ignore next */ +const pkgConfigPath = function () { if (process.platform !== 'win32') { - /* node:coverage ignore next 4 */ const brewPkgConfigPath = spawnSync( 'which brew >/dev/null 2>&1 && brew environment --plain | grep PKG_CONFIG_LIBDIR | cut -d" " -f2', spawnSyncOptions ).stdout || ''; return [ brewPkgConfigPath.trim(), - process.env.PKG_CONFIG_PATH, + env.PKG_CONFIG_PATH, '/usr/local/lib/pkgconfig', '/usr/lib/pkgconfig', '/usr/local/libdata/pkgconfig', @@ -166,42 +111,30 @@ const pkgConfigPath = () => { } }; -const skipSearch = (status, reason, logger) => { - if (logger) { - logger(`Detected ${reason}, skipping search for globally-installed libvips`); +const useGlobalLibvips = function () { + if (Boolean(env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) { + return false; } - return status; -}; - -const useGlobalLibvips = (logger) => { - if (Boolean(process.env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) { - return skipSearch(false, 'SHARP_IGNORE_GLOBAL_LIBVIPS', logger); - } - if (Boolean(process.env.SHARP_FORCE_GLOBAL_LIBVIPS) === true) { - return skipSearch(true, 'SHARP_FORCE_GLOBAL_LIBVIPS', logger); - } - /* node:coverage ignore next 3 */ + /* istanbul ignore next */ if (isRosetta()) { - return skipSearch(false, 'Rosetta', logger); + log('Detected Rosetta, skipping search for globally-installed libvips'); + return false; } const globalVipsVersion = globalLibvipsVersion(); - /* node:coverage ignore next */ - return !!globalVipsVersion && semverGreaterThanOrEqualTo(globalVipsVersion, minimumLibvipsVersion); + return !!globalVipsVersion && /* istanbul ignore next */ + semverGreaterThanOrEqualTo(globalVipsVersion, minimumLibvipsVersion); }; module.exports = { minimumLibvipsVersion, - prebuiltPlatforms, - buildPlatformArch, - buildSharpLibvipsIncludeDir, - buildSharpLibvipsCPlusPlusDir, - buildSharpLibvipsLibDir, - isUnsupportedNodeRuntime, - runtimePlatformArch, + minimumLibvipsVersionLabelled, + cachePath, + integrity, log, - yarnLocator, - spawnRebuild, globalLibvipsVersion, + hasVendoredLibvips, + removeVendoredLibvips, pkgConfigPath, - useGlobalLibvips + useGlobalLibvips, + mkdirSync }; diff --git a/backend/node_modules/sharp/lib/operation.js b/backend/node_modules/sharp/lib/operation.js index ebbf54e9..199a0ebe 100644 --- a/backend/node_modules/sharp/lib/operation.js +++ b/backend/node_modules/sharp/lib/operation.js @@ -1,42 +1,44 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 +'use strict'; + +const color = require('color'); const is = require('./is'); /** - * How accurate an operation should be. - * @member - * @private - */ -const vipsPrecision = { - integer: 'integer', - float: 'float', - approximate: 'approximate' -}; - -/** - * Rotate the output image. + * Rotate the output image by either an explicit angle + * or auto-orient based on the EXIF `Orientation` tag. * - * The provided angle is converted to a valid positive degree rotation. + * If an angle is provided, it is converted to a valid positive degree rotation. * For example, `-450` will produce a 270 degree rotation. * * When rotating by an angle other than a multiple of 90, * the background colour can be provided with the `background` option. * - * For backwards compatibility, if no angle is provided, `.autoOrient()` will be called. + * If no angle is provided, it is determined from the EXIF data. + * Mirroring is supported and may infer the use of a flip operation. * - * Only one rotation can occur per pipeline (aside from an initial call without - * arguments to orient via EXIF data). Previous calls to `rotate` in the same - * pipeline will be ignored. + * The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any. * - * Multi-page images can only be rotated by 180 degrees. + * Only one rotation can occur per pipeline. + * Previous calls to `rotate` in the same pipeline will be ignored. * * Method order is important when rotating, resizing and/or extracting regions, * for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`. * * @example + * const pipeline = sharp() + * .rotate() + * .resize(null, 200) + * .toBuffer(function (err, outputBuffer, info) { + * // outputBuffer contains 200px high JPEG image data, + * // auto-rotated using EXIF Orientation tag + * // info.width and info.height contain the dimensions of the resized image + * }); + * readableStream.pipe(pipeline); + * + * @example * const rotateThenResize = await sharp(input) * .rotate(90) * .resize({ width: 16, height: 8, fit: 'fill' }) @@ -53,20 +55,23 @@ const vipsPrecision = { * @throws {Error} Invalid parameters */ function rotate (angle, options) { - if (!is.defined(angle)) { - return this.autoOrient(); - } - if (this.options.angle || this.options.rotationAngle) { + if (this.options.useExifOrientation || this.options.angle || this.options.rotationAngle) { this.options.debuglog('ignoring previous rotate options'); - this.options.angle = 0; - this.options.rotationAngle = 0; } - if (is.integer(angle) && !(angle % 90)) { + if (!is.defined(angle)) { + this.options.useExifOrientation = true; + } else if (is.integer(angle) && !(angle % 90)) { this.options.angle = angle; } else if (is.number(angle)) { this.options.rotationAngle = angle; if (is.object(options) && options.background) { - this._setBackgroundColourOption('rotationBackground', options.background); + const backgroundColour = color(options.background); + this.options.rotationBackground = [ + backgroundColour.red(), + backgroundColour.green(), + backgroundColour.blue(), + Math.round(backgroundColour.alpha() * 255) + ]; } } else { throw is.invalidParameterError('angle', 'numeric', angle); @@ -74,34 +79,6 @@ function rotate (angle, options) { return this; } -/** - * Auto-orient based on the EXIF `Orientation` tag, then remove the tag. - * Mirroring is supported and may infer the use of a flip operation. - * - * Previous or subsequent use of `rotate(angle)` and either `flip()` or `flop()` - * will logically occur after auto-orientation, regardless of call order. - * - * @example - * const output = await sharp(input).autoOrient().toBuffer(); - * - * @example - * const pipeline = sharp() - * .autoOrient() - * .resize(null, 200) - * .toBuffer(function (err, outputBuffer, info) { - * // outputBuffer contains 200px high JPEG image data, - * // auto-oriented using EXIF Orientation tag - * // info.width and info.height contain the dimensions of the resized image - * }); - * readableStream.pipe(pipeline); - * - * @returns {Sharp} - */ -function autoOrient () { - this.options.input.autoOrient = true; - return this; -} - /** * Mirror the image vertically (up-down) about the x-axis. * This always occurs before rotation, if any. @@ -138,7 +115,7 @@ function flop (flop) { * Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any. * * You must provide an array of length 4 or a 2x2 affine transformation matrix. - * By default, new pixels are filled with a black background. You can provide a background colour with the `background` option. + * By default, new pixels are filled with a black background. You can provide a background color with the `background` option. * A particular interpolator may also be specified. Set the `interpolator` option to an attribute of the `sharp.interpolators` Object e.g. `sharp.interpolators.nohalo`. * * In the case of a 2x2 matrix, the transform is: @@ -239,7 +216,7 @@ function affine (matrix, options) { * When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space. * Fine-grained control over the level of sharpening in "flat" (m1) and "jagged" (m2) areas is available. * - * See {@link https://www.libvips.org/API/current/method.Image.sharpen.html libvips sharpen} operation. + * See {@link https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen|libvips sharpen} operation. * * @example * const data = await sharp(input).sharpen().toBuffer(); @@ -388,104 +365,30 @@ function median (size) { * .blur(5) * .toBuffer(); * - * @param {Object|number|Boolean} [options] - * @param {number} [options.sigma] a value between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`. - * @param {string} [options.precision='integer'] How accurate the operation should be, one of: integer, float, approximate. - * @param {number} [options.minAmplitude=0.2] A value between 0.001 and 1. A smaller value will generate a larger, more accurate mask. + * @param {number} [sigma] a value between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`. * @returns {Sharp} * @throws {Error} Invalid parameters */ -function blur (options) { - let sigma; - if (is.number(options)) { - sigma = options; - } else if (is.plainObject(options)) { - if (!is.number(options.sigma)) { - throw is.invalidParameterError('options.sigma', 'number between 0.3 and 1000', sigma); - } - sigma = options.sigma; - if ('precision' in options) { - if (is.string(vipsPrecision[options.precision])) { - this.options.precision = vipsPrecision[options.precision]; - } else { - throw is.invalidParameterError('precision', 'one of: integer, float, approximate', options.precision); - } - } - if ('minAmplitude' in options) { - if (is.number(options.minAmplitude) && is.inRange(options.minAmplitude, 0.001, 1)) { - this.options.minAmpl = options.minAmplitude; - } else { - throw is.invalidParameterError('minAmplitude', 'number between 0.001 and 1', options.minAmplitude); - } - } - } - - if (!is.defined(options)) { +function blur (sigma) { + if (!is.defined(sigma)) { // No arguments: default to mild blur this.options.blurSigma = -1; - } else if (is.bool(options)) { + } else if (is.bool(sigma)) { // Boolean argument: apply mild blur? - this.options.blurSigma = options ? -1 : 0; + this.options.blurSigma = sigma ? -1 : 0; } else if (is.number(sigma) && is.inRange(sigma, 0.3, 1000)) { // Numeric argument: specific sigma this.options.blurSigma = sigma; } else { throw is.invalidParameterError('sigma', 'number between 0.3 and 1000', sigma); } - - return this; -} - -/** - * Expand foreground objects using the dilate morphological operator. - * - * @example - * const output = await sharp(input) - * .dilate() - * .toBuffer(); - * - * @param {Number} [width=1] dilation width in pixels. - * @returns {Sharp} - * @throws {Error} Invalid parameters - */ -function dilate (width) { - if (!is.defined(width)) { - this.options.dilateWidth = 1; - } else if (is.integer(width) && width > 0) { - this.options.dilateWidth = width; - } else { - throw is.invalidParameterError('dilate', 'positive integer', dilate); - } - return this; -} - -/** - * Shrink foreground objects using the erode morphological operator. - * - * @example - * const output = await sharp(input) - * .erode() - * .toBuffer(); - * - * @param {Number} [width=1] erosion width in pixels. - * @returns {Sharp} - * @throws {Error} Invalid parameters - */ -function erode (width) { - if (!is.defined(width)) { - this.options.erodeWidth = 1; - } else if (is.integer(width) && width > 0) { - this.options.erodeWidth = width; - } else { - throw is.invalidParameterError('erode', 'positive integer', erode); - } return this; } /** * Merge alpha transparency channel, if any, with a background, then remove the alpha channel. * - * See also {@link /api-channel#removealpha removeAlpha}. + * See also {@link /api-channel#removealpha|removeAlpha}. * * @example * await sharp(rgbaInput) @@ -660,7 +563,7 @@ function normalize (options) { /** * Perform contrast limiting adaptive histogram equalization - * {@link https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE CLAHE}. + * {@link https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE|CLAHE}. * * This will, in general, enhance the clarity of the image by bringing out darker details. * @@ -742,7 +645,9 @@ function convolve (kernel) { } // Default scale is sum of kernel values if (!is.integer(kernel.scale)) { - kernel.scale = kernel.kernel.reduce((a, b) => a + b, 0); + kernel.scale = kernel.kernel.reduce(function (a, b) { + return a + b; + }, 0); } // Clip scale to a minimum value of 1 if (kernel.scale < 1) { @@ -880,22 +785,24 @@ function linear (a, b) { * // With this example input, a sepia filter has been applied * }); * - * @param {Array>} inputMatrix - 3x3 or 4x4 Recombination matrix + * @param {Array>} inputMatrix - 3x3 Recombination matrix * @returns {Sharp} * @throws {Error} Invalid parameters */ function recomb (inputMatrix) { - if (!Array.isArray(inputMatrix)) { - throw is.invalidParameterError('inputMatrix', 'array', inputMatrix); + if (!Array.isArray(inputMatrix) || inputMatrix.length !== 3 || + inputMatrix[0].length !== 3 || + inputMatrix[1].length !== 3 || + inputMatrix[2].length !== 3 + ) { + // must pass in a kernel + throw new Error('Invalid recombination matrix'); } - if (inputMatrix.length !== 3 && inputMatrix.length !== 4) { - throw is.invalidParameterError('inputMatrix', '3x3 or 4x4 array', inputMatrix.length); - } - const recombMatrix = inputMatrix.flat().map(Number); - if (recombMatrix.length !== 9 && recombMatrix.length !== 16) { - throw is.invalidParameterError('inputMatrix', 'cardinality of 9 or 16', recombMatrix.length); - } - this.options.recombMatrix = recombMatrix; + this.options.recombMatrix = [ + inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2], + inputMatrix[1][0], inputMatrix[1][1], inputMatrix[1][2], + inputMatrix[2][0], inputMatrix[2][1], inputMatrix[2][2] + ].map(Number); return this; } @@ -984,19 +891,15 @@ function modulate (options) { /** * Decorate the Sharp prototype with operation-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Object.assign(Sharp.prototype, { - autoOrient, rotate, flip, flop, affine, sharpen, - erode, - dilate, median, blur, flatten, diff --git a/backend/node_modules/sharp/lib/output.js b/backend/node_modules/sharp/lib/output.js index 27a6ac47..dcf78e01 100644 --- a/backend/node_modules/sharp/lib/output.js +++ b/backend/node_modules/sharp/lib/output.js @@ -1,9 +1,9 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -const path = require('node:path'); +'use strict'; + +const path = require('path'); const is = require('./is'); const sharp = require('./sharp'); @@ -43,7 +43,7 @@ const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math * Note that raw pixel data is only supported for buffer output. * * By default all metadata will be removed, which includes EXIF-based orientation. - * See {@link #withmetadata withMetadata} for control over this. + * See {@link #withmetadata|withMetadata} for control over this. * * The caller is responsible for ensuring directory structures and permissions exist. * @@ -65,7 +65,6 @@ const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math * `channels` and `premultiplied` (indicating if premultiplication was used). * When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`. * When using the attention crop strategy also contains `attentionX` and `attentionY`, the focal point of the cropped region. - * Animated output will also contain `pageHeight` and `pages`. * May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text. * @returns {Promise} - when no callback is provided * @throws {Error} Invalid parameters @@ -87,8 +86,7 @@ function toFile (fileOut, callback) { } } else { this.options.fileOut = fileOut; - const stack = Error(); - return this._pipeline(callback, stack); + return this._pipeline(callback); } return this; } @@ -97,12 +95,12 @@ function toFile (fileOut, callback) { * Write output to a Buffer. * JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported. * - * Use {@link #toformat toFormat} or one of the format-specific functions such as {@link #jpeg jpeg}, {@link #png png} etc. to set the output format. + * Use {@link #toformat|toFormat} or one of the format-specific functions such as {@link jpeg}, {@link png} etc. to set the output format. * * If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output. * * By default all metadata will be removed, which includes EXIF-based orientation. - * See {@link #withmetadata withMetadata} for control over this. + * See {@link #withmetadata|withMetadata} for control over this. * * `callback`, if present, gets three arguments `(err, data, info)` where: * - `err` is an error, if any. @@ -110,7 +108,6 @@ function toFile (fileOut, callback) { * - `info` contains the output image `format`, `size` (bytes), `width`, `height`, * `channels` and `premultiplied` (indicating if premultiplication was used). * When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`. - * Animated output will also contain `pageHeight` and `pages`. * May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text. * * A `Promise` is returned when `callback` is not provided. @@ -160,248 +157,41 @@ function toBuffer (options, callback) { this.options.resolveWithObject = false; } this.options.fileOut = ''; - const stack = Error(); - return this._pipeline(is.fn(options) ? options : callback, stack); + return this._pipeline(is.fn(options) ? options : callback); } /** - * Keep all EXIF metadata from the input image in the output image. + * Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. + * This will also convert to and add a web-friendly sRGB ICC profile if appropriate, + * unless a custom output profile is provided. + * + * The default behaviour, when `withMetadata` is not used, is to convert to the device-independent + * sRGB colour space and strip all metadata, including the removal of any ICC profile. * * EXIF metadata is unsupported for TIFF output. * - * @since 0.33.0 - * * @example - * const outputWithExif = await sharp(inputWithExif) - * .keepExif() - * .toBuffer(); - * - * @returns {Sharp} - */ -function keepExif () { - this.options.keepMetadata |= 0b00001; - return this; -} - -/** - * Set EXIF metadata in the output image, ignoring any EXIF in the input image. - * - * @since 0.33.0 - * - * @example - * const dataWithExif = await sharp(input) - * .withExif({ - * IFD0: { - * Copyright: 'The National Gallery' - * }, - * IFD3: { - * GPSLatitudeRef: 'N', - * GPSLatitude: '51/1 30/1 3230/100', - * GPSLongitudeRef: 'W', - * GPSLongitude: '0/1 7/1 4366/100' - * } - * }) - * .toBuffer(); - * - * @param {Object>} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. - * @returns {Sharp} - * @throws {Error} Invalid parameters - */ -function withExif (exif) { - if (is.object(exif)) { - for (const [ifd, entries] of Object.entries(exif)) { - if (is.object(entries)) { - for (const [k, v] of Object.entries(entries)) { - if (is.string(v)) { - this.options.withExif[`exif-${ifd.toLowerCase()}-${k}`] = v; - } else { - throw is.invalidParameterError(`${ifd}.${k}`, 'string', v); - } - } - } else { - throw is.invalidParameterError(ifd, 'object', entries); - } - } - } else { - throw is.invalidParameterError('exif', 'object', exif); - } - this.options.withExifMerge = false; - return this.keepExif(); -} - -/** - * Update EXIF metadata from the input image in the output image. - * - * @since 0.33.0 - * - * @example - * const dataWithMergedExif = await sharp(inputWithExif) - * .withExifMerge({ - * IFD0: { - * Copyright: 'The National Gallery' - * } - * }) - * .toBuffer(); - * - * @param {Object>} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. - * @returns {Sharp} - * @throws {Error} Invalid parameters - */ -function withExifMerge (exif) { - this.withExif(exif); - this.options.withExifMerge = true; - return this; -} - -/** - * Keep ICC profile from the input image in the output image. - * - * When input and output colour spaces differ, use with {@link /api-colour/#tocolourspace toColourspace} and optionally {@link /api-colour/#pipelinecolourspace pipelineColourspace}. - * - * @since 0.33.0 - * - * @example - * const outputWithIccProfile = await sharp(inputWithIccProfile) - * .keepIccProfile() - * .toBuffer(); - * - * @example - * const cmykOutputWithIccProfile = await sharp(cmykInputWithIccProfile) - * .pipelineColourspace('cmyk') - * .toColourspace('cmyk') - * .keepIccProfile() - * .toBuffer(); - * - * @returns {Sharp} - */ -function keepIccProfile () { - this.options.keepMetadata |= 0b01000; - return this; -} - -/** - * Transform using an ICC profile and attach to the output image. - * - * This can either be an absolute filesystem path or - * built-in profile name (`srgb`, `p3`, `cmyk`). - * - * @since 0.33.0 - * - * @example - * const outputWithP3 = await sharp(input) - * .withIccProfile('p3') - * .toBuffer(); - * - * @param {string} icc - Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk). - * @param {Object} [options] - * @param {number} [options.attach=true] Should the ICC profile be included in the output image metadata? - * @returns {Sharp} - * @throws {Error} Invalid parameters - */ -function withIccProfile (icc, options) { - if (is.string(icc)) { - this.options.withIccProfile = icc; - } else { - throw is.invalidParameterError('icc', 'string', icc); - } - this.keepIccProfile(); - if (is.object(options)) { - if (is.defined(options.attach)) { - if (is.bool(options.attach)) { - if (!options.attach) { - this.options.keepMetadata &= ~0b01000; - } - } else { - throw is.invalidParameterError('attach', 'boolean', options.attach); - } - } - } - return this; -} - -/** - * Keep XMP metadata from the input image in the output image. - * - * @since 0.34.3 - * - * @example - * const outputWithXmp = await sharp(inputWithXmp) - * .keepXmp() - * .toBuffer(); - * - * @returns {Sharp} - */ -function keepXmp () { - this.options.keepMetadata |= 0b00010; - return this; -} - -/** - * Set XMP metadata in the output image. - * - * Supported by PNG, JPEG, WebP, and TIFF output. - * - * @since 0.34.3 - * - * @example - * const xmpString = ` - * - * - * - * - * John Doe - * - * - * `; - * - * const data = await sharp(input) - * .withXmp(xmpString) - * .toBuffer(); - * - * @param {string} xmp String containing XMP metadata to be embedded in the output image. - * @returns {Sharp} - * @throws {Error} Invalid parameters - */ -function withXmp (xmp) { - if (is.string(xmp) && xmp.length > 0) { - this.options.withXmp = xmp; - this.options.keepMetadata |= 0b00010; - } else { - throw is.invalidParameterError('xmp', 'non-empty string', xmp); - } - return this; -} - -/** - * Keep all metadata (EXIF, ICC, XMP, IPTC) from the input image in the output image. - * - * The default behaviour, when `keepMetadata` is not used, is to convert to the device-independent - * sRGB colour space and strip all metadata, including the removal of any ICC profile. - * - * @since 0.33.0 - * - * @example - * const outputWithMetadata = await sharp(inputWithMetadata) - * .keepMetadata() - * .toBuffer(); - * - * @returns {Sharp} - */ -function keepMetadata () { - this.options.keepMetadata = 0b11111; - return this; -} - -/** - * Keep most metadata (EXIF, XMP, IPTC) from the input image in the output image. - * - * This will also convert to and add a web-friendly sRGB ICC profile if appropriate. - * - * Allows orientation and density to be set or updated. - * - * @example - * const outputSrgbWithMetadata = await sharp(inputRgbWithMetadata) + * sharp('input.jpg') * .withMetadata() + * .toFile('output-with-metadata.jpg') + * .then(info => { ... }); + * + * @example + * // Set output EXIF metadata + * const data = await sharp(input) + * .withMetadata({ + * exif: { + * IFD0: { + * Copyright: 'The National Gallery' + * }, + * IFD3: { + * GPSLatitudeRef: 'N', + * GPSLatitude: '51/1 30/1 3230/100', + * GPSLongitudeRef: 'W', + * GPSLongitude: '0/1 7/1 4366/100' + * } + * } + * }) * .toBuffer(); * * @example @@ -411,14 +201,15 @@ function keepMetadata () { * .toBuffer(); * * @param {Object} [options] - * @param {number} [options.orientation] Used to update the EXIF `Orientation` tag, integer between 1 and 8. + * @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag. + * @param {string} [options.icc='srgb'] Filesystem path to output ICC profile, relative to `process.cwd()`, defaults to built-in sRGB. + * @param {Object} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. * @param {number} [options.density] Number of pixels per inch (DPI). * @returns {Sharp} * @throws {Error} Invalid parameters */ function withMetadata (options) { - this.keepMetadata(); - this.withIccProfile('srgb'); + this.options.withMetadata = is.bool(options) ? options : true; if (is.object(options)) { if (is.defined(options.orientation)) { if (is.integer(options.orientation) && is.inRange(options.orientation, 1, 8)) { @@ -435,10 +226,30 @@ function withMetadata (options) { } } if (is.defined(options.icc)) { - this.withIccProfile(options.icc); + if (is.string(options.icc)) { + this.options.withMetadataIcc = options.icc; + } else { + throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc); + } } if (is.defined(options.exif)) { - this.withExifMerge(options.exif); + if (is.object(options.exif)) { + for (const [ifd, entries] of Object.entries(options.exif)) { + if (is.object(entries)) { + for (const [k, v] of Object.entries(entries)) { + if (is.string(v)) { + this.options.withMetadataStrs[`exif-${ifd.toLowerCase()}-${k}`] = v; + } else { + throw is.invalidParameterError(`exif.${ifd}.${k}`, 'string', v); + } + } + } else { + throw is.invalidParameterError(`exif.${ifd}`, 'object', entries); + } + } + } else { + throw is.invalidParameterError('exif', 'object', options.exif); + } } } return this; @@ -566,14 +377,10 @@ function jpeg (options) { /** * Use these PNG options for output image. * - * By default, PNG output is full colour at 8 bits per pixel. - * + * By default, PNG output is full colour at 8 or 16 bits per pixel. * Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel. * Set `palette` to `true` for slower, indexed PNG output. * - * For 16 bits per pixel output, convert to `rgb16` via - * {@link /api-colour/#tocolourspace toColourspace}. - * * @example * // Convert any input to full colour PNG output * const data = await sharp(input) @@ -586,13 +393,6 @@ function jpeg (options) { * .png({ palette: true }) * .toBuffer(); * - * @example - * // Output 16 bits per pixel RGB(A) - * const data = await sharp(input) - * .toColourspace('rgb16') - * .png() - * .toBuffer(); - * * @param {Object} [options] * @param {boolean} [options.progressive=false] - use progressive (interlace) scan * @param {number} [options.compressionLevel=6] - zlib compression level, 0 (fastest, largest) to 9 (slowest, smallest) @@ -683,7 +483,6 @@ function png (options) { * @param {boolean} [options.lossless=false] - use lossless compression mode * @param {boolean} [options.nearLossless=false] - use near_lossless compression mode * @param {boolean} [options.smartSubsample=false] - use high quality chroma subsampling - * @param {boolean} [options.smartDeblock=false] - auto-adjust the deblocking filter, can improve low contrast edges (slow) * @param {string} [options.preset='default'] - named preset for preprocessing/filtering, one of: default, photo, picture, drawing, icon, text * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 6 (slowest) * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation @@ -719,9 +518,6 @@ function webp (options) { if (is.defined(options.smartSubsample)) { this._setBooleanOption('webpSmartSubsample', options.smartSubsample); } - if (is.defined(options.smartDeblock)) { - this._setBooleanOption('webpSmartDeblock', options.smartDeblock); - } if (is.defined(options.preset)) { if (is.string(options.preset) && is.inArray(options.preset, ['default', 'photo', 'picture', 'drawing', 'icon', 'text'])) { this.options.webpPreset = options.preset; @@ -789,7 +585,6 @@ function webp (options) { * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most) * @param {number} [options.interFrameMaxError=0] - maximum inter-frame error for transparency, between 0 (lossless) and 32 * @param {number} [options.interPaletteMaxError=3] - maximum inter-palette error for palette reuse, between 0 and 256 - * @param {boolean} [options.keepDuplicateFrames=false] - keep duplicate frames in the output instead of combining them * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds) * @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format @@ -840,24 +635,18 @@ function gif (options) { throw is.invalidParameterError('interPaletteMaxError', 'number between 0.0 and 256.0', options.interPaletteMaxError); } } - if (is.defined(options.keepDuplicateFrames)) { - if (is.bool(options.keepDuplicateFrames)) { - this._setBooleanOption('gifKeepDuplicateFrames', options.keepDuplicateFrames); - } else { - throw is.invalidParameterError('keepDuplicateFrames', 'boolean', options.keepDuplicateFrames); - } - } } trySetAnimationOptions(options, this.options); return this._updateFormatOut('gif', options); } +/* istanbul ignore next */ /** * Use these JP2 options for output image. * * Requires libvips compiled with support for OpenJPEG. * The prebuilt binaries do not include this - see - * {@link /install#custom-libvips installing a custom libvips}. + * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}. * * @example * // Convert any input to lossless JP2 output @@ -886,7 +675,6 @@ function gif (options) { * @throws {Error} Invalid options */ function jp2 (options) { - /* node:coverage ignore next 41 */ if (!this.constructor.format.jp2k.output.buffer) { throw errJp2Save(); } @@ -966,7 +754,7 @@ function trySetAnimationOptions (source, target) { /** * Use these TIFF options for output image. * - * The `density` can be set in pixels/inch via {@link #withmetadata withMetadata} + * The `density` can be set in pixels/inch via {@link #withmetadata|withMetadata} * instead of providing `xres` and `yres` in pixels/mm. * * @example @@ -983,7 +771,6 @@ function trySetAnimationOptions (source, target) { * @param {number} [options.quality=80] - quality, integer 1-100 * @param {boolean} [options.force=true] - force TIFF output, otherwise attempt to use input format * @param {string} [options.compression='jpeg'] - compression options: none, jpeg, deflate, packbits, ccittfax4, lzw, webp, zstd, jp2k - * @param {boolean} [options.bigtiff=false] - use BigTIFF variant (has no effect when compression is none) * @param {string} [options.predictor='horizontal'] - compression predictor options: none, horizontal, float * @param {boolean} [options.pyramid=false] - write an image pyramid * @param {boolean} [options.tile=false] - write a tiled tiff @@ -993,7 +780,6 @@ function trySetAnimationOptions (source, target) { * @param {number} [options.yres=1.0] - vertical resolution in pixels/mm * @param {string} [options.resolutionUnit='inch'] - resolution unit options: inch, cm * @param {number} [options.bitdepth=8] - reduce bitdepth to 1, 2 or 4 bit - * @param {boolean} [options.miniswhite=false] - write 1-bit images as miniswhite * @returns {Sharp} * @throws {Error} Invalid options */ @@ -1031,10 +817,6 @@ function tiff (options) { throw is.invalidParameterError('tileHeight', 'integer greater than zero', options.tileHeight); } } - // miniswhite - if (is.defined(options.miniswhite)) { - this._setBooleanOption('tiffMiniswhite', options.miniswhite); - } // pyramid if (is.defined(options.pyramid)) { this._setBooleanOption('tiffPyramid', options.pyramid); @@ -1062,10 +844,6 @@ function tiff (options) { throw is.invalidParameterError('compression', 'one of: none, jpeg, deflate, packbits, ccittfax4, lzw, webp, zstd, jp2k', options.compression); } } - // bigtiff - if (is.defined(options.bigtiff)) { - this._setBooleanOption('tiffBigtiff', options.bigtiff); - } // predictor if (is.defined(options.predictor)) { if (is.string(options.predictor) && is.inArray(options.predictor, ['none', 'horizontal', 'float'])) { @@ -1089,11 +867,10 @@ function tiff (options) { /** * Use these AVIF options for output image. * - * AVIF image sequences are not supported. - * Prebuilt binaries support a bitdepth of 8 only. + * Whilst it is possible to create AVIF images smaller than 16x16 pixels, + * most web browsers do not display these properly. * - * This feature is experimental on the Windows ARM64 platform - * and requires a CPU with ARM64v8.4 or later. + * AVIF image sequences are not supported. * * @example * const data = await sharp(input) @@ -1112,7 +889,6 @@ function tiff (options) { * @param {boolean} [options.lossless=false] - use lossless compression * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest) * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling - * @param {number} [options.bitdepth=8] - set bitdepth to 8, 10 or 12 bit * @returns {Sharp} * @throws {Error} Invalid options */ @@ -1133,23 +909,17 @@ function avif (options) { * * @since 0.23.0 * - * @param {Object} options - output options - * @param {string} options.compression - compression format: av1, hevc + * @param {Object} [options] - output options * @param {number} [options.quality=50] - quality, integer 1-100 + * @param {string} [options.compression='av1'] - compression format: av1, hevc * @param {boolean} [options.lossless=false] - use lossless compression * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest) * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling - * @param {number} [options.bitdepth=8] - set bitdepth to 8, 10 or 12 bit * @returns {Sharp} * @throws {Error} Invalid options */ function heif (options) { if (is.object(options)) { - if (is.string(options.compression) && is.inArray(options.compression, ['av1', 'hevc'])) { - this.options.heifCompression = options.compression; - } else { - throw is.invalidParameterError('compression', 'one of: av1, hevc', options.compression); - } if (is.defined(options.quality)) { if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) { this.options.heifQuality = options.quality; @@ -1164,6 +934,13 @@ function heif (options) { throw is.invalidParameterError('lossless', 'boolean', options.lossless); } } + if (is.defined(options.compression)) { + if (is.string(options.compression) && is.inArray(options.compression, ['av1', 'hevc'])) { + this.options.heifCompression = options.compression; + } else { + throw is.invalidParameterError('compression', 'one of: av1, hevc', options.compression); + } + } if (is.defined(options.effort)) { if (is.integer(options.effort) && is.inRange(options.effort, 0, 9)) { this.options.heifEffort = options.effort; @@ -1178,18 +955,6 @@ function heif (options) { throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling); } } - if (is.defined(options.bitdepth)) { - if (is.integer(options.bitdepth) && is.inArray(options.bitdepth, [8, 10, 12])) { - if (options.bitdepth !== 8 && this.constructor.versions.heif) { - throw is.invalidParameterError('bitdepth when using prebuilt binaries', 8, options.bitdepth); - } - this.options.heifBitdepth = options.bitdepth; - } else { - throw is.invalidParameterError('bitdepth', '8, 10 or 12', options.bitdepth); - } - } - } else { - throw is.invalidParameterError('options', 'Object', options); } return this._updateFormatOut('heif', options); } @@ -1201,7 +966,9 @@ function heif (options) { * * Requires libvips compiled with support for libjxl. * The prebuilt binaries do not include this - see - * {@link /install/#custom-libvips installing a custom libvips}. + * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}. + * + * Image metadata (EXIF, XMP) is unsupported. * * @since 0.31.3 * @@ -1210,9 +977,7 @@ function heif (options) { * @param {number} [options.quality] - calculate `distance` based on JPEG-like quality, between 1 and 100, overrides distance if specified * @param {number} [options.decodingTier=0] - target decode speed tier, between 0 (highest quality) and 4 (lowest quality) * @param {boolean} [options.lossless=false] - use lossless compression - * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 9 (slowest) - * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation - * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds) + * @param {number} [options.effort=7] - CPU effort, between 3 (fastest) and 9 (slowest) * @returns {Sharp} * @throws {Error} Invalid options */ @@ -1249,14 +1014,13 @@ function jxl (options) { } } if (is.defined(options.effort)) { - if (is.integer(options.effort) && is.inRange(options.effort, 1, 9)) { + if (is.integer(options.effort) && is.inRange(options.effort, 3, 9)) { this.options.jxlEffort = options.effort; } else { - throw is.invalidParameterError('effort', 'integer between 1 and 9', options.effort); + throw is.invalidParameterError('effort', 'integer between 3 and 9', options.effort); } } } - trySetAnimationOptions(options, this.options); return this._updateFormatOut('jxl', options); } @@ -1308,6 +1072,10 @@ function raw (options) { * * The container will be set to `zip` when the output is a Buffer or Stream, otherwise it will default to `fs`. * + * Requires libvips compiled with support for libgsf. + * The prebuilt binaries do not include this - see + * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}. + * * @example * sharp('input.tiff') * .png() @@ -1336,7 +1104,7 @@ function raw (options) { * @param {number} [options.angle=0] tile angle of rotation, must be a multiple of 90. * @param {string|Object} [options.background={r: 255, g: 255, b: 255, alpha: 1}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to white without transparency. * @param {string} [options.depth] how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout. - * @param {number} [options.skipBlanks=-1] Threshold to skip tile generation. Range is 0-255 for 8-bit images, 0-65535 for 16-bit images. Default is 5 for `google` layout, -1 (no skip) otherwise. + * @param {number} [options.skipBlanks=-1] threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images * @param {string} [options.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file). * @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `iiif3`, `zoomify` or `google`. * @param {boolean} [options.centre=false] centre image in tile. @@ -1514,10 +1282,10 @@ function _setBooleanOption (key, val) { * @private */ function _read () { + /* istanbul ignore else */ if (!this.options.streamOut) { this.options.streamOut = true; - const stack = Error(); - this._pipeline(undefined, stack); + this._pipeline(); } } @@ -1526,30 +1294,18 @@ function _read () { * Supports callback, stream and promise variants * @private */ -function _pipeline (callback, stack) { +function _pipeline (callback) { if (typeof callback === 'function') { // output=file/buffer if (this._isStreamInput()) { // output=file/buffer, input=stream this.on('finish', () => { this._flattenBufferIn(); - sharp.pipeline(this.options, (err, data, info) => { - if (err) { - callback(is.nativeError(err, stack)); - } else { - callback(null, data, info); - } - }); + sharp.pipeline(this.options, callback); }); } else { // output=file/buffer, input=file/buffer - sharp.pipeline(this.options, (err, data, info) => { - if (err) { - callback(is.nativeError(err, stack)); - } else { - callback(null, data, info); - } - }); + sharp.pipeline(this.options, callback); } return this; } else if (this.options.streamOut) { @@ -1560,7 +1316,7 @@ function _pipeline (callback, stack) { this._flattenBufferIn(); sharp.pipeline(this.options, (err, data, info) => { if (err) { - this.emit('error', is.nativeError(err, stack)); + this.emit('error', err); } else { this.emit('info', info); this.push(data); @@ -1576,7 +1332,7 @@ function _pipeline (callback, stack) { // output=stream, input=file/buffer sharp.pipeline(this.options, (err, data, info) => { if (err) { - this.emit('error', is.nativeError(err, stack)); + this.emit('error', err); } else { this.emit('info', info); this.push(data); @@ -1595,7 +1351,7 @@ function _pipeline (callback, stack) { this._flattenBufferIn(); sharp.pipeline(this.options, (err, data, info) => { if (err) { - reject(is.nativeError(err, stack)); + reject(err); } else { if (this.options.resolveWithObject) { resolve({ data, info }); @@ -1611,10 +1367,10 @@ function _pipeline (callback, stack) { return new Promise((resolve, reject) => { sharp.pipeline(this.options, (err, data, info) => { if (err) { - reject(is.nativeError(err, stack)); + reject(err); } else { if (this.options.resolveWithObject) { - resolve({ data, info }); + resolve({ data: data, info: info }); } else { resolve(data); } @@ -1627,22 +1383,13 @@ function _pipeline (callback, stack) { /** * Decorate the Sharp prototype with output-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Object.assign(Sharp.prototype, { // Public toFile, toBuffer, - keepExif, - withExif, - withExifMerge, - keepIccProfile, - withIccProfile, - keepXmp, - withXmp, - keepMetadata, withMetadata, toFormat, jpeg, diff --git a/backend/node_modules/sharp/lib/platform.js b/backend/node_modules/sharp/lib/platform.js new file mode 100644 index 00000000..71454e3e --- /dev/null +++ b/backend/node_modules/sharp/lib/platform.js @@ -0,0 +1,30 @@ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; + +const detectLibc = require('detect-libc'); + +const env = process.env; + +module.exports = function () { + const arch = env.npm_config_arch || process.arch; + const platform = env.npm_config_platform || process.platform; + const libc = process.env.npm_config_libc || + /* istanbul ignore next */ + (detectLibc.isNonGlibcLinuxSync() ? detectLibc.familySync() : ''); + const libcId = platform !== 'linux' || libc === detectLibc.GLIBC ? '' : libc; + + const platformId = [`${platform}${libcId}`]; + + if (arch === 'arm') { + const fallback = process.versions.electron ? '7' : '6'; + platformId.push(`armv${env.npm_config_arm_version || process.config.variables.arm_version || fallback}`); + } else if (arch === 'arm64') { + platformId.push(`arm64v${env.npm_config_arm_version || '8'}`); + } else { + platformId.push(arch); + } + + return platformId.join('-'); +}; diff --git a/backend/node_modules/sharp/lib/resize.js b/backend/node_modules/sharp/lib/resize.js index 544fbba3..6677dba7 100644 --- a/backend/node_modules/sharp/lib/resize.js +++ b/backend/node_modules/sharp/lib/resize.js @@ -1,7 +1,7 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; const is = require('./is'); @@ -68,13 +68,10 @@ const strategy = { */ const kernel = { nearest: 'nearest', - linear: 'linear', cubic: 'cubic', mitchell: 'mitchell', lanczos2: 'lanczos2', - lanczos3: 'lanczos3', - mks2013: 'mks2013', - mks2021: 'mks2021' + lanczos3: 'lanczos3' }; /** @@ -107,7 +104,7 @@ const mapFitToCanvas = { * @private */ function isRotationExpected (options) { - return (options.angle % 360) !== 0 || options.rotationAngle !== 0; + return (options.angle % 360) !== 0 || options.useExifOrientation === true || options.rotationAngle !== 0; } /** @@ -129,7 +126,7 @@ function isResizeExpected (options) { * * Some of these values are based on the [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) CSS property. * - * Examples of various values for the fit property when resizing + * Examples of various values for the fit property when resizing * * When using a **fit** of `cover` or `contain`, the default **position** is `centre`. Other options are: * - `sharp.position`: `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`. @@ -138,23 +135,17 @@ function isResizeExpected (options) { * * Some of these values are based on the [object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) CSS property. * - * The strategy-based approach initially resizes so one dimension is at its target length + * The experimental strategy-based approach resizes so one dimension is at its target length * then repeatedly ranks edge regions, discarding the edge with the lowest score based on the selected strategy. * - `entropy`: focus on the region with the highest [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29). * - `attention`: focus on the region with the highest luminance frequency, colour saturation and presence of skin tones. * - * Possible downsizing kernels are: + * Possible interpolation kernels are: * - `nearest`: Use [nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation). - * - `linear`: Use a [triangle filter](https://en.wikipedia.org/wiki/Triangular_function). * - `cubic`: Use a [Catmull-Rom spline](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline). * - `mitchell`: Use a [Mitchell-Netravali spline](https://www.cs.utexas.edu/~fussell/courses/cs384g-fall2013/lectures/mitchell/Mitchell.pdf). * - `lanczos2`: Use a [Lanczos kernel](https://en.wikipedia.org/wiki/Lanczos_resampling#Lanczos_kernel) with `a=2`. * - `lanczos3`: Use a Lanczos kernel with `a=3` (the default). - * - `mks2013`: Use a [Magic Kernel Sharp](https://johncostella.com/magic/mks.pdf) 2013 kernel, as adopted by Facebook. - * - `mks2021`: Use a Magic Kernel Sharp 2021 kernel, with more accurate (reduced) sharpening than the 2013 version. - * - * When upsampling, these kernels map to `nearest`, `linear` and `cubic` interpolators. - * Downsampling kernels without a matching upsampling interpolator map to `cubic`. * * Only one resize can occur per pipeline. * Previous calls to `resize` in the same pipeline will be ignored. @@ -248,7 +239,7 @@ function isResizeExpected (options) { * @param {String} [options.fit='cover'] - How the image should be resized/cropped to fit the target dimension(s), one of `cover`, `contain`, `fill`, `inside` or `outside`. * @param {String} [options.position='centre'] - A position, gravity or strategy to use when `fit` is `cover` or `contain`. * @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when `fit` is `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency. - * @param {String} [options.kernel='lanczos3'] - The kernel to use for image reduction and the inferred interpolator to use for upsampling. Use the `fastShrinkOnLoad` option to control kernel vs shrink-on-load. + * @param {String} [options.kernel='lanczos3'] - The kernel to use for image reduction. Use the `fastShrinkOnLoad` option to control kernel vs shrink-on-load. * @param {Boolean} [options.withoutEnlargement=false] - Do not scale up if the width *or* height are already less than the target dimensions, equivalent to GraphicsMagick's `>` geometry option. This may result in output dimensions smaller than the target dimensions. * @param {Boolean} [options.withoutReduction=false] - Do not scale down if the width *or* height are already greater than the target dimensions, equivalent to GraphicsMagick's `<` geometry option. This may still result in a crop to reach the target dimensions. * @param {Boolean} [options.fastShrinkOnLoad=true] - Take greater advantage of the JPEG and WebP shrink-on-load feature, which can lead to a slight moiré pattern or round-down of an auto-scaled dimension. @@ -259,9 +250,6 @@ function resize (widthOrOptions, height, options) { if (isResizeExpected(this.options)) { this.options.debuglog('ignoring previous resize options'); } - if (this.options.widthPost !== -1) { - this.options.debuglog('operation order will be: extract, resize, extract'); - } if (is.defined(widthOrOptions)) { if (is.object(widthOrOptions) && !is.defined(options)) { options = widthOrOptions; @@ -343,7 +331,7 @@ function resize (widthOrOptions, height, options) { } } if (isRotationExpected(this.options) && isResizeExpected(this.options)) { - this.options.rotateBefore = true; + this.options.rotateBeforePreExtract = true; } return this; } @@ -449,7 +437,7 @@ function extend (extend) { * * - Use `extract` before `resize` for pre-resize extraction. * - Use `extract` after `resize` for post-resize extraction. - * - Use `extract` twice and `resize` once for extract-then-resize-then-extract in a fixed operation order. + * - Use `extract` before and after for both. * * @example * sharp(input) @@ -490,12 +478,9 @@ function extract (options) { // Ensure existing rotation occurs before pre-resize extraction if (isRotationExpected(this.options) && !isResizeExpected(this.options)) { if (this.options.widthPre === -1 || this.options.widthPost === -1) { - this.options.rotateBefore = true; + this.options.rotateBeforePreExtract = true; } } - if (this.options.input.autoOrient) { - this.options.orientBefore = true; - } return this; } @@ -506,80 +491,82 @@ function extract (options) { * * If the result of this operation would trim an image to nothing then no change is made. * - * The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` properties. + * The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`, + * will contain `trimOffsetLeft` and `trimOffsetTop` properties. * * @example * // Trim pixels with a colour similar to that of the top-left pixel. - * await sharp(input) + * sharp(input) * .trim() - * .toFile(output); - * + * .toFile(output, function(err, info) { + * ... + * }); * @example * // Trim pixels with the exact same colour as that of the top-left pixel. - * await sharp(input) - * .trim({ - * threshold: 0 - * }) - * .toFile(output); - * + * sharp(input) + * .trim(0) + * .toFile(output, function(err, info) { + * ... + * }); * @example - * // Assume input is line art and trim only pixels with a similar colour to red. - * const output = await sharp(input) - * .trim({ - * background: "#FF0000", - * lineArt: true - * }) - * .toBuffer(); - * + * // Trim only pixels with a similar colour to red. + * sharp(input) + * .trim("#FF0000") + * .toFile(output, function(err, info) { + * ... + * }); * @example * // Trim all "yellow-ish" pixels, being more lenient with the higher threshold. - * const output = await sharp(input) + * sharp(input) * .trim({ * background: "yellow", * threshold: 42, * }) - * .toBuffer(); + * .toFile(output, function(err, info) { + * ... + * }); * - * @param {Object} [options] - * @param {string|Object} [options.background='top-left pixel'] - Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel. - * @param {number} [options.threshold=10] - Allowed difference from the above colour, a positive number. - * @param {boolean} [options.lineArt=false] - Does the input more closely resemble line art (e.g. vector) rather than being photographic? + * @param {string|number|Object} trim - the specific background colour to trim, the threshold for doing so or an Object with both. + * @param {string|Object} [trim.background='top-left pixel'] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel. + * @param {number} [trim.threshold=10] - the allowed difference from the above colour, a positive number. * @returns {Sharp} * @throws {Error} Invalid parameters */ -function trim (options) { - this.options.trimThreshold = 10; - if (is.defined(options)) { - if (is.object(options)) { - if (is.defined(options.background)) { - this._setBackgroundColourOption('trimBackground', options.background); - } - if (is.defined(options.threshold)) { - if (is.number(options.threshold) && options.threshold >= 0) { - this.options.trimThreshold = options.threshold; - } else { - throw is.invalidParameterError('threshold', 'positive number', options.threshold); - } - } - if (is.defined(options.lineArt)) { - this._setBooleanOption('trimLineArt', options.lineArt); - } +function trim (trim) { + if (!is.defined(trim)) { + this.options.trimThreshold = 10; + } else if (is.string(trim)) { + this._setBackgroundColourOption('trimBackground', trim); + this.options.trimThreshold = 10; + } else if (is.number(trim)) { + if (trim >= 0) { + this.options.trimThreshold = trim; } else { - throw is.invalidParameterError('trim', 'object', options); + throw is.invalidParameterError('threshold', 'positive number', trim); } + } else if (is.object(trim)) { + this._setBackgroundColourOption('trimBackground', trim.background); + if (!is.defined(trim.threshold)) { + this.options.trimThreshold = 10; + } else if (is.number(trim.threshold) && trim.threshold >= 0) { + this.options.trimThreshold = trim.threshold; + } else { + throw is.invalidParameterError('threshold', 'positive number', trim); + } + } else { + throw is.invalidParameterError('trim', 'string, number or object', trim); } if (isRotationExpected(this.options)) { - this.options.rotateBefore = true; + this.options.rotateBeforePreExtract = true; } return this; } /** * Decorate the Sharp prototype with resize-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Object.assign(Sharp.prototype, { resize, extend, diff --git a/backend/node_modules/sharp/lib/sharp.js b/backend/node_modules/sharp/lib/sharp.js index 1081c931..a41e83d0 100644 --- a/backend/node_modules/sharp/lib/sharp.js +++ b/backend/node_modules/sharp/lib/sharp.js @@ -1,121 +1,38 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -// Inspects the runtime environment and exports the relevant sharp.node binary +'use strict'; -const { familySync, versionSync } = require('detect-libc'); +const platformAndArch = require('./platform')(); -const { runtimePlatformArch, isUnsupportedNodeRuntime, prebuiltPlatforms, minimumLibvipsVersion } = require('./libvips'); -const runtimePlatform = runtimePlatformArch(); - -const paths = [ - `../src/build/Release/sharp-${runtimePlatform}.node`, - '../src/build/Release/sharp-wasm32.node', - `@img/sharp-${runtimePlatform}/sharp.node`, - '@img/sharp-wasm32/sharp.node' -]; - -/* node:coverage disable */ - -let path, sharp; -const errors = []; -for (path of paths) { - try { - sharp = require(path); - break; - } catch (err) { - errors.push(err); - } -} - -if (sharp && path.startsWith('@img/sharp-linux-x64') && !sharp._isUsingX64V2()) { - const err = new Error('Prebuilt binaries for linux-x64 require v2 microarchitecture'); - err.code = 'Unsupported CPU'; - errors.push(err); - sharp = null; -} - -if (sharp) { - module.exports = sharp; -} else { - const [isLinux, isMacOs, isWindows] = ['linux', 'darwin', 'win32'].map(os => runtimePlatform.startsWith(os)); - - const help = [`Could not load the "sharp" module using the ${runtimePlatform} runtime`]; - errors.forEach(err => { - if (err.code !== 'MODULE_NOT_FOUND') { - help.push(`${err.code}: ${err.message}`); - } - }); - const messages = errors.map(err => err.message).join(' '); - help.push('Possible solutions:'); - // Common error messages - if (isUnsupportedNodeRuntime()) { - const { found, expected } = isUnsupportedNodeRuntime(); - help.push( - '- Please upgrade Node.js:', - ` Found ${found}`, - ` Requires ${expected}` - ); - } else if (prebuiltPlatforms.includes(runtimePlatform)) { - const [os, cpu] = runtimePlatform.split('-'); - const libc = os.endsWith('musl') ? ' --libc=musl' : ''; - help.push( - '- Ensure optional dependencies can be installed:', - ' npm install --include=optional sharp', - '- Ensure your package manager supports multi-platform installation:', - ' See https://sharp.pixelplumbing.com/install#cross-platform', - '- Add platform-specific dependencies:', - ` npm install --os=${os.replace('musl', '')}${libc} --cpu=${cpu} sharp` - ); +/* istanbul ignore next */ +try { + module.exports = require(`../build/Release/sharp-${platformAndArch}.node`); +} catch (err) { + // Bail early if bindings aren't available + const help = ['', 'Something went wrong installing the "sharp" module', '', err.message, '', 'Possible solutions:']; + if (/dylib/.test(err.message) && /Incompatible library version/.test(err.message)) { + help.push('- Update Homebrew: "brew update && brew upgrade vips"'); } else { + const [platform, arch] = platformAndArch.split('-'); + if (platform === 'linux' && /Module did not self-register/.test(err.message)) { + help.push('- Using worker threads? See https://sharp.pixelplumbing.com/install#worker-threads'); + } help.push( - `- Manually install libvips >= ${minimumLibvipsVersion}`, - '- Add experimental WebAssembly-based dependencies:', - ' npm install --cpu=wasm32 sharp', - ' npm install @img/sharp-wasm32' - ); - } - if (isLinux && /(symbol not found|CXXABI_)/i.test(messages)) { - try { - const { config } = require(`@img/sharp-libvips-${runtimePlatform}/package`); - const libcFound = `${familySync()} ${versionSync()}`; - const libcRequires = `${config.musl ? 'musl' : 'glibc'} ${config.musl || config.glibc}`; - help.push( - '- Update your OS:', - ` Found ${libcFound}`, - ` Requires ${libcRequires}` - ); - } catch (_errEngines) {} - } - if (isLinux && /\/snap\/core[0-9]{2}/.test(messages)) { - help.push( - '- Remove the Node.js Snap, which does not support native modules', - ' snap remove node' - ); - } - if (isMacOs && /Incompatible library version/.test(messages)) { - help.push( - '- Update Homebrew:', - ' brew update && brew upgrade vips' - ); - } - if (errors.some(err => err.code === 'ERR_DLOPEN_DISABLED')) { - help.push('- Run Node.js without using the --no-addons flag'); - } - // Link to installation docs - if (isWindows && /The specified procedure could not be found/.test(messages)) { - help.push( - '- Using the canvas package on Windows?', - ' See https://sharp.pixelplumbing.com/install#canvas-and-windows', - '- Check for outdated versions of sharp in the dependency tree:', - ' npm ls sharp' + '- Install with verbose logging and look for errors: "npm install --ignore-scripts=false --foreground-scripts --verbose sharp"', + `- Install for the current ${platformAndArch} runtime: "npm install --platform=${platform} --arch=${arch} sharp"` ); } help.push( - '- Consult the installation documentation:', - ' See https://sharp.pixelplumbing.com/install' + '- Consult the installation documentation: https://sharp.pixelplumbing.com/install' ); + // Check loaded + if (process.platform === 'win32' || /symbol/.test(err.message)) { + const loadedModule = Object.keys(require.cache).find((i) => /[\\/]build[\\/]Release[\\/]sharp(.*)\.node$/.test(i)); + if (loadedModule) { + const [, loadedPackage] = loadedModule.match(/node_modules[\\/]([^\\/]+)[\\/]/); + help.push(`- Ensure the version of sharp aligns with the ${loadedPackage} package: "npm ls sharp"`); + } + } throw new Error(help.join('\n')); } diff --git a/backend/node_modules/sharp/lib/utility.js b/backend/node_modules/sharp/lib/utility.js index c0ad39f8..8b1a42e5 100644 --- a/backend/node_modules/sharp/lib/utility.js +++ b/backend/node_modules/sharp/lib/utility.js @@ -1,18 +1,17 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -const events = require('node:events'); +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const events = require('events'); const detectLibc = require('detect-libc'); const is = require('./is'); -const { runtimePlatformArch } = require('./libvips'); +const platformAndArch = require('./platform')(); const sharp = require('./sharp'); -const runtimePlatform = runtimePlatformArch(); -const libvipsVersion = sharp.libvipsVersion(); - /** * An Object containing nested boolean values representing the available input and output formats/methods. * @member @@ -47,40 +46,33 @@ const interpolators = { }; /** - * An Object containing the version numbers of sharp, libvips - * and (when using prebuilt binaries) its dependencies. - * + * An Object containing the version numbers of sharp, libvips and its dependencies. * @member * @example * console.log(sharp.versions); */ let versions = { - vips: libvipsVersion.semver + vips: sharp.libvipsVersion() }; -/* node:coverage ignore next 15 */ -if (!libvipsVersion.isGlobal) { - if (!libvipsVersion.isWasm) { - try { - versions = require(`@img/sharp-${runtimePlatform}/versions`); - } catch (_) { - try { - versions = require(`@img/sharp-libvips-${runtimePlatform}/versions`); - } catch (_) {} - } - } else { - try { - versions = require('@img/sharp-wasm32/versions'); - } catch (_) {} - } -} +try { + versions = require(`../vendor/${versions.vips}/${platformAndArch}/versions.json`); +} catch (_err) { /* ignore */ } versions.sharp = require('../package.json').version; -/* node:coverage ignore next 5 */ -if (versions.heif && format.heif) { - // Prebuilt binaries provide AV1 - format.heif.input.fileSuffix = ['.avif']; - format.heif.output.alias = ['avif']; -} +/** + * An Object containing the platform and architecture + * of the current and installed vendored binaries. + * @member + * @example + * console.log(sharp.vendor); + */ +const vendor = { + current: platformAndArch, + installed: [] +}; +try { + vendor.installed = fs.readdirSync(path.join(__dirname, `../vendor/${versions.vips}`)); +} catch (_err) { /* ignore */ } /** * Gets or, when options are provided, sets the limits of _libvips'_ operation cache. @@ -135,9 +127,15 @@ cache(true); * e.g. libaom manages its own 4 threads when encoding AVIF images, * and these are independent of the value set here. * - * :::note - * Further {@link /performance/ control over performance} is available. - * ::: + * The maximum number of images that sharp can process in parallel + * is controlled by libuv's `UV_THREADPOOL_SIZE` environment variable, + * which defaults to 4. + * + * https://nodejs.org/api/cli.html#uv_threadpool_sizesize + * + * For example, by default, a machine with 8 CPU cores will process + * 4 images in parallel and use up to 8 threads per image, + * so there will be up to 32 concurrent threads. * * @example * const threads = sharp.concurrency(); // 4 @@ -150,13 +148,10 @@ cache(true); function concurrency (concurrency) { return sharp.concurrency(is.integer(concurrency) ? concurrency : null); } -/* node:coverage ignore next 7 */ +/* istanbul ignore next */ if (detectLibc.familySync() === detectLibc.GLIBC && !sharp._isUsingJemalloc()) { // Reduce default concurrency to 1 when using glibc memory allocator sharp.concurrency(1); -} else if (detectLibc.familySync() === detectLibc.MUSL && sharp.concurrency() === 1024) { - // Reduce default concurrency when musl thread over-subscription detected - sharp.concurrency(require('node:os').availableParallelism()); } /** @@ -187,17 +182,17 @@ function counters () { /** * Get and set use of SIMD vector unit instructions. - * Requires libvips to have been compiled with highway support. + * Requires libvips to have been compiled with liborc support. * * Improves the performance of `resize`, `blur` and `sharpen` operations * by taking advantage of the SIMD vector unit of the CPU, e.g. Intel SSE and ARM NEON. * * @example * const simd = sharp.simd(); - * // simd is `true` if the runtime use of highway is currently enabled + * // simd is `true` if the runtime use of liborc is currently enabled * @example * const simd = sharp.simd(false); - * // prevent libvips from using highway at runtime + * // prevent libvips from using liborc at runtime * * @param {boolean} [simd=true] * @returns {boolean} @@ -205,6 +200,7 @@ function counters () { function simd (simd) { return sharp.simd(is.bool(simd) ? simd : null); } +simd(true); /** * Block libvips operations at runtime. @@ -274,10 +270,9 @@ function unblock (options) { /** * Decorate the Sharp class with utility-related functions. - * @module Sharp * @private */ -module.exports = (Sharp) => { +module.exports = function (Sharp) { Sharp.cache = cache; Sharp.concurrency = concurrency; Sharp.counters = counters; @@ -285,6 +280,7 @@ module.exports = (Sharp) => { Sharp.format = format; Sharp.interpolators = interpolators; Sharp.versions = versions; + Sharp.vendor = vendor; Sharp.queue = queue; Sharp.block = block; Sharp.unblock = unblock; diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/LICENSE.md b/backend/node_modules/sharp/node_modules/node-addon-api/LICENSE.md new file mode 100644 index 00000000..e2fad666 --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/LICENSE.md @@ -0,0 +1,13 @@ +The MIT License (MIT) +===================== + +Copyright (c) 2017 Node.js API collaborators +----------------------------------- + +*Node.js API collaborators listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/README.md b/backend/node_modules/sharp/node_modules/node-addon-api/README.md new file mode 100644 index 00000000..5dd8db81 --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/README.md @@ -0,0 +1,317 @@ +NOTE: The default branch has been renamed! +master is now named main + +If you have a local clone, you can update it by running: + +```shell +git branch -m master main +git fetch origin +git branch -u origin/main main +``` + +# **node-addon-api module** +This module contains **header-only C++ wrapper classes** which simplify +the use of the C based [Node-API](https://nodejs.org/dist/latest/docs/api/n-api.html) +provided by Node.js when using C++. It provides a C++ object model +and exception handling semantics with low overhead. + +There are three options for implementing addons: Node-API, nan, or direct +use of internal V8, libuv, and Node.js libraries. Unless there is a need for +direct access to functionality that is not exposed by Node-API as outlined +in [C/C++ addons](https://nodejs.org/dist/latest/docs/api/addons.html) +in Node.js core, use Node-API. Refer to +[C/C++ addons with Node-API](https://nodejs.org/dist/latest/docs/api/n-api.html) +for more information on Node-API. + +Node-API is an ABI stable C interface provided by Node.js for building native +addons. It is independent of the underlying JavaScript runtime (e.g. V8 or ChakraCore) +and is maintained as part of Node.js itself. It is intended to insulate +native addons from changes in the underlying JavaScript engine and allow +modules compiled for one version to run on later versions of Node.js without +recompilation. + +The `node-addon-api` module, which is not part of Node.js, preserves the benefits +of the Node-API as it consists only of inline code that depends only on the stable API +provided by Node-API. As such, modules built against one version of Node.js +using node-addon-api should run without having to be rebuilt with newer versions +of Node.js. + +It is important to remember that *other* Node.js interfaces such as +`libuv` (included in a project via `#include `) are not ABI-stable across +Node.js major versions. Thus, an addon must use Node-API and/or `node-addon-api` +exclusively and build against a version of Node.js that includes an +implementation of Node-API (meaning an active LTS version of Node.js) in +order to benefit from ABI stability across Node.js major versions. Node.js +provides an [ABI stability guide][] containing a detailed explanation of ABI +stability in general, and the Node-API ABI stability guarantee in particular. + +As new APIs are added to Node-API, node-addon-api must be updated to provide +wrappers for those new APIs. For this reason, node-addon-api provides +methods that allow callers to obtain the underlying Node-API handles so +direct calls to Node-API and the use of the objects/methods provided by +node-addon-api can be used together. For example, in order to be able +to use an API for which the node-addon-api does not yet provide a wrapper. + +APIs exposed by node-addon-api are generally used to create and +manipulate JavaScript values. Concepts and operations generally map +to ideas specified in the **ECMA262 Language Specification**. + +The [Node-API Resource](https://nodejs.github.io/node-addon-examples/) offers an +excellent orientation and tips for developers just getting started with Node-API +and node-addon-api. + +- **[Setup](#setup)** +- **[API Documentation](#api)** +- **[Examples](#examples)** +- **[Tests](#tests)** +- **[More resource and info about native Addons](#resources)** +- **[Badges](#badges)** +- **[Code of Conduct](CODE_OF_CONDUCT.md)** +- **[Contributors](#contributors)** +- **[License](#license)** + +## **Current version: 6.1.0** + +(See [CHANGELOG.md](CHANGELOG.md) for complete Changelog) + +[![NPM](https://nodei.co/npm/node-addon-api.png?downloads=true&downloadRank=true)](https://nodei.co/npm/node-addon-api/) [![NPM](https://nodei.co/npm-dl/node-addon-api.png?months=6&height=1)](https://nodei.co/npm/node-addon-api/) + + + +node-addon-api is based on [Node-API](https://nodejs.org/api/n-api.html) and supports using different Node-API versions. +This allows addons built with it to run with Node.js versions which support the targeted Node-API version. +**However** the node-addon-api support model is to support only the active LTS Node.js versions. This means that +every year there will be a new major which drops support for the Node.js LTS version which has gone out of service. + +The oldest Node.js version supported by the current version of node-addon-api is Node.js 14.x. + +## Setup + - [Installation and usage](doc/setup.md) + - [node-gyp](doc/node-gyp.md) + - [cmake-js](doc/cmake-js.md) + - [Conversion tool](doc/conversion-tool.md) + - [Checker tool](doc/checker-tool.md) + - [Generator](doc/generator.md) + - [Prebuild tools](doc/prebuild_tools.md) + + + +### **API Documentation** + +The following is the documentation for node-addon-api. + + - [Full Class Hierarchy](doc/hierarchy.md) + - [Addon Structure](doc/addon.md) + - Data Types: + - [Env](doc/env.md) + - [CallbackInfo](doc/callbackinfo.md) + - [Reference](doc/reference.md) + - [Value](doc/value.md) + - [Name](doc/name.md) + - [Symbol](doc/symbol.md) + - [String](doc/string.md) + - [Number](doc/number.md) + - [Date](doc/date.md) + - [BigInt](doc/bigint.md) + - [Boolean](doc/boolean.md) + - [External](doc/external.md) + - [Object](doc/object.md) + - [Array](doc/array.md) + - [ObjectReference](doc/object_reference.md) + - [PropertyDescriptor](doc/property_descriptor.md) + - [Function](doc/function.md) + - [FunctionReference](doc/function_reference.md) + - [ObjectWrap](doc/object_wrap.md) + - [ClassPropertyDescriptor](doc/class_property_descriptor.md) + - [Buffer](doc/buffer.md) + - [ArrayBuffer](doc/array_buffer.md) + - [TypedArray](doc/typed_array.md) + - [TypedArrayOf](doc/typed_array_of.md) + - [DataView](doc/dataview.md) + - [Error Handling](doc/error_handling.md) + - [Error](doc/error.md) + - [TypeError](doc/type_error.md) + - [RangeError](doc/range_error.md) + - [Object Lifetime Management](doc/object_lifetime_management.md) + - [HandleScope](doc/handle_scope.md) + - [EscapableHandleScope](doc/escapable_handle_scope.md) + - [Memory Management](doc/memory_management.md) + - [Async Operations](doc/async_operations.md) + - [AsyncWorker](doc/async_worker.md) + - [AsyncContext](doc/async_context.md) + - [AsyncWorker Variants](doc/async_worker_variants.md) + - [Thread-safe Functions](doc/threadsafe.md) + - [ThreadSafeFunction](doc/threadsafe_function.md) + - [TypedThreadSafeFunction](doc/typed_threadsafe_function.md) + - [Promises](doc/promises.md) + - [Version management](doc/version_management.md) + + + +### **Examples** + +Are you new to **node-addon-api**? Take a look at our **[examples](https://github.com/nodejs/node-addon-examples)** + +- **[Hello World](https://github.com/nodejs/node-addon-examples/tree/HEAD/1_hello_world/node-addon-api)** +- **[Pass arguments to a function](https://github.com/nodejs/node-addon-examples/tree/HEAD/2_function_arguments/node-addon-api)** +- **[Callbacks](https://github.com/nodejs/node-addon-examples/tree/HEAD/3_callbacks/node-addon-api)** +- **[Object factory](https://github.com/nodejs/node-addon-examples/tree/HEAD/4_object_factory/node-addon-api)** +- **[Function factory](https://github.com/nodejs/node-addon-examples/tree/HEAD/5_function_factory/node-addon-api)** +- **[Wrapping C++ Object](https://github.com/nodejs/node-addon-examples/tree/HEAD/6_object_wrap/node-addon-api)** +- **[Factory of wrapped object](https://github.com/nodejs/node-addon-examples/tree/HEAD/7_factory_wrap/node-addon-api)** +- **[Passing wrapped object around](https://github.com/nodejs/node-addon-examples/tree/HEAD/8_passing_wrapped/node-addon-api)** + + + +### **Tests** + +To run the **node-addon-api** tests do: + +``` +npm install +npm test +``` + +To avoid testing the deprecated portions of the API run +``` +npm install +npm test --disable-deprecated +``` + +To run the tests targeting a specific version of Node-API run +``` +npm install +export NAPI_VERSION=X +npm test --NAPI_VERSION=X +``` + +where X is the version of Node-API you want to target. + +To run a specific unit test, filter conditions are available + +**Example:** + compile and run only tests on objectwrap.cc and objectwrap.js + ``` + npm run unit --filter=objectwrap + ``` + +Multiple unit tests cane be selected with wildcards + +**Example:** +compile and run all test files ending with "reference" -> function_reference.cc, object_reference.cc, reference.cc + ``` + npm run unit --filter=*reference + ``` + +Multiple filter conditions can be joined to broaden the test selection + +**Example:** + compile and run all tests under folders threadsafe_function and typed_threadsafe_function and also the objectwrap.cc file + npm run unit --filter='*function objectwrap' + +### **Debug** + +To run the **node-addon-api** tests with `--debug` option: + +``` +npm run-script dev +``` + +If you want a faster build, you might use the following option: + +``` +npm run-script dev:incremental +``` + +Take a look and get inspired by our **[test suite](https://github.com/nodejs/node-addon-api/tree/HEAD/test)** + +### **Benchmarks** + +You can run the available benchmarks using the following command: + +``` +npm run-script benchmark +``` + +See [benchmark/README.md](benchmark/README.md) for more details about running and adding benchmarks. + + + +### **More resource and info about native Addons** +- **[C++ Addons](https://nodejs.org/dist/latest/docs/api/addons.html)** +- **[Node-API](https://nodejs.org/dist/latest/docs/api/n-api.html)** +- **[Node-API - Next Generation Node API for Native Modules](https://youtu.be/-Oniup60Afs)** +- **[How We Migrated Realm JavaScript From NAN to Node-API](https://developer.mongodb.com/article/realm-javascript-nan-to-n-api)** + +As node-addon-api's core mission is to expose the plain C Node-API as C++ +wrappers, tools that facilitate n-api/node-addon-api providing more +convenient patterns for developing a Node.js add-on with n-api/node-addon-api +can be published to NPM as standalone packages. It is also recommended to tag +such packages with `node-addon-api` to provide more visibility to the community. + +Quick links to NPM searches: [keywords:node-addon-api](https://www.npmjs.com/search?q=keywords%3Anode-addon-api). + + + +### **Other bindings** + +- **[napi-rs](https://napi.rs)** - (`Rust`) + + + +### **Badges** + +The use of badges is recommended to indicate the minimum version of Node-API +required for the module. This helps to determine which Node.js major versions are +supported. Addon maintainers can consult the [Node-API support matrix][] to determine +which Node.js versions provide a given Node-API version. The following badges are +available: + +![Node-API v1 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v1%20Badge.svg) +![Node-API v2 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v2%20Badge.svg) +![Node-API v3 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v3%20Badge.svg) +![Node-API v4 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v4%20Badge.svg) +![Node-API v5 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v5%20Badge.svg) +![Node-API v6 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v6%20Badge.svg) +![Node-API v7 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v7%20Badge.svg) +![Node-API v8 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v8%20Badge.svg) +![Node-API Experimental Version Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20Experimental%20Version%20Badge.svg) + +## **Contributing** + +We love contributions from the community to **node-addon-api**! +See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on our philosophy around extending this module. + + + +## Team members + +### Active +| Name | GitHub Link | +| ------------------- | ----------------------------------------------------- | +| Anna Henningsen | [addaleax](https://github.com/addaleax) | +| Chengzhong Wu | [legendecas](https://github.com/legendecas) | +| Jack Xia | [JckXia](https://github.com/JckXia) | +| Kevin Eady | [KevinEady](https://github.com/KevinEady) | +| Michael Dawson | [mhdawson](https://github.com/mhdawson) | +| Nicola Del Gobbo | [NickNaso](https://github.com/NickNaso) | +| Vladimir Morozov | [vmoroz](https://github.com/vmoroz) | + +### Emeritus +| Name | GitHub Link | +| ------------------- | ----------------------------------------------------- | +| Arunesh Chandra | [aruneshchandra](https://github.com/aruneshchandra) | +| Benjamin Byholm | [kkoopa](https://github.com/kkoopa) | +| Gabriel Schulhof | [gabrielschulhof](https://github.com/gabrielschulhof) | +| Hitesh Kanwathirtha | [digitalinfinity](https://github.com/digitalinfinity) | +| Jason Ginchereau | [jasongin](https://github.com/jasongin) | +| Jim Schlight | [jschlight](https://github.com/jschlight) | +| Sampson Gao | [sampsongao](https://github.com/sampsongao) | +| Taylor Woll | [boingoing](https://github.com/boingoing) | + + + +Licensed under [MIT](./LICENSE.md) + +[ABI stability guide]: https://nodejs.org/en/docs/guides/abi-stability/ +[Node-API support matrix]: https://nodejs.org/dist/latest/docs/api/n-api.html#n_api_n_api_version_matrix diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/common.gypi b/backend/node_modules/sharp/node_modules/node-addon-api/common.gypi new file mode 100644 index 00000000..9be254f0 --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/common.gypi @@ -0,0 +1,21 @@ +{ + 'variables': { + 'NAPI_VERSION%': " +inline PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, + Getter getter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, nullptr}); + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), getter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + napi_value name, + Getter getter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, nullptr}); + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Name name, Getter getter, napi_property_attributes attributes, void* data) { + napi_value nameValue = name; + return PropertyDescriptor::Accessor(nameValue, getter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::AccessorCallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, setter, nullptr}); + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), getter, setter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + napi_value name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* /*data*/) { + using CbData = details::AccessorCallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({getter, setter, nullptr}); + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + napi_value nameValue = name; + return PropertyDescriptor::Accessor( + nameValue, getter, setter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + const char* utf8name, + Callable cb, + napi_property_attributes attributes, + void* /*data*/) { + using ReturnType = decltype(cb(CallbackInfo(nullptr, nullptr))); + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({cb, nullptr}); + + return PropertyDescriptor({utf8name, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return Function(utf8name.c_str(), cb, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + napi_value name, + Callable cb, + napi_property_attributes attributes, + void* /*data*/) { + using ReturnType = decltype(cb(CallbackInfo(nullptr, nullptr))); + using CbData = details::CallbackData; + // TODO: Delete when the function is destroyed + auto callbackData = new CbData({cb, nullptr}); + + return PropertyDescriptor({nullptr, + name, + CbData::Wrapper, + nullptr, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Name name, Callable cb, napi_property_attributes attributes, void* data) { + napi_value nameValue = name; + return PropertyDescriptor::Function(nameValue, cb, attributes, data); +} + +#endif // !SRC_NAPI_INL_DEPRECATED_H_ diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/napi-inl.h b/backend/node_modules/sharp/node_modules/node-addon-api/napi-inl.h new file mode 100644 index 00000000..c6ef0fbb --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/napi-inl.h @@ -0,0 +1,6588 @@ +#ifndef SRC_NAPI_INL_H_ +#define SRC_NAPI_INL_H_ + +//////////////////////////////////////////////////////////////////////////////// +// Node-API C++ Wrapper Classes +// +// Inline header-only implementations for "Node-API" ABI-stable C APIs for +// Node.js. +//////////////////////////////////////////////////////////////////////////////// + +// Note: Do not include this file directly! Include "napi.h" instead. + +#include +#include +#if NAPI_HAS_THREADS +#include +#endif // NAPI_HAS_THREADS +#include +#include + +namespace Napi { + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +namespace NAPI_CPP_CUSTOM_NAMESPACE { +#endif + +// Helpers to handle functions exposed from C++ and internal constants. +namespace details { + +// New napi_status constants not yet available in all supported versions of +// Node.js releases. Only necessary when they are used in napi.h and napi-inl.h. +constexpr int napi_no_external_buffers_allowed = 22; + +// Attach a data item to an object and delete it when the object gets +// garbage-collected. +// TODO: Replace this code with `napi_add_finalizer()` whenever it becomes +// available on all supported versions of Node.js. +template +inline napi_status AttachData(napi_env env, + napi_value obj, + FreeType* data, + napi_finalize finalizer = nullptr, + void* hint = nullptr) { + napi_status status; + if (finalizer == nullptr) { + finalizer = [](napi_env /*env*/, void* data, void* /*hint*/) { + delete static_cast(data); + }; + } +#if (NAPI_VERSION < 5) + napi_value symbol, external; + status = napi_create_symbol(env, nullptr, &symbol); + if (status == napi_ok) { + status = napi_create_external(env, data, finalizer, hint, &external); + if (status == napi_ok) { + napi_property_descriptor desc = {nullptr, + symbol, + nullptr, + nullptr, + nullptr, + external, + napi_default, + nullptr}; + status = napi_define_properties(env, obj, 1, &desc); + } + } +#else // NAPI_VERSION >= 5 + status = napi_add_finalizer(env, obj, data, finalizer, hint, nullptr); +#endif + return status; +} + +// For use in JS to C++ callback wrappers to catch any Napi::Error exceptions +// and rethrow them as JavaScript exceptions before returning from the callback. +template +inline napi_value WrapCallback(Callable callback) { +#ifdef NAPI_CPP_EXCEPTIONS + try { + return callback(); + } catch (const Error& e) { + e.ThrowAsJavaScriptException(); + return nullptr; + } +#else // NAPI_CPP_EXCEPTIONS + // When C++ exceptions are disabled, errors are immediately thrown as JS + // exceptions, so there is no need to catch and rethrow them here. + return callback(); +#endif // NAPI_CPP_EXCEPTIONS +} + +// For use in JS to C++ void callback wrappers to catch any Napi::Error +// exceptions and rethrow them as JavaScript exceptions before returning from +// the callback. +template +inline void WrapVoidCallback(Callable callback) { +#ifdef NAPI_CPP_EXCEPTIONS + try { + callback(); + } catch (const Error& e) { + e.ThrowAsJavaScriptException(); + } +#else // NAPI_CPP_EXCEPTIONS + // When C++ exceptions are disabled, errors are immediately thrown as JS + // exceptions, so there is no need to catch and rethrow them here. + callback(); +#endif // NAPI_CPP_EXCEPTIONS +} + +template +struct CallbackData { + static inline napi_value Wrapper(napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + CallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->callback(callbackInfo); + }); + } + + Callable callback; + void* data; +}; + +template +struct CallbackData { + static inline napi_value Wrapper(napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + CallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->callback(callbackInfo); + return nullptr; + }); + } + + Callable callback; + void* data; +}; + +template +napi_value TemplatedVoidCallback(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback([&] { + CallbackInfo cbInfo(env, info); + Callback(cbInfo); + return nullptr; + }); +} + +template +napi_value TemplatedCallback(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback([&] { + CallbackInfo cbInfo(env, info); + return Callback(cbInfo); + }); +} + +template +napi_value TemplatedInstanceCallback(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback([&] { + CallbackInfo cbInfo(env, info); + T* instance = T::Unwrap(cbInfo.This().As()); + return (instance->*UnwrapCallback)(cbInfo); + }); +} + +template +napi_value TemplatedInstanceVoidCallback(napi_env env, napi_callback_info info) + NAPI_NOEXCEPT { + return details::WrapCallback([&] { + CallbackInfo cbInfo(env, info); + T* instance = T::Unwrap(cbInfo.This().As()); + (instance->*UnwrapCallback)(cbInfo); + return nullptr; + }); +} + +template +struct FinalizeData { + static inline void Wrapper(napi_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback([&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(Env(env), static_cast(data)); + delete finalizeData; + }); + } + + static inline void WrapperWithHint(napi_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback([&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback( + Env(env), static_cast(data), finalizeData->hint); + delete finalizeData; + }); + } + + Finalizer callback; + Hint* hint; +}; + +#if (NAPI_VERSION > 3 && NAPI_HAS_THREADS) +template , + typename FinalizerDataType = void> +struct ThreadSafeFinalize { + static inline void Wrapper(napi_env env, + void* rawFinalizeData, + void* /* rawContext */) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback(Env(env)); + delete finalizeData; + } + + static inline void FinalizeWrapperWithData(napi_env env, + void* rawFinalizeData, + void* /* rawContext */) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback(Env(env), finalizeData->data); + delete finalizeData; + } + + static inline void FinalizeWrapperWithContext(napi_env env, + void* rawFinalizeData, + void* rawContext) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback(Env(env), static_cast(rawContext)); + delete finalizeData; + } + + static inline void FinalizeFinalizeWrapperWithDataAndContext( + napi_env env, void* rawFinalizeData, void* rawContext) { + if (rawFinalizeData == nullptr) return; + + ThreadSafeFinalize* finalizeData = + static_cast(rawFinalizeData); + finalizeData->callback( + Env(env), finalizeData->data, static_cast(rawContext)); + delete finalizeData; + } + + FinalizerDataType* data; + Finalizer callback; +}; + +template +inline typename std::enable_if(nullptr)>::type +CallJsWrapper(napi_env env, napi_value jsCallback, void* context, void* data) { + call(env, + Function(env, jsCallback), + static_cast(context), + static_cast(data)); +} + +template +inline typename std::enable_if(nullptr)>::type +CallJsWrapper(napi_env env, + napi_value jsCallback, + void* /*context*/, + void* /*data*/) { + if (jsCallback != nullptr) { + Function(env, jsCallback).Call(0, nullptr); + } +} + +#if NAPI_VERSION > 4 + +template +napi_value DefaultCallbackWrapper(napi_env /*env*/, std::nullptr_t /*cb*/) { + return nullptr; +} + +template +napi_value DefaultCallbackWrapper(napi_env /*env*/, Napi::Function cb) { + return cb; +} + +#else +template +napi_value DefaultCallbackWrapper(napi_env env, Napi::Function cb) { + if (cb.IsEmpty()) { + return TSFN::EmptyFunctionFactory(env); + } + return cb; +} +#endif // NAPI_VERSION > 4 +#endif // NAPI_VERSION > 3 && NAPI_HAS_THREADS + +template +struct AccessorCallbackData { + static inline napi_value GetterWrapper(napi_env env, + napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + AccessorCallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->getterCallback(callbackInfo); + }); + } + + static inline napi_value SetterWrapper(napi_env env, + napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + AccessorCallbackData* callbackData = + static_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->setterCallback(callbackInfo); + return nullptr; + }); + } + + Getter getterCallback; + Setter setterCallback; + void* data; +}; + +} // namespace details + +#ifndef NODE_ADDON_API_DISABLE_DEPRECATED +#include "napi-inl.deprecated.h" +#endif // !NODE_ADDON_API_DISABLE_DEPRECATED + +//////////////////////////////////////////////////////////////////////////////// +// Module registration +//////////////////////////////////////////////////////////////////////////////// + +// Register an add-on based on an initializer function. +#define NODE_API_MODULE(modname, regfunc) \ + static napi_value __napi_##regfunc(napi_env env, napi_value exports) { \ + return Napi::RegisterModule(env, exports, regfunc); \ + } \ + NAPI_MODULE(modname, __napi_##regfunc) + +// Register an add-on based on a subclass of `Addon` with a custom Node.js +// module name. +#define NODE_API_NAMED_ADDON(modname, classname) \ + static napi_value __napi_##classname(napi_env env, napi_value exports) { \ + return Napi::RegisterModule(env, exports, &classname::Init); \ + } \ + NAPI_MODULE(modname, __napi_##classname) + +// Register an add-on based on a subclass of `Addon` with the Node.js module +// name given by node-gyp from the `target_name` in binding.gyp. +#define NODE_API_ADDON(classname) \ + NODE_API_NAMED_ADDON(NODE_GYP_MODULE_NAME, classname) + +// Adapt the NAPI_MODULE registration function: +// - Wrap the arguments in NAPI wrappers. +// - Catch any NAPI errors and rethrow as JS exceptions. +inline napi_value RegisterModule(napi_env env, + napi_value exports, + ModuleRegisterCallback registerCallback) { + return details::WrapCallback([&] { + return napi_value( + registerCallback(Napi::Env(env), Napi::Object(env, exports))); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Maybe class +//////////////////////////////////////////////////////////////////////////////// + +template +bool Maybe::IsNothing() const { + return !_has_value; +} + +template +bool Maybe::IsJust() const { + return _has_value; +} + +template +void Maybe::Check() const { + NAPI_CHECK(IsJust(), "Napi::Maybe::Check", "Maybe value is Nothing."); +} + +template +T Maybe::Unwrap() const { + NAPI_CHECK(IsJust(), "Napi::Maybe::Unwrap", "Maybe value is Nothing."); + return _value; +} + +template +T Maybe::UnwrapOr(const T& default_value) const { + return _has_value ? _value : default_value; +} + +template +bool Maybe::UnwrapTo(T* out) const { + if (IsJust()) { + *out = _value; + return true; + }; + return false; +} + +template +bool Maybe::operator==(const Maybe& other) const { + return (IsJust() == other.IsJust()) && + (!IsJust() || Unwrap() == other.Unwrap()); +} + +template +bool Maybe::operator!=(const Maybe& other) const { + return !operator==(other); +} + +template +Maybe::Maybe() : _has_value(false) {} + +template +Maybe::Maybe(const T& t) : _has_value(true), _value(t) {} + +template +inline Maybe Nothing() { + return Maybe(); +} + +template +inline Maybe Just(const T& t) { + return Maybe(t); +} + +//////////////////////////////////////////////////////////////////////////////// +// Env class +//////////////////////////////////////////////////////////////////////////////// + +inline Env::Env(napi_env env) : _env(env) {} + +inline Env::operator napi_env() const { + return _env; +} + +inline Object Env::Global() const { + napi_value value; + napi_status status = napi_get_global(*this, &value); + NAPI_THROW_IF_FAILED(*this, status, Object()); + return Object(*this, value); +} + +inline Value Env::Undefined() const { + napi_value value; + napi_status status = napi_get_undefined(*this, &value); + NAPI_THROW_IF_FAILED(*this, status, Value()); + return Value(*this, value); +} + +inline Value Env::Null() const { + napi_value value; + napi_status status = napi_get_null(*this, &value); + NAPI_THROW_IF_FAILED(*this, status, Value()); + return Value(*this, value); +} + +inline bool Env::IsExceptionPending() const { + bool result; + napi_status status = napi_is_exception_pending(_env, &result); + if (status != napi_ok) + result = false; // Checking for a pending exception shouldn't throw. + return result; +} + +inline Error Env::GetAndClearPendingException() const { + napi_value value; + napi_status status = napi_get_and_clear_last_exception(_env, &value); + if (status != napi_ok) { + // Don't throw another exception when failing to get the exception! + return Error(); + } + return Error(_env, value); +} + +inline MaybeOrValue Env::RunScript(const char* utf8script) const { + String script = String::New(_env, utf8script); + return RunScript(script); +} + +inline MaybeOrValue Env::RunScript(const std::string& utf8script) const { + return RunScript(utf8script.c_str()); +} + +inline MaybeOrValue Env::RunScript(String script) const { + napi_value result; + napi_status status = napi_run_script(_env, script, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Value(_env, result), Napi::Value); +} + +#if NAPI_VERSION > 2 +template +void Env::CleanupHook::Wrapper(void* data) NAPI_NOEXCEPT { + auto* cleanupData = + static_cast::CleanupData*>( + data); + cleanupData->hook(); + delete cleanupData; +} + +template +void Env::CleanupHook::WrapperWithArg(void* data) NAPI_NOEXCEPT { + auto* cleanupData = + static_cast::CleanupData*>( + data); + cleanupData->hook(static_cast(cleanupData->arg)); + delete cleanupData; +} +#endif // NAPI_VERSION > 2 + +#if NAPI_VERSION > 5 +template fini> +inline void Env::SetInstanceData(T* data) const { + napi_status status = napi_set_instance_data( + _env, + data, + [](napi_env env, void* data, void*) { fini(env, static_cast(data)); }, + nullptr); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +template fini> +inline void Env::SetInstanceData(DataType* data, HintType* hint) const { + napi_status status = napi_set_instance_data( + _env, + data, + [](napi_env env, void* data, void* hint) { + fini(env, static_cast(data), static_cast(hint)); + }, + hint); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +template +inline T* Env::GetInstanceData() const { + void* data = nullptr; + + napi_status status = napi_get_instance_data(_env, &data); + NAPI_THROW_IF_FAILED(_env, status, nullptr); + + return static_cast(data); +} + +template +void Env::DefaultFini(Env, T* data) { + delete data; +} + +template +void Env::DefaultFiniWithHint(Env, DataType* data, HintType*) { + delete data; +} +#endif // NAPI_VERSION > 5 + +//////////////////////////////////////////////////////////////////////////////// +// Value class +//////////////////////////////////////////////////////////////////////////////// + +inline Value::Value() : _env(nullptr), _value(nullptr) {} + +inline Value::Value(napi_env env, napi_value value) + : _env(env), _value(value) {} + +inline Value::operator napi_value() const { + return _value; +} + +inline bool Value::operator==(const Value& other) const { + return StrictEquals(other); +} + +inline bool Value::operator!=(const Value& other) const { + return !this->operator==(other); +} + +inline bool Value::StrictEquals(const Value& other) const { + bool result; + napi_status status = napi_strict_equals(_env, *this, other, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline Napi::Env Value::Env() const { + return Napi::Env(_env); +} + +inline bool Value::IsEmpty() const { + return _value == nullptr; +} + +inline napi_valuetype Value::Type() const { + if (IsEmpty()) { + return napi_undefined; + } + + napi_valuetype type; + napi_status status = napi_typeof(_env, _value, &type); + NAPI_THROW_IF_FAILED(_env, status, napi_undefined); + return type; +} + +inline bool Value::IsUndefined() const { + return Type() == napi_undefined; +} + +inline bool Value::IsNull() const { + return Type() == napi_null; +} + +inline bool Value::IsBoolean() const { + return Type() == napi_boolean; +} + +inline bool Value::IsNumber() const { + return Type() == napi_number; +} + +#if NAPI_VERSION > 5 +inline bool Value::IsBigInt() const { + return Type() == napi_bigint; +} +#endif // NAPI_VERSION > 5 + +#if (NAPI_VERSION > 4) +inline bool Value::IsDate() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_date(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} +#endif + +inline bool Value::IsString() const { + return Type() == napi_string; +} + +inline bool Value::IsSymbol() const { + return Type() == napi_symbol; +} + +inline bool Value::IsArray() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_array(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsArrayBuffer() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_arraybuffer(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsTypedArray() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_typedarray(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsObject() const { + return Type() == napi_object || IsFunction(); +} + +inline bool Value::IsFunction() const { + return Type() == napi_function; +} + +inline bool Value::IsPromise() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_promise(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsDataView() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_dataview(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsBuffer() const { + if (IsEmpty()) { + return false; + } + + bool result; + napi_status status = napi_is_buffer(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +inline bool Value::IsExternal() const { + return Type() == napi_external; +} + +template +inline T Value::As() const { +#ifdef NODE_ADDON_API_ENABLE_TYPE_CHECK_ON_AS + T::CheckCast(_env, _value); +#endif + return T(_env, _value); +} + +inline MaybeOrValue Value::ToBoolean() const { + napi_value result; + napi_status status = napi_coerce_to_bool(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Boolean(_env, result), Napi::Boolean); +} + +inline MaybeOrValue Value::ToNumber() const { + napi_value result; + napi_status status = napi_coerce_to_number(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Number(_env, result), Napi::Number); +} + +inline MaybeOrValue Value::ToString() const { + napi_value result; + napi_status status = napi_coerce_to_string(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::String(_env, result), Napi::String); +} + +inline MaybeOrValue Value::ToObject() const { + napi_value result; + napi_status status = napi_coerce_to_object(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Object(_env, result), Napi::Object); +} + +//////////////////////////////////////////////////////////////////////////////// +// Boolean class +//////////////////////////////////////////////////////////////////////////////// + +inline Boolean Boolean::New(napi_env env, bool val) { + napi_value value; + napi_status status = napi_get_boolean(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, Boolean()); + return Boolean(env, value); +} + +inline void Boolean::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Boolean::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Boolean::CheckCast", "napi_typeof failed"); + NAPI_CHECK( + type == napi_boolean, "Boolean::CheckCast", "value is not napi_boolean"); +} + +inline Boolean::Boolean() : Napi::Value() {} + +inline Boolean::Boolean(napi_env env, napi_value value) + : Napi::Value(env, value) {} + +inline Boolean::operator bool() const { + return Value(); +} + +inline bool Boolean::Value() const { + bool result; + napi_status status = napi_get_value_bool(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +// Number class +//////////////////////////////////////////////////////////////////////////////// + +inline Number Number::New(napi_env env, double val) { + napi_value value; + napi_status status = napi_create_double(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, Number()); + return Number(env, value); +} + +inline void Number::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Number::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Number::CheckCast", "napi_typeof failed"); + NAPI_CHECK( + type == napi_number, "Number::CheckCast", "value is not napi_number"); +} + +inline Number::Number() : Value() {} + +inline Number::Number(napi_env env, napi_value value) : Value(env, value) {} + +inline Number::operator int32_t() const { + return Int32Value(); +} + +inline Number::operator uint32_t() const { + return Uint32Value(); +} + +inline Number::operator int64_t() const { + return Int64Value(); +} + +inline Number::operator float() const { + return FloatValue(); +} + +inline Number::operator double() const { + return DoubleValue(); +} + +inline int32_t Number::Int32Value() const { + int32_t result; + napi_status status = napi_get_value_int32(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline uint32_t Number::Uint32Value() const { + uint32_t result; + napi_status status = napi_get_value_uint32(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline int64_t Number::Int64Value() const { + int64_t result; + napi_status status = napi_get_value_int64(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline float Number::FloatValue() const { + return static_cast(DoubleValue()); +} + +inline double Number::DoubleValue() const { + double result; + napi_status status = napi_get_value_double(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +#if NAPI_VERSION > 5 +//////////////////////////////////////////////////////////////////////////////// +// BigInt Class +//////////////////////////////////////////////////////////////////////////////// + +inline BigInt BigInt::New(napi_env env, int64_t val) { + napi_value value; + napi_status status = napi_create_bigint_int64(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline BigInt BigInt::New(napi_env env, uint64_t val) { + napi_value value; + napi_status status = napi_create_bigint_uint64(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline BigInt BigInt::New(napi_env env, + int sign_bit, + size_t word_count, + const uint64_t* words) { + napi_value value; + napi_status status = + napi_create_bigint_words(env, sign_bit, word_count, words, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline void BigInt::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "BigInt::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "BigInt::CheckCast", "napi_typeof failed"); + NAPI_CHECK( + type == napi_bigint, "BigInt::CheckCast", "value is not napi_bigint"); +} + +inline BigInt::BigInt() : Value() {} + +inline BigInt::BigInt(napi_env env, napi_value value) : Value(env, value) {} + +inline int64_t BigInt::Int64Value(bool* lossless) const { + int64_t result; + napi_status status = + napi_get_value_bigint_int64(_env, _value, &result, lossless); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline uint64_t BigInt::Uint64Value(bool* lossless) const { + uint64_t result; + napi_status status = + napi_get_value_bigint_uint64(_env, _value, &result, lossless); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline size_t BigInt::WordCount() const { + size_t word_count; + napi_status status = + napi_get_value_bigint_words(_env, _value, nullptr, &word_count, nullptr); + NAPI_THROW_IF_FAILED(_env, status, 0); + return word_count; +} + +inline void BigInt::ToWords(int* sign_bit, + size_t* word_count, + uint64_t* words) { + napi_status status = + napi_get_value_bigint_words(_env, _value, sign_bit, word_count, words); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} +#endif // NAPI_VERSION > 5 + +#if (NAPI_VERSION > 4) +//////////////////////////////////////////////////////////////////////////////// +// Date Class +//////////////////////////////////////////////////////////////////////////////// + +inline Date Date::New(napi_env env, double val) { + napi_value value; + napi_status status = napi_create_date(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, Date()); + return Date(env, value); +} + +inline void Date::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Date::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_date(env, value, &result); + NAPI_CHECK(status == napi_ok, "Date::CheckCast", "napi_is_date failed"); + NAPI_CHECK(result, "Date::CheckCast", "value is not date"); +} + +inline Date::Date() : Value() {} + +inline Date::Date(napi_env env, napi_value value) : Value(env, value) {} + +inline Date::operator double() const { + return ValueOf(); +} + +inline double Date::ValueOf() const { + double result; + napi_status status = napi_get_date_value(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Name class +//////////////////////////////////////////////////////////////////////////////// +inline void Name::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Name::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Name::CheckCast", "napi_typeof failed"); + NAPI_CHECK(type == napi_string || type == napi_symbol, + "Name::CheckCast", + "value is not napi_string or napi_symbol"); +} + +inline Name::Name() : Value() {} + +inline Name::Name(napi_env env, napi_value value) : Value(env, value) {} + +//////////////////////////////////////////////////////////////////////////////// +// String class +//////////////////////////////////////////////////////////////////////////////// + +inline String String::New(napi_env env, const std::string& val) { + return String::New(env, val.c_str(), val.size()); +} + +inline String String::New(napi_env env, const std::u16string& val) { + return String::New(env, val.c_str(), val.size()); +} + +inline String String::New(napi_env env, const char* val) { + // TODO(@gabrielschulhof) Remove if-statement when core's error handling is + // available in all supported versions. + if (val == nullptr) { + // Throw an error that looks like it came from core. + NAPI_THROW_IF_FAILED(env, napi_invalid_arg, String()); + } + napi_value value; + napi_status status = + napi_create_string_utf8(env, val, std::strlen(val), &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline String String::New(napi_env env, const char16_t* val) { + napi_value value; + // TODO(@gabrielschulhof) Remove if-statement when core's error handling is + // available in all supported versions. + if (val == nullptr) { + // Throw an error that looks like it came from core. + NAPI_THROW_IF_FAILED(env, napi_invalid_arg, String()); + } + napi_status status = + napi_create_string_utf16(env, val, std::u16string(val).size(), &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline String String::New(napi_env env, const char* val, size_t length) { + napi_value value; + napi_status status = napi_create_string_utf8(env, val, length, &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline String String::New(napi_env env, const char16_t* val, size_t length) { + napi_value value; + napi_status status = napi_create_string_utf16(env, val, length, &value); + NAPI_THROW_IF_FAILED(env, status, String()); + return String(env, value); +} + +inline void String::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "String::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "String::CheckCast", "napi_typeof failed"); + NAPI_CHECK( + type == napi_string, "String::CheckCast", "value is not napi_string"); +} + +inline String::String() : Name() {} + +inline String::String(napi_env env, napi_value value) : Name(env, value) {} + +inline String::operator std::string() const { + return Utf8Value(); +} + +inline String::operator std::u16string() const { + return Utf16Value(); +} + +inline std::string String::Utf8Value() const { + size_t length; + napi_status status = + napi_get_value_string_utf8(_env, _value, nullptr, 0, &length); + NAPI_THROW_IF_FAILED(_env, status, ""); + + std::string value; + value.reserve(length + 1); + value.resize(length); + status = napi_get_value_string_utf8( + _env, _value, &value[0], value.capacity(), nullptr); + NAPI_THROW_IF_FAILED(_env, status, ""); + return value; +} + +inline std::u16string String::Utf16Value() const { + size_t length; + napi_status status = + napi_get_value_string_utf16(_env, _value, nullptr, 0, &length); + NAPI_THROW_IF_FAILED(_env, status, NAPI_WIDE_TEXT("")); + + std::u16string value; + value.reserve(length + 1); + value.resize(length); + status = napi_get_value_string_utf16( + _env, _value, &value[0], value.capacity(), nullptr); + NAPI_THROW_IF_FAILED(_env, status, NAPI_WIDE_TEXT("")); + return value; +} + +//////////////////////////////////////////////////////////////////////////////// +// Symbol class +//////////////////////////////////////////////////////////////////////////////// + +inline Symbol Symbol::New(napi_env env, const char* description) { + napi_value descriptionValue = description != nullptr + ? String::New(env, description) + : static_cast(nullptr); + return Symbol::New(env, descriptionValue); +} + +inline Symbol Symbol::New(napi_env env, const std::string& description) { + napi_value descriptionValue = String::New(env, description); + return Symbol::New(env, descriptionValue); +} + +inline Symbol Symbol::New(napi_env env, String description) { + napi_value descriptionValue = description; + return Symbol::New(env, descriptionValue); +} + +inline Symbol Symbol::New(napi_env env, napi_value description) { + napi_value value; + napi_status status = napi_create_symbol(env, description, &value); + NAPI_THROW_IF_FAILED(env, status, Symbol()); + return Symbol(env, value); +} + +inline MaybeOrValue Symbol::WellKnown(napi_env env, + const std::string& name) { +#if defined(NODE_ADDON_API_ENABLE_MAYBE) + Value symbol_obj; + Value symbol_value; + if (Napi::Env(env).Global().Get("Symbol").UnwrapTo(&symbol_obj) && + symbol_obj.As().Get(name).UnwrapTo(&symbol_value)) { + return Just(symbol_value.As()); + } + return Nothing(); +#else + return Napi::Env(env) + .Global() + .Get("Symbol") + .As() + .Get(name) + .As(); +#endif +} + +inline MaybeOrValue Symbol::For(napi_env env, + const std::string& description) { + napi_value descriptionValue = String::New(env, description); + return Symbol::For(env, descriptionValue); +} + +inline MaybeOrValue Symbol::For(napi_env env, const char* description) { + napi_value descriptionValue = String::New(env, description); + return Symbol::For(env, descriptionValue); +} + +inline MaybeOrValue Symbol::For(napi_env env, String description) { + return Symbol::For(env, static_cast(description)); +} + +inline MaybeOrValue Symbol::For(napi_env env, napi_value description) { +#if defined(NODE_ADDON_API_ENABLE_MAYBE) + Value symbol_obj; + Value symbol_for_value; + Value symbol_value; + if (Napi::Env(env).Global().Get("Symbol").UnwrapTo(&symbol_obj) && + symbol_obj.As().Get("for").UnwrapTo(&symbol_for_value) && + symbol_for_value.As() + .Call(symbol_obj, {description}) + .UnwrapTo(&symbol_value)) { + return Just(symbol_value.As()); + } + return Nothing(); +#else + Object symbol_obj = Napi::Env(env).Global().Get("Symbol").As(); + return symbol_obj.Get("for") + .As() + .Call(symbol_obj, {description}) + .As(); +#endif +} + +inline void Symbol::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Symbol::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Symbol::CheckCast", "napi_typeof failed"); + NAPI_CHECK( + type == napi_symbol, "Symbol::CheckCast", "value is not napi_symbol"); +} + +inline Symbol::Symbol() : Name() {} + +inline Symbol::Symbol(napi_env env, napi_value value) : Name(env, value) {} + +//////////////////////////////////////////////////////////////////////////////// +// Automagic value creation +//////////////////////////////////////////////////////////////////////////////// + +namespace details { +template +struct vf_number { + static Number From(napi_env env, T value) { + return Number::New(env, static_cast(value)); + } +}; + +template <> +struct vf_number { + static Boolean From(napi_env env, bool value) { + return Boolean::New(env, value); + } +}; + +struct vf_utf8_charp { + static String From(napi_env env, const char* value) { + return String::New(env, value); + } +}; + +struct vf_utf16_charp { + static String From(napi_env env, const char16_t* value) { + return String::New(env, value); + } +}; +struct vf_utf8_string { + static String From(napi_env env, const std::string& value) { + return String::New(env, value); + } +}; + +struct vf_utf16_string { + static String From(napi_env env, const std::u16string& value) { + return String::New(env, value); + } +}; + +template +struct vf_fallback { + static Value From(napi_env env, const T& value) { return Value(env, value); } +}; + +template +struct disjunction : std::false_type {}; +template +struct disjunction : B {}; +template +struct disjunction + : std::conditional>::type {}; + +template +struct can_make_string + : disjunction::type, + typename std::is_convertible::type, + typename std::is_convertible::type, + typename std::is_convertible::type> {}; +} // namespace details + +template +Value Value::From(napi_env env, const T& value) { + using Helper = typename std::conditional< + std::is_integral::value || std::is_floating_point::value, + details::vf_number, + typename std::conditional::value, + String, + details::vf_fallback>::type>::type; + return Helper::From(env, value); +} + +template +String String::From(napi_env env, const T& value) { + struct Dummy {}; + using Helper = typename std::conditional< + std::is_convertible::value, + details::vf_utf8_charp, + typename std::conditional< + std::is_convertible::value, + details::vf_utf16_charp, + typename std::conditional< + std::is_convertible::value, + details::vf_utf8_string, + typename std::conditional< + std::is_convertible::value, + details::vf_utf16_string, + Dummy>::type>::type>::type>::type; + return Helper::From(env, value); +} + +//////////////////////////////////////////////////////////////////////////////// +// TypeTaggable class +//////////////////////////////////////////////////////////////////////////////// + +inline TypeTaggable::TypeTaggable() : Value() {} + +inline TypeTaggable::TypeTaggable(napi_env _env, napi_value _value) + : Value(_env, _value) {} + +#if NAPI_VERSION >= 8 + +inline void TypeTaggable::TypeTag(const napi_type_tag* type_tag) const { + napi_status status = napi_type_tag_object(_env, _value, type_tag); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline bool TypeTaggable::CheckTypeTag(const napi_type_tag* type_tag) const { + bool result; + napi_status status = + napi_check_object_type_tag(_env, _value, type_tag, &result); + NAPI_THROW_IF_FAILED(_env, status, false); + return result; +} + +#endif // NAPI_VERSION >= 8 + +//////////////////////////////////////////////////////////////////////////////// +// Object class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Object::PropertyLValue::operator Value() const { + MaybeOrValue val = Object(_env, _object).Get(_key); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + return val.Unwrap(); +#else + return val; +#endif +} + +template +template +inline Object::PropertyLValue& Object::PropertyLValue::operator=( + ValueType value) { +#ifdef NODE_ADDON_API_ENABLE_MAYBE + MaybeOrValue result = +#endif + Object(_env, _object).Set(_key, value); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + result.Unwrap(); +#endif + return *this; +} + +template +inline Object::PropertyLValue::PropertyLValue(Object object, Key key) + : _env(object.Env()), _object(object), _key(key) {} + +inline Object Object::New(napi_env env) { + napi_value value; + napi_status status = napi_create_object(env, &value); + NAPI_THROW_IF_FAILED(env, status, Object()); + return Object(env, value); +} + +inline void Object::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Object::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Object::CheckCast", "napi_typeof failed"); + NAPI_CHECK( + type == napi_object, "Object::CheckCast", "value is not napi_object"); +} + +inline Object::Object() : TypeTaggable() {} + +inline Object::Object(napi_env env, napi_value value) + : TypeTaggable(env, value) {} + +inline Object::PropertyLValue Object::operator[]( + const char* utf8name) { + return PropertyLValue(*this, utf8name); +} + +inline Object::PropertyLValue Object::operator[]( + const std::string& utf8name) { + return PropertyLValue(*this, utf8name); +} + +inline Object::PropertyLValue Object::operator[](uint32_t index) { + return PropertyLValue(*this, index); +} + +inline Object::PropertyLValue Object::operator[](Value index) const { + return PropertyLValue(*this, index); +} + +inline MaybeOrValue Object::operator[](const char* utf8name) const { + return Get(utf8name); +} + +inline MaybeOrValue Object::operator[]( + const std::string& utf8name) const { + return Get(utf8name); +} + +inline MaybeOrValue Object::operator[](uint32_t index) const { + return Get(index); +} + +inline MaybeOrValue Object::Has(napi_value key) const { + bool result; + napi_status status = napi_has_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Has(Value key) const { + bool result; + napi_status status = napi_has_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Has(const char* utf8name) const { + bool result; + napi_status status = napi_has_named_property(_env, _value, utf8name, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Has(const std::string& utf8name) const { + return Has(utf8name.c_str()); +} + +inline MaybeOrValue Object::HasOwnProperty(napi_value key) const { + bool result; + napi_status status = napi_has_own_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::HasOwnProperty(Value key) const { + bool result; + napi_status status = napi_has_own_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::HasOwnProperty(const char* utf8name) const { + napi_value key; + napi_status status = + napi_create_string_utf8(_env, utf8name, std::strlen(utf8name), &key); + NAPI_MAYBE_THROW_IF_FAILED(_env, status, bool); + return HasOwnProperty(key); +} + +inline MaybeOrValue Object::HasOwnProperty( + const std::string& utf8name) const { + return HasOwnProperty(utf8name.c_str()); +} + +inline MaybeOrValue Object::Get(napi_value key) const { + napi_value result; + napi_status status = napi_get_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, result), Value); +} + +inline MaybeOrValue Object::Get(Value key) const { + napi_value result; + napi_status status = napi_get_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, result), Value); +} + +inline MaybeOrValue Object::Get(const char* utf8name) const { + napi_value result; + napi_status status = napi_get_named_property(_env, _value, utf8name, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, result), Value); +} + +inline MaybeOrValue Object::Get(const std::string& utf8name) const { + return Get(utf8name.c_str()); +} + +template +inline MaybeOrValue Object::Set(napi_value key, + const ValueType& value) const { + napi_status status = + napi_set_property(_env, _value, key, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +template +inline MaybeOrValue Object::Set(Value key, const ValueType& value) const { + napi_status status = + napi_set_property(_env, _value, key, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +template +inline MaybeOrValue Object::Set(const char* utf8name, + const ValueType& value) const { + napi_status status = + napi_set_named_property(_env, _value, utf8name, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +template +inline MaybeOrValue Object::Set(const std::string& utf8name, + const ValueType& value) const { + return Set(utf8name.c_str(), value); +} + +inline MaybeOrValue Object::Delete(napi_value key) const { + bool result; + napi_status status = napi_delete_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Delete(Value key) const { + bool result; + napi_status status = napi_delete_property(_env, _value, key, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Delete(const char* utf8name) const { + return Delete(String::New(_env, utf8name)); +} + +inline MaybeOrValue Object::Delete(const std::string& utf8name) const { + return Delete(String::New(_env, utf8name)); +} + +inline MaybeOrValue Object::Has(uint32_t index) const { + bool result; + napi_status status = napi_has_element(_env, _value, index, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::Get(uint32_t index) const { + napi_value value; + napi_status status = napi_get_element(_env, _value, index, &value); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Value(_env, value), Value); +} + +template +inline MaybeOrValue Object::Set(uint32_t index, + const ValueType& value) const { + napi_status status = + napi_set_element(_env, _value, index, Value::From(_env, value)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::Delete(uint32_t index) const { + bool result; + napi_status status = napi_delete_element(_env, _value, index, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +inline MaybeOrValue Object::GetPropertyNames() const { + napi_value result; + napi_status status = napi_get_property_names(_env, _value, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, Array(_env, result), Array); +} + +inline MaybeOrValue Object::DefineProperty( + const PropertyDescriptor& property) const { + napi_status status = napi_define_properties( + _env, + _value, + 1, + reinterpret_cast(&property)); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::DefineProperties( + const std::initializer_list& properties) const { + napi_status status = napi_define_properties( + _env, + _value, + properties.size(), + reinterpret_cast(properties.begin())); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::DefineProperties( + const std::vector& properties) const { + napi_status status = napi_define_properties( + _env, + _value, + properties.size(), + reinterpret_cast(properties.data())); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::InstanceOf( + const Function& constructor) const { + bool result; + napi_status status = napi_instanceof(_env, _value, constructor, &result); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, result, bool); +} + +template +inline void Object::AddFinalizer(Finalizer finalizeCallback, T* data) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = + details::AttachData(_env, + *this, + data, + details::FinalizeData::Wrapper, + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +template +inline void Object::AddFinalizer(Finalizer finalizeCallback, + T* data, + Hint* finalizeHint) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = details::AttachData( + _env, + *this, + data, + details::FinalizeData::WrapperWithHint, + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +#ifdef NAPI_CPP_EXCEPTIONS +inline Object::const_iterator::const_iterator(const Object* object, + const Type type) { + _object = object; + _keys = object->GetPropertyNames(); + _index = type == Type::BEGIN ? 0 : _keys.Length(); +} + +inline Object::const_iterator Napi::Object::begin() const { + const_iterator it(this, Object::const_iterator::Type::BEGIN); + return it; +} + +inline Object::const_iterator Napi::Object::end() const { + const_iterator it(this, Object::const_iterator::Type::END); + return it; +} + +inline Object::const_iterator& Object::const_iterator::operator++() { + ++_index; + return *this; +} + +inline bool Object::const_iterator::operator==( + const const_iterator& other) const { + return _index == other._index; +} + +inline bool Object::const_iterator::operator!=( + const const_iterator& other) const { + return _index != other._index; +} + +inline const std::pair> +Object::const_iterator::operator*() const { + const Value key = _keys[_index]; + const PropertyLValue value = (*_object)[key]; + return {key, value}; +} + +inline Object::iterator::iterator(Object* object, const Type type) { + _object = object; + _keys = object->GetPropertyNames(); + _index = type == Type::BEGIN ? 0 : _keys.Length(); +} + +inline Object::iterator Napi::Object::begin() { + iterator it(this, Object::iterator::Type::BEGIN); + return it; +} + +inline Object::iterator Napi::Object::end() { + iterator it(this, Object::iterator::Type::END); + return it; +} + +inline Object::iterator& Object::iterator::operator++() { + ++_index; + return *this; +} + +inline bool Object::iterator::operator==(const iterator& other) const { + return _index == other._index; +} + +inline bool Object::iterator::operator!=(const iterator& other) const { + return _index != other._index; +} + +inline std::pair> +Object::iterator::operator*() { + Value key = _keys[_index]; + PropertyLValue value = (*_object)[key]; + return {key, value}; +} +#endif // NAPI_CPP_EXCEPTIONS + +#if NAPI_VERSION >= 8 +inline MaybeOrValue Object::Freeze() const { + napi_status status = napi_object_freeze(_env, _value); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} + +inline MaybeOrValue Object::Seal() const { + napi_status status = napi_object_seal(_env, _value); + NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool); +} +#endif // NAPI_VERSION >= 8 + +//////////////////////////////////////////////////////////////////////////////// +// External class +//////////////////////////////////////////////////////////////////////////////// + +template +inline External External::New(napi_env env, T* data) { + napi_value value; + napi_status status = + napi_create_external(env, data, nullptr, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, External()); + return External(env, value); +} + +template +template +inline External External::New(napi_env env, + T* data, + Finalizer finalizeCallback) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = + napi_create_external(env, + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, External()); + } + return External(env, value); +} + +template +template +inline External External::New(napi_env env, + T* data, + Finalizer finalizeCallback, + Hint* finalizeHint) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = napi_create_external( + env, + data, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, External()); + } + return External(env, value); +} + +template +inline void External::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "External::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "External::CheckCast", "napi_typeof failed"); + NAPI_CHECK(type == napi_external, + "External::CheckCast", + "value is not napi_external"); +} + +template +inline External::External() : TypeTaggable() {} + +template +inline External::External(napi_env env, napi_value value) + : TypeTaggable(env, value) {} + +template +inline T* External::Data() const { + void* data; + napi_status status = napi_get_value_external(_env, _value, &data); + NAPI_THROW_IF_FAILED(_env, status, nullptr); + return reinterpret_cast(data); +} + +//////////////////////////////////////////////////////////////////////////////// +// Array class +//////////////////////////////////////////////////////////////////////////////// + +inline Array Array::New(napi_env env) { + napi_value value; + napi_status status = napi_create_array(env, &value); + NAPI_THROW_IF_FAILED(env, status, Array()); + return Array(env, value); +} + +inline Array Array::New(napi_env env, size_t length) { + napi_value value; + napi_status status = napi_create_array_with_length(env, length, &value); + NAPI_THROW_IF_FAILED(env, status, Array()); + return Array(env, value); +} + +inline void Array::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Array::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_array(env, value, &result); + NAPI_CHECK(status == napi_ok, "Array::CheckCast", "napi_is_array failed"); + NAPI_CHECK(result, "Array::CheckCast", "value is not array"); +} + +inline Array::Array() : Object() {} + +inline Array::Array(napi_env env, napi_value value) : Object(env, value) {} + +inline uint32_t Array::Length() const { + uint32_t result; + napi_status status = napi_get_array_length(_env, _value, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +// ArrayBuffer class +//////////////////////////////////////////////////////////////////////////////// + +inline ArrayBuffer ArrayBuffer::New(napi_env env, size_t byteLength) { + napi_value value; + void* data; + napi_status status = napi_create_arraybuffer(env, byteLength, &data, &value); + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + + return ArrayBuffer(env, value); +} + +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +inline ArrayBuffer ArrayBuffer::New(napi_env env, + void* externalData, + size_t byteLength) { + napi_value value; + napi_status status = napi_create_external_arraybuffer( + env, externalData, byteLength, nullptr, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + + return ArrayBuffer(env, value); +} + +template +inline ArrayBuffer ArrayBuffer::New(napi_env env, + void* externalData, + size_t byteLength, + Finalizer finalizeCallback) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = napi_create_external_arraybuffer( + env, + externalData, + byteLength, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + } + + return ArrayBuffer(env, value); +} + +template +inline ArrayBuffer ArrayBuffer::New(napi_env env, + void* externalData, + size_t byteLength, + Finalizer finalizeCallback, + Hint* finalizeHint) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = napi_create_external_arraybuffer( + env, + externalData, + byteLength, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, ArrayBuffer()); + } + + return ArrayBuffer(env, value); +} +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + +inline void ArrayBuffer::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "ArrayBuffer::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_arraybuffer(env, value, &result); + NAPI_CHECK(status == napi_ok, + "ArrayBuffer::CheckCast", + "napi_is_arraybuffer failed"); + NAPI_CHECK(result, "ArrayBuffer::CheckCast", "value is not arraybuffer"); +} + +inline ArrayBuffer::ArrayBuffer() : Object() {} + +inline ArrayBuffer::ArrayBuffer(napi_env env, napi_value value) + : Object(env, value) {} + +inline void* ArrayBuffer::Data() { + void* data; + napi_status status = napi_get_arraybuffer_info(_env, _value, &data, nullptr); + NAPI_THROW_IF_FAILED(_env, status, nullptr); + return data; +} + +inline size_t ArrayBuffer::ByteLength() { + size_t length; + napi_status status = + napi_get_arraybuffer_info(_env, _value, nullptr, &length); + NAPI_THROW_IF_FAILED(_env, status, 0); + return length; +} + +#if NAPI_VERSION >= 7 +inline bool ArrayBuffer::IsDetached() const { + bool detached; + napi_status status = napi_is_detached_arraybuffer(_env, _value, &detached); + NAPI_THROW_IF_FAILED(_env, status, false); + return detached; +} + +inline void ArrayBuffer::Detach() { + napi_status status = napi_detach_arraybuffer(_env, _value); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} +#endif // NAPI_VERSION >= 7 + +//////////////////////////////////////////////////////////////////////////////// +// DataView class +//////////////////////////////////////////////////////////////////////////////// +inline DataView DataView::New(napi_env env, Napi::ArrayBuffer arrayBuffer) { + return New(env, arrayBuffer, 0, arrayBuffer.ByteLength()); +} + +inline DataView DataView::New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset) { + if (byteOffset > arrayBuffer.ByteLength()) { + NAPI_THROW(RangeError::New( + env, "Start offset is outside the bounds of the buffer"), + DataView()); + } + return New( + env, arrayBuffer, byteOffset, arrayBuffer.ByteLength() - byteOffset); +} + +inline DataView DataView::New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset, + size_t byteLength) { + if (byteOffset + byteLength > arrayBuffer.ByteLength()) { + NAPI_THROW(RangeError::New(env, "Invalid DataView length"), DataView()); + } + napi_value value; + napi_status status = + napi_create_dataview(env, byteLength, arrayBuffer, byteOffset, &value); + NAPI_THROW_IF_FAILED(env, status, DataView()); + return DataView(env, value); +} + +inline void DataView::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "DataView::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_dataview(env, value, &result); + NAPI_CHECK( + status == napi_ok, "DataView::CheckCast", "napi_is_dataview failed"); + NAPI_CHECK(result, "DataView::CheckCast", "value is not dataview"); +} + +inline DataView::DataView() : Object() {} + +inline DataView::DataView(napi_env env, napi_value value) : Object(env, value) { + napi_status status = napi_get_dataview_info(_env, + _value /* dataView */, + &_length /* byteLength */, + &_data /* data */, + nullptr /* arrayBuffer */, + nullptr /* byteOffset */); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline Napi::ArrayBuffer DataView::ArrayBuffer() const { + napi_value arrayBuffer; + napi_status status = napi_get_dataview_info(_env, + _value /* dataView */, + nullptr /* byteLength */, + nullptr /* data */, + &arrayBuffer /* arrayBuffer */, + nullptr /* byteOffset */); + NAPI_THROW_IF_FAILED(_env, status, Napi::ArrayBuffer()); + return Napi::ArrayBuffer(_env, arrayBuffer); +} + +inline size_t DataView::ByteOffset() const { + size_t byteOffset; + napi_status status = napi_get_dataview_info(_env, + _value /* dataView */, + nullptr /* byteLength */, + nullptr /* data */, + nullptr /* arrayBuffer */, + &byteOffset /* byteOffset */); + NAPI_THROW_IF_FAILED(_env, status, 0); + return byteOffset; +} + +inline size_t DataView::ByteLength() const { + return _length; +} + +inline void* DataView::Data() const { + return _data; +} + +inline float DataView::GetFloat32(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline double DataView::GetFloat64(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline int8_t DataView::GetInt8(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline int16_t DataView::GetInt16(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline int32_t DataView::GetInt32(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline uint8_t DataView::GetUint8(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline uint16_t DataView::GetUint16(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline uint32_t DataView::GetUint32(size_t byteOffset) const { + return ReadData(byteOffset); +} + +inline void DataView::SetFloat32(size_t byteOffset, float value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetFloat64(size_t byteOffset, double value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetInt8(size_t byteOffset, int8_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetInt16(size_t byteOffset, int16_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetInt32(size_t byteOffset, int32_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetUint8(size_t byteOffset, uint8_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetUint16(size_t byteOffset, uint16_t value) const { + WriteData(byteOffset, value); +} + +inline void DataView::SetUint32(size_t byteOffset, uint32_t value) const { + WriteData(byteOffset, value); +} + +template +inline T DataView::ReadData(size_t byteOffset) const { + if (byteOffset + sizeof(T) > _length || + byteOffset + sizeof(T) < byteOffset) { // overflow + NAPI_THROW( + RangeError::New(_env, "Offset is outside the bounds of the DataView"), + 0); + } + + return *reinterpret_cast(static_cast(_data) + byteOffset); +} + +template +inline void DataView::WriteData(size_t byteOffset, T value) const { + if (byteOffset + sizeof(T) > _length || + byteOffset + sizeof(T) < byteOffset) { // overflow + NAPI_THROW_VOID( + RangeError::New(_env, "Offset is outside the bounds of the DataView")); + } + + *reinterpret_cast(static_cast(_data) + byteOffset) = value; +} + +//////////////////////////////////////////////////////////////////////////////// +// TypedArray class +//////////////////////////////////////////////////////////////////////////////// +inline void TypedArray::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "TypedArray::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_typedarray(env, value, &result); + NAPI_CHECK( + status == napi_ok, "TypedArray::CheckCast", "napi_is_typedarray failed"); + NAPI_CHECK(result, "TypedArray::CheckCast", "value is not typedarray"); +} + +inline TypedArray::TypedArray() + : Object(), _type(napi_typedarray_type::napi_int8_array), _length(0) {} + +inline TypedArray::TypedArray(napi_env env, napi_value value) + : Object(env, value), + _type(napi_typedarray_type::napi_int8_array), + _length(0) { + if (value != nullptr) { + napi_status status = + napi_get_typedarray_info(_env, + _value, + &const_cast(this)->_type, + &const_cast(this)->_length, + nullptr, + nullptr, + nullptr); + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +inline TypedArray::TypedArray(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length) + : Object(env, value), _type(type), _length(length) {} + +inline napi_typedarray_type TypedArray::TypedArrayType() const { + return _type; +} + +inline uint8_t TypedArray::ElementSize() const { + switch (_type) { + case napi_int8_array: + case napi_uint8_array: + case napi_uint8_clamped_array: + return 1; + case napi_int16_array: + case napi_uint16_array: + return 2; + case napi_int32_array: + case napi_uint32_array: + case napi_float32_array: + return 4; + case napi_float64_array: +#if (NAPI_VERSION > 5) + case napi_bigint64_array: + case napi_biguint64_array: +#endif // (NAPI_VERSION > 5) + return 8; + default: + return 0; + } +} + +inline size_t TypedArray::ElementLength() const { + return _length; +} + +inline size_t TypedArray::ByteOffset() const { + size_t byteOffset; + napi_status status = napi_get_typedarray_info( + _env, _value, nullptr, nullptr, nullptr, nullptr, &byteOffset); + NAPI_THROW_IF_FAILED(_env, status, 0); + return byteOffset; +} + +inline size_t TypedArray::ByteLength() const { + return ElementSize() * ElementLength(); +} + +inline Napi::ArrayBuffer TypedArray::ArrayBuffer() const { + napi_value arrayBuffer; + napi_status status = napi_get_typedarray_info( + _env, _value, nullptr, nullptr, nullptr, &arrayBuffer, nullptr); + NAPI_THROW_IF_FAILED(_env, status, Napi::ArrayBuffer()); + return Napi::ArrayBuffer(_env, arrayBuffer); +} + +//////////////////////////////////////////////////////////////////////////////// +// TypedArrayOf class +//////////////////////////////////////////////////////////////////////////////// +template +inline void TypedArrayOf::CheckCast(napi_env env, napi_value value) { + TypedArray::CheckCast(env, value); + napi_typedarray_type type; + napi_status status = napi_get_typedarray_info( + env, value, &type, nullptr, nullptr, nullptr, nullptr); + NAPI_CHECK(status == napi_ok, + "TypedArrayOf::CheckCast", + "napi_is_typedarray failed"); + + NAPI_CHECK( + (type == TypedArrayTypeForPrimitiveType() || + (type == napi_uint8_clamped_array && std::is_same::value)), + "TypedArrayOf::CheckCast", + "Array type must match the template parameter. (Uint8 arrays may " + "optionally have the \"clamped\" array type.)"); +} + +template +inline TypedArrayOf TypedArrayOf::New(napi_env env, + size_t elementLength, + napi_typedarray_type type) { + Napi::ArrayBuffer arrayBuffer = + Napi::ArrayBuffer::New(env, elementLength * sizeof(T)); + return New(env, elementLength, arrayBuffer, 0, type); +} + +template +inline TypedArrayOf TypedArrayOf::New(napi_env env, + size_t elementLength, + Napi::ArrayBuffer arrayBuffer, + size_t bufferOffset, + napi_typedarray_type type) { + napi_value value; + napi_status status = napi_create_typedarray( + env, type, elementLength, arrayBuffer, bufferOffset, &value); + NAPI_THROW_IF_FAILED(env, status, TypedArrayOf()); + + return TypedArrayOf( + env, + value, + type, + elementLength, + reinterpret_cast(reinterpret_cast(arrayBuffer.Data()) + + bufferOffset)); +} + +template +inline TypedArrayOf::TypedArrayOf() : TypedArray(), _data(nullptr) {} + +template +inline TypedArrayOf::TypedArrayOf(napi_env env, napi_value value) + : TypedArray(env, value), _data(nullptr) { + napi_status status = napi_ok; + if (value != nullptr) { + void* data = nullptr; + status = napi_get_typedarray_info( + _env, _value, &_type, &_length, &data, nullptr, nullptr); + _data = static_cast(data); + } else { + _type = TypedArrayTypeForPrimitiveType(); + _length = 0; + } + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +template +inline TypedArrayOf::TypedArrayOf(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length, + T* data) + : TypedArray(env, value, type, length), _data(data) { + if (!(type == TypedArrayTypeForPrimitiveType() || + (type == napi_uint8_clamped_array && + std::is_same::value))) { + NAPI_THROW_VOID(TypeError::New( + env, + "Array type must match the template parameter. " + "(Uint8 arrays may optionally have the \"clamped\" array type.)")); + } +} + +template +inline T& TypedArrayOf::operator[](size_t index) { + return _data[index]; +} + +template +inline const T& TypedArrayOf::operator[](size_t index) const { + return _data[index]; +} + +template +inline T* TypedArrayOf::Data() { + return _data; +} + +template +inline const T* TypedArrayOf::Data() const { + return _data; +} + +//////////////////////////////////////////////////////////////////////////////// +// Function class +//////////////////////////////////////////////////////////////////////////////// + +template +inline napi_status CreateFunction(napi_env env, + const char* utf8name, + napi_callback cb, + CbData* data, + napi_value* result) { + napi_status status = + napi_create_function(env, utf8name, NAPI_AUTO_LENGTH, cb, data, result); + if (status == napi_ok) { + status = Napi::details::AttachData(env, *result, data); + } + + return status; +} + +template +inline Function Function::New(napi_env env, const char* utf8name, void* data) { + napi_value result = nullptr; + napi_status status = napi_create_function(env, + utf8name, + NAPI_AUTO_LENGTH, + details::TemplatedVoidCallback, + data, + &result); + NAPI_THROW_IF_FAILED(env, status, Function()); + return Function(env, result); +} + +template +inline Function Function::New(napi_env env, const char* utf8name, void* data) { + napi_value result = nullptr; + napi_status status = napi_create_function(env, + utf8name, + NAPI_AUTO_LENGTH, + details::TemplatedCallback, + data, + &result); + NAPI_THROW_IF_FAILED(env, status, Function()); + return Function(env, result); +} + +template +inline Function Function::New(napi_env env, + const std::string& utf8name, + void* data) { + return Function::New(env, utf8name.c_str(), data); +} + +template +inline Function Function::New(napi_env env, + const std::string& utf8name, + void* data) { + return Function::New(env, utf8name.c_str(), data); +} + +template +inline Function Function::New(napi_env env, + Callable cb, + const char* utf8name, + void* data) { + using ReturnType = decltype(cb(CallbackInfo(nullptr, nullptr))); + using CbData = details::CallbackData; + auto callbackData = new CbData{std::move(cb), data}; + + napi_value value; + napi_status status = + CreateFunction(env, utf8name, CbData::Wrapper, callbackData, &value); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, Function()); + } + + return Function(env, value); +} + +template +inline Function Function::New(napi_env env, + Callable cb, + const std::string& utf8name, + void* data) { + return New(env, cb, utf8name.c_str(), data); +} + +inline void Function::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Function::CheckCast", "empty value"); + + napi_valuetype type; + napi_status status = napi_typeof(env, value, &type); + NAPI_CHECK(status == napi_ok, "Function::CheckCast", "napi_typeof failed"); + NAPI_CHECK(type == napi_function, + "Function::CheckCast", + "value is not napi_function"); +} + +inline Function::Function() : Object() {} + +inline Function::Function(napi_env env, napi_value value) + : Object(env, value) {} + +inline MaybeOrValue Function::operator()( + const std::initializer_list& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call( + const std::initializer_list& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call( + const std::vector& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call( + const std::vector& args) const { + return Call(Env().Undefined(), args); +} + +inline MaybeOrValue Function::Call(size_t argc, + const napi_value* args) const { + return Call(Env().Undefined(), argc, args); +} + +inline MaybeOrValue Function::Call( + napi_value recv, const std::initializer_list& args) const { + return Call(recv, args.size(), args.begin()); +} + +inline MaybeOrValue Function::Call( + napi_value recv, const std::vector& args) const { + return Call(recv, args.size(), args.data()); +} + +inline MaybeOrValue Function::Call( + napi_value recv, const std::vector& args) const { + const size_t argc = args.size(); + const size_t stackArgsCount = 6; + napi_value stackArgs[stackArgsCount]; + std::vector heapArgs; + napi_value* argv; + if (argc <= stackArgsCount) { + argv = stackArgs; + } else { + heapArgs.resize(argc); + argv = heapArgs.data(); + } + + for (size_t index = 0; index < argc; index++) { + argv[index] = static_cast(args[index]); + } + + return Call(recv, argc, argv); +} + +inline MaybeOrValue Function::Call(napi_value recv, + size_t argc, + const napi_value* args) const { + napi_value result; + napi_status status = + napi_call_function(_env, recv, _value, argc, args, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Value(_env, result), Napi::Value); +} + +inline MaybeOrValue Function::MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context) const { + return MakeCallback(recv, args.size(), args.begin(), context); +} + +inline MaybeOrValue Function::MakeCallback( + napi_value recv, + const std::vector& args, + napi_async_context context) const { + return MakeCallback(recv, args.size(), args.data(), context); +} + +inline MaybeOrValue Function::MakeCallback( + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context) const { + napi_value result; + napi_status status = + napi_make_callback(_env, context, recv, _value, argc, args, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Value(_env, result), Napi::Value); +} + +inline MaybeOrValue Function::New( + const std::initializer_list& args) const { + return New(args.size(), args.begin()); +} + +inline MaybeOrValue Function::New( + const std::vector& args) const { + return New(args.size(), args.data()); +} + +inline MaybeOrValue Function::New(size_t argc, + const napi_value* args) const { + napi_value result; + napi_status status = napi_new_instance(_env, _value, argc, args, &result); + NAPI_RETURN_OR_THROW_IF_FAILED( + _env, status, Napi::Object(_env, result), Napi::Object); +} + +//////////////////////////////////////////////////////////////////////////////// +// Promise class +//////////////////////////////////////////////////////////////////////////////// + +inline Promise::Deferred Promise::Deferred::New(napi_env env) { + return Promise::Deferred(env); +} + +inline Promise::Deferred::Deferred(napi_env env) : _env(env) { + napi_status status = napi_create_promise(_env, &_deferred, &_promise); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline Promise Promise::Deferred::Promise() const { + return Napi::Promise(_env, _promise); +} + +inline Napi::Env Promise::Deferred::Env() const { + return Napi::Env(_env); +} + +inline void Promise::Deferred::Resolve(napi_value value) const { + napi_status status = napi_resolve_deferred(_env, _deferred, value); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline void Promise::Deferred::Reject(napi_value value) const { + napi_status status = napi_reject_deferred(_env, _deferred, value); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline void Promise::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Promise::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_promise(env, value, &result); + NAPI_CHECK(status == napi_ok, "Promise::CheckCast", "napi_is_promise failed"); + NAPI_CHECK(result, "Promise::CheckCast", "value is not promise"); +} + +inline Promise::Promise(napi_env env, napi_value value) : Object(env, value) {} + +//////////////////////////////////////////////////////////////////////////////// +// Buffer class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Buffer Buffer::New(napi_env env, size_t length) { + napi_value value; + void* data; + napi_status status = + napi_create_buffer(env, length * sizeof(T), &data, &value); + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value, length, static_cast(data)); +} + +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +template +inline Buffer Buffer::New(napi_env env, T* data, size_t length) { + napi_value value; + napi_status status = napi_create_external_buffer( + env, length * sizeof(T), data, nullptr, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value, length, data); +} + +template +template +inline Buffer Buffer::New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + napi_status status = + napi_create_external_buffer(env, + length * sizeof(T), + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value, length, data); +} + +template +template +inline Buffer Buffer::New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint) { + napi_value value; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = napi_create_external_buffer( + env, + length * sizeof(T), + data, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value, length, data); +} +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + +template +inline Buffer Buffer::NewOrCopy(napi_env env, T* data, size_t length) { +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + napi_value value; + napi_status status = napi_create_external_buffer( + env, length * sizeof(T), data, nullptr, nullptr, &value); + if (status == details::napi_no_external_buffers_allowed) { +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + // If we can't create an external buffer, we'll just copy the data. + return Buffer::Copy(env, data, length); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + } + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value, length, data); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +} + +template +template +inline Buffer Buffer::NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback) { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + napi_value value; + napi_status status = + napi_create_external_buffer(env, + length * sizeof(T), + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); + if (status == details::napi_no_external_buffers_allowed) { +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + // If we can't create an external buffer, we'll just copy the data. + Buffer ret = Buffer::Copy(env, data, length); + details::FinalizeData::Wrapper(env, data, finalizeData); + return ret; +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + } + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value, length, data); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +} + +template +template +inline Buffer Buffer::NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint) { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + napi_value value; + napi_status status = napi_create_external_buffer( + env, + length * sizeof(T), + data, + details::FinalizeData::WrapperWithHint, + finalizeData, + &value); + if (status == details::napi_no_external_buffers_allowed) { +#endif + // If we can't create an external buffer, we'll just copy the data. + Buffer ret = Buffer::Copy(env, data, length); + details::FinalizeData::WrapperWithHint( + env, data, finalizeData); + return ret; +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + } + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, Buffer()); + } + return Buffer(env, value, length, data); +#endif +} + +template +inline Buffer Buffer::Copy(napi_env env, const T* data, size_t length) { + napi_value value; + napi_status status = + napi_create_buffer_copy(env, length * sizeof(T), data, nullptr, &value); + NAPI_THROW_IF_FAILED(env, status, Buffer()); + return Buffer(env, value); +} + +template +inline void Buffer::CheckCast(napi_env env, napi_value value) { + NAPI_CHECK(value != nullptr, "Buffer::CheckCast", "empty value"); + + bool result; + napi_status status = napi_is_buffer(env, value, &result); + NAPI_CHECK(status == napi_ok, "Buffer::CheckCast", "napi_is_buffer failed"); + NAPI_CHECK(result, "Buffer::CheckCast", "value is not buffer"); +} + +template +inline Buffer::Buffer() : Uint8Array(), _length(0), _data(nullptr) {} + +template +inline Buffer::Buffer(napi_env env, napi_value value) + : Uint8Array(env, value), _length(0), _data(nullptr) {} + +template +inline Buffer::Buffer(napi_env env, napi_value value, size_t length, T* data) + : Uint8Array(env, value), _length(length), _data(data) {} + +template +inline size_t Buffer::Length() const { + EnsureInfo(); + return _length; +} + +template +inline T* Buffer::Data() const { + EnsureInfo(); + return _data; +} + +template +inline void Buffer::EnsureInfo() const { + // The Buffer instance may have been constructed from a napi_value whose + // length/data are not yet known. Fetch and cache these values just once, + // since they can never change during the lifetime of the Buffer. + if (_data == nullptr) { + size_t byteLength; + void* voidData; + napi_status status = + napi_get_buffer_info(_env, _value, &voidData, &byteLength); + NAPI_THROW_IF_FAILED_VOID(_env, status); + _length = byteLength / sizeof(T); + _data = static_cast(voidData); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Error class +//////////////////////////////////////////////////////////////////////////////// + +inline Error Error::New(napi_env env) { + napi_status status; + napi_value error = nullptr; + bool is_exception_pending; + napi_extended_error_info last_error_info_copy; + + { + // We must retrieve the last error info before doing anything else because + // doing anything else will replace the last error info. + const napi_extended_error_info* last_error_info; + status = napi_get_last_error_info(env, &last_error_info); + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_get_last_error_info"); + + // All fields of the `napi_extended_error_info` structure gets reset in + // subsequent Node-API function calls on the same `env`. This includes a + // call to `napi_is_exception_pending()`. So here it is necessary to make a + // copy of the information as the `error_code` field is used later on. + memcpy(&last_error_info_copy, + last_error_info, + sizeof(napi_extended_error_info)); + } + + status = napi_is_exception_pending(env, &is_exception_pending); + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_is_exception_pending"); + + // A pending exception takes precedence over any internal error status. + if (is_exception_pending) { + status = napi_get_and_clear_last_exception(env, &error); + NAPI_FATAL_IF_FAILED( + status, "Error::New", "napi_get_and_clear_last_exception"); + } else { + const char* error_message = last_error_info_copy.error_message != nullptr + ? last_error_info_copy.error_message + : "Error in native callback"; + + napi_value message; + status = napi_create_string_utf8( + env, error_message, std::strlen(error_message), &message); + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_create_string_utf8"); + + switch (last_error_info_copy.error_code) { + case napi_object_expected: + case napi_string_expected: + case napi_boolean_expected: + case napi_number_expected: + status = napi_create_type_error(env, nullptr, message, &error); + break; + default: + status = napi_create_error(env, nullptr, message, &error); + break; + } + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_create_error"); + } + + return Error(env, error); +} + +inline Error Error::New(napi_env env, const char* message) { + return Error::New( + env, message, std::strlen(message), napi_create_error); +} + +inline Error Error::New(napi_env env, const std::string& message) { + return Error::New( + env, message.c_str(), message.size(), napi_create_error); +} + +inline NAPI_NO_RETURN void Error::Fatal(const char* location, + const char* message) { + napi_fatal_error(location, NAPI_AUTO_LENGTH, message, NAPI_AUTO_LENGTH); +} + +inline Error::Error() : ObjectReference() {} + +inline Error::Error(napi_env env, napi_value value) + : ObjectReference(env, nullptr) { + if (value != nullptr) { + // Attempting to create a reference on the error object. + // If it's not a Object/Function/Symbol, this call will return an error + // status. + napi_status status = napi_create_reference(env, value, 1, &_ref); + + if (status != napi_ok) { + napi_value wrappedErrorObj; + + // Create an error object + status = napi_create_object(env, &wrappedErrorObj); + NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_create_object"); + + // property flag that we attach to show the error object is wrapped + napi_property_descriptor wrapObjFlag = { + ERROR_WRAP_VALUE(), // Unique GUID identifier since Symbol isn't a + // viable option + nullptr, + nullptr, + nullptr, + nullptr, + Value::From(env, value), + napi_enumerable, + nullptr}; + + status = napi_define_properties(env, wrappedErrorObj, 1, &wrapObjFlag); + NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_define_properties"); + + // Create a reference on the newly wrapped object + status = napi_create_reference(env, wrappedErrorObj, 1, &_ref); + } + + // Avoid infinite recursion in the failure case. + NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_create_reference"); + } +} + +inline Object Error::Value() const { + if (_ref == nullptr) { + return Object(_env, nullptr); + } + + napi_value refValue; + napi_status status = napi_get_reference_value(_env, _ref, &refValue); + NAPI_THROW_IF_FAILED(_env, status, Object()); + + napi_valuetype type; + status = napi_typeof(_env, refValue, &type); + NAPI_THROW_IF_FAILED(_env, status, Object()); + + // If refValue isn't a symbol, then we proceed to whether the refValue has the + // wrapped error flag + if (type != napi_symbol) { + // We are checking if the object is wrapped + bool isWrappedObject = false; + + status = napi_has_property(_env, + refValue, + String::From(_env, ERROR_WRAP_VALUE()), + &isWrappedObject); + + // Don't care about status + if (isWrappedObject) { + napi_value unwrappedValue; + status = napi_get_property(_env, + refValue, + String::From(_env, ERROR_WRAP_VALUE()), + &unwrappedValue); + NAPI_THROW_IF_FAILED(_env, status, Object()); + + return Object(_env, unwrappedValue); + } + } + + return Object(_env, refValue); +} + +inline Error::Error(Error&& other) : ObjectReference(std::move(other)) {} + +inline Error& Error::operator=(Error&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline Error::Error(const Error& other) : ObjectReference(other) {} + +inline Error& Error::operator=(const Error& other) { + Reset(); + + _env = other.Env(); + HandleScope scope(_env); + + napi_value value = other.Value(); + if (value != nullptr) { + napi_status status = napi_create_reference(_env, value, 1, &_ref); + NAPI_THROW_IF_FAILED(_env, status, *this); + } + + return *this; +} + +inline const std::string& Error::Message() const NAPI_NOEXCEPT { + if (_message.size() == 0 && _env != nullptr) { +#ifdef NAPI_CPP_EXCEPTIONS + try { + _message = Get("message").As(); + } catch (...) { + // Catch all errors here, to include e.g. a std::bad_alloc from + // the std::string::operator=, because this method may not throw. + } +#else // NAPI_CPP_EXCEPTIONS +#if defined(NODE_ADDON_API_ENABLE_MAYBE) + Napi::Value message_val; + if (Get("message").UnwrapTo(&message_val)) { + _message = message_val.As(); + } +#else + _message = Get("message").As(); +#endif +#endif // NAPI_CPP_EXCEPTIONS + } + return _message; +} + +// we created an object on the &_ref +inline void Error::ThrowAsJavaScriptException() const { + HandleScope scope(_env); + if (!IsEmpty()) { +#ifdef NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS + bool pendingException = false; + + // check if there is already a pending exception. If so don't try to throw a + // new one as that is not allowed/possible + napi_status status = napi_is_exception_pending(_env, &pendingException); + + if ((status != napi_ok) || + ((status == napi_ok) && (pendingException == false))) { + // We intentionally don't use `NAPI_THROW_*` macros here to ensure + // that there is no possible recursion as `ThrowAsJavaScriptException` + // is part of `NAPI_THROW_*` macro definition for noexcept. + + status = napi_throw(_env, Value()); + + if (status == napi_pending_exception) { + // The environment must be terminating as we checked earlier and there + // was no pending exception. In this case continuing will result + // in a fatal error and there is nothing the author has done incorrectly + // in their code that is worth flagging through a fatal error + return; + } + } else { + status = napi_pending_exception; + } +#else + // We intentionally don't use `NAPI_THROW_*` macros here to ensure + // that there is no possible recursion as `ThrowAsJavaScriptException` + // is part of `NAPI_THROW_*` macro definition for noexcept. + + napi_status status = napi_throw(_env, Value()); +#endif + +#ifdef NAPI_CPP_EXCEPTIONS + if (status != napi_ok) { + throw Error::New(_env); + } +#else // NAPI_CPP_EXCEPTIONS + NAPI_FATAL_IF_FAILED( + status, "Error::ThrowAsJavaScriptException", "napi_throw"); +#endif // NAPI_CPP_EXCEPTIONS + } +} + +#ifdef NAPI_CPP_EXCEPTIONS + +inline const char* Error::what() const NAPI_NOEXCEPT { + return Message().c_str(); +} + +#endif // NAPI_CPP_EXCEPTIONS + +inline const char* Error::ERROR_WRAP_VALUE() NAPI_NOEXCEPT { + return "4bda9e7e-4913-4dbc-95de-891cbf66598e-errorVal"; +} + +template +inline TError Error::New(napi_env env, + const char* message, + size_t length, + create_error_fn create_error) { + napi_value str; + napi_status status = napi_create_string_utf8(env, message, length, &str); + NAPI_THROW_IF_FAILED(env, status, TError()); + + napi_value error; + status = create_error(env, nullptr, str, &error); + NAPI_THROW_IF_FAILED(env, status, TError()); + + return TError(env, error); +} + +inline TypeError TypeError::New(napi_env env, const char* message) { + return Error::New( + env, message, std::strlen(message), napi_create_type_error); +} + +inline TypeError TypeError::New(napi_env env, const std::string& message) { + return Error::New( + env, message.c_str(), message.size(), napi_create_type_error); +} + +inline TypeError::TypeError() : Error() {} + +inline TypeError::TypeError(napi_env env, napi_value value) + : Error(env, value) {} + +inline RangeError RangeError::New(napi_env env, const char* message) { + return Error::New( + env, message, std::strlen(message), napi_create_range_error); +} + +inline RangeError RangeError::New(napi_env env, const std::string& message) { + return Error::New( + env, message.c_str(), message.size(), napi_create_range_error); +} + +inline RangeError::RangeError() : Error() {} + +inline RangeError::RangeError(napi_env env, napi_value value) + : Error(env, value) {} + +//////////////////////////////////////////////////////////////////////////////// +// Reference class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Reference Reference::New(const T& value, + uint32_t initialRefcount) { + napi_env env = value.Env(); + napi_value val = value; + + if (val == nullptr) { + return Reference(env, nullptr); + } + + napi_ref ref; + napi_status status = napi_create_reference(env, value, initialRefcount, &ref); + NAPI_THROW_IF_FAILED(env, status, Reference()); + + return Reference(env, ref); +} + +template +inline Reference::Reference() + : _env(nullptr), _ref(nullptr), _suppressDestruct(false) {} + +template +inline Reference::Reference(napi_env env, napi_ref ref) + : _env(env), _ref(ref), _suppressDestruct(false) {} + +template +inline Reference::~Reference() { + if (_ref != nullptr) { + if (!_suppressDestruct) { + napi_delete_reference(_env, _ref); + } + + _ref = nullptr; + } +} + +template +inline Reference::Reference(Reference&& other) + : _env(other._env), + _ref(other._ref), + _suppressDestruct(other._suppressDestruct) { + other._env = nullptr; + other._ref = nullptr; + other._suppressDestruct = false; +} + +template +inline Reference& Reference::operator=(Reference&& other) { + Reset(); + _env = other._env; + _ref = other._ref; + _suppressDestruct = other._suppressDestruct; + other._env = nullptr; + other._ref = nullptr; + other._suppressDestruct = false; + return *this; +} + +template +inline Reference::Reference(const Reference& other) + : _env(other._env), _ref(nullptr), _suppressDestruct(false) { + HandleScope scope(_env); + + napi_value value = other.Value(); + if (value != nullptr) { + // Copying is a limited scenario (currently only used for Error object) and + // always creates a strong reference to the given value even if the incoming + // reference is weak. + napi_status status = napi_create_reference(_env, value, 1, &_ref); + NAPI_FATAL_IF_FAILED( + status, "Reference::Reference", "napi_create_reference"); + } +} + +template +inline Reference::operator napi_ref() const { + return _ref; +} + +template +inline bool Reference::operator==(const Reference& other) const { + HandleScope scope(_env); + return this->Value().StrictEquals(other.Value()); +} + +template +inline bool Reference::operator!=(const Reference& other) const { + return !this->operator==(other); +} + +template +inline Napi::Env Reference::Env() const { + return Napi::Env(_env); +} + +template +inline bool Reference::IsEmpty() const { + return _ref == nullptr; +} + +template +inline T Reference::Value() const { + if (_ref == nullptr) { + return T(_env, nullptr); + } + + napi_value value; + napi_status status = napi_get_reference_value(_env, _ref, &value); + NAPI_THROW_IF_FAILED(_env, status, T()); + return T(_env, value); +} + +template +inline uint32_t Reference::Ref() const { + uint32_t result; + napi_status status = napi_reference_ref(_env, _ref, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +template +inline uint32_t Reference::Unref() const { + uint32_t result; + napi_status status = napi_reference_unref(_env, _ref, &result); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +template +inline void Reference::Reset() { + if (_ref != nullptr) { + napi_status status = napi_delete_reference(_env, _ref); + NAPI_THROW_IF_FAILED_VOID(_env, status); + _ref = nullptr; + } +} + +template +inline void Reference::Reset(const T& value, uint32_t refcount) { + Reset(); + _env = value.Env(); + + napi_value val = value; + if (val != nullptr) { + napi_status status = napi_create_reference(_env, value, refcount, &_ref); + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +template +inline void Reference::SuppressDestruct() { + _suppressDestruct = true; +} + +template +inline Reference Weak(T value) { + return Reference::New(value, 0); +} + +inline ObjectReference Weak(Object value) { + return Reference::New(value, 0); +} + +inline FunctionReference Weak(Function value) { + return Reference::New(value, 0); +} + +template +inline Reference Persistent(T value) { + return Reference::New(value, 1); +} + +inline ObjectReference Persistent(Object value) { + return Reference::New(value, 1); +} + +inline FunctionReference Persistent(Function value) { + return Reference::New(value, 1); +} + +//////////////////////////////////////////////////////////////////////////////// +// ObjectReference class +//////////////////////////////////////////////////////////////////////////////// + +inline ObjectReference::ObjectReference() : Reference() {} + +inline ObjectReference::ObjectReference(napi_env env, napi_ref ref) + : Reference(env, ref) {} + +inline ObjectReference::ObjectReference(Reference&& other) + : Reference(std::move(other)) {} + +inline ObjectReference& ObjectReference::operator=(Reference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline ObjectReference::ObjectReference(ObjectReference&& other) + : Reference(std::move(other)) {} + +inline ObjectReference& ObjectReference::operator=(ObjectReference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline ObjectReference::ObjectReference(const ObjectReference& other) + : Reference(other) {} + +inline MaybeOrValue ObjectReference::Get( + const char* utf8name) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Get(utf8name); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue ObjectReference::Get( + const std::string& utf8name) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Get(utf8name); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + napi_value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + Napi::Value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + const char* utf8value) const { + HandleScope scope(_env); + return Value().Set(utf8name, utf8value); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + bool boolValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, boolValue); +} + +inline MaybeOrValue ObjectReference::Set(const char* utf8name, + double numberValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, numberValue); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + napi_value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + Napi::Value value) const { + HandleScope scope(_env); + return Value().Set(utf8name, value); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + std::string& utf8value) const { + HandleScope scope(_env); + return Value().Set(utf8name, utf8value); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + bool boolValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, boolValue); +} + +inline MaybeOrValue ObjectReference::Set(const std::string& utf8name, + double numberValue) const { + HandleScope scope(_env); + return Value().Set(utf8name, numberValue); +} + +inline MaybeOrValue ObjectReference::Get(uint32_t index) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Get(index); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + napi_value value) const { + HandleScope scope(_env); + return Value().Set(index, value); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + Napi::Value value) const { + HandleScope scope(_env); + return Value().Set(index, value); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + const char* utf8value) const { + HandleScope scope(_env); + return Value().Set(index, utf8value); +} + +inline MaybeOrValue ObjectReference::Set( + uint32_t index, const std::string& utf8value) const { + HandleScope scope(_env); + return Value().Set(index, utf8value); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + bool boolValue) const { + HandleScope scope(_env); + return Value().Set(index, boolValue); +} + +inline MaybeOrValue ObjectReference::Set(uint32_t index, + double numberValue) const { + HandleScope scope(_env); + return Value().Set(index, numberValue); +} + +//////////////////////////////////////////////////////////////////////////////// +// FunctionReference class +//////////////////////////////////////////////////////////////////////////////// + +inline FunctionReference::FunctionReference() : Reference() {} + +inline FunctionReference::FunctionReference(napi_env env, napi_ref ref) + : Reference(env, ref) {} + +inline FunctionReference::FunctionReference(Reference&& other) + : Reference(std::move(other)) {} + +inline FunctionReference& FunctionReference::operator=( + Reference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline FunctionReference::FunctionReference(FunctionReference&& other) + : Reference(std::move(other)) {} + +inline FunctionReference& FunctionReference::operator=( + FunctionReference&& other) { + static_cast*>(this)->operator=(std::move(other)); + return *this; +} + +inline MaybeOrValue FunctionReference::operator()( + const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value()(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + const std::vector& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + napi_value recv, const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(recv, args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + napi_value recv, const std::vector& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(recv, args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::Call( + napi_value recv, size_t argc, const napi_value* args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().Call(recv, argc, args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().MakeCallback(recv, args, context); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::MakeCallback( + napi_value recv, + const std::vector& args, + napi_async_context context) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().MakeCallback(recv, args, context); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::MakeCallback( + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = + Value().MakeCallback(recv, argc, args, context); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap())); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Value(); + } + return scope.Escape(result); +#endif +} + +inline MaybeOrValue FunctionReference::New( + const std::initializer_list& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().New(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap()).As()); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Object(); + } + return scope.Escape(result).As(); +#endif +} + +inline MaybeOrValue FunctionReference::New( + const std::vector& args) const { + EscapableHandleScope scope(_env); + MaybeOrValue result = Value().New(args); +#ifdef NODE_ADDON_API_ENABLE_MAYBE + if (result.IsJust()) { + return Just(scope.Escape(result.Unwrap()).As()); + } + return result; +#else + if (scope.Env().IsExceptionPending()) { + return Object(); + } + return scope.Escape(result).As(); +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// CallbackInfo class +//////////////////////////////////////////////////////////////////////////////// + +inline CallbackInfo::CallbackInfo(napi_env env, napi_callback_info info) + : _env(env), + _info(info), + _this(nullptr), + _dynamicArgs(nullptr), + _data(nullptr) { + _argc = _staticArgCount; + _argv = _staticArgs; + napi_status status = + napi_get_cb_info(env, info, &_argc, _argv, &_this, &_data); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + if (_argc > _staticArgCount) { + // Use either a fixed-size array (on the stack) or a dynamically-allocated + // array (on the heap) depending on the number of args. + _dynamicArgs = new napi_value[_argc]; + _argv = _dynamicArgs; + + status = napi_get_cb_info(env, info, &_argc, _argv, nullptr, nullptr); + NAPI_THROW_IF_FAILED_VOID(_env, status); + } +} + +inline CallbackInfo::~CallbackInfo() { + if (_dynamicArgs != nullptr) { + delete[] _dynamicArgs; + } +} + +inline CallbackInfo::operator napi_callback_info() const { + return _info; +} + +inline Value CallbackInfo::NewTarget() const { + napi_value newTarget; + napi_status status = napi_get_new_target(_env, _info, &newTarget); + NAPI_THROW_IF_FAILED(_env, status, Value()); + return Value(_env, newTarget); +} + +inline bool CallbackInfo::IsConstructCall() const { + return !NewTarget().IsEmpty(); +} + +inline Napi::Env CallbackInfo::Env() const { + return Napi::Env(_env); +} + +inline size_t CallbackInfo::Length() const { + return _argc; +} + +inline const Value CallbackInfo::operator[](size_t index) const { + return index < _argc ? Value(_env, _argv[index]) : Env().Undefined(); +} + +inline Value CallbackInfo::This() const { + if (_this == nullptr) { + return Env().Undefined(); + } + return Object(_env, _this); +} + +inline void* CallbackInfo::Data() const { + return _data; +} + +inline void CallbackInfo::SetData(void* data) { + _data = data; +} + +//////////////////////////////////////////////////////////////////////////////// +// PropertyDescriptor class +//////////////////////////////////////////////////////////////////////////////// + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.utf8name = utf8name; + desc.getter = details::TemplatedCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), attributes, data); +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + Name name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.name = name; + desc.getter = details::TemplatedCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.utf8name = utf8name; + desc.getter = details::TemplatedCallback; + desc.setter = details::TemplatedVoidCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + const std::string& utf8name, + napi_property_attributes attributes, + void* data) { + return Accessor(utf8name.c_str(), attributes, data); +} + +template +PropertyDescriptor PropertyDescriptor::Accessor( + Name name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + + desc.name = name; + desc.getter = details::TemplatedCallback; + desc.setter = details::TemplatedVoidCallback; + desc.attributes = attributes; + desc.data = data; + + return desc; +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + napi_property_attributes attributes, + void* data) { + using CbData = details::CallbackData; + auto callbackData = new CbData({getter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes, + void* data) { + return Accessor(env, object, utf8name.c_str(), getter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + napi_property_attributes attributes, + void* data) { + using CbData = details::CallbackData; + auto callbackData = new CbData({getter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::Wrapper, + nullptr, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + using CbData = details::AccessorCallbackData; + auto callbackData = new CbData({getter, setter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + return Accessor( + env, object, utf8name.c_str(), getter, setter, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes, + void* data) { + using CbData = details::AccessorCallbackData; + auto callbackData = new CbData({getter, setter, data}); + + napi_status status = AttachData(env, object, callbackData); + if (status != napi_ok) { + delete callbackData; + NAPI_THROW_IF_FAILED(env, status, napi_property_descriptor()); + } + + return PropertyDescriptor({nullptr, + name, + nullptr, + CbData::GetterWrapper, + CbData::SetterWrapper, + nullptr, + attributes, + callbackData}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Napi::Env env, + Napi::Object /*object*/, + const char* utf8name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + nullptr, + nullptr, + Napi::Function::New(env, cb, utf8name, data), + attributes, + nullptr}); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return Function(env, object, utf8name.c_str(), cb, attributes, data); +} + +template +inline PropertyDescriptor PropertyDescriptor::Function( + Napi::Env env, + Napi::Object /*object*/, + Name name, + Callable cb, + napi_property_attributes attributes, + void* data) { + return PropertyDescriptor({nullptr, + name, + nullptr, + nullptr, + nullptr, + Napi::Function::New(env, cb, nullptr, data), + attributes, + nullptr}); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + const char* utf8name, + napi_value value, + napi_property_attributes attributes) { + return PropertyDescriptor({utf8name, + nullptr, + nullptr, + nullptr, + nullptr, + value, + attributes, + nullptr}); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + const std::string& utf8name, + napi_value value, + napi_property_attributes attributes) { + return Value(utf8name.c_str(), value, attributes); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + napi_value name, napi_value value, napi_property_attributes attributes) { + return PropertyDescriptor( + {nullptr, name, nullptr, nullptr, nullptr, value, attributes, nullptr}); +} + +inline PropertyDescriptor PropertyDescriptor::Value( + Name name, Napi::Value value, napi_property_attributes attributes) { + napi_value nameValue = name; + napi_value valueValue = value; + return PropertyDescriptor::Value(nameValue, valueValue, attributes); +} + +inline PropertyDescriptor::PropertyDescriptor(napi_property_descriptor desc) + : _desc(desc) {} + +inline PropertyDescriptor::operator napi_property_descriptor&() { + return _desc; +} + +inline PropertyDescriptor::operator const napi_property_descriptor&() const { + return _desc; +} + +//////////////////////////////////////////////////////////////////////////////// +// InstanceWrap class +//////////////////////////////////////////////////////////////////////////////// + +template +inline void InstanceWrap::AttachPropData( + napi_env env, napi_value value, const napi_property_descriptor* prop) { + napi_status status; + if (!(prop->attributes & napi_static)) { + if (prop->method == T::InstanceVoidMethodCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED_VOID(env, status); + } else if (prop->method == T::InstanceMethodCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED_VOID(env, status); + } else if (prop->getter == T::InstanceGetterCallbackWrapper || + prop->setter == T::InstanceSetterCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED_VOID(env, status); + } + } +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceVoidMethodCallbackData* callbackData = + new InstanceVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::InstanceVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, + InstanceMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceMethodCallbackData* callbackData = + new InstanceMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::InstanceMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceVoidMethodCallbackData* callbackData = + new InstanceVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::InstanceVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, + InstanceMethodCallback method, + napi_property_attributes attributes, + void* data) { + InstanceMethodCallbackData* callbackData = + new InstanceMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::InstanceMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceVoidMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedInstanceVoidCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedInstanceCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceVoidMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedInstanceVoidCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceMethodCallback method> +inline ClassPropertyDescriptor InstanceWrap::InstanceMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedInstanceCallback; + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + const char* utf8name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes, + void* data) { + InstanceAccessorCallbackData* callbackData = + new InstanceAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = getter != nullptr ? T::InstanceGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::InstanceSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + Symbol name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes, + void* data) { + InstanceAccessorCallbackData* callbackData = + new InstanceAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = getter != nullptr ? T::InstanceGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::InstanceSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceGetterCallback getter, + typename InstanceWrap::InstanceSetterCallback setter> +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = details::TemplatedInstanceCallback; + desc.setter = This::WrapSetter(This::SetterTag()); + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +template ::InstanceGetterCallback getter, + typename InstanceWrap::InstanceSetterCallback setter> +inline ClassPropertyDescriptor InstanceWrap::InstanceAccessor( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = details::TemplatedInstanceCallback; + desc.setter = This::WrapSetter(This::SetterTag()); + desc.data = data; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.value = value; + desc.attributes = attributes; + return desc; +} + +template +inline ClassPropertyDescriptor InstanceWrap::InstanceValue( + Symbol name, Napi::Value value, napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.value = value; + desc.attributes = attributes; + return desc; +} + +template +inline napi_value InstanceWrap::InstanceVoidMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + InstanceVoidMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->callback; + (instance->*cb)(callbackInfo); + return nullptr; + }); +} + +template +inline napi_value InstanceWrap::InstanceMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + InstanceMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->callback; + return (instance->*cb)(callbackInfo); + }); +} + +template +inline napi_value InstanceWrap::InstanceGetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + InstanceAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->getterCallback; + return (instance->*cb)(callbackInfo); + }); +} + +template +inline napi_value InstanceWrap::InstanceSetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + InstanceAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + T* instance = T::Unwrap(callbackInfo.This().As()); + auto cb = callbackData->setterCallback; + (instance->*cb)(callbackInfo, callbackInfo[0]); + return nullptr; + }); +} + +template +template ::InstanceSetterCallback method> +inline napi_value InstanceWrap::WrappedMethod( + napi_env env, napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback([&] { + const CallbackInfo cbInfo(env, info); + T* instance = T::Unwrap(cbInfo.This().As()); + (instance->*method)(cbInfo, cbInfo[0]); + return nullptr; + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// ObjectWrap class +//////////////////////////////////////////////////////////////////////////////// + +template +inline ObjectWrap::ObjectWrap(const Napi::CallbackInfo& callbackInfo) { + napi_env env = callbackInfo.Env(); + napi_value wrapper = callbackInfo.This(); + napi_status status; + napi_ref ref; + T* instance = static_cast(this); + status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref); + NAPI_THROW_IF_FAILED_VOID(env, status); + + Reference* instanceRef = instance; + *instanceRef = Reference(env, ref); +} + +template +inline ObjectWrap::~ObjectWrap() { + // If the JS object still exists at this point, remove the finalizer added + // through `napi_wrap()`. + if (!IsEmpty()) { + Object object = Value(); + // It is not valid to call `napi_remove_wrap()` with an empty `object`. + // This happens e.g. during garbage collection. + if (!object.IsEmpty() && _construction_failed) { + napi_remove_wrap(Env(), object, nullptr); + } + } +} + +template +inline T* ObjectWrap::Unwrap(Object wrapper) { + void* unwrapped; + napi_status status = napi_unwrap(wrapper.Env(), wrapper, &unwrapped); + NAPI_THROW_IF_FAILED(wrapper.Env(), status, nullptr); + return static_cast(unwrapped); +} + +template +inline Function ObjectWrap::DefineClass( + Napi::Env env, + const char* utf8name, + const size_t props_count, + const napi_property_descriptor* descriptors, + void* data) { + napi_status status; + std::vector props(props_count); + + // We copy the descriptors to a local array because before defining the class + // we must replace static method property descriptors with value property + // descriptors such that the value is a function-valued `napi_value` created + // with `CreateFunction()`. + // + // This replacement could be made for instance methods as well, but V8 aborts + // if we do that, because it expects methods defined on the prototype template + // to have `FunctionTemplate`s. + for (size_t index = 0; index < props_count; index++) { + props[index] = descriptors[index]; + napi_property_descriptor* prop = &props[index]; + if (prop->method == T::StaticMethodCallbackWrapper) { + status = + CreateFunction(env, + utf8name, + prop->method, + static_cast(prop->data), + &(prop->value)); + NAPI_THROW_IF_FAILED(env, status, Function()); + prop->method = nullptr; + prop->data = nullptr; + } else if (prop->method == T::StaticVoidMethodCallbackWrapper) { + status = + CreateFunction(env, + utf8name, + prop->method, + static_cast(prop->data), + &(prop->value)); + NAPI_THROW_IF_FAILED(env, status, Function()); + prop->method = nullptr; + prop->data = nullptr; + } + } + + napi_value value; + status = napi_define_class(env, + utf8name, + NAPI_AUTO_LENGTH, + T::ConstructorCallbackWrapper, + data, + props_count, + props.data(), + &value); + NAPI_THROW_IF_FAILED(env, status, Function()); + + // After defining the class we iterate once more over the property descriptors + // and attach the data associated with accessors and instance methods to the + // newly created JavaScript class. + for (size_t idx = 0; idx < props_count; idx++) { + const napi_property_descriptor* prop = &props[idx]; + + if (prop->getter == T::StaticGetterCallbackWrapper || + prop->setter == T::StaticSetterCallbackWrapper) { + status = Napi::details::AttachData( + env, value, static_cast(prop->data)); + NAPI_THROW_IF_FAILED(env, status, Function()); + } else { + // InstanceWrap::AttachPropData is responsible for attaching the data + // of instance methods and accessors. + T::AttachPropData(env, value, prop); + } + } + + return Function(env, value); +} + +template +inline Function ObjectWrap::DefineClass( + Napi::Env env, + const char* utf8name, + const std::initializer_list>& properties, + void* data) { + return DefineClass( + env, + utf8name, + properties.size(), + reinterpret_cast(properties.begin()), + data); +} + +template +inline Function ObjectWrap::DefineClass( + Napi::Env env, + const char* utf8name, + const std::vector>& properties, + void* data) { + return DefineClass( + env, + utf8name, + properties.size(), + reinterpret_cast(properties.data()), + data); +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, + StaticVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticVoidMethodCallbackData* callbackData = + new StaticVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::StaticVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, + StaticMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticMethodCallbackData* callbackData = + new StaticMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = T::StaticMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, + StaticVoidMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticVoidMethodCallbackData* callbackData = + new StaticVoidMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::StaticVoidMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, + StaticMethodCallback method, + napi_property_attributes attributes, + void* data) { + StaticMethodCallbackData* callbackData = + new StaticMethodCallbackData({method, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = T::StaticMethodCallbackWrapper; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticVoidMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedVoidCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticVoidMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedVoidCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.method = details::TemplatedCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticMethodCallback method> +inline ClassPropertyDescriptor ObjectWrap::StaticMethod( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.method = details::TemplatedCallback; + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + const char* utf8name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes, + void* data) { + StaticAccessorCallbackData* callbackData = + new StaticAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = getter != nullptr ? T::StaticGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::StaticSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + Symbol name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes, + void* data) { + StaticAccessorCallbackData* callbackData = + new StaticAccessorCallbackData({getter, setter, data}); + + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = getter != nullptr ? T::StaticGetterCallbackWrapper : nullptr; + desc.setter = setter != nullptr ? T::StaticSetterCallbackWrapper : nullptr; + desc.data = callbackData; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticGetterCallback getter, + typename ObjectWrap::StaticSetterCallback setter> +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + const char* utf8name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.getter = details::TemplatedCallback; + desc.setter = This::WrapStaticSetter(This::StaticSetterTag()); + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +template ::StaticGetterCallback getter, + typename ObjectWrap::StaticSetterCallback setter> +inline ClassPropertyDescriptor ObjectWrap::StaticAccessor( + Symbol name, napi_property_attributes attributes, void* data) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.getter = details::TemplatedCallback; + desc.setter = This::WrapStaticSetter(This::StaticSetterTag()); + desc.data = data; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.utf8name = utf8name; + desc.value = value; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline ClassPropertyDescriptor ObjectWrap::StaticValue( + Symbol name, Napi::Value value, napi_property_attributes attributes) { + napi_property_descriptor desc = napi_property_descriptor(); + desc.name = name; + desc.value = value; + desc.attributes = + static_cast(attributes | napi_static); + return desc; +} + +template +inline Value ObjectWrap::OnCalledAsFunction( + const Napi::CallbackInfo& callbackInfo) { + NAPI_THROW( + TypeError::New(callbackInfo.Env(), + "Class constructors cannot be invoked without 'new'"), + Napi::Value()); +} + +template +inline void ObjectWrap::Finalize(Napi::Env /*env*/) {} + +template +inline napi_value ObjectWrap::ConstructorCallbackWrapper( + napi_env env, napi_callback_info info) { + napi_value new_target; + napi_status status = napi_get_new_target(env, info, &new_target); + if (status != napi_ok) return nullptr; + + bool isConstructCall = (new_target != nullptr); + if (!isConstructCall) { + return details::WrapCallback( + [&] { return T::OnCalledAsFunction(CallbackInfo(env, info)); }); + } + + napi_value wrapper = details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + T* instance = new T(callbackInfo); +#ifdef NAPI_CPP_EXCEPTIONS + instance->_construction_failed = false; +#else + if (callbackInfo.Env().IsExceptionPending()) { + // We need to clear the exception so that removing the wrap might work. + Error e = callbackInfo.Env().GetAndClearPendingException(); + delete instance; + e.ThrowAsJavaScriptException(); + } else { + instance->_construction_failed = false; + } +#endif // NAPI_CPP_EXCEPTIONS + return callbackInfo.This(); + }); + + return wrapper; +} + +template +inline napi_value ObjectWrap::StaticVoidMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + StaticVoidMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->callback(callbackInfo); + return nullptr; + }); +} + +template +inline napi_value ObjectWrap::StaticMethodCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + StaticMethodCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->callback(callbackInfo); + }); +} + +template +inline napi_value ObjectWrap::StaticGetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + StaticAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + return callbackData->getterCallback(callbackInfo); + }); +} + +template +inline napi_value ObjectWrap::StaticSetterCallbackWrapper( + napi_env env, napi_callback_info info) { + return details::WrapCallback([&] { + CallbackInfo callbackInfo(env, info); + StaticAccessorCallbackData* callbackData = + reinterpret_cast(callbackInfo.Data()); + callbackInfo.SetData(callbackData->data); + callbackData->setterCallback(callbackInfo, callbackInfo[0]); + return nullptr; + }); +} + +template +inline void ObjectWrap::FinalizeCallback(napi_env env, + void* data, + void* /*hint*/) { + HandleScope scope(env); + T* instance = static_cast(data); + instance->Finalize(Napi::Env(env)); + delete instance; +} + +template +template ::StaticSetterCallback method> +inline napi_value ObjectWrap::WrappedMethod( + napi_env env, napi_callback_info info) NAPI_NOEXCEPT { + return details::WrapCallback([&] { + const CallbackInfo cbInfo(env, info); + method(cbInfo, cbInfo[0]); + return nullptr; + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// HandleScope class +//////////////////////////////////////////////////////////////////////////////// + +inline HandleScope::HandleScope(napi_env env, napi_handle_scope scope) + : _env(env), _scope(scope) {} + +inline HandleScope::HandleScope(Napi::Env env) : _env(env) { + napi_status status = napi_open_handle_scope(_env, &_scope); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline HandleScope::~HandleScope() { + napi_status status = napi_close_handle_scope(_env, _scope); + NAPI_FATAL_IF_FAILED( + status, "HandleScope::~HandleScope", "napi_close_handle_scope"); +} + +inline HandleScope::operator napi_handle_scope() const { + return _scope; +} + +inline Napi::Env HandleScope::Env() const { + return Napi::Env(_env); +} + +//////////////////////////////////////////////////////////////////////////////// +// EscapableHandleScope class +//////////////////////////////////////////////////////////////////////////////// + +inline EscapableHandleScope::EscapableHandleScope( + napi_env env, napi_escapable_handle_scope scope) + : _env(env), _scope(scope) {} + +inline EscapableHandleScope::EscapableHandleScope(Napi::Env env) : _env(env) { + napi_status status = napi_open_escapable_handle_scope(_env, &_scope); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline EscapableHandleScope::~EscapableHandleScope() { + napi_status status = napi_close_escapable_handle_scope(_env, _scope); + NAPI_FATAL_IF_FAILED(status, + "EscapableHandleScope::~EscapableHandleScope", + "napi_close_escapable_handle_scope"); +} + +inline EscapableHandleScope::operator napi_escapable_handle_scope() const { + return _scope; +} + +inline Napi::Env EscapableHandleScope::Env() const { + return Napi::Env(_env); +} + +inline Value EscapableHandleScope::Escape(napi_value escapee) { + napi_value result; + napi_status status = napi_escape_handle(_env, _scope, escapee, &result); + NAPI_THROW_IF_FAILED(_env, status, Value()); + return Value(_env, result); +} + +#if (NAPI_VERSION > 2) +//////////////////////////////////////////////////////////////////////////////// +// CallbackScope class +//////////////////////////////////////////////////////////////////////////////// + +inline CallbackScope::CallbackScope(napi_env env, napi_callback_scope scope) + : _env(env), _scope(scope) {} + +inline CallbackScope::CallbackScope(napi_env env, napi_async_context context) + : _env(env) { + napi_status status = + napi_open_callback_scope(_env, Object::New(env), context, &_scope); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline CallbackScope::~CallbackScope() { + napi_status status = napi_close_callback_scope(_env, _scope); + NAPI_FATAL_IF_FAILED( + status, "CallbackScope::~CallbackScope", "napi_close_callback_scope"); +} + +inline CallbackScope::operator napi_callback_scope() const { + return _scope; +} + +inline Napi::Env CallbackScope::Env() const { + return Napi::Env(_env); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// AsyncContext class +//////////////////////////////////////////////////////////////////////////////// + +inline AsyncContext::AsyncContext(napi_env env, const char* resource_name) + : AsyncContext(env, resource_name, Object::New(env)) {} + +inline AsyncContext::AsyncContext(napi_env env, + const char* resource_name, + const Object& resource) + : _env(env), _context(nullptr) { + napi_value resource_id; + napi_status status = napi_create_string_utf8( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + status = napi_async_init(_env, resource, resource_id, &_context); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline AsyncContext::~AsyncContext() { + if (_context != nullptr) { + napi_async_destroy(_env, _context); + _context = nullptr; + } +} + +inline AsyncContext::AsyncContext(AsyncContext&& other) { + _env = other._env; + other._env = nullptr; + _context = other._context; + other._context = nullptr; +} + +inline AsyncContext& AsyncContext::operator=(AsyncContext&& other) { + _env = other._env; + other._env = nullptr; + _context = other._context; + other._context = nullptr; + return *this; +} + +inline AsyncContext::operator napi_async_context() const { + return _context; +} + +inline Napi::Env AsyncContext::Env() const { + return Napi::Env(_env); +} + +//////////////////////////////////////////////////////////////////////////////// +// AsyncWorker class +//////////////////////////////////////////////////////////////////////////////// + +#if NAPI_HAS_THREADS + +inline AsyncWorker::AsyncWorker(const Function& callback) + : AsyncWorker(callback, "generic") {} + +inline AsyncWorker::AsyncWorker(const Function& callback, + const char* resource_name) + : AsyncWorker(callback, resource_name, Object::New(callback.Env())) {} + +inline AsyncWorker::AsyncWorker(const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncWorker( + Object::New(callback.Env()), callback, resource_name, resource) {} + +inline AsyncWorker::AsyncWorker(const Object& receiver, + const Function& callback) + : AsyncWorker(receiver, callback, "generic") {} + +inline AsyncWorker::AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name) + : AsyncWorker( + receiver, callback, resource_name, Object::New(callback.Env())) {} + +inline AsyncWorker::AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource) + : _env(callback.Env()), + _receiver(Napi::Persistent(receiver)), + _callback(Napi::Persistent(callback)), + _suppress_destruct(false) { + napi_value resource_id; + napi_status status = napi_create_string_latin1( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + status = napi_create_async_work(_env, + resource, + resource_id, + OnAsyncWorkExecute, + OnAsyncWorkComplete, + this, + &_work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline AsyncWorker::AsyncWorker(Napi::Env env) : AsyncWorker(env, "generic") {} + +inline AsyncWorker::AsyncWorker(Napi::Env env, const char* resource_name) + : AsyncWorker(env, resource_name, Object::New(env)) {} + +inline AsyncWorker::AsyncWorker(Napi::Env env, + const char* resource_name, + const Object& resource) + : _env(env), _receiver(), _callback(), _suppress_destruct(false) { + napi_value resource_id; + napi_status status = napi_create_string_latin1( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + status = napi_create_async_work(_env, + resource, + resource_id, + OnAsyncWorkExecute, + OnAsyncWorkComplete, + this, + &_work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline AsyncWorker::~AsyncWorker() { + if (_work != nullptr) { + napi_delete_async_work(_env, _work); + _work = nullptr; + } +} + +inline void AsyncWorker::Destroy() { + delete this; +} + +inline AsyncWorker::operator napi_async_work() const { + return _work; +} + +inline Napi::Env AsyncWorker::Env() const { + return Napi::Env(_env); +} + +inline void AsyncWorker::Queue() { + napi_status status = napi_queue_async_work(_env, _work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline void AsyncWorker::Cancel() { + napi_status status = napi_cancel_async_work(_env, _work); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline ObjectReference& AsyncWorker::Receiver() { + return _receiver; +} + +inline FunctionReference& AsyncWorker::Callback() { + return _callback; +} + +inline void AsyncWorker::SuppressDestruct() { + _suppress_destruct = true; +} + +inline void AsyncWorker::OnOK() { + if (!_callback.IsEmpty()) { + _callback.Call(_receiver.Value(), GetResult(_callback.Env())); + } +} + +inline void AsyncWorker::OnError(const Error& e) { + if (!_callback.IsEmpty()) { + _callback.Call(_receiver.Value(), + std::initializer_list{e.Value()}); + } +} + +inline void AsyncWorker::SetError(const std::string& error) { + _error = error; +} + +inline std::vector AsyncWorker::GetResult(Napi::Env /*env*/) { + return {}; +} +// The OnAsyncWorkExecute method receives an napi_env argument. However, do NOT +// use it within this method, as it does not run on the JavaScript thread and +// must not run any method that would cause JavaScript to run. In practice, +// this means that almost any use of napi_env will be incorrect. +inline void AsyncWorker::OnAsyncWorkExecute(napi_env env, void* asyncworker) { + AsyncWorker* self = static_cast(asyncworker); + self->OnExecute(env); +} +// The OnExecute method receives an napi_env argument. However, do NOT +// use it within this method, as it does not run on the JavaScript thread and +// must not run any method that would cause JavaScript to run. In practice, +// this means that almost any use of napi_env will be incorrect. +inline void AsyncWorker::OnExecute(Napi::Env /*DO_NOT_USE*/) { +#ifdef NAPI_CPP_EXCEPTIONS + try { + Execute(); + } catch (const std::exception& e) { + SetError(e.what()); + } +#else // NAPI_CPP_EXCEPTIONS + Execute(); +#endif // NAPI_CPP_EXCEPTIONS +} + +inline void AsyncWorker::OnAsyncWorkComplete(napi_env env, + napi_status status, + void* asyncworker) { + AsyncWorker* self = static_cast(asyncworker); + self->OnWorkComplete(env, status); +} +inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { + if (status != napi_cancelled) { + HandleScope scope(_env); + details::WrapCallback([&] { + if (_error.size() == 0) { + OnOK(); + } else { + OnError(Error::New(_env, _error)); + } + return nullptr; + }); + } + if (!_suppress_destruct) { + Destroy(); + } +} + +#endif // NAPI_HAS_THREADS + +#if (NAPI_VERSION > 3 && NAPI_HAS_THREADS) +//////////////////////////////////////////////////////////////////////////////// +// TypedThreadSafeFunction class +//////////////////////////////////////////////////////////////////////////////// + +// Starting with NAPI 5, the JavaScript function `func` parameter of +// `napi_create_threadsafe_function` is optional. +#if NAPI_VERSION > 4 +// static, with Callback [missing] Resource [missing] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [passed] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [missing] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, + nullptr, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [passed] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, + nullptr, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} +#endif + +// static, with Callback [passed] Resource [missing] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + callback, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [passed] Resource [passed] Finalizer [missing] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + TypedThreadSafeFunction tsfn; + + napi_status status = + napi_create_threadsafe_function(env, + callback, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with Callback [passed] Resource [missing] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, + callback, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +// static, with: Callback [passed] Resource [passed] Finalizer [passed] +template +template +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( + napi_env env, + CallbackType callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + TypedThreadSafeFunction tsfn; + + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, + details::DefaultCallbackWrapper< + CallbackType, + TypedThreadSafeFunction>(env, + callback), + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, + CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); + } + + return tsfn; +} + +template +inline TypedThreadSafeFunction:: + TypedThreadSafeFunction() + : _tsfn() {} + +template +inline TypedThreadSafeFunction:: + TypedThreadSafeFunction(napi_threadsafe_function tsfn) + : _tsfn(tsfn) {} + +template +inline TypedThreadSafeFunction:: +operator napi_threadsafe_function() const { + return _tsfn; +} + +template +inline napi_status +TypedThreadSafeFunction::BlockingCall( + DataType* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); +} + +template +inline napi_status +TypedThreadSafeFunction::NonBlockingCall( + DataType* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); +} + +template +inline void TypedThreadSafeFunction::Ref( + napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_ref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +template +inline void TypedThreadSafeFunction::Unref( + napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_unref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +template +inline napi_status +TypedThreadSafeFunction::Acquire() const { + return napi_acquire_threadsafe_function(_tsfn); +} + +template +inline napi_status +TypedThreadSafeFunction::Release() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); +} + +template +inline napi_status +TypedThreadSafeFunction::Abort() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); +} + +template +inline ContextType* +TypedThreadSafeFunction::GetContext() const { + void* context; + napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); + NAPI_FATAL_IF_FAILED(status, + "TypedThreadSafeFunction::GetContext", + "napi_get_threadsafe_function_context"); + return static_cast(context); +} + +// static +template +void TypedThreadSafeFunction::CallJsInternal( + napi_env env, napi_value jsCallback, void* context, void* data) { + details::CallJsWrapper( + env, jsCallback, context, data); +} + +#if NAPI_VERSION == 4 +// static +template +Napi::Function +TypedThreadSafeFunction::EmptyFunctionFactory( + Napi::Env env) { + return Napi::Function::New(env, [](const CallbackInfo& cb) {}); +} + +// static +template +Napi::Function +TypedThreadSafeFunction::FunctionOrEmpty( + Napi::Env env, Napi::Function& callback) { + if (callback.IsEmpty()) { + return EmptyFunctionFactory(env); + } + return callback; +} + +#else +// static +template +std::nullptr_t +TypedThreadSafeFunction::EmptyFunctionFactory( + Napi::Env /*env*/) { + return nullptr; +} + +// static +template +Napi::Function +TypedThreadSafeFunction::FunctionOrEmpty( + Napi::Env /*env*/, Napi::Function& callback) { + return callback; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////// +// ThreadSafeFunction class +//////////////////////////////////////////////////////////////////////////////// + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount) { + return New( + env, callback, Object(), resourceName, maxQueueSize, initialThreadCount); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + context); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + finalizeCallback); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + finalizeCallback, + data); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New(env, + callback, + Object(), + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback, + data); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + static_cast(nullptr) /* context */); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + context, + [](Env, ContextType*) {} /* empty finalizer */); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + static_cast(nullptr) /* context */, + finalizeCallback, + static_cast(nullptr) /* data */, + details::ThreadSafeFinalize::Wrapper); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New(env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + static_cast(nullptr) /* context */, + finalizeCallback, + data, + details::ThreadSafeFinalize:: + FinalizeWrapperWithData); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback) { + return New( + env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback, + static_cast(nullptr) /* data */, + details::ThreadSafeFinalize::FinalizeWrapperWithContext); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { + return New( + env, + callback, + resource, + resourceName, + maxQueueSize, + initialThreadCount, + context, + finalizeCallback, + data, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext); +} + +inline ThreadSafeFunction::ThreadSafeFunction() : _tsfn() {} + +inline ThreadSafeFunction::ThreadSafeFunction(napi_threadsafe_function tsfn) + : _tsfn(tsfn) {} + +inline ThreadSafeFunction::operator napi_threadsafe_function() const { + return _tsfn; +} + +inline napi_status ThreadSafeFunction::BlockingCall() const { + return CallInternal(nullptr, napi_tsfn_blocking); +} + +template <> +inline napi_status ThreadSafeFunction::BlockingCall(void* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); +} + +template +inline napi_status ThreadSafeFunction::BlockingCall(Callback callback) const { + return CallInternal(new CallbackWrapper(callback), napi_tsfn_blocking); +} + +template +inline napi_status ThreadSafeFunction::BlockingCall(DataType* data, + Callback callback) const { + auto wrapper = [data, callback](Env env, Function jsCallback) { + callback(env, jsCallback, data); + }; + return CallInternal(new CallbackWrapper(wrapper), napi_tsfn_blocking); +} + +inline napi_status ThreadSafeFunction::NonBlockingCall() const { + return CallInternal(nullptr, napi_tsfn_nonblocking); +} + +template <> +inline napi_status ThreadSafeFunction::NonBlockingCall(void* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); +} + +template +inline napi_status ThreadSafeFunction::NonBlockingCall( + Callback callback) const { + return CallInternal(new CallbackWrapper(callback), napi_tsfn_nonblocking); +} + +template +inline napi_status ThreadSafeFunction::NonBlockingCall( + DataType* data, Callback callback) const { + auto wrapper = [data, callback](Env env, Function jsCallback) { + callback(env, jsCallback, data); + }; + return CallInternal(new CallbackWrapper(wrapper), napi_tsfn_nonblocking); +} + +inline void ThreadSafeFunction::Ref(napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_ref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +inline void ThreadSafeFunction::Unref(napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_unref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +inline napi_status ThreadSafeFunction::Acquire() const { + return napi_acquire_threadsafe_function(_tsfn); +} + +inline napi_status ThreadSafeFunction::Release() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); +} + +inline napi_status ThreadSafeFunction::Abort() const { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); +} + +inline ThreadSafeFunction::ConvertibleContext ThreadSafeFunction::GetContext() + const { + void* context; + napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); + NAPI_FATAL_IF_FAILED(status, + "ThreadSafeFunction::GetContext", + "napi_get_threadsafe_function_context"); + return ConvertibleContext({context}); +} + +// static +template +inline ThreadSafeFunction ThreadSafeFunction::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper) { + static_assert(details::can_make_string::value || + std::is_convertible::value, + "Resource name should be convertible to the string type"); + + ThreadSafeFunction tsfn; + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = + napi_create_threadsafe_function(env, + callback, + resource, + Value::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, + wrapper, + context, + CallJS, + &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, ThreadSafeFunction()); + } + + return tsfn; +} + +inline napi_status ThreadSafeFunction::CallInternal( + CallbackWrapper* callbackWrapper, + napi_threadsafe_function_call_mode mode) const { + napi_status status = + napi_call_threadsafe_function(_tsfn, callbackWrapper, mode); + if (status != napi_ok && callbackWrapper != nullptr) { + delete callbackWrapper; + } + + return status; +} + +// static +inline void ThreadSafeFunction::CallJS(napi_env env, + napi_value jsCallback, + void* /* context */, + void* data) { + if (env == nullptr && jsCallback == nullptr) { + return; + } + + if (data != nullptr) { + auto* callbackWrapper = static_cast(data); + (*callbackWrapper)(env, Function(env, jsCallback)); + delete callbackWrapper; + } else if (jsCallback != nullptr) { + Function(env, jsCallback).Call({}); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Async Progress Worker Base class +//////////////////////////////////////////////////////////////////////////////// +template +inline AsyncProgressWorkerBase::AsyncProgressWorkerBase( + const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource, + size_t queue_size) + : AsyncWorker(receiver, callback, resource_name, resource) { + // Fill all possible arguments to work around ambiguous + // ThreadSafeFunction::New signatures. + _tsfn = ThreadSafeFunction::New(callback.Env(), + callback, + resource, + resource_name, + queue_size, + /** initialThreadCount */ 1, + /** context */ this, + OnThreadSafeFunctionFinalize, + /** finalizeData */ this); +} + +#if NAPI_VERSION > 4 +template +inline AsyncProgressWorkerBase::AsyncProgressWorkerBase( + Napi::Env env, + const char* resource_name, + const Object& resource, + size_t queue_size) + : AsyncWorker(env, resource_name, resource) { + // TODO: Once the changes to make the callback optional for threadsafe + // functions are available on all versions we can remove the dummy Function + // here. + Function callback; + // Fill all possible arguments to work around ambiguous + // ThreadSafeFunction::New signatures. + _tsfn = ThreadSafeFunction::New(env, + callback, + resource, + resource_name, + queue_size, + /** initialThreadCount */ 1, + /** context */ this, + OnThreadSafeFunctionFinalize, + /** finalizeData */ this); +} +#endif + +template +inline AsyncProgressWorkerBase::~AsyncProgressWorkerBase() { + // Abort pending tsfn call. + // Don't send progress events after we've already completed. + // It's ok to call ThreadSafeFunction::Abort and ThreadSafeFunction::Release + // duplicated. + _tsfn.Abort(); +} + +template +inline void AsyncProgressWorkerBase::OnAsyncWorkProgress( + Napi::Env /* env */, Napi::Function /* jsCallback */, void* data) { + ThreadSafeData* tsd = static_cast(data); + tsd->asyncprogressworker()->OnWorkProgress(tsd->data()); + delete tsd; +} + +template +inline napi_status AsyncProgressWorkerBase::NonBlockingCall( + DataType* data) { + auto tsd = new AsyncProgressWorkerBase::ThreadSafeData(this, data); + auto ret = _tsfn.NonBlockingCall(tsd, OnAsyncWorkProgress); + if (ret != napi_ok) { + delete tsd; + } + return ret; +} + +template +inline void AsyncProgressWorkerBase::OnWorkComplete( + Napi::Env /* env */, napi_status status) { + _work_completed = true; + _complete_status = status; + _tsfn.Release(); +} + +template +inline void AsyncProgressWorkerBase::OnThreadSafeFunctionFinalize( + Napi::Env env, void* /* data */, AsyncProgressWorkerBase* context) { + if (context->_work_completed) { + context->AsyncWorker::OnWorkComplete(env, context->_complete_status); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Async Progress Worker class +//////////////////////////////////////////////////////////////////////////////// +template +inline AsyncProgressWorker::AsyncProgressWorker(const Function& callback) + : AsyncProgressWorker(callback, "generic") {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Function& callback, + const char* resource_name) + : AsyncProgressWorker( + callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncProgressWorker( + Object::New(callback.Env()), callback, resource_name, resource) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Object& receiver, + const Function& callback) + : AsyncProgressWorker(receiver, callback, "generic") {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name) + : AsyncProgressWorker( + receiver, callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncProgressWorkerBase(receiver, callback, resource_name, resource), + _asyncdata(nullptr), + _asyncsize(0), + _signaled(false) {} + +#if NAPI_VERSION > 4 +template +inline AsyncProgressWorker::AsyncProgressWorker(Napi::Env env) + : AsyncProgressWorker(env, "generic") {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(Napi::Env env, + const char* resource_name) + : AsyncProgressWorker(env, resource_name, Object::New(env)) {} + +template +inline AsyncProgressWorker::AsyncProgressWorker(Napi::Env env, + const char* resource_name, + const Object& resource) + : AsyncProgressWorkerBase(env, resource_name, resource), + _asyncdata(nullptr), + _asyncsize(0) {} +#endif + +template +inline AsyncProgressWorker::~AsyncProgressWorker() { + { + std::lock_guard lock(this->_mutex); + _asyncdata = nullptr; + _asyncsize = 0; + } +} + +template +inline void AsyncProgressWorker::Execute() { + ExecutionProgress progress(this); + Execute(progress); +} + +template +inline void AsyncProgressWorker::OnWorkProgress(void*) { + T* data; + size_t size; + bool signaled; + { + std::lock_guard lock(this->_mutex); + data = this->_asyncdata; + size = this->_asyncsize; + signaled = this->_signaled; + this->_asyncdata = nullptr; + this->_asyncsize = 0; + this->_signaled = false; + } + + /** + * The callback of ThreadSafeFunction is not been invoked immediately on the + * callback of uv_async_t (uv io poll), rather the callback of TSFN is + * invoked on the right next uv idle callback. There are chances that during + * the deferring the signal of uv_async_t is been sent again, i.e. potential + * not coalesced two calls of the TSFN callback. + */ + if (data == nullptr && !signaled) { + return; + } + + this->OnProgress(data, size); + delete[] data; +} + +template +inline void AsyncProgressWorker::SendProgress_(const T* data, size_t count) { + T* new_data = new T[count]; + std::copy(data, data + count, new_data); + + T* old_data; + { + std::lock_guard lock(this->_mutex); + old_data = _asyncdata; + _asyncdata = new_data; + _asyncsize = count; + _signaled = false; + } + this->NonBlockingCall(nullptr); + + delete[] old_data; +} + +template +inline void AsyncProgressWorker::Signal() { + { + std::lock_guard lock(this->_mutex); + _signaled = true; + } + this->NonBlockingCall(static_cast(nullptr)); +} + +template +inline void AsyncProgressWorker::ExecutionProgress::Signal() const { + this->_worker->Signal(); +} + +template +inline void AsyncProgressWorker::ExecutionProgress::Send( + const T* data, size_t count) const { + _worker->SendProgress_(data, count); +} + +//////////////////////////////////////////////////////////////////////////////// +// Async Progress Queue Worker class +//////////////////////////////////////////////////////////////////////////////// +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Function& callback) + : AsyncProgressQueueWorker(callback, "generic") {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Function& callback, const char* resource_name) + : AsyncProgressQueueWorker( + callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Function& callback, const char* resource_name, const Object& resource) + : AsyncProgressQueueWorker( + Object::New(callback.Env()), callback, resource_name, resource) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Object& receiver, const Function& callback) + : AsyncProgressQueueWorker(receiver, callback, "generic") {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Object& receiver, const Function& callback, const char* resource_name) + : AsyncProgressQueueWorker( + receiver, callback, resource_name, Object::New(callback.Env())) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource) + : AsyncProgressWorkerBase>( + receiver, + callback, + resource_name, + resource, + /** unlimited queue size */ 0) {} + +#if NAPI_VERSION > 4 +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker(Napi::Env env) + : AsyncProgressQueueWorker(env, "generic") {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + Napi::Env env, const char* resource_name) + : AsyncProgressQueueWorker(env, resource_name, Object::New(env)) {} + +template +inline AsyncProgressQueueWorker::AsyncProgressQueueWorker( + Napi::Env env, const char* resource_name, const Object& resource) + : AsyncProgressWorkerBase>( + env, resource_name, resource, /** unlimited queue size */ 0) {} +#endif + +template +inline void AsyncProgressQueueWorker::Execute() { + ExecutionProgress progress(this); + Execute(progress); +} + +template +inline void AsyncProgressQueueWorker::OnWorkProgress( + std::pair* datapair) { + if (datapair == nullptr) { + return; + } + + T* data = datapair->first; + size_t size = datapair->second; + + this->OnProgress(data, size); + delete datapair; + delete[] data; +} + +template +inline void AsyncProgressQueueWorker::SendProgress_(const T* data, + size_t count) { + T* new_data = new T[count]; + std::copy(data, data + count, new_data); + + auto pair = new std::pair(new_data, count); + this->NonBlockingCall(pair); +} + +template +inline void AsyncProgressQueueWorker::Signal() const { + this->SendProgress_(static_cast(nullptr), 0); +} + +template +inline void AsyncProgressQueueWorker::OnWorkComplete(Napi::Env env, + napi_status status) { + // Draining queued items in TSFN. + AsyncProgressWorkerBase>::OnWorkComplete(env, status); +} + +template +inline void AsyncProgressQueueWorker::ExecutionProgress::Signal() const { + _worker->SendProgress_(static_cast(nullptr), 0); +} + +template +inline void AsyncProgressQueueWorker::ExecutionProgress::Send( + const T* data, size_t count) const { + _worker->SendProgress_(data, count); +} +#endif // NAPI_VERSION > 3 && NAPI_HAS_THREADS + +//////////////////////////////////////////////////////////////////////////////// +// Memory Management class +//////////////////////////////////////////////////////////////////////////////// + +inline int64_t MemoryManagement::AdjustExternalMemory(Env env, + int64_t change_in_bytes) { + int64_t result; + napi_status status = + napi_adjust_external_memory(env, change_in_bytes, &result); + NAPI_THROW_IF_FAILED(env, status, 0); + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +// Version Management class +//////////////////////////////////////////////////////////////////////////////// + +inline uint32_t VersionManagement::GetNapiVersion(Env env) { + uint32_t result; + napi_status status = napi_get_version(env, &result); + NAPI_THROW_IF_FAILED(env, status, 0); + return result; +} + +inline const napi_node_version* VersionManagement::GetNodeVersion(Env env) { + const napi_node_version* result; + napi_status status = napi_get_node_version(env, &result); + NAPI_THROW_IF_FAILED(env, status, 0); + return result; +} + +#if NAPI_VERSION > 5 +//////////////////////////////////////////////////////////////////////////////// +// Addon class +//////////////////////////////////////////////////////////////////////////////// + +template +inline Object Addon::Init(Env env, Object exports) { + T* addon = new T(env, exports); + env.SetInstanceData(addon); + return addon->entry_point_; +} + +template +inline T* Addon::Unwrap(Object wrapper) { + return wrapper.Env().GetInstanceData(); +} + +template +inline void Addon::DefineAddon( + Object exports, const std::initializer_list& props) { + DefineProperties(exports, props); + entry_point_ = exports; +} + +template +inline Napi::Object Addon::DefineProperties( + Object object, const std::initializer_list& props) { + const napi_property_descriptor* properties = + reinterpret_cast(props.begin()); + size_t size = props.size(); + napi_status status = + napi_define_properties(object.Env(), object, size, properties); + NAPI_THROW_IF_FAILED(object.Env(), status, object); + for (size_t idx = 0; idx < size; idx++) + T::AttachPropData(object.Env(), object, &properties[idx]); + return object; +} +#endif // NAPI_VERSION > 5 + +#if NAPI_VERSION > 2 +template +Env::CleanupHook Env::AddCleanupHook(Hook hook, Arg* arg) { + return CleanupHook(*this, hook, arg); +} + +template +Env::CleanupHook Env::AddCleanupHook(Hook hook) { + return CleanupHook(*this, hook); +} + +template +Env::CleanupHook::CleanupHook() { + data = nullptr; +} + +template +Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook) + : wrapper(Env::CleanupHook::Wrapper) { + data = new CleanupData{std::move(hook), nullptr}; + napi_status status = napi_add_env_cleanup_hook(env, wrapper, data); + if (status != napi_ok) { + delete data; + data = nullptr; + } +} + +template +Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook, Arg* arg) + : wrapper(Env::CleanupHook::WrapperWithArg) { + data = new CleanupData{std::move(hook), arg}; + napi_status status = napi_add_env_cleanup_hook(env, wrapper, data); + if (status != napi_ok) { + delete data; + data = nullptr; + } +} + +template +bool Env::CleanupHook::Remove(Env env) { + napi_status status = napi_remove_env_cleanup_hook(env, wrapper, data); + delete data; + data = nullptr; + return status == napi_ok; +} + +template +bool Env::CleanupHook::IsEmpty() const { + return data == nullptr; +} +#endif // NAPI_VERSION > 2 + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +} // namespace NAPI_CPP_CUSTOM_NAMESPACE +#endif + +} // namespace Napi + +#endif // SRC_NAPI_INL_H_ diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/napi.h b/backend/node_modules/sharp/node_modules/node-addon-api/napi.h new file mode 100644 index 00000000..f3119d7d --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/napi.h @@ -0,0 +1,3191 @@ +#ifndef SRC_NAPI_H_ +#define SRC_NAPI_H_ + +#ifndef NAPI_HAS_THREADS +#if !defined(__wasm__) || (defined(__EMSCRIPTEN_PTHREADS__) || \ + (defined(__wasi__) && defined(_REENTRANT))) +#define NAPI_HAS_THREADS 1 +#else +#define NAPI_HAS_THREADS 0 +#endif +#endif + +#include +#include +#include +#include +#if NAPI_HAS_THREADS +#include +#endif // NAPI_HAS_THREADS +#include +#include + +// VS2015 RTM has bugs with constexpr, so require min of VS2015 Update 3 (known +// good version) +#if !defined(_MSC_VER) || _MSC_FULL_VER >= 190024210 +#define NAPI_HAS_CONSTEXPR 1 +#endif + +// VS2013 does not support char16_t literal strings, so we'll work around it +// using wchar_t strings and casting them. This is safe as long as the character +// sizes are the same. +#if defined(_MSC_VER) && _MSC_VER <= 1800 +static_assert(sizeof(char16_t) == sizeof(wchar_t), + "Size mismatch between char16_t and wchar_t"); +#define NAPI_WIDE_TEXT(x) reinterpret_cast(L##x) +#else +#define NAPI_WIDE_TEXT(x) u##x +#endif + +// If C++ exceptions are not explicitly enabled or disabled, enable them +// if exceptions were enabled in the compiler settings. +#if !defined(NAPI_CPP_EXCEPTIONS) && !defined(NAPI_DISABLE_CPP_EXCEPTIONS) +#if defined(_CPPUNWIND) || defined(__EXCEPTIONS) +#define NAPI_CPP_EXCEPTIONS +#else +#error Exception support not detected. \ + Define either NAPI_CPP_EXCEPTIONS or NAPI_DISABLE_CPP_EXCEPTIONS. +#endif +#endif + +// If C++ NAPI_CPP_EXCEPTIONS are enabled, NODE_ADDON_API_ENABLE_MAYBE should +// not be set +#if defined(NAPI_CPP_EXCEPTIONS) && defined(NODE_ADDON_API_ENABLE_MAYBE) +#error NODE_ADDON_API_ENABLE_MAYBE should not be set when \ + NAPI_CPP_EXCEPTIONS is defined. +#endif + +#ifdef _NOEXCEPT +#define NAPI_NOEXCEPT _NOEXCEPT +#else +#define NAPI_NOEXCEPT noexcept +#endif + +#ifdef NAPI_CPP_EXCEPTIONS + +// When C++ exceptions are enabled, Errors are thrown directly. There is no need +// to return anything after the throw statements. The variadic parameter is an +// optional return value that is ignored. +// We need _VOID versions of the macros to avoid warnings resulting from +// leaving the NAPI_THROW_* `...` argument empty. + +#define NAPI_THROW(e, ...) throw e +#define NAPI_THROW_VOID(e) throw e + +#define NAPI_THROW_IF_FAILED(env, status, ...) \ + if ((status) != napi_ok) throw Napi::Error::New(env); + +#define NAPI_THROW_IF_FAILED_VOID(env, status) \ + if ((status) != napi_ok) throw Napi::Error::New(env); + +#else // NAPI_CPP_EXCEPTIONS + +// When C++ exceptions are disabled, Errors are thrown as JavaScript exceptions, +// which are pending until the callback returns to JS. The variadic parameter +// is an optional return value; usually it is an empty result. +// We need _VOID versions of the macros to avoid warnings resulting from +// leaving the NAPI_THROW_* `...` argument empty. + +#define NAPI_THROW(e, ...) \ + do { \ + (e).ThrowAsJavaScriptException(); \ + return __VA_ARGS__; \ + } while (0) + +#define NAPI_THROW_VOID(e) \ + do { \ + (e).ThrowAsJavaScriptException(); \ + return; \ + } while (0) + +#define NAPI_THROW_IF_FAILED(env, status, ...) \ + if ((status) != napi_ok) { \ + Napi::Error::New(env).ThrowAsJavaScriptException(); \ + return __VA_ARGS__; \ + } + +#define NAPI_THROW_IF_FAILED_VOID(env, status) \ + if ((status) != napi_ok) { \ + Napi::Error::New(env).ThrowAsJavaScriptException(); \ + return; \ + } + +#endif // NAPI_CPP_EXCEPTIONS + +#ifdef NODE_ADDON_API_ENABLE_MAYBE +#define NAPI_MAYBE_THROW_IF_FAILED(env, status, type) \ + NAPI_THROW_IF_FAILED(env, status, Napi::Nothing()) + +#define NAPI_RETURN_OR_THROW_IF_FAILED(env, status, result, type) \ + NAPI_MAYBE_THROW_IF_FAILED(env, status, type); \ + return Napi::Just(result); +#else +#define NAPI_MAYBE_THROW_IF_FAILED(env, status, type) \ + NAPI_THROW_IF_FAILED(env, status, type()) + +#define NAPI_RETURN_OR_THROW_IF_FAILED(env, status, result, type) \ + NAPI_MAYBE_THROW_IF_FAILED(env, status, type); \ + return result; +#endif + +#define NAPI_DISALLOW_ASSIGN(CLASS) void operator=(const CLASS&) = delete; +#define NAPI_DISALLOW_COPY(CLASS) CLASS(const CLASS&) = delete; + +#define NAPI_DISALLOW_ASSIGN_COPY(CLASS) \ + NAPI_DISALLOW_ASSIGN(CLASS) \ + NAPI_DISALLOW_COPY(CLASS) + +#define NAPI_CHECK(condition, location, message) \ + do { \ + if (!(condition)) { \ + Napi::Error::Fatal((location), (message)); \ + } \ + } while (0) + +#define NAPI_FATAL_IF_FAILED(status, location, message) \ + NAPI_CHECK((status) == napi_ok, location, message) + +//////////////////////////////////////////////////////////////////////////////// +/// Node-API C++ Wrapper Classes +/// +/// These classes wrap the "Node-API" ABI-stable C APIs for Node.js, providing a +/// C++ object model and C++ exception-handling semantics with low overhead. +/// The wrappers are all header-only so that they do not affect the ABI. +//////////////////////////////////////////////////////////////////////////////// +namespace Napi { + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +// NAPI_CPP_CUSTOM_NAMESPACE can be #define'd per-addon to avoid symbol +// conflicts between different instances of node-addon-api + +// First dummy definition of the namespace to make sure that Napi::(name) still +// refers to the right things inside this file. +namespace NAPI_CPP_CUSTOM_NAMESPACE {} +using namespace NAPI_CPP_CUSTOM_NAMESPACE; + +namespace NAPI_CPP_CUSTOM_NAMESPACE { +#endif + +// Forward declarations +class Env; +class Value; +class Boolean; +class Number; +#if NAPI_VERSION > 5 +class BigInt; +#endif // NAPI_VERSION > 5 +#if (NAPI_VERSION > 4) +class Date; +#endif +class String; +class Object; +class Array; +class ArrayBuffer; +class Function; +class Error; +class PropertyDescriptor; +class CallbackInfo; +class TypedArray; +template +class TypedArrayOf; + +using Int8Array = + TypedArrayOf; ///< Typed-array of signed 8-bit integers +using Uint8Array = + TypedArrayOf; ///< Typed-array of unsigned 8-bit integers +using Int16Array = + TypedArrayOf; ///< Typed-array of signed 16-bit integers +using Uint16Array = + TypedArrayOf; ///< Typed-array of unsigned 16-bit integers +using Int32Array = + TypedArrayOf; ///< Typed-array of signed 32-bit integers +using Uint32Array = + TypedArrayOf; ///< Typed-array of unsigned 32-bit integers +using Float32Array = + TypedArrayOf; ///< Typed-array of 32-bit floating-point values +using Float64Array = + TypedArrayOf; ///< Typed-array of 64-bit floating-point values +#if NAPI_VERSION > 5 +using BigInt64Array = + TypedArrayOf; ///< Typed array of signed 64-bit integers +using BigUint64Array = + TypedArrayOf; ///< Typed array of unsigned 64-bit integers +#endif // NAPI_VERSION > 5 + +/// Defines the signature of a Node-API C++ module's registration callback +/// (init) function. +using ModuleRegisterCallback = Object (*)(Env env, Object exports); + +class MemoryManagement; + +/// A simple Maybe type, representing an object which may or may not have a +/// value. +/// +/// If an API method returns a Maybe<>, the API method can potentially fail +/// either because an exception is thrown, or because an exception is pending, +/// e.g. because a previous API call threw an exception that hasn't been +/// caught yet. In that case, a "Nothing" value is returned. +template +class Maybe { + public: + bool IsNothing() const; + bool IsJust() const; + + /// Short-hand for Unwrap(), which doesn't return a value. Could be used + /// where the actual value of the Maybe is not needed like Object::Set. + /// If this Maybe is nothing (empty), node-addon-api will crash the + /// process. + void Check() const; + + /// Return the value of type T contained in the Maybe. If this Maybe is + /// nothing (empty), node-addon-api will crash the process. + T Unwrap() const; + + /// Return the value of type T contained in the Maybe, or using a default + /// value if this Maybe is nothing (empty). + T UnwrapOr(const T& default_value) const; + + /// Converts this Maybe to a value of type T in the out. If this Maybe is + /// nothing (empty), `false` is returned and `out` is left untouched. + bool UnwrapTo(T* out) const; + + bool operator==(const Maybe& other) const; + bool operator!=(const Maybe& other) const; + + private: + Maybe(); + explicit Maybe(const T& t); + + bool _has_value; + T _value; + + template + friend Maybe Nothing(); + template + friend Maybe Just(const U& u); +}; + +template +inline Maybe Nothing(); + +template +inline Maybe Just(const T& t); + +#if defined(NODE_ADDON_API_ENABLE_MAYBE) +template +using MaybeOrValue = Maybe; +#else +template +using MaybeOrValue = T; +#endif + +/// Environment for Node-API values and operations. +/// +/// All Node-API values and operations must be associated with an environment. +/// An environment instance is always provided to callback functions; that +/// environment must then be used for any creation of Node-API values or other +/// Node-API operations within the callback. (Many methods infer the +/// environment from the `this` instance that the method is called on.) +/// +/// In the future, multiple environments per process may be supported, +/// although current implementations only support one environment per process. +/// +/// In the V8 JavaScript engine, a Node-API environment approximately +/// corresponds to an Isolate. +class Env { + private: + napi_env _env; +#if NAPI_VERSION > 5 + template + static void DefaultFini(Env, T* data); + template + static void DefaultFiniWithHint(Env, DataType* data, HintType* hint); +#endif // NAPI_VERSION > 5 + public: + Env(napi_env env); + + operator napi_env() const; + + Object Global() const; + Value Undefined() const; + Value Null() const; + + bool IsExceptionPending() const; + Error GetAndClearPendingException() const; + + MaybeOrValue RunScript(const char* utf8script) const; + MaybeOrValue RunScript(const std::string& utf8script) const; + MaybeOrValue RunScript(String script) const; + +#if NAPI_VERSION > 2 + template + class CleanupHook; + + template + CleanupHook AddCleanupHook(Hook hook); + + template + CleanupHook AddCleanupHook(Hook hook, Arg* arg); +#endif // NAPI_VERSION > 2 + +#if NAPI_VERSION > 5 + template + T* GetInstanceData() const; + + template + using Finalizer = void (*)(Env, T*); + template fini = Env::DefaultFini> + void SetInstanceData(T* data) const; + + template + using FinalizerWithHint = void (*)(Env, DataType*, HintType*); + template fini = + Env::DefaultFiniWithHint> + void SetInstanceData(DataType* data, HintType* hint) const; +#endif // NAPI_VERSION > 5 + +#if NAPI_VERSION > 2 + template + class CleanupHook { + public: + CleanupHook(); + CleanupHook(Env env, Hook hook, Arg* arg); + CleanupHook(Env env, Hook hook); + bool Remove(Env env); + bool IsEmpty() const; + + private: + static inline void Wrapper(void* data) NAPI_NOEXCEPT; + static inline void WrapperWithArg(void* data) NAPI_NOEXCEPT; + + void (*wrapper)(void* arg); + struct CleanupData { + Hook hook; + Arg* arg; + } * data; + }; +#endif // NAPI_VERSION > 2 +}; + +/// A JavaScript value of unknown type. +/// +/// For type-specific operations, convert to one of the Value subclasses using a +/// `To*` or `As()` method. The `To*` methods do type coercion; the `As()` +/// method does not. +/// +/// Napi::Value value = ... +/// if (!value.IsString()) throw Napi::TypeError::New(env, "Invalid +/// arg..."); Napi::String str = value.As(); // Cast to a +/// string value +/// +/// Napi::Value anotherValue = ... +/// bool isTruthy = anotherValue.ToBoolean(); // Coerce to a boolean value +class Value { + public: + Value(); ///< Creates a new _empty_ Value instance. + Value(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + /// Creates a JS value from a C++ primitive. + /// + /// `value` may be any of: + /// - bool + /// - Any integer type + /// - Any floating point type + /// - const char* (encoded using UTF-8, null-terminated) + /// - const char16_t* (encoded using UTF-16-LE, null-terminated) + /// - std::string (encoded using UTF-8) + /// - std::u16string + /// - napi::Value + /// - napi_value + template + static Value From(napi_env env, const T& value); + + /// Converts to a Node-API value primitive. + /// + /// If the instance is _empty_, this returns `nullptr`. + operator napi_value() const; + + /// Tests if this value strictly equals another value. + bool operator==(const Value& other) const; + + /// Tests if this value does not strictly equal another value. + bool operator!=(const Value& other) const; + + /// Tests if this value strictly equals another value. + bool StrictEquals(const Value& other) const; + + /// Gets the environment the value is associated with. + Napi::Env Env() const; + + /// Checks if the value is empty (uninitialized). + /// + /// An empty value is invalid, and most attempts to perform an operation on an + /// empty value will result in an exception. Note an empty value is distinct + /// from JavaScript `null` or `undefined`, which are valid values. + /// + /// When C++ exceptions are disabled at compile time, a method with a `Value` + /// return type may return an empty value to indicate a pending exception. So + /// when not using C++ exceptions, callers should check whether the value is + /// empty before attempting to use it. + bool IsEmpty() const; + + napi_valuetype Type() const; ///< Gets the type of the value. + + bool IsUndefined() + const; ///< Tests if a value is an undefined JavaScript value. + bool IsNull() const; ///< Tests if a value is a null JavaScript value. + bool IsBoolean() const; ///< Tests if a value is a JavaScript boolean. + bool IsNumber() const; ///< Tests if a value is a JavaScript number. +#if NAPI_VERSION > 5 + bool IsBigInt() const; ///< Tests if a value is a JavaScript bigint. +#endif // NAPI_VERSION > 5 +#if (NAPI_VERSION > 4) + bool IsDate() const; ///< Tests if a value is a JavaScript date. +#endif + bool IsString() const; ///< Tests if a value is a JavaScript string. + bool IsSymbol() const; ///< Tests if a value is a JavaScript symbol. + bool IsArray() const; ///< Tests if a value is a JavaScript array. + bool IsArrayBuffer() + const; ///< Tests if a value is a JavaScript array buffer. + bool IsTypedArray() const; ///< Tests if a value is a JavaScript typed array. + bool IsObject() const; ///< Tests if a value is a JavaScript object. + bool IsFunction() const; ///< Tests if a value is a JavaScript function. + bool IsPromise() const; ///< Tests if a value is a JavaScript promise. + bool IsDataView() const; ///< Tests if a value is a JavaScript data view. + bool IsBuffer() const; ///< Tests if a value is a Node buffer. + bool IsExternal() const; ///< Tests if a value is a pointer to external data. + + /// Casts to another type of `Napi::Value`, when the actual type is known or + /// assumed. + /// + /// This conversion does NOT coerce the type. Calling any methods + /// inappropriate for the actual value type will throw `Napi::Error`. + /// + /// If `NODE_ADDON_API_ENABLE_TYPE_CHECK_ON_AS` is defined, this method + /// asserts that the actual type is the expected type. + template + T As() const; + + MaybeOrValue ToBoolean() + const; ///< Coerces a value to a JavaScript boolean. + MaybeOrValue ToNumber() + const; ///< Coerces a value to a JavaScript number. + MaybeOrValue ToString() + const; ///< Coerces a value to a JavaScript string. + MaybeOrValue ToObject() + const; ///< Coerces a value to a JavaScript object. + + protected: + /// !cond INTERNAL + napi_env _env; + napi_value _value; + /// !endcond +}; + +/// A JavaScript boolean value. +class Boolean : public Value { + public: + static Boolean New(napi_env env, ///< Node-API environment + bool value ///< Boolean value + ); + + static void CheckCast(napi_env env, napi_value value); + + Boolean(); ///< Creates a new _empty_ Boolean instance. + Boolean(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + operator bool() const; ///< Converts a Boolean value to a boolean primitive. + bool Value() const; ///< Converts a Boolean value to a boolean primitive. +}; + +/// A JavaScript number value. +class Number : public Value { + public: + static Number New(napi_env env, ///< Node-API environment + double value ///< Number value + ); + + static void CheckCast(napi_env env, napi_value value); + + Number(); ///< Creates a new _empty_ Number instance. + Number(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + operator int32_t() + const; ///< Converts a Number value to a 32-bit signed integer value. + operator uint32_t() + const; ///< Converts a Number value to a 32-bit unsigned integer value. + operator int64_t() + const; ///< Converts a Number value to a 64-bit signed integer value. + operator float() + const; ///< Converts a Number value to a 32-bit floating-point value. + operator double() + const; ///< Converts a Number value to a 64-bit floating-point value. + + int32_t Int32Value() + const; ///< Converts a Number value to a 32-bit signed integer value. + uint32_t Uint32Value() + const; ///< Converts a Number value to a 32-bit unsigned integer value. + int64_t Int64Value() + const; ///< Converts a Number value to a 64-bit signed integer value. + float FloatValue() + const; ///< Converts a Number value to a 32-bit floating-point value. + double DoubleValue() + const; ///< Converts a Number value to a 64-bit floating-point value. +}; + +#if NAPI_VERSION > 5 +/// A JavaScript bigint value. +class BigInt : public Value { + public: + static BigInt New(napi_env env, ///< Node-API environment + int64_t value ///< Number value + ); + static BigInt New(napi_env env, ///< Node-API environment + uint64_t value ///< Number value + ); + + /// Creates a new BigInt object using a specified sign bit and a + /// specified list of digits/words. + /// The resulting number is calculated as: + /// (-1)^sign_bit * (words[0] * (2^64)^0 + words[1] * (2^64)^1 + ...) + static BigInt New(napi_env env, ///< Node-API environment + int sign_bit, ///< Sign bit. 1 if negative. + size_t word_count, ///< Number of words in array + const uint64_t* words ///< Array of words + ); + + static void CheckCast(napi_env env, napi_value value); + + BigInt(); ///< Creates a new _empty_ BigInt instance. + BigInt(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + int64_t Int64Value(bool* lossless) + const; ///< Converts a BigInt value to a 64-bit signed integer value. + uint64_t Uint64Value(bool* lossless) + const; ///< Converts a BigInt value to a 64-bit unsigned integer value. + + size_t WordCount() const; ///< The number of 64-bit words needed to store + ///< the result of ToWords(). + + /// Writes the contents of this BigInt to a specified memory location. + /// `sign_bit` must be provided and will be set to 1 if this BigInt is + /// negative. + /// `*word_count` has to be initialized to the length of the `words` array. + /// Upon return, it will be set to the actual number of words that would + /// be needed to store this BigInt (i.e. the return value of `WordCount()`). + void ToWords(int* sign_bit, size_t* word_count, uint64_t* words); +}; +#endif // NAPI_VERSION > 5 + +#if (NAPI_VERSION > 4) +/// A JavaScript date value. +class Date : public Value { + public: + /// Creates a new Date value from a double primitive. + static Date New(napi_env env, ///< Node-API environment + double value ///< Number value + ); + + static void CheckCast(napi_env env, napi_value value); + + Date(); ///< Creates a new _empty_ Date instance. + Date(napi_env env, napi_value value); ///< Wraps a Node-API value primitive. + operator double() const; ///< Converts a Date value to double primitive + + double ValueOf() const; ///< Converts a Date value to a double primitive. +}; +#endif + +/// A JavaScript string or symbol value (that can be used as a property name). +class Name : public Value { + public: + static void CheckCast(napi_env env, napi_value value); + + Name(); ///< Creates a new _empty_ Name instance. + Name(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. +}; + +/// A JavaScript string value. +class String : public Name { + public: + /// Creates a new String value from a UTF-8 encoded C++ string. + static String New(napi_env env, ///< Node-API environment + const std::string& value ///< UTF-8 encoded C++ string + ); + + /// Creates a new String value from a UTF-16 encoded C++ string. + static String New(napi_env env, ///< Node-API environment + const std::u16string& value ///< UTF-16 encoded C++ string + ); + + /// Creates a new String value from a UTF-8 encoded C string. + static String New( + napi_env env, ///< Node-API environment + const char* value ///< UTF-8 encoded null-terminated C string + ); + + /// Creates a new String value from a UTF-16 encoded C string. + static String New( + napi_env env, ///< Node-API environment + const char16_t* value ///< UTF-16 encoded null-terminated C string + ); + + /// Creates a new String value from a UTF-8 encoded C string with specified + /// length. + static String New(napi_env env, ///< Node-API environment + const char* value, ///< UTF-8 encoded C string (not + ///< necessarily null-terminated) + size_t length ///< length of the string in bytes + ); + + /// Creates a new String value from a UTF-16 encoded C string with specified + /// length. + static String New( + napi_env env, ///< Node-API environment + const char16_t* value, ///< UTF-16 encoded C string (not necessarily + ///< null-terminated) + size_t length ///< Length of the string in 2-byte code units + ); + + /// Creates a new String based on the original object's type. + /// + /// `value` may be any of: + /// - const char* (encoded using UTF-8, null-terminated) + /// - const char16_t* (encoded using UTF-16-LE, null-terminated) + /// - std::string (encoded using UTF-8) + /// - std::u16string + template + static String From(napi_env env, const T& value); + + static void CheckCast(napi_env env, napi_value value); + + String(); ///< Creates a new _empty_ String instance. + String(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + operator std::string() + const; ///< Converts a String value to a UTF-8 encoded C++ string. + operator std::u16string() + const; ///< Converts a String value to a UTF-16 encoded C++ string. + std::string Utf8Value() + const; ///< Converts a String value to a UTF-8 encoded C++ string. + std::u16string Utf16Value() + const; ///< Converts a String value to a UTF-16 encoded C++ string. +}; + +/// A JavaScript symbol value. +class Symbol : public Name { + public: + /// Creates a new Symbol value with an optional description. + static Symbol New( + napi_env env, ///< Node-API environment + const char* description = + nullptr ///< Optional UTF-8 encoded null-terminated C string + /// describing the symbol + ); + + /// Creates a new Symbol value with a description. + static Symbol New( + napi_env env, ///< Node-API environment + const std::string& + description ///< UTF-8 encoded C++ string describing the symbol + ); + + /// Creates a new Symbol value with a description. + static Symbol New(napi_env env, ///< Node-API environment + String description ///< String value describing the symbol + ); + + /// Creates a new Symbol value with a description. + static Symbol New( + napi_env env, ///< Node-API environment + napi_value description ///< String value describing the symbol + ); + + /// Get a public Symbol (e.g. Symbol.iterator). + static MaybeOrValue WellKnown(napi_env, const std::string& name); + + // Create a symbol in the global registry, UTF-8 Encoded cpp string + static MaybeOrValue For(napi_env env, const std::string& description); + + // Create a symbol in the global registry, C style string (null terminated) + static MaybeOrValue For(napi_env env, const char* description); + + // Create a symbol in the global registry, String value describing the symbol + static MaybeOrValue For(napi_env env, String description); + + // Create a symbol in the global registry, napi_value describing the symbol + static MaybeOrValue For(napi_env env, napi_value description); + + static void CheckCast(napi_env env, napi_value value); + + Symbol(); ///< Creates a new _empty_ Symbol instance. + Symbol(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. +}; + +class TypeTaggable : public Value { + public: +#if NAPI_VERSION >= 8 + void TypeTag(const napi_type_tag* type_tag) const; + bool CheckTypeTag(const napi_type_tag* type_tag) const; +#endif // NAPI_VERSION >= 8 + protected: + TypeTaggable(); + TypeTaggable(napi_env env, napi_value value); +}; + +/// A JavaScript object value. +class Object : public TypeTaggable { + public: + /// Enables property and element assignments using indexing syntax. + /// + /// This is a convenient helper to get and set object properties. As + /// getting and setting object properties may throw with JavaScript + /// exceptions, it is notable that these operations may fail. + /// When NODE_ADDON_API_ENABLE_MAYBE is defined, the process will abort + /// on JavaScript exceptions. + /// + /// Example: + /// + /// Napi::Value propertyValue = object1['A']; + /// object2['A'] = propertyValue; + /// Napi::Value elementValue = array[0]; + /// array[1] = elementValue; + template + class PropertyLValue { + public: + /// Converts an L-value to a value. + operator Value() const; + + /// Assigns a value to the property. The type of value can be + /// anything supported by `Object::Set`. + template + PropertyLValue& operator=(ValueType value); + + private: + PropertyLValue() = delete; + PropertyLValue(Object object, Key key); + napi_env _env; + napi_value _object; + Key _key; + + friend class Napi::Object; + }; + + /// Creates a new Object value. + static Object New(napi_env env ///< Node-API environment + ); + + static void CheckCast(napi_env env, napi_value value); + + Object(); ///< Creates a new _empty_ Object instance. + Object(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + /// Gets or sets a named property. + PropertyLValue operator[]( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ); + + /// Gets or sets a named property. + PropertyLValue operator[]( + const std::string& utf8name ///< UTF-8 encoded property name + ); + + /// Gets or sets an indexed property or array element. + PropertyLValue operator[]( + uint32_t index /// Property / element index + ); + + /// Gets or sets an indexed property or array element. + PropertyLValue operator[](Value index /// Property / element index + ) const; + + /// Gets a named property. + MaybeOrValue operator[]( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Gets a named property. + MaybeOrValue operator[]( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Gets an indexed property or array element. + MaybeOrValue operator[](uint32_t index ///< Property / element index + ) const; + + /// Checks whether a property is present. + MaybeOrValue Has(napi_value key ///< Property key primitive + ) const; + + /// Checks whether a property is present. + MaybeOrValue Has(Value key ///< Property key + ) const; + + /// Checks whether a named property is present. + MaybeOrValue Has( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Checks whether a named property is present. + MaybeOrValue Has( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty(napi_value key ///< Property key primitive + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty(Value key ///< Property key + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Checks whether a own property is present. + MaybeOrValue HasOwnProperty( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Gets a property. + MaybeOrValue Get(napi_value key ///< Property key primitive + ) const; + + /// Gets a property. + MaybeOrValue Get(Value key ///< Property key + ) const; + + /// Gets a named property. + MaybeOrValue Get( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Gets a named property. + MaybeOrValue Get( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Sets a property. + template + MaybeOrValue Set(napi_value key, ///< Property key primitive + const ValueType& value ///< Property value primitive + ) const; + + /// Sets a property. + template + MaybeOrValue Set(Value key, ///< Property key + const ValueType& value ///< Property value + ) const; + + /// Sets a named property. + template + MaybeOrValue Set( + const char* utf8name, ///< UTF-8 encoded null-terminated property name + const ValueType& value) const; + + /// Sets a named property. + template + MaybeOrValue Set( + const std::string& utf8name, ///< UTF-8 encoded property name + const ValueType& value ///< Property value primitive + ) const; + + /// Delete property. + MaybeOrValue Delete(napi_value key ///< Property key primitive + ) const; + + /// Delete property. + MaybeOrValue Delete(Value key ///< Property key + ) const; + + /// Delete property. + MaybeOrValue Delete( + const char* utf8name ///< UTF-8 encoded null-terminated property name + ) const; + + /// Delete property. + MaybeOrValue Delete( + const std::string& utf8name ///< UTF-8 encoded property name + ) const; + + /// Checks whether an indexed property is present. + MaybeOrValue Has(uint32_t index ///< Property / element index + ) const; + + /// Gets an indexed property or array element. + MaybeOrValue Get(uint32_t index ///< Property / element index + ) const; + + /// Sets an indexed property or array element. + template + MaybeOrValue Set(uint32_t index, ///< Property / element index + const ValueType& value ///< Property value primitive + ) const; + + /// Deletes an indexed property or array element. + MaybeOrValue Delete(uint32_t index ///< Property / element index + ) const; + + /// This operation can fail in case of Proxy.[[OwnPropertyKeys]] and + /// Proxy.[[GetOwnProperty]] calling into JavaScript. See: + /// - + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-ownpropertykeys + /// - + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getownproperty-p + MaybeOrValue GetPropertyNames() const; ///< Get all property names + + /// Defines a property on the object. + /// + /// This operation can fail in case of Proxy.[[DefineOwnProperty]] calling + /// into JavaScript. See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc + MaybeOrValue DefineProperty( + const PropertyDescriptor& + property ///< Descriptor for the property to be defined + ) const; + + /// Defines properties on the object. + /// + /// This operation can fail in case of Proxy.[[DefineOwnProperty]] calling + /// into JavaScript. See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc + MaybeOrValue DefineProperties( + const std::initializer_list& properties + ///< List of descriptors for the properties to be defined + ) const; + + /// Defines properties on the object. + /// + /// This operation can fail in case of Proxy.[[DefineOwnProperty]] calling + /// into JavaScript. See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc + MaybeOrValue DefineProperties( + const std::vector& properties + ///< Vector of descriptors for the properties to be defined + ) const; + + /// Checks if an object is an instance created by a constructor function. + /// + /// This is equivalent to the JavaScript `instanceof` operator. + /// + /// This operation can fail in case of Proxy.[[GetPrototypeOf]] calling into + /// JavaScript. + /// See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof + MaybeOrValue InstanceOf( + const Function& constructor ///< Constructor function + ) const; + + template + inline void AddFinalizer(Finalizer finalizeCallback, T* data) const; + + template + inline void AddFinalizer(Finalizer finalizeCallback, + T* data, + Hint* finalizeHint) const; + +#ifdef NAPI_CPP_EXCEPTIONS + class const_iterator; + + inline const_iterator begin() const; + + inline const_iterator end() const; + + class iterator; + + inline iterator begin(); + + inline iterator end(); +#endif // NAPI_CPP_EXCEPTIONS + +#if NAPI_VERSION >= 8 + /// This operation can fail in case of Proxy.[[GetPrototypeOf]] calling into + /// JavaScript. + /// See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof + MaybeOrValue Freeze() const; + /// This operation can fail in case of Proxy.[[GetPrototypeOf]] calling into + /// JavaScript. + /// See + /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof + MaybeOrValue Seal() const; +#endif // NAPI_VERSION >= 8 +}; + +template +class External : public TypeTaggable { + public: + static External New(napi_env env, T* data); + + // Finalizer must implement `void operator()(Env env, T* data)`. + template + static External New(napi_env env, T* data, Finalizer finalizeCallback); + // Finalizer must implement `void operator()(Env env, T* data, Hint* hint)`. + template + static External New(napi_env env, + T* data, + Finalizer finalizeCallback, + Hint* finalizeHint); + + static void CheckCast(napi_env env, napi_value value); + + External(); + External(napi_env env, napi_value value); + + T* Data() const; +}; + +class Array : public Object { + public: + static Array New(napi_env env); + static Array New(napi_env env, size_t length); + + static void CheckCast(napi_env env, napi_value value); + + Array(); + Array(napi_env env, napi_value value); + + uint32_t Length() const; +}; + +#ifdef NAPI_CPP_EXCEPTIONS +class Object::const_iterator { + private: + enum class Type { BEGIN, END }; + + inline const_iterator(const Object* object, const Type type); + + public: + inline const_iterator& operator++(); + + inline bool operator==(const const_iterator& other) const; + + inline bool operator!=(const const_iterator& other) const; + + inline const std::pair> operator*() + const; + + private: + const Napi::Object* _object; + Array _keys; + uint32_t _index; + + friend class Object; +}; + +class Object::iterator { + private: + enum class Type { BEGIN, END }; + + inline iterator(Object* object, const Type type); + + public: + inline iterator& operator++(); + + inline bool operator==(const iterator& other) const; + + inline bool operator!=(const iterator& other) const; + + inline std::pair> operator*(); + + private: + Napi::Object* _object; + Array _keys; + uint32_t _index; + + friend class Object; +}; +#endif // NAPI_CPP_EXCEPTIONS + +/// A JavaScript array buffer value. +class ArrayBuffer : public Object { + public: + /// Creates a new ArrayBuffer instance over a new automatically-allocated + /// buffer. + static ArrayBuffer New( + napi_env env, ///< Node-API environment + size_t byteLength ///< Length of the buffer to be allocated, in bytes + ); + +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + /// Creates a new ArrayBuffer instance, using an external buffer with + /// specified byte length. + static ArrayBuffer New( + napi_env env, ///< Node-API environment + void* externalData, ///< Pointer to the external buffer to be used by + ///< the array + size_t byteLength ///< Length of the external buffer to be used by the + ///< array, in bytes + ); + + /// Creates a new ArrayBuffer instance, using an external buffer with + /// specified byte length. + template + static ArrayBuffer New( + napi_env env, ///< Node-API environment + void* externalData, ///< Pointer to the external buffer to be used by + ///< the array + size_t byteLength, ///< Length of the external buffer to be used by the + ///< array, + /// in bytes + Finalizer finalizeCallback ///< Function to be called when the array + ///< buffer is destroyed; + /// must implement `void operator()(Env env, + /// void* externalData)` + ); + + /// Creates a new ArrayBuffer instance, using an external buffer with + /// specified byte length. + template + static ArrayBuffer New( + napi_env env, ///< Node-API environment + void* externalData, ///< Pointer to the external buffer to be used by + ///< the array + size_t byteLength, ///< Length of the external buffer to be used by the + ///< array, + /// in bytes + Finalizer finalizeCallback, ///< Function to be called when the array + ///< buffer is destroyed; + /// must implement `void operator()(Env + /// env, void* externalData, Hint* hint)` + Hint* finalizeHint ///< Hint (second parameter) to be passed to the + ///< finalize callback + ); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + + static void CheckCast(napi_env env, napi_value value); + + ArrayBuffer(); ///< Creates a new _empty_ ArrayBuffer instance. + ArrayBuffer(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + void* Data(); ///< Gets a pointer to the data buffer. + size_t ByteLength(); ///< Gets the length of the array buffer in bytes. + +#if NAPI_VERSION >= 7 + bool IsDetached() const; + void Detach(); +#endif // NAPI_VERSION >= 7 +}; + +/// A JavaScript typed-array value with unknown array type. +/// +/// For type-specific operations, cast to a `TypedArrayOf` instance using the +/// `As()` method: +/// +/// Napi::TypedArray array = ... +/// if (t.TypedArrayType() == napi_int32_array) { +/// Napi::Int32Array int32Array = t.As(); +/// } +class TypedArray : public Object { + public: + static void CheckCast(napi_env env, napi_value value); + + TypedArray(); ///< Creates a new _empty_ TypedArray instance. + TypedArray(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + napi_typedarray_type TypedArrayType() + const; ///< Gets the type of this typed-array. + Napi::ArrayBuffer ArrayBuffer() const; ///< Gets the backing array buffer. + + uint8_t ElementSize() + const; ///< Gets the size in bytes of one element in the array. + size_t ElementLength() const; ///< Gets the number of elements in the array. + size_t ByteOffset() + const; ///< Gets the offset into the buffer where the array starts. + size_t ByteLength() const; ///< Gets the length of the array in bytes. + + protected: + /// !cond INTERNAL + napi_typedarray_type _type; + size_t _length; + + TypedArray(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length); + + template + static +#if defined(NAPI_HAS_CONSTEXPR) + constexpr +#endif + napi_typedarray_type + TypedArrayTypeForPrimitiveType() { + return std::is_same::value ? napi_int8_array + : std::is_same::value ? napi_uint8_array + : std::is_same::value ? napi_int16_array + : std::is_same::value ? napi_uint16_array + : std::is_same::value ? napi_int32_array + : std::is_same::value ? napi_uint32_array + : std::is_same::value ? napi_float32_array + : std::is_same::value ? napi_float64_array +#if NAPI_VERSION > 5 + : std::is_same::value ? napi_bigint64_array + : std::is_same::value ? napi_biguint64_array +#endif // NAPI_VERSION > 5 + : napi_int8_array; + } + /// !endcond +}; + +/// A JavaScript typed-array value with known array type. +/// +/// Note while it is possible to create and access Uint8 "clamped" arrays using +/// this class, the _clamping_ behavior is only applied in JavaScript. +template +class TypedArrayOf : public TypedArray { + public: + /// Creates a new TypedArray instance over a new automatically-allocated array + /// buffer. + /// + /// The array type parameter can normally be omitted (because it is inferred + /// from the template parameter T), except when creating a "clamped" array: + /// + /// Uint8Array::New(env, length, napi_uint8_clamped_array) + static TypedArrayOf New( + napi_env env, ///< Node-API environment + size_t elementLength, ///< Length of the created array, as a number of + ///< elements +#if defined(NAPI_HAS_CONSTEXPR) + napi_typedarray_type type = + TypedArray::TypedArrayTypeForPrimitiveType() +#else + napi_typedarray_type type +#endif + ///< Type of array, if different from the default array type for the + ///< template parameter T. + ); + + /// Creates a new TypedArray instance over a provided array buffer. + /// + /// The array type parameter can normally be omitted (because it is inferred + /// from the template parameter T), except when creating a "clamped" array: + /// + /// Uint8Array::New(env, length, buffer, 0, napi_uint8_clamped_array) + static TypedArrayOf New( + napi_env env, ///< Node-API environment + size_t elementLength, ///< Length of the created array, as a number of + ///< elements + Napi::ArrayBuffer arrayBuffer, ///< Backing array buffer instance to use + size_t bufferOffset, ///< Offset into the array buffer where the + ///< typed-array starts +#if defined(NAPI_HAS_CONSTEXPR) + napi_typedarray_type type = + TypedArray::TypedArrayTypeForPrimitiveType() +#else + napi_typedarray_type type +#endif + ///< Type of array, if different from the default array type for the + ///< template parameter T. + ); + + static void CheckCast(napi_env env, napi_value value); + + TypedArrayOf(); ///< Creates a new _empty_ TypedArrayOf instance. + TypedArrayOf(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + T& operator[](size_t index); ///< Gets or sets an element in the array. + const T& operator[](size_t index) const; ///< Gets an element in the array. + + /// Gets a pointer to the array's backing buffer. + /// + /// This is not necessarily the same as the `ArrayBuffer::Data()` pointer, + /// because the typed-array may have a non-zero `ByteOffset()` into the + /// `ArrayBuffer`. + T* Data(); + + /// Gets a pointer to the array's backing buffer. + /// + /// This is not necessarily the same as the `ArrayBuffer::Data()` pointer, + /// because the typed-array may have a non-zero `ByteOffset()` into the + /// `ArrayBuffer`. + const T* Data() const; + + private: + T* _data; + + TypedArrayOf(napi_env env, + napi_value value, + napi_typedarray_type type, + size_t length, + T* data); +}; + +/// The DataView provides a low-level interface for reading/writing multiple +/// number types in an ArrayBuffer irrespective of the platform's endianness. +class DataView : public Object { + public: + static DataView New(napi_env env, Napi::ArrayBuffer arrayBuffer); + static DataView New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset); + static DataView New(napi_env env, + Napi::ArrayBuffer arrayBuffer, + size_t byteOffset, + size_t byteLength); + + static void CheckCast(napi_env env, napi_value value); + + DataView(); ///< Creates a new _empty_ DataView instance. + DataView(napi_env env, + napi_value value); ///< Wraps a Node-API value primitive. + + Napi::ArrayBuffer ArrayBuffer() const; ///< Gets the backing array buffer. + size_t ByteOffset() + const; ///< Gets the offset into the buffer where the array starts. + size_t ByteLength() const; ///< Gets the length of the array in bytes. + + void* Data() const; + + float GetFloat32(size_t byteOffset) const; + double GetFloat64(size_t byteOffset) const; + int8_t GetInt8(size_t byteOffset) const; + int16_t GetInt16(size_t byteOffset) const; + int32_t GetInt32(size_t byteOffset) const; + uint8_t GetUint8(size_t byteOffset) const; + uint16_t GetUint16(size_t byteOffset) const; + uint32_t GetUint32(size_t byteOffset) const; + + void SetFloat32(size_t byteOffset, float value) const; + void SetFloat64(size_t byteOffset, double value) const; + void SetInt8(size_t byteOffset, int8_t value) const; + void SetInt16(size_t byteOffset, int16_t value) const; + void SetInt32(size_t byteOffset, int32_t value) const; + void SetUint8(size_t byteOffset, uint8_t value) const; + void SetUint16(size_t byteOffset, uint16_t value) const; + void SetUint32(size_t byteOffset, uint32_t value) const; + + private: + template + T ReadData(size_t byteOffset) const; + + template + void WriteData(size_t byteOffset, T value) const; + + void* _data; + size_t _length; +}; + +class Function : public Object { + public: + using VoidCallback = void (*)(const CallbackInfo& info); + using Callback = Value (*)(const CallbackInfo& info); + + template + static Function New(napi_env env, + const char* utf8name = nullptr, + void* data = nullptr); + + template + static Function New(napi_env env, + const char* utf8name = nullptr, + void* data = nullptr); + + template + static Function New(napi_env env, + const std::string& utf8name, + void* data = nullptr); + + template + static Function New(napi_env env, + const std::string& utf8name, + void* data = nullptr); + + /// Callable must implement operator() accepting a const CallbackInfo& + /// and return either void or Value. + template + static Function New(napi_env env, + Callable cb, + const char* utf8name = nullptr, + void* data = nullptr); + /// Callable must implement operator() accepting a const CallbackInfo& + /// and return either void or Value. + template + static Function New(napi_env env, + Callable cb, + const std::string& utf8name, + void* data = nullptr); + + static void CheckCast(napi_env env, napi_value value); + + Function(); + Function(napi_env env, napi_value value); + + MaybeOrValue operator()( + const std::initializer_list& args) const; + + MaybeOrValue Call(const std::initializer_list& args) const; + MaybeOrValue Call(const std::vector& args) const; + MaybeOrValue Call(const std::vector& args) const; + MaybeOrValue Call(size_t argc, const napi_value* args) const; + MaybeOrValue Call(napi_value recv, + const std::initializer_list& args) const; + MaybeOrValue Call(napi_value recv, + const std::vector& args) const; + MaybeOrValue Call(napi_value recv, + const std::vector& args) const; + MaybeOrValue Call(napi_value recv, + size_t argc, + const napi_value* args) const; + + MaybeOrValue MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback(napi_value recv, + const std::vector& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback(napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context = nullptr) const; + + MaybeOrValue New(const std::initializer_list& args) const; + MaybeOrValue New(const std::vector& args) const; + MaybeOrValue New(size_t argc, const napi_value* args) const; +}; + +class Promise : public Object { + public: + class Deferred { + public: + static Deferred New(napi_env env); + Deferred(napi_env env); + + Napi::Promise Promise() const; + Napi::Env Env() const; + + void Resolve(napi_value value) const; + void Reject(napi_value value) const; + + private: + napi_env _env; + napi_deferred _deferred; + napi_value _promise; + }; + + static void CheckCast(napi_env env, napi_value value); + + Promise(napi_env env, napi_value value); +}; + +template +class Buffer : public Uint8Array { + public: + static Buffer New(napi_env env, size_t length); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + static Buffer New(napi_env env, T* data, size_t length); + + // Finalizer must implement `void operator()(Env env, T* data)`. + template + static Buffer New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback); + // Finalizer must implement `void operator()(Env env, T* data, Hint* hint)`. + template + static Buffer New(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + + static Buffer NewOrCopy(napi_env env, T* data, size_t length); + // Finalizer must implement `void operator()(Env env, T* data)`. + template + static Buffer NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback); + // Finalizer must implement `void operator()(Env env, T* data, Hint* hint)`. + template + static Buffer NewOrCopy(napi_env env, + T* data, + size_t length, + Finalizer finalizeCallback, + Hint* finalizeHint); + + static Buffer Copy(napi_env env, const T* data, size_t length); + + static void CheckCast(napi_env env, napi_value value); + + Buffer(); + Buffer(napi_env env, napi_value value); + size_t Length() const; + T* Data() const; + + private: + mutable size_t _length; + mutable T* _data; + + Buffer(napi_env env, napi_value value, size_t length, T* data); + void EnsureInfo() const; +}; + +/// Holds a counted reference to a value; initially a weak reference unless +/// otherwise specified, may be changed to/from a strong reference by adjusting +/// the refcount. +/// +/// The referenced value is not immediately destroyed when the reference count +/// is zero; it is merely then eligible for garbage-collection if there are no +/// other references to the value. +template +class Reference { + public: + static Reference New(const T& value, uint32_t initialRefcount = 0); + + Reference(); + Reference(napi_env env, napi_ref ref); + ~Reference(); + + // A reference can be moved but cannot be copied. + Reference(Reference&& other); + Reference& operator=(Reference&& other); + NAPI_DISALLOW_ASSIGN(Reference) + + operator napi_ref() const; + bool operator==(const Reference& other) const; + bool operator!=(const Reference& other) const; + + Napi::Env Env() const; + bool IsEmpty() const; + + // Note when getting the value of a Reference it is usually correct to do so + // within a HandleScope so that the value handle gets cleaned up efficiently. + T Value() const; + + uint32_t Ref() const; + uint32_t Unref() const; + void Reset(); + void Reset(const T& value, uint32_t refcount = 0); + + // Call this on a reference that is declared as static data, to prevent its + // destructor from running at program shutdown time, which would attempt to + // reset the reference when the environment is no longer valid. Avoid using + // this if at all possible. If you do need to use static data, MAKE SURE to + // warn your users that your addon is NOT threadsafe. + void SuppressDestruct(); + + protected: + Reference(const Reference&); + + /// !cond INTERNAL + napi_env _env; + napi_ref _ref; + /// !endcond + + private: + bool _suppressDestruct; +}; + +class ObjectReference : public Reference { + public: + ObjectReference(); + ObjectReference(napi_env env, napi_ref ref); + + // A reference can be moved but cannot be copied. + ObjectReference(Reference&& other); + ObjectReference& operator=(Reference&& other); + ObjectReference(ObjectReference&& other); + ObjectReference& operator=(ObjectReference&& other); + NAPI_DISALLOW_ASSIGN(ObjectReference) + + MaybeOrValue Get(const char* utf8name) const; + MaybeOrValue Get(const std::string& utf8name) const; + MaybeOrValue Set(const char* utf8name, napi_value value) const; + MaybeOrValue Set(const char* utf8name, Napi::Value value) const; + MaybeOrValue Set(const char* utf8name, const char* utf8value) const; + MaybeOrValue Set(const char* utf8name, bool boolValue) const; + MaybeOrValue Set(const char* utf8name, double numberValue) const; + MaybeOrValue Set(const std::string& utf8name, napi_value value) const; + MaybeOrValue Set(const std::string& utf8name, Napi::Value value) const; + MaybeOrValue Set(const std::string& utf8name, + std::string& utf8value) const; + MaybeOrValue Set(const std::string& utf8name, bool boolValue) const; + MaybeOrValue Set(const std::string& utf8name, double numberValue) const; + + MaybeOrValue Get(uint32_t index) const; + MaybeOrValue Set(uint32_t index, const napi_value value) const; + MaybeOrValue Set(uint32_t index, const Napi::Value value) const; + MaybeOrValue Set(uint32_t index, const char* utf8value) const; + MaybeOrValue Set(uint32_t index, const std::string& utf8value) const; + MaybeOrValue Set(uint32_t index, bool boolValue) const; + MaybeOrValue Set(uint32_t index, double numberValue) const; + + protected: + ObjectReference(const ObjectReference&); +}; + +class FunctionReference : public Reference { + public: + FunctionReference(); + FunctionReference(napi_env env, napi_ref ref); + + // A reference can be moved but cannot be copied. + FunctionReference(Reference&& other); + FunctionReference& operator=(Reference&& other); + FunctionReference(FunctionReference&& other); + FunctionReference& operator=(FunctionReference&& other); + NAPI_DISALLOW_ASSIGN_COPY(FunctionReference) + + MaybeOrValue operator()( + const std::initializer_list& args) const; + + MaybeOrValue Call( + const std::initializer_list& args) const; + MaybeOrValue Call(const std::vector& args) const; + MaybeOrValue Call( + napi_value recv, const std::initializer_list& args) const; + MaybeOrValue Call(napi_value recv, + const std::vector& args) const; + MaybeOrValue Call(napi_value recv, + size_t argc, + const napi_value* args) const; + + MaybeOrValue MakeCallback( + napi_value recv, + const std::initializer_list& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback( + napi_value recv, + const std::vector& args, + napi_async_context context = nullptr) const; + MaybeOrValue MakeCallback( + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context = nullptr) const; + + MaybeOrValue New(const std::initializer_list& args) const; + MaybeOrValue New(const std::vector& args) const; +}; + +// Shortcuts to creating a new reference with inferred type and refcount = 0. +template +Reference Weak(T value); +ObjectReference Weak(Object value); +FunctionReference Weak(Function value); + +// Shortcuts to creating a new reference with inferred type and refcount = 1. +template +Reference Persistent(T value); +ObjectReference Persistent(Object value); +FunctionReference Persistent(Function value); + +/// A persistent reference to a JavaScript error object. Use of this class +/// depends somewhat on whether C++ exceptions are enabled at compile time. +/// +/// ### Handling Errors With C++ Exceptions +/// +/// If C++ exceptions are enabled, then the `Error` class extends +/// `std::exception` and enables integrated error-handling for C++ exceptions +/// and JavaScript exceptions. +/// +/// If a Node-API call fails without executing any JavaScript code (for +/// example due to an invalid argument), then the Node-API wrapper +/// automatically converts and throws the error as a C++ exception of type +/// `Napi::Error`. Or if a JavaScript function called by C++ code via Node-API +/// throws a JavaScript exception, then the Node-API wrapper automatically +/// converts and throws it as a C++ exception of type `Napi::Error`. +/// +/// If a C++ exception of type `Napi::Error` escapes from a Node-API C++ +/// callback, then the Node-API wrapper automatically converts and throws it +/// as a JavaScript exception. Therefore, catching a C++ exception of type +/// `Napi::Error` prevents a JavaScript exception from being thrown. +/// +/// #### Example 1A - Throwing a C++ exception: +/// +/// Napi::Env env = ... +/// throw Napi::Error::New(env, "Example exception"); +/// +/// Following C++ statements will not be executed. The exception will bubble +/// up as a C++ exception of type `Napi::Error`, until it is either caught +/// while still in C++, or else automatically propataged as a JavaScript +/// exception when the callback returns to JavaScript. +/// +/// #### Example 2A - Propagating a Node-API C++ exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result = jsFunctionThatThrows({ arg1, arg2 }); +/// +/// Following C++ statements will not be executed. The exception will bubble +/// up as a C++ exception of type `Napi::Error`, until it is either caught +/// while still in C++, or else automatically propagated as a JavaScript +/// exception when the callback returns to JavaScript. +/// +/// #### Example 3A - Handling a Node-API C++ exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result; +/// try { +/// result = jsFunctionThatThrows({ arg1, arg2 }); +/// } catch (const Napi::Error& e) { +/// cerr << "Caught JavaScript exception: " + e.what(); +/// } +/// +/// Since the exception was caught here, it will not be propagated as a +/// JavaScript exception. +/// +/// ### Handling Errors Without C++ Exceptions +/// +/// If C++ exceptions are disabled (by defining `NAPI_DISABLE_CPP_EXCEPTIONS`) +/// then this class does not extend `std::exception`, and APIs in the `Napi` +/// namespace do not throw C++ exceptions when they fail. Instead, they raise +/// _pending_ JavaScript exceptions and return _empty_ `Value`s. Calling code +/// should check `Value::IsEmpty()` before attempting to use a returned value, +/// and may use methods on the `Env` class to check for, get, and clear a +/// pending JavaScript exception. If the pending exception is not cleared, it +/// will be thrown when the native callback returns to JavaScript. +/// +/// #### Example 1B - Throwing a JS exception +/// +/// Napi::Env env = ... +/// Napi::Error::New(env, "Example +/// exception").ThrowAsJavaScriptException(); return; +/// +/// After throwing a JS exception, the code should generally return +/// immediately from the native callback, after performing any necessary +/// cleanup. +/// +/// #### Example 2B - Propagating a Node-API JS exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result = jsFunctionThatThrows({ arg1, arg2 }); +/// if (result.IsEmpty()) return; +/// +/// An empty value result from a Node-API call indicates an error occurred, +/// and a JavaScript exception is pending. To let the exception propagate, the +/// code should generally return immediately from the native callback, after +/// performing any necessary cleanup. +/// +/// #### Example 3B - Handling a Node-API JS exception: +/// +/// Napi::Function jsFunctionThatThrows = someObj.As(); +/// Napi::Value result = jsFunctionThatThrows({ arg1, arg2 }); +/// if (result.IsEmpty()) { +/// Napi::Error e = env.GetAndClearPendingException(); +/// cerr << "Caught JavaScript exception: " + e.Message(); +/// } +/// +/// Since the exception was cleared here, it will not be propagated as a +/// JavaScript exception after the native callback returns. +class Error : public ObjectReference +#ifdef NAPI_CPP_EXCEPTIONS + , + public std::exception +#endif // NAPI_CPP_EXCEPTIONS +{ + public: + static Error New(napi_env env); + static Error New(napi_env env, const char* message); + static Error New(napi_env env, const std::string& message); + + static NAPI_NO_RETURN void Fatal(const char* location, const char* message); + + Error(); + Error(napi_env env, napi_value value); + + // An error can be moved or copied. + Error(Error&& other); + Error& operator=(Error&& other); + Error(const Error&); + Error& operator=(const Error&); + + const std::string& Message() const NAPI_NOEXCEPT; + void ThrowAsJavaScriptException() const; + + Object Value() const; + +#ifdef NAPI_CPP_EXCEPTIONS + const char* what() const NAPI_NOEXCEPT override; +#endif // NAPI_CPP_EXCEPTIONS + + protected: + /// !cond INTERNAL + using create_error_fn = napi_status (*)(napi_env envb, + napi_value code, + napi_value msg, + napi_value* result); + + template + static TError New(napi_env env, + const char* message, + size_t length, + create_error_fn create_error); + /// !endcond + + private: + static inline const char* ERROR_WRAP_VALUE() NAPI_NOEXCEPT; + mutable std::string _message; +}; + +class TypeError : public Error { + public: + static TypeError New(napi_env env, const char* message); + static TypeError New(napi_env env, const std::string& message); + + TypeError(); + TypeError(napi_env env, napi_value value); +}; + +class RangeError : public Error { + public: + static RangeError New(napi_env env, const char* message); + static RangeError New(napi_env env, const std::string& message); + + RangeError(); + RangeError(napi_env env, napi_value value); +}; + +class CallbackInfo { + public: + CallbackInfo(napi_env env, napi_callback_info info); + ~CallbackInfo(); + + // Disallow copying to prevent multiple free of _dynamicArgs + NAPI_DISALLOW_ASSIGN_COPY(CallbackInfo) + + Napi::Env Env() const; + Value NewTarget() const; + bool IsConstructCall() const; + size_t Length() const; + const Value operator[](size_t index) const; + Value This() const; + void* Data() const; + void SetData(void* data); + explicit operator napi_callback_info() const; + + private: + const size_t _staticArgCount = 6; + napi_env _env; + napi_callback_info _info; + napi_value _this; + size_t _argc; + napi_value* _argv; + napi_value _staticArgs[6]; + napi_value* _dynamicArgs; + void* _data; +}; + +class PropertyDescriptor { + public: + using GetterCallback = Napi::Value (*)(const Napi::CallbackInfo& info); + using SetterCallback = void (*)(const Napi::CallbackInfo& info); + +#ifndef NODE_ADDON_API_DISABLE_DEPRECATED + template + static PropertyDescriptor Accessor( + const char* utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + napi_value name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Name name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + napi_value name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + const char* utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + napi_value name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Name name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); +#endif // !NODE_ADDON_API_DISABLE_DEPRECATED + + template + static PropertyDescriptor Accessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + Name name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + const std::string& utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + Name name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Accessor( + Napi::Env env, + Napi::Object object, + Name name, + Getter getter, + Setter setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Napi::Env env, + Napi::Object object, + const char* utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Napi::Env env, + Napi::Object object, + const std::string& utf8name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor Function( + Napi::Env env, + Napi::Object object, + Name name, + Callable cb, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor Value( + const char* utf8name, + napi_value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor Value( + const std::string& utf8name, + napi_value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor Value( + napi_value name, + napi_value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor Value( + Name name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + + PropertyDescriptor(napi_property_descriptor desc); + + operator napi_property_descriptor&(); + operator const napi_property_descriptor&() const; + + private: + napi_property_descriptor _desc; +}; + +/// Property descriptor for use with `ObjectWrap::DefineClass()`. +/// +/// This is different from the standalone `PropertyDescriptor` because it is +/// specific to each `ObjectWrap` subclass. This prevents using descriptors +/// from a different class when defining a new class (preventing the callbacks +/// from having incorrect `this` pointers). +template +class ClassPropertyDescriptor { + public: + ClassPropertyDescriptor(napi_property_descriptor desc) : _desc(desc) {} + + operator napi_property_descriptor&() { return _desc; } + operator const napi_property_descriptor&() const { return _desc; } + + private: + napi_property_descriptor _desc; +}; + +template +struct MethodCallbackData { + TCallback callback; + void* data; +}; + +template +struct AccessorCallbackData { + TGetterCallback getterCallback; + TSetterCallback setterCallback; + void* data; +}; + +template +class InstanceWrap { + public: + using InstanceVoidMethodCallback = void (T::*)(const CallbackInfo& info); + using InstanceMethodCallback = Napi::Value (T::*)(const CallbackInfo& info); + using InstanceGetterCallback = Napi::Value (T::*)(const CallbackInfo& info); + using InstanceSetterCallback = void (T::*)(const CallbackInfo& info, + const Napi::Value& value); + + using PropertyDescriptor = ClassPropertyDescriptor; + + static PropertyDescriptor InstanceMethod( + const char* utf8name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceMethod( + const char* utf8name, + InstanceMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceMethod( + Symbol name, + InstanceVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceMethod( + Symbol name, + InstanceMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceAccessor( + const char* utf8name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceAccessor( + Symbol name, + InstanceGetterCallback getter, + InstanceSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceAccessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor InstanceAccessor( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor InstanceValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor InstanceValue( + Symbol name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + + protected: + static void AttachPropData(napi_env env, + napi_value value, + const napi_property_descriptor* prop); + + private: + using This = InstanceWrap; + + using InstanceVoidMethodCallbackData = + MethodCallbackData; + using InstanceMethodCallbackData = + MethodCallbackData; + using InstanceAccessorCallbackData = + AccessorCallbackData; + + static napi_value InstanceVoidMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value InstanceMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value InstanceGetterCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value InstanceSetterCallbackWrapper(napi_env env, + napi_callback_info info); + + template + static napi_value WrappedMethod(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT; + + template + struct SetterTag {}; + + template + static napi_callback WrapSetter(SetterTag) NAPI_NOEXCEPT { + return &This::WrappedMethod; + } + static napi_callback WrapSetter(SetterTag) NAPI_NOEXCEPT { + return nullptr; + } +}; + +/// Base class to be extended by C++ classes exposed to JavaScript; each C++ +/// class instance gets "wrapped" by a JavaScript object that is managed by this +/// class. +/// +/// At initialization time, the `DefineClass()` method must be used to +/// hook up the accessor and method callbacks. It takes a list of +/// property descriptors, which can be constructed via the various +/// static methods on the base class. +/// +/// #### Example: +/// +/// class Example: public Napi::ObjectWrap { +/// public: +/// static void Initialize(Napi::Env& env, Napi::Object& target) { +/// Napi::Function constructor = DefineClass(env, "Example", { +/// InstanceAccessor<&Example::GetSomething, +/// &Example::SetSomething>("value"), +/// InstanceMethod<&Example::DoSomething>("doSomething"), +/// }); +/// target.Set("Example", constructor); +/// } +/// +/// Example(const Napi::CallbackInfo& info); // Constructor +/// Napi::Value GetSomething(const Napi::CallbackInfo& info); +/// void SetSomething(const Napi::CallbackInfo& info, const Napi::Value& +/// value); Napi::Value DoSomething(const Napi::CallbackInfo& info); +/// } +template +class ObjectWrap : public InstanceWrap, public Reference { + public: + ObjectWrap(const CallbackInfo& callbackInfo); + virtual ~ObjectWrap(); + + static T* Unwrap(Object wrapper); + + // Methods exposed to JavaScript must conform to one of these callback + // signatures. + using StaticVoidMethodCallback = void (*)(const CallbackInfo& info); + using StaticMethodCallback = Napi::Value (*)(const CallbackInfo& info); + using StaticGetterCallback = Napi::Value (*)(const CallbackInfo& info); + using StaticSetterCallback = void (*)(const CallbackInfo& info, + const Napi::Value& value); + + using PropertyDescriptor = ClassPropertyDescriptor; + + static Function DefineClass( + Napi::Env env, + const char* utf8name, + const std::initializer_list& properties, + void* data = nullptr); + static Function DefineClass(Napi::Env env, + const char* utf8name, + const std::vector& properties, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + const char* utf8name, + StaticVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + const char* utf8name, + StaticMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + Symbol name, + StaticVoidMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticMethod( + Symbol name, + StaticMethodCallback method, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticMethod( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticAccessor( + const char* utf8name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticAccessor( + Symbol name, + StaticGetterCallback getter, + StaticSetterCallback setter, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticAccessor( + const char* utf8name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + template + static PropertyDescriptor StaticAccessor( + Symbol name, + napi_property_attributes attributes = napi_default, + void* data = nullptr); + static PropertyDescriptor StaticValue( + const char* utf8name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + static PropertyDescriptor StaticValue( + Symbol name, + Napi::Value value, + napi_property_attributes attributes = napi_default); + static Napi::Value OnCalledAsFunction(const Napi::CallbackInfo& callbackInfo); + virtual void Finalize(Napi::Env env); + + private: + using This = ObjectWrap; + + static napi_value ConstructorCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticVoidMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticMethodCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticGetterCallbackWrapper(napi_env env, + napi_callback_info info); + static napi_value StaticSetterCallbackWrapper(napi_env env, + napi_callback_info info); + static void FinalizeCallback(napi_env env, void* data, void* hint); + static Function DefineClass(Napi::Env env, + const char* utf8name, + const size_t props_count, + const napi_property_descriptor* props, + void* data = nullptr); + + using StaticVoidMethodCallbackData = + MethodCallbackData; + using StaticMethodCallbackData = MethodCallbackData; + + using StaticAccessorCallbackData = + AccessorCallbackData; + + template + static napi_value WrappedMethod(napi_env env, + napi_callback_info info) NAPI_NOEXCEPT; + + template + struct StaticSetterTag {}; + + template + static napi_callback WrapStaticSetter(StaticSetterTag) NAPI_NOEXCEPT { + return &This::WrappedMethod; + } + static napi_callback WrapStaticSetter(StaticSetterTag) + NAPI_NOEXCEPT { + return nullptr; + } + + bool _construction_failed = true; +}; + +class HandleScope { + public: + HandleScope(napi_env env, napi_handle_scope scope); + explicit HandleScope(Napi::Env env); + ~HandleScope(); + + // Disallow copying to prevent double close of napi_handle_scope + NAPI_DISALLOW_ASSIGN_COPY(HandleScope) + + operator napi_handle_scope() const; + + Napi::Env Env() const; + + private: + napi_env _env; + napi_handle_scope _scope; +}; + +class EscapableHandleScope { + public: + EscapableHandleScope(napi_env env, napi_escapable_handle_scope scope); + explicit EscapableHandleScope(Napi::Env env); + ~EscapableHandleScope(); + + // Disallow copying to prevent double close of napi_escapable_handle_scope + NAPI_DISALLOW_ASSIGN_COPY(EscapableHandleScope) + + operator napi_escapable_handle_scope() const; + + Napi::Env Env() const; + Value Escape(napi_value escapee); + + private: + napi_env _env; + napi_escapable_handle_scope _scope; +}; + +#if (NAPI_VERSION > 2) +class CallbackScope { + public: + CallbackScope(napi_env env, napi_callback_scope scope); + CallbackScope(napi_env env, napi_async_context context); + virtual ~CallbackScope(); + + // Disallow copying to prevent double close of napi_callback_scope + NAPI_DISALLOW_ASSIGN_COPY(CallbackScope) + + operator napi_callback_scope() const; + + Napi::Env Env() const; + + private: + napi_env _env; + napi_callback_scope _scope; +}; +#endif + +class AsyncContext { + public: + explicit AsyncContext(napi_env env, const char* resource_name); + explicit AsyncContext(napi_env env, + const char* resource_name, + const Object& resource); + virtual ~AsyncContext(); + + AsyncContext(AsyncContext&& other); + AsyncContext& operator=(AsyncContext&& other); + NAPI_DISALLOW_ASSIGN_COPY(AsyncContext) + + operator napi_async_context() const; + + Napi::Env Env() const; + + private: + napi_env _env; + napi_async_context _context; +}; + +#if NAPI_HAS_THREADS +class AsyncWorker { + public: + virtual ~AsyncWorker(); + + NAPI_DISALLOW_ASSIGN_COPY(AsyncWorker) + + operator napi_async_work() const; + + Napi::Env Env() const; + + void Queue(); + void Cancel(); + void SuppressDestruct(); + + ObjectReference& Receiver(); + FunctionReference& Callback(); + + virtual void OnExecute(Napi::Env env); + virtual void OnWorkComplete(Napi::Env env, napi_status status); + + protected: + explicit AsyncWorker(const Function& callback); + explicit AsyncWorker(const Function& callback, const char* resource_name); + explicit AsyncWorker(const Function& callback, + const char* resource_name, + const Object& resource); + explicit AsyncWorker(const Object& receiver, const Function& callback); + explicit AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name); + explicit AsyncWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource); + + explicit AsyncWorker(Napi::Env env); + explicit AsyncWorker(Napi::Env env, const char* resource_name); + explicit AsyncWorker(Napi::Env env, + const char* resource_name, + const Object& resource); + + virtual void Execute() = 0; + virtual void OnOK(); + virtual void OnError(const Error& e); + virtual void Destroy(); + virtual std::vector GetResult(Napi::Env env); + + void SetError(const std::string& error); + + private: + static inline void OnAsyncWorkExecute(napi_env env, void* asyncworker); + static inline void OnAsyncWorkComplete(napi_env env, + napi_status status, + void* asyncworker); + + napi_env _env; + napi_async_work _work; + ObjectReference _receiver; + FunctionReference _callback; + std::string _error; + bool _suppress_destruct; +}; +#endif // NAPI_HAS_THREADS + +#if (NAPI_VERSION > 3 && NAPI_HAS_THREADS) +class ThreadSafeFunction { + public: + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + Finalizer finalizeCallback, + FinalizerDataType* data); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback); + + // This API may only be called from the main thread. + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data); + + ThreadSafeFunction(); + ThreadSafeFunction(napi_threadsafe_function tsFunctionValue); + + operator napi_threadsafe_function() const; + + // This API may be called from any thread. + napi_status BlockingCall() const; + + // This API may be called from any thread. + template + napi_status BlockingCall(Callback callback) const; + + // This API may be called from any thread. + template + napi_status BlockingCall(DataType* data, Callback callback) const; + + // This API may be called from any thread. + napi_status NonBlockingCall() const; + + // This API may be called from any thread. + template + napi_status NonBlockingCall(Callback callback) const; + + // This API may be called from any thread. + template + napi_status NonBlockingCall(DataType* data, Callback callback) const; + + // This API may only be called from the main thread. + void Ref(napi_env env) const; + + // This API may only be called from the main thread. + void Unref(napi_env env) const; + + // This API may be called from any thread. + napi_status Acquire() const; + + // This API may be called from any thread. + napi_status Release() const; + + // This API may be called from any thread. + napi_status Abort() const; + + struct ConvertibleContext { + template + operator T*() { + return static_cast(context); + } + void* context; + }; + + // This API may be called from any thread. + ConvertibleContext GetContext() const; + + private: + using CallbackWrapper = std::function; + + template + static ThreadSafeFunction New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper); + + napi_status CallInternal(CallbackWrapper* callbackWrapper, + napi_threadsafe_function_call_mode mode) const; + + static void CallJS(napi_env env, + napi_value jsCallback, + void* context, + void* data); + + napi_threadsafe_function _tsfn; +}; + +// A TypedThreadSafeFunction by default has no context (nullptr) and can +// accept any type (void) to its CallJs. +template +class TypedThreadSafeFunction { + public: + // This API may only be called from the main thread. + // Helper function that returns nullptr if running Node-API 5+, otherwise a + // non-empty, no-op Function. This provides the ability to specify at + // compile-time a callback parameter to `New` that safely does no action + // when targeting _any_ Node-API version. +#if NAPI_VERSION > 4 + static std::nullptr_t EmptyFunctionFactory(Napi::Env env); +#else + static Napi::Function EmptyFunctionFactory(Napi::Env env); +#endif + static Napi::Function FunctionOrEmpty(Napi::Env env, + Napi::Function& callback); + +#if NAPI_VERSION > 4 + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [missing] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [passed] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [missing] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [passed] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); +#endif + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [missing] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [passed] Finalizer [missing] + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [missing] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [passed] Finalizer [passed] + template + static TypedThreadSafeFunction New( + napi_env env, + CallbackType callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); + + TypedThreadSafeFunction(); + TypedThreadSafeFunction(napi_threadsafe_function tsFunctionValue); + + operator napi_threadsafe_function() const; + + // This API may be called from any thread. + napi_status BlockingCall(DataType* data = nullptr) const; + + // This API may be called from any thread. + napi_status NonBlockingCall(DataType* data = nullptr) const; + + // This API may only be called from the main thread. + void Ref(napi_env env) const; + + // This API may only be called from the main thread. + void Unref(napi_env env) const; + + // This API may be called from any thread. + napi_status Acquire() const; + + // This API may be called from any thread. + napi_status Release() const; + + // This API may be called from any thread. + napi_status Abort() const; + + // This API may be called from any thread. + ContextType* GetContext() const; + + private: + template + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper); + + static void CallJsInternal(napi_env env, + napi_value jsCallback, + void* context, + void* data); + + protected: + napi_threadsafe_function _tsfn; +}; +template +class AsyncProgressWorkerBase : public AsyncWorker { + public: + virtual void OnWorkProgress(DataType* data) = 0; + class ThreadSafeData { + public: + ThreadSafeData(AsyncProgressWorkerBase* asyncprogressworker, DataType* data) + : _asyncprogressworker(asyncprogressworker), _data(data) {} + + AsyncProgressWorkerBase* asyncprogressworker() { + return _asyncprogressworker; + }; + DataType* data() { return _data; }; + + private: + AsyncProgressWorkerBase* _asyncprogressworker; + DataType* _data; + }; + void OnWorkComplete(Napi::Env env, napi_status status) override; + + protected: + explicit AsyncProgressWorkerBase(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource, + size_t queue_size = 1); + virtual ~AsyncProgressWorkerBase(); + +// Optional callback of Napi::ThreadSafeFunction only available after +// NAPI_VERSION 4. Refs: https://github.com/nodejs/node/pull/27791 +#if NAPI_VERSION > 4 + explicit AsyncProgressWorkerBase(Napi::Env env, + const char* resource_name, + const Object& resource, + size_t queue_size = 1); +#endif + + static inline void OnAsyncWorkProgress(Napi::Env env, + Napi::Function jsCallback, + void* data); + + napi_status NonBlockingCall(DataType* data); + + private: + ThreadSafeFunction _tsfn; + bool _work_completed = false; + napi_status _complete_status; + static inline void OnThreadSafeFunctionFinalize( + Napi::Env env, void* data, AsyncProgressWorkerBase* context); +}; + +template +class AsyncProgressWorker : public AsyncProgressWorkerBase { + public: + virtual ~AsyncProgressWorker(); + + class ExecutionProgress { + friend class AsyncProgressWorker; + + public: + void Signal() const; + void Send(const T* data, size_t count) const; + + private: + explicit ExecutionProgress(AsyncProgressWorker* worker) : _worker(worker) {} + AsyncProgressWorker* const _worker; + }; + + void OnWorkProgress(void*) override; + + protected: + explicit AsyncProgressWorker(const Function& callback); + explicit AsyncProgressWorker(const Function& callback, + const char* resource_name); + explicit AsyncProgressWorker(const Function& callback, + const char* resource_name, + const Object& resource); + explicit AsyncProgressWorker(const Object& receiver, + const Function& callback); + explicit AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name); + explicit AsyncProgressWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource); + +// Optional callback of Napi::ThreadSafeFunction only available after +// NAPI_VERSION 4. Refs: https://github.com/nodejs/node/pull/27791 +#if NAPI_VERSION > 4 + explicit AsyncProgressWorker(Napi::Env env); + explicit AsyncProgressWorker(Napi::Env env, const char* resource_name); + explicit AsyncProgressWorker(Napi::Env env, + const char* resource_name, + const Object& resource); +#endif + virtual void Execute(const ExecutionProgress& progress) = 0; + virtual void OnProgress(const T* data, size_t count) = 0; + + private: + void Execute() override; + void Signal(); + void SendProgress_(const T* data, size_t count); + + std::mutex _mutex; + T* _asyncdata; + size_t _asyncsize; + bool _signaled; +}; + +template +class AsyncProgressQueueWorker + : public AsyncProgressWorkerBase> { + public: + virtual ~AsyncProgressQueueWorker(){}; + + class ExecutionProgress { + friend class AsyncProgressQueueWorker; + + public: + void Signal() const; + void Send(const T* data, size_t count) const; + + private: + explicit ExecutionProgress(AsyncProgressQueueWorker* worker) + : _worker(worker) {} + AsyncProgressQueueWorker* const _worker; + }; + + void OnWorkComplete(Napi::Env env, napi_status status) override; + void OnWorkProgress(std::pair*) override; + + protected: + explicit AsyncProgressQueueWorker(const Function& callback); + explicit AsyncProgressQueueWorker(const Function& callback, + const char* resource_name); + explicit AsyncProgressQueueWorker(const Function& callback, + const char* resource_name, + const Object& resource); + explicit AsyncProgressQueueWorker(const Object& receiver, + const Function& callback); + explicit AsyncProgressQueueWorker(const Object& receiver, + const Function& callback, + const char* resource_name); + explicit AsyncProgressQueueWorker(const Object& receiver, + const Function& callback, + const char* resource_name, + const Object& resource); + +// Optional callback of Napi::ThreadSafeFunction only available after +// NAPI_VERSION 4. Refs: https://github.com/nodejs/node/pull/27791 +#if NAPI_VERSION > 4 + explicit AsyncProgressQueueWorker(Napi::Env env); + explicit AsyncProgressQueueWorker(Napi::Env env, const char* resource_name); + explicit AsyncProgressQueueWorker(Napi::Env env, + const char* resource_name, + const Object& resource); +#endif + virtual void Execute(const ExecutionProgress& progress) = 0; + virtual void OnProgress(const T* data, size_t count) = 0; + + private: + void Execute() override; + void Signal() const; + void SendProgress_(const T* data, size_t count); +}; +#endif // NAPI_VERSION > 3 && NAPI_HAS_THREADS + +// Memory management. +class MemoryManagement { + public: + static int64_t AdjustExternalMemory(Env env, int64_t change_in_bytes); +}; + +// Version management +class VersionManagement { + public: + static uint32_t GetNapiVersion(Env env); + static const napi_node_version* GetNodeVersion(Env env); +}; + +#if NAPI_VERSION > 5 +template +class Addon : public InstanceWrap { + public: + static inline Object Init(Env env, Object exports); + static T* Unwrap(Object wrapper); + + protected: + using AddonProp = ClassPropertyDescriptor; + void DefineAddon(Object exports, + const std::initializer_list& props); + Napi::Object DefineProperties(Object object, + const std::initializer_list& props); + + private: + Object entry_point_; +}; +#endif // NAPI_VERSION > 5 + +#ifdef NAPI_CPP_CUSTOM_NAMESPACE +} // namespace NAPI_CPP_CUSTOM_NAMESPACE +#endif + +} // namespace Napi + +// Inline implementations of all the above class methods are included here. +#include "napi-inl.h" + +#endif // SRC_NAPI_H_ diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/node_api.gyp b/backend/node_modules/sharp/node_modules/node-addon-api/node_api.gyp new file mode 100644 index 00000000..4ff0ae7d --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/node_api.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'nothing', + 'type': 'static_library', + 'sources': [ 'nothing.c' ] + } + ] +} diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/noexcept.gypi b/backend/node_modules/sharp/node_modules/node-addon-api/noexcept.gypi new file mode 100644 index 00000000..404a05f3 --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/noexcept.gypi @@ -0,0 +1,26 @@ +{ + 'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], + 'cflags': [ '-fno-exceptions' ], + 'cflags_cc': [ '-fno-exceptions' ], + 'conditions': [ + ["OS=='win'", { + # _HAS_EXCEPTIONS is already defined and set to 0 in common.gypi + #"defines": [ + # "_HAS_EXCEPTIONS=0" + #], + "msvs_settings": { + "VCCLCompilerTool": { + 'ExceptionHandling': 0, + 'EnablePREfast': 'true', + }, + }, + }], + ["OS=='mac'", { + 'xcode_settings': { + 'CLANG_CXX_LIBRARY': 'libc++', + 'MACOSX_DEPLOYMENT_TARGET': '10.7', + 'GCC_ENABLE_CPP_EXCEPTIONS': 'NO', + }, + }], + ], +} diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/nothing.c b/backend/node_modules/sharp/node_modules/node-addon-api/nothing.c new file mode 100644 index 00000000..e69de29b diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/package-support.json b/backend/node_modules/sharp/node_modules/node-addon-api/package-support.json new file mode 100644 index 00000000..10d3607a --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/package-support.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "*", + "target": { + "node": "active" + }, + "response": { + "type": "time-permitting", + "paid": false, + "contact": { + "name": "node-addon-api team", + "url": "https://github.com/nodejs/node-addon-api/issues" + } + }, + "backing": [ { "project": "https://github.com/nodejs" }, + { "foundation": "https://openjsf.org/" } + ] + } + ] +} diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/package.json b/backend/node_modules/sharp/node_modules/node-addon-api/package.json new file mode 100644 index 00000000..0bd3933a --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/package.json @@ -0,0 +1,464 @@ +{ + "bugs": { + "url": "https://github.com/nodejs/node-addon-api/issues" + }, + "contributors": [ + { + "name": "Abhishek Kumar Singh", + "url": "https://github.com/abhi11210646" + }, + { + "name": "Alba Mendez", + "url": "https://github.com/jmendeth" + }, + { + "name": "Alexander Floh", + "url": "https://github.com/alexanderfloh" + }, + { + "name": "Ammar Faizi", + "url": "https://github.com/ammarfaizi2" + }, + { + "name": "András Timár, Dr", + "url": "https://github.com/timarandras" + }, + { + "name": "Andrew Petersen", + "url": "https://github.com/kirbysayshi" + }, + { + "name": "Anisha Rohra", + "url": "https://github.com/anisha-rohra" + }, + { + "name": "Anna Henningsen", + "url": "https://github.com/addaleax" + }, + { + "name": "Arnaud Botella", + "url": "https://github.com/BotellaA" + }, + { + "name": "Arunesh Chandra", + "url": "https://github.com/aruneshchandra" + }, + { + "name": "Azlan Mukhtar", + "url": "https://github.com/azlan" + }, + { + "name": "Ben Berman", + "url": "https://github.com/rivertam" + }, + { + "name": "Benjamin Byholm", + "url": "https://github.com/kkoopa" + }, + { + "name": "Bill Gallafent", + "url": "https://github.com/gallafent" + }, + { + "name": "blagoev", + "url": "https://github.com/blagoev" + }, + { + "name": "Bruce A. MacNaughton", + "url": "https://github.com/bmacnaughton" + }, + { + "name": "Cory Mickelson", + "url": "https://github.com/corymickelson" + }, + { + "name": "Daniel Bevenius", + "url": "https://github.com/danbev" + }, + { + "name": "Dante Calderón", + "url": "https://github.com/dantehemerson" + }, + { + "name": "Darshan Sen", + "url": "https://github.com/RaisinTen" + }, + { + "name": "David Halls", + "url": "https://github.com/davedoesdev" + }, + { + "name": "Deepak Rajamohan", + "url": "https://github.com/deepakrkris" + }, + { + "name": "Dmitry Ashkadov", + "url": "https://github.com/dmitryash" + }, + { + "name": "Dongjin Na", + "url": "https://github.com/nadongguri" + }, + { + "name": "Doni Rubiagatra", + "url": "https://github.com/rubiagatra" + }, + { + "name": "Eric Bickle", + "url": "https://github.com/ebickle" + }, + { + "name": "extremeheat", + "url": "https://github.com/extremeheat" + }, + { + "name": "Feng Yu", + "url": "https://github.com/F3n67u" + }, + { + "name": "Ferdinand Holzer", + "url": "https://github.com/fholzer" + }, + { + "name": "Gabriel Schulhof", + "url": "https://github.com/gabrielschulhof" + }, + { + "name": "Guenter Sandner", + "url": "https://github.com/gms1" + }, + { + "name": "Gus Caplan", + "url": "https://github.com/devsnek" + }, + { + "name": "Helio Frota", + "url": "https://github.com/helio-frota" + }, + { + "name": "Hitesh Kanwathirtha", + "url": "https://github.com/digitalinfinity" + }, + { + "name": "ikokostya", + "url": "https://github.com/ikokostya" + }, + { + "name": "Jack Xia", + "url": "https://github.com/JckXia" + }, + { + "name": "Jake Barnes", + "url": "https://github.com/DuBistKomisch" + }, + { + "name": "Jake Yoon", + "url": "https://github.com/yjaeseok" + }, + { + "name": "Jason Ginchereau", + "url": "https://github.com/jasongin" + }, + { + "name": "Jenny", + "url": "https://github.com/egg-bread" + }, + { + "name": "Jeroen Janssen", + "url": "https://github.com/japj" + }, + { + "name": "Jim Schlight", + "url": "https://github.com/jschlight" + }, + { + "name": "Jinho Bang", + "url": "https://github.com/romandev" + }, + { + "name": "José Expósito", + "url": "https://github.com/JoseExposito" + }, + { + "name": "joshgarde", + "url": "https://github.com/joshgarde" + }, + { + "name": "Julian Mesa", + "url": "https://github.com/julianmesa-gitkraken" + }, + { + "name": "Kasumi Hanazuki", + "url": "https://github.com/hanazuki" + }, + { + "name": "Kelvin", + "url": "https://github.com/kelvinhammond" + }, + { + "name": "Kevin Eady", + "url": "https://github.com/KevinEady" + }, + { + "name": "Kévin VOYER", + "url": "https://github.com/kecsou" + }, + { + "name": "kidneysolo", + "url": "https://github.com/kidneysolo" + }, + { + "name": "Koki Nishihara", + "url": "https://github.com/Nishikoh" + }, + { + "name": "Konstantin Tarkus", + "url": "https://github.com/koistya" + }, + { + "name": "Kyle Farnung", + "url": "https://github.com/kfarnung" + }, + { + "name": "Kyle Kovacs", + "url": "https://github.com/nullromo" + }, + { + "name": "legendecas", + "url": "https://github.com/legendecas" + }, + { + "name": "LongYinan", + "url": "https://github.com/Brooooooklyn" + }, + { + "name": "Lovell Fuller", + "url": "https://github.com/lovell" + }, + { + "name": "Luciano Martorella", + "url": "https://github.com/lmartorella" + }, + { + "name": "mastergberry", + "url": "https://github.com/mastergberry" + }, + { + "name": "Mathias Küsel", + "url": "https://github.com/mathiask88" + }, + { + "name": "Mathias Stearn", + "url": "https://github.com/RedBeard0531" + }, + { + "name": "Matteo Collina", + "url": "https://github.com/mcollina" + }, + { + "name": "Michael Dawson", + "url": "https://github.com/mhdawson" + }, + { + "name": "Michael Price", + "url": "https://github.com/mikepricedev" + }, + { + "name": "Michele Campus", + "url": "https://github.com/kYroL01" + }, + { + "name": "Mikhail Cheshkov", + "url": "https://github.com/mcheshkov" + }, + { + "name": "nempoBu4", + "url": "https://github.com/nempoBu4" + }, + { + "name": "Nicola Del Gobbo", + "url": "https://github.com/NickNaso" + }, + { + "name": "Nick Soggin", + "url": "https://github.com/iSkore" + }, + { + "name": "Nikolai Vavilov", + "url": "https://github.com/seishun" + }, + { + "name": "Nurbol Alpysbayev", + "url": "https://github.com/anurbol" + }, + { + "name": "pacop", + "url": "https://github.com/pacop" + }, + { + "name": "Peter Šándor", + "url": "https://github.com/petersandor" + }, + { + "name": "Philipp Renoth", + "url": "https://github.com/DaAitch" + }, + { + "name": "rgerd", + "url": "https://github.com/rgerd" + }, + { + "name": "Richard Lau", + "url": "https://github.com/richardlau" + }, + { + "name": "Rolf Timmermans", + "url": "https://github.com/rolftimmermans" + }, + { + "name": "Ross Weir", + "url": "https://github.com/ross-weir" + }, + { + "name": "Ryuichi Okumura", + "url": "https://github.com/okuryu" + }, + { + "name": "Saint Gabriel", + "url": "https://github.com/chineduG" + }, + { + "name": "Sampson Gao", + "url": "https://github.com/sampsongao" + }, + { + "name": "Sam Roberts", + "url": "https://github.com/sam-github" + }, + { + "name": "strager", + "url": "https://github.com/strager" + }, + { + "name": "Taylor Woll", + "url": "https://github.com/boingoing" + }, + { + "name": "Thomas Gentilhomme", + "url": "https://github.com/fraxken" + }, + { + "name": "Tim Rach", + "url": "https://github.com/timrach" + }, + { + "name": "Tobias Nießen", + "url": "https://github.com/tniessen" + }, + { + "name": "todoroff", + "url": "https://github.com/todoroff" + }, + { + "name": "Toyo Li", + "url": "https://github.com/toyobayashi" + }, + { + "name": "Tux3", + "url": "https://github.com/tux3" + }, + { + "name": "Vlad Velmisov", + "url": "https://github.com/Velmisov" + }, + { + "name": "Vladimir Morozov", + "url": "https://github.com/vmoroz" + + }, + { + "name": "WenheLI", + "url": "https://github.com/WenheLI" + }, + { + "name": "Xuguang Mei", + "url": "https://github.com/meixg" + }, + { + "name": "Yohei Kishimoto", + "url": "https://github.com/morokosi" + }, + { + "name": "Yulong Wang", + "url": "https://github.com/fs-eire" + }, + { + "name": "Ziqiu Zhao", + "url": "https://github.com/ZzqiZQute" + }, + { + "name": "Feng Yu", + "url": "https://github.com/F3n67u" + } + ], + "description": "Node.js API (Node-API)", + "devDependencies": { + "benchmark": "^2.1.4", + "bindings": "^1.5.0", + "clang-format": "^1.4.0", + "eslint": "^7.32.0", + "eslint-config-semistandard": "^16.0.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.24.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "fs-extra": "^9.0.1", + "path": "^0.12.7", + "pre-commit": "^1.2.2", + "safe-buffer": "^5.1.1" + }, + "directories": {}, + "gypfile": false, + "homepage": "https://github.com/nodejs/node-addon-api", + "keywords": [ + "n-api", + "napi", + "addon", + "native", + "bindings", + "c", + "c++", + "nan", + "node-addon-api" + ], + "license": "MIT", + "main": "index.js", + "name": "node-addon-api", + "readme": "README.md", + "repository": { + "type": "git", + "url": "git://github.com/nodejs/node-addon-api.git" + }, + "files": [ + "*.{c,h,gyp,gypi}", + "package-support.json", + "tools/" + ], + "scripts": { + "prebenchmark": "node-gyp rebuild -C benchmark", + "benchmark": "node benchmark", + "pretest": "node-gyp rebuild -C test", + "test": "node test", + "test:debug": "node-gyp rebuild -C test --debug && NODE_API_BUILD_CONFIG=Debug node ./test/index.js", + "predev": "node-gyp rebuild -C test --debug", + "dev": "node test", + "predev:incremental": "node-gyp configure build -C test --debug", + "dev:incremental": "node test", + "doc": "doxygen doc/Doxyfile", + "lint": "node tools/eslint-format && node tools/clang-format", + "lint:fix": "node tools/clang-format --fix && node tools/eslint-format --fix" + }, + "pre-commit": "lint", + "version": "6.1.0", + "support": true +} diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/tools/README.md b/backend/node_modules/sharp/node_modules/node-addon-api/tools/README.md new file mode 100644 index 00000000..6b80e94f --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/tools/README.md @@ -0,0 +1,73 @@ +# Tools + +## clang-format + +The clang-format checking tools is designed to check changed lines of code compared to given git-refs. + +## Migration Script + +The migration tool is designed to reduce repetitive work in the migration process. However, the script is not aiming to convert every thing for you. There are usually some small fixes and major reconstruction required. + +### How To Use + +To run the conversion script, first make sure you have the latest `node-addon-api` in your `node_modules` directory. +``` +npm install node-addon-api +``` + +Then run the script passing your project directory +``` +node ./node_modules/node-addon-api/tools/conversion.js ./ +``` + +After finish, recompile and debug things that are missed by the script. + + +### Quick Fixes +Here is the list of things that can be fixed easily. + 1. Change your methods' return value to void if it doesn't return value to JavaScript. + 2. Use `.` to access attribute or to invoke member function in Napi::Object instead of `->`. + 3. `Napi::New(env, value);` to `Napi::[Type]::New(env, value); + + +### Major Reconstructions +The implementation of `Napi::ObjectWrap` is significantly different from NAN's. `Napi::ObjectWrap` takes a pointer to the wrapped object and creates a reference to the wrapped object inside ObjectWrap constructor. `Napi::ObjectWrap` also associates wrapped object's instance methods to Javascript module instead of static methods like NAN. + +So if you use Nan::ObjectWrap in your module, you will need to execute the following steps. + + 1. Convert your [ClassName]::New function to a constructor function that takes a `Napi::CallbackInfo`. Declare it as +``` +[ClassName](const Napi::CallbackInfo& info); +``` +and define it as +``` +[ClassName]::[ClassName](const Napi::CallbackInfo& info) : Napi::ObjectWrap<[ClassName]>(info){ + ... +} +``` +This way, the `Napi::ObjectWrap` constructor will be invoked after the object has been instantiated and `Napi::ObjectWrap` can use the `this` pointer to create a reference to the wrapped object. + + 2. Move your original constructor code into the new constructor. Delete your original constructor. + 3. In your class initialization function, associate native methods in the following way. +``` +Napi::FunctionReference constructor; + +void [ClassName]::Init(Napi::Env env, Napi::Object exports, Napi::Object module) { + Napi::HandleScope scope(env); + Napi::Function ctor = DefineClass(env, "Canvas", { + InstanceMethod<&[ClassName]::Func1>("Func1"), + InstanceMethod<&[ClassName]::Func2>("Func2"), + InstanceAccessor<&[ClassName]::ValueGetter>("Value"), + StaticMethod<&[ClassName]::StaticMethod>("MethodName"), + InstanceValue("Value", Napi::[Type]::New(env, value)), + }); + + constructor = Napi::Persistent(ctor); + constructor .SuppressDestruct(); + exports.Set("[ClassName]", ctor); +} +``` + 4. In function where you need to Unwrap the ObjectWrap in NAN like `[ClassName]* native = Nan::ObjectWrap::Unwrap<[ClassName]>(info.This());`, use `this` pointer directly as the unwrapped object as each ObjectWrap instance is associated with a unique object instance. + + +If you still find issues after following this guide, please leave us an issue describing your problem and we will try to resolve it. diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/tools/check-napi.js b/backend/node_modules/sharp/node_modules/node-addon-api/tools/check-napi.js new file mode 100644 index 00000000..9199af33 --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/tools/check-napi.js @@ -0,0 +1,99 @@ +'use strict'; +// Descend into a directory structure and, for each file matching *.node, output +// based on the imports found in the file whether it's an N-API module or not. + +const fs = require('fs'); +const path = require('path'); + +// Read the output of the command, break it into lines, and use the reducer to +// decide whether the file is an N-API module or not. +function checkFile (file, command, argv, reducer) { + const child = require('child_process').spawn(command, argv, { + stdio: ['inherit', 'pipe', 'inherit'] + }); + let leftover = ''; + let isNapi; + child.stdout.on('data', (chunk) => { + if (isNapi === undefined) { + chunk = (leftover + chunk.toString()).split(/[\r\n]+/); + leftover = chunk.pop(); + isNapi = chunk.reduce(reducer, isNapi); + if (isNapi !== undefined) { + child.kill(); + } + } + }); + child.on('close', (code, signal) => { + if ((code === null && signal !== null) || (code !== 0)) { + console.log( + command + ' exited with code: ' + code + ' and signal: ' + signal); + } else { + // Green if it's a N-API module, red otherwise. + console.log( + '\x1b[' + (isNapi ? '42' : '41') + 'm' + + (isNapi ? ' N-API' : 'Not N-API') + + '\x1b[0m: ' + file); + } + }); +} + +// Use nm -a to list symbols. +function checkFileUNIX (file) { + checkFile(file, 'nm', ['-a', file], (soFar, line) => { + if (soFar === undefined) { + line = line.match(/([0-9a-f]*)? ([a-zA-Z]) (.*$)/); + if (line[2] === 'U') { + if (/^napi/.test(line[3])) { + soFar = true; + } + } + } + return soFar; + }); +} + +// Use dumpbin /imports to list symbols. +function checkFileWin32 (file) { + checkFile(file, 'dumpbin', ['/imports', file], (soFar, line) => { + if (soFar === undefined) { + line = line.match(/([0-9a-f]*)? +([a-zA-Z0-9]) (.*$)/); + if (line && /^napi/.test(line[line.length - 1])) { + soFar = true; + } + } + return soFar; + }); +} + +// Descend into a directory structure and pass each file ending in '.node' to +// one of the above checks, depending on the OS. +function recurse (top) { + fs.readdir(top, (error, items) => { + if (error) { + throw new Error('error reading directory ' + top + ': ' + error); + } + items.forEach((item) => { + item = path.join(top, item); + fs.stat(item, ((item) => (error, stats) => { + if (error) { + throw new Error('error about ' + item + ': ' + error); + } + if (stats.isDirectory()) { + recurse(item); + } else if (/[.]node$/.test(item) && + // Explicitly ignore files called 'nothing.node' because they are + // artefacts of node-addon-api having identified a version of + // Node.js that ships with a correct implementation of N-API. + path.basename(item) !== 'nothing.node') { + process.platform === 'win32' + ? checkFileWin32(item) + : checkFileUNIX(item); + } + })(item)); + }); + }); +} + +// Start with the directory given on the command line or the current directory +// if nothing was given. +recurse(process.argv.length > 3 ? process.argv[2] : '.'); diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/tools/clang-format.js b/backend/node_modules/sharp/node_modules/node-addon-api/tools/clang-format.js new file mode 100644 index 00000000..e4bb4f52 --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/tools/clang-format.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +const spawn = require('child_process').spawnSync; +const path = require('path'); + +const filesToCheck = ['*.h', '*.cc']; +const FORMAT_START = process.env.FORMAT_START || 'main'; + +function main (args) { + let fix = false; + while (args.length > 0) { + switch (args[0]) { + case '-f': + case '--fix': + fix = true; + break; + default: + } + args.shift(); + } + + const clangFormatPath = path.dirname(require.resolve('clang-format')); + const binary = process.platform === 'win32' + ? 'node_modules\\.bin\\clang-format.cmd' + : 'node_modules/.bin/clang-format'; + const options = ['--binary=' + binary, '--style=file']; + if (fix) { + options.push(FORMAT_START); + } else { + options.push('--diff', FORMAT_START); + } + + const gitClangFormatPath = path.join(clangFormatPath, 'bin/git-clang-format'); + const result = spawn( + 'python', + [gitClangFormatPath, ...options, '--', ...filesToCheck], + { encoding: 'utf-8' } + ); + + if (result.stderr) { + console.error('Error running git-clang-format:', result.stderr); + return 2; + } + + const clangFormatOutput = result.stdout.trim(); + // Bail fast if in fix mode. + if (fix) { + console.log(clangFormatOutput); + return 0; + } + // Detect if there is any complains from clang-format + if ( + clangFormatOutput !== '' && + clangFormatOutput !== 'no modified files to format' && + clangFormatOutput !== 'clang-format did not modify any files' + ) { + console.error(clangFormatOutput); + const fixCmd = 'npm run lint:fix'; + console.error(` + ERROR: please run "${fixCmd}" to format changes in your commit + Note that when running the command locally, please keep your local + main branch and working branch up to date with nodejs/node-addon-api + to exclude un-related complains. + Or you can run "env FORMAT_START=upstream/main ${fixCmd}".`); + return 1; + } +} + +if (require.main === module) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/tools/conversion.js b/backend/node_modules/sharp/node_modules/node-addon-api/tools/conversion.js new file mode 100755 index 00000000..f89245ac --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/tools/conversion.js @@ -0,0 +1,301 @@ +#! /usr/bin/env node + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const args = process.argv.slice(2); +const dir = args[0]; +if (!dir) { + console.log('Usage: node ' + path.basename(__filename) + ' '); + process.exit(1); +} + +const NodeApiVersion = require('../package.json').version; + +const disable = args[1]; +let ConfigFileOperations; +if (disable !== '--disable' && dir !== '--disable') { + ConfigFileOperations = { + 'package.json': [ + [/([ ]*)"dependencies": {/g, '$1"dependencies": {\n$1 "node-addon-api": "' + NodeApiVersion + '",'], + [/[ ]*"nan": *"[^"]+"(,|)[\n\r]/g, ''] + ], + 'binding.gyp': [ + [/([ ]*)'include_dirs': \[/g, '$1\'include_dirs\': [\n$1 \'\s+(\w+)\s*=\s*Nan::New\([\w\d:]+\);(?:\w+->Reset\(\1\))?\s+\1->SetClassName\(Nan::String::New\("(\w+)"\)\);/g, 'Napi::Function $1 = DefineClass(env, "$2", {'], + [/Local\s+(\w+)\s*=\s*Nan::New\([\w\d:]+\);\s+(\w+)\.Reset\((\1)\);\s+\1->SetClassName\((Nan::String::New|Nan::New<(v8::)*String>)\("(.+?)"\)\);/g, 'Napi::Function $1 = DefineClass(env, "$6", {'], + [/Local\s+(\w+)\s*=\s*Nan::New\([\w\d:]+\);(?:\w+->Reset\(\1\))?\s+\1->SetClassName\(Nan::String::New\("(\w+)"\)\);/g, 'Napi::Function $1 = DefineClass(env, "$2", {'], + [/Nan::New\(([\w\d:]+)\)->GetFunction\(\)/g, 'Napi::Function::New(env, $1)'], + [/Nan::New\(([\w\d:]+)\)->GetFunction()/g, 'Napi::Function::New(env, $1);'], + [/Nan::New\(([\w\d:]+)\)/g, 'Napi::Function::New(env, $1)'], + [/Nan::New\(([\w\d:]+)\)/g, 'Napi::Function::New(env, $1)'], + + // FunctionTemplate to FunctionReference + [/Nan::Persistent<(v8::)*FunctionTemplate>/g, 'Napi::FunctionReference'], + [/Nan::Persistent<(v8::)*Function>/g, 'Napi::FunctionReference'], + [/v8::Local/g, 'Napi::FunctionReference'], + [/Local/g, 'Napi::FunctionReference'], + [/v8::FunctionTemplate/g, 'Napi::FunctionReference'], + [/FunctionTemplate/g, 'Napi::FunctionReference'], + + [/([ ]*)Nan::SetPrototypeMethod\(\w+, "(\w+)", (\w+)\);/g, '$1InstanceMethod("$2", &$3),'], + [/([ ]*)(?:\w+\.Reset\(\w+\);\s+)?\(target\)\.Set\("(\w+)",\s*Nan::GetFunction\((\w+)\)\);/gm, + '});\n\n' + + '$1constructor = Napi::Persistent($3);\n' + + '$1constructor.SuppressDestruct();\n' + + '$1target.Set("$2", $3);'], + + // TODO: Other attribute combinations + [/static_cast\(ReadOnly\s*\|\s*DontDelete\)/gm, + 'static_cast(napi_enumerable | napi_configurable)'], + + [/([\w\d:<>]+?)::Cast\((.+?)\)/g, '$2.As<$1>()'], + + [/\*Nan::Utf8String\(([^)]+)\)/g, '$1->As().Utf8Value().c_str()'], + [/Nan::Utf8String +(\w+)\(([^)]+)\)/g, 'std::string $1 = $2.As()'], + [/Nan::Utf8String/g, 'std::string'], + + [/v8::String::Utf8Value (.+?)\((.+?)\)/g, 'Napi::String $1(env, $2)'], + [/String::Utf8Value (.+?)\((.+?)\)/g, 'Napi::String $1(env, $2)'], + [/\.length\(\)/g, '.Length()'], + + [/Nan::MakeCallback\(([^,]+),[\s\\]+([^,]+),/gm, '$2.MakeCallback($1,'], + + [/class\s+(\w+)\s*:\s*public\s+Nan::ObjectWrap/g, 'class $1 : public Napi::ObjectWrap<$1>'], + [/(\w+)\(([^)]*)\)\s*:\s*Nan::ObjectWrap\(\)\s*(,)?/gm, '$1($2) : Napi::ObjectWrap<$1>()$3'], + + // HandleOKCallback to OnOK + [/HandleOKCallback/g, 'OnOK'], + // HandleErrorCallback to OnError + [/HandleErrorCallback/g, 'OnError'], + + // ex. .As() to .As() + [/\.As\(\)/g, '.As()'], + [/\.As<(Value|Boolean|String|Number|Object|Array|Symbol|External|Function)>\(\)/g, '.As()'], + + // ex. Nan::New(info[0]) to Napi::Number::New(info[0]) + [/Nan::New<(v8::)*Integer>\((.+?)\)/g, 'Napi::Number::New(env, $2)'], + [/Nan::New\(([0-9.]+)\)/g, 'Napi::Number::New(env, $1)'], + [/Nan::New<(v8::)*String>\("(.+?)"\)/g, 'Napi::String::New(env, "$2")'], + [/Nan::New\("(.+?)"\)/g, 'Napi::String::New(env, "$1")'], + [/Nan::New<(v8::)*(.+?)>\(\)/g, 'Napi::$2::New(env)'], + [/Nan::New<(.+?)>\(\)/g, 'Napi::$1::New(env)'], + [/Nan::New<(v8::)*(.+?)>\(/g, 'Napi::$2::New(env, '], + [/Nan::New<(.+?)>\(/g, 'Napi::$1::New(env, '], + [/Nan::NewBuffer\(/g, 'Napi::Buffer::New(env, '], + // TODO: Properly handle this + [/Nan::New\(/g, 'Napi::New(env, '], + + [/\.IsInt32\(\)/g, '.IsNumber()'], + [/->IsInt32\(\)/g, '.IsNumber()'], + + [/(.+?)->BooleanValue\(\)/g, '$1.As().Value()'], + [/(.+?)->Int32Value\(\)/g, '$1.As().Int32Value()'], + [/(.+?)->Uint32Value\(\)/g, '$1.As().Uint32Value()'], + [/(.+?)->IntegerValue\(\)/g, '$1.As().Int64Value()'], + [/(.+?)->NumberValue\(\)/g, '$1.As().DoubleValue()'], + + // ex. Nan::To(info[0]) to info[0].Value() + [/Nan::To\((.+?)\)/g, '$2.To()'], + [/Nan::To<(Boolean|String|Number|Object|Array|Symbol|Function)>\((.+?)\)/g, '$2.To()'], + // ex. Nan::To(info[0]) to info[0].As().Value() + [/Nan::To\((.+?)\)/g, '$1.As().Value()'], + // ex. Nan::To(info[0]) to info[0].As().Int32Value() + [/Nan::To\((.+?)\)/g, '$1.As().Int32Value()'], + // ex. Nan::To(info[0]) to info[0].As().Int32Value() + [/Nan::To\((.+?)\)/g, '$1.As().Int32Value()'], + // ex. Nan::To(info[0]) to info[0].As().Uint32Value() + [/Nan::To\((.+?)\)/g, '$1.As().Uint32Value()'], + // ex. Nan::To(info[0]) to info[0].As().Int64Value() + [/Nan::To\((.+?)\)/g, '$1.As().Int64Value()'], + // ex. Nan::To(info[0]) to info[0].As().FloatValue() + [/Nan::To\((.+?)\)/g, '$1.As().FloatValue()'], + // ex. Nan::To(info[0]) to info[0].As().DoubleValue() + [/Nan::To\((.+?)\)/g, '$1.As().DoubleValue()'], + + [/Nan::New\((\w+)\)->HasInstance\((\w+)\)/g, '$2.InstanceOf($1.Value())'], + + [/Nan::Has\(([^,]+),\s*/gm, '($1).Has('], + [/\.Has\([\s|\\]*Nan::New<(v8::)*String>\(([^)]+)\)\)/gm, '.Has($1)'], + [/\.Has\([\s|\\]*Nan::New\(([^)]+)\)\)/gm, '.Has($1)'], + + [/Nan::Get\(([^,]+),\s*/gm, '($1).Get('], + [/\.Get\([\s|\\]*Nan::New<(v8::)*String>\(([^)]+)\)\)/gm, '.Get($1)'], + [/\.Get\([\s|\\]*Nan::New\(([^)]+)\)\)/gm, '.Get($1)'], + + [/Nan::Set\(([^,]+),\s*/gm, '($1).Set('], + [/\.Set\([\s|\\]*Nan::New<(v8::)*String>\(([^)]+)\)\s*,/gm, '.Set($1,'], + [/\.Set\([\s|\\]*Nan::New\(([^)]+)\)\s*,/gm, '.Set($1,'], + + // ex. node::Buffer::HasInstance(info[0]) to info[0].IsBuffer() + [/node::Buffer::HasInstance\((.+?)\)/g, '$1.IsBuffer()'], + // ex. node::Buffer::Length(info[0]) to info[0].Length() + [/node::Buffer::Length\((.+?)\)/g, '$1.As>().Length()'], + // ex. node::Buffer::Data(info[0]) to info[0].Data() + [/node::Buffer::Data\((.+?)\)/g, '$1.As>().Data()'], + [/Nan::CopyBuffer\(/g, 'Napi::Buffer::Copy(env, '], + + // Nan::AsyncQueueWorker(worker) + [/Nan::AsyncQueueWorker\((.+)\);/g, '$1.Queue();'], + [/Nan::(Undefined|Null|True|False)\(\)/g, 'env.$1()'], + + // Nan::ThrowError(error) to Napi::Error::New(env, error).ThrowAsJavaScriptException() + [/([ ]*)return Nan::Throw(\w*?)Error\((.+?)\);/g, '$1Napi::$2Error::New(env, $3).ThrowAsJavaScriptException();\n$1return env.Null();'], + [/Nan::Throw(\w*?)Error\((.+?)\);\n(\s*)return;/g, 'Napi::$1Error::New(env, $2).ThrowAsJavaScriptException();\n$3return env.Null();'], + [/Nan::Throw(\w*?)Error\((.+?)\);/g, 'Napi::$1Error::New(env, $2).ThrowAsJavaScriptException();\n'], + // Nan::RangeError(error) to Napi::RangeError::New(env, error) + [/Nan::(\w*?)Error\((.+)\)/g, 'Napi::$1Error::New(env, $2)'], + + [/Nan::Set\((.+?),\n* *(.+?),\n* *(.+?),\n* *(.+?)\)/g, '$1.Set($2, $3, $4)'], + + [/Nan::(Escapable)?HandleScope\s+(\w+)\s*;/g, 'Napi::$1HandleScope $2(env);'], + [/Nan::(Escapable)?HandleScope/g, 'Napi::$1HandleScope'], + [/Nan::ForceSet\(([^,]+), ?/g, '$1->DefineProperty('], + [/\.ForceSet\(Napi::String::New\(env, "(\w+)"\),\s*?/g, '.DefineProperty("$1", '], + // [ /Nan::GetPropertyNames\(([^,]+)\)/, '$1->GetPropertyNames()' ], + [/Nan::Equals\(([^,]+),/g, '$1.StrictEquals('], + + [/(.+)->Set\(/g, '$1.Set('], + + [/Nan::Callback/g, 'Napi::FunctionReference'], + + [/Nan::Persistent/g, 'Napi::ObjectReference'], + [/Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target/g, 'Napi::Env& env, Napi::Object& target'], + + [/(\w+)\*\s+(\w+)\s*=\s*Nan::ObjectWrap::Unwrap<\w+>\(info\.This\(\)\);/g, '$1* $2 = this;'], + [/Nan::ObjectWrap::Unwrap<(\w+)>\((.*)\);/g, '$2.Unwrap<$1>();'], + + [/Nan::NAN_METHOD_RETURN_TYPE/g, 'void'], + [/NAN_INLINE/g, 'inline'], + + [/Nan::NAN_METHOD_ARGS_TYPE/g, 'const Napi::CallbackInfo&'], + [/NAN_METHOD\(([\w\d:]+?)\)/g, 'Napi::Value $1(const Napi::CallbackInfo& info)'], + [/static\s*NAN_GETTER\(([\w\d:]+?)\)/g, 'Napi::Value $1(const Napi::CallbackInfo& info)'], + [/NAN_GETTER\(([\w\d:]+?)\)/g, 'Napi::Value $1(const Napi::CallbackInfo& info)'], + [/static\s*NAN_SETTER\(([\w\d:]+?)\)/g, 'void $1(const Napi::CallbackInfo& info, const Napi::Value& value)'], + [/NAN_SETTER\(([\w\d:]+?)\)/g, 'void $1(const Napi::CallbackInfo& info, const Napi::Value& value)'], + [/void Init\((v8::)*Local<(v8::)*Object> exports\)/g, 'Napi::Object Init(Napi::Env env, Napi::Object exports)'], + [/NAN_MODULE_INIT\(([\w\d:]+?)\);/g, 'Napi::Object $1(Napi::Env env, Napi::Object exports);'], + [/NAN_MODULE_INIT\(([\w\d:]+?)\)/g, 'Napi::Object $1(Napi::Env env, Napi::Object exports)'], + + [/::(Init(?:ialize)?)\(target\)/g, '::$1(env, target, module)'], + [/constructor_template/g, 'constructor'], + + [/Nan::FunctionCallbackInfo<(v8::)?Value>[ ]*& [ ]*info\)[ ]*{\n*([ ]*)/gm, 'Napi::CallbackInfo& info) {\n$2Napi::Env env = info.Env();\n$2'], + [/Nan::FunctionCallbackInfo<(v8::)*Value>\s*&\s*info\);/g, 'Napi::CallbackInfo& info);'], + [/Nan::FunctionCallbackInfo<(v8::)*Value>\s*&/g, 'Napi::CallbackInfo&'], + + [/Buffer::HasInstance\(([^)]+)\)/g, '$1.IsBuffer()'], + + [/info\[(\d+)\]->/g, 'info[$1].'], + [/info\[([\w\d]+)\]->/g, 'info[$1].'], + [/info\.This\(\)->/g, 'info.This().'], + [/->Is(Object|String|Int32|Number)\(\)/g, '.Is$1()'], + [/info.GetReturnValue\(\).SetUndefined\(\)/g, 'return env.Undefined()'], + [/info\.GetReturnValue\(\)\.Set\(((\n|.)+?)\);/g, 'return $1;'], + + // ex. Local to Napi::Value + [/v8::Local/g, 'Napi::$1'], + [/Local<(Value|Boolean|String|Number|Object|Array|Symbol|External|Function)>/g, 'Napi::$1'], + + // Declare an env in helper functions that take a Napi::Value + [/(\w+)\(Napi::Value (\w+)(,\s*[^()]+)?\)\s*{\n*([ ]*)/gm, '$1(Napi::Value $2$3) {\n$4Napi::Env env = $2.Env();\n$4'], + + // delete #include and/or + [/#include +(<|")(?:node|nan).h("|>)/g, '#include $1napi.h$2\n#include $1uv.h$2'], + // NODE_MODULE to NODE_API_MODULE + [/NODE_MODULE/g, 'NODE_API_MODULE'], + [/Nan::/g, 'Napi::'], + [/nan.h/g, 'napi.h'], + + // delete .FromJust() + [/\.FromJust\(\)/g, ''], + // delete .ToLocalCheck() + [/\.ToLocalChecked\(\)/g, ''], + [/^.*->SetInternalFieldCount\(.*$/gm, ''], + + // replace using node; and/or using v8; to using Napi; + [/using (node|v8);/g, 'using Napi;'], + [/using namespace (node|Nan|v8);/g, 'using namespace Napi;'], + // delete using v8::Local; + [/using v8::Local;\n/g, ''], + // replace using v8::XXX; with using Napi::XXX + [/using v8::([A-Za-z]+);/g, 'using Napi::$1;'] + +]; + +const paths = listFiles(dir); +paths.forEach(function (dirEntry) { + const filename = dirEntry.split('\\').pop().split('/').pop(); + + // Check whether the file is a source file or a config file + // then execute function accordingly + const sourcePattern = /.+\.h|.+\.cc|.+\.cpp/; + if (sourcePattern.test(filename)) { + convertFile(dirEntry, SourceFileOperations); + } else if (ConfigFileOperations[filename] != null) { + convertFile(dirEntry, ConfigFileOperations[filename]); + } +}); + +function listFiles (dir, filelist) { + const files = fs.readdirSync(dir); + filelist = filelist || []; + files.forEach(function (file) { + if (file === 'node_modules') { + return; + } + + if (fs.statSync(path.join(dir, file)).isDirectory()) { + filelist = listFiles(path.join(dir, file), filelist); + } else { + filelist.push(path.join(dir, file)); + } + }); + return filelist; +} + +function convert (content, operations) { + for (let i = 0; i < operations.length; i++) { + const operation = operations[i]; + content = content.replace(operation[0], operation[1]); + } + return content; +} + +function convertFile (fileName, operations) { + fs.readFile(fileName, 'utf-8', function (err, file) { + if (err) throw err; + + file = convert(file, operations); + + fs.writeFile(fileName, file, function (err) { + if (err) throw err; + }); + }); +} diff --git a/backend/node_modules/sharp/node_modules/node-addon-api/tools/eslint-format.js b/backend/node_modules/sharp/node_modules/node-addon-api/tools/eslint-format.js new file mode 100644 index 00000000..6923ab7b --- /dev/null +++ b/backend/node_modules/sharp/node_modules/node-addon-api/tools/eslint-format.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +const spawn = require('child_process').spawnSync; + +const filesToCheck = '*.js'; +const FORMAT_START = process.env.FORMAT_START || 'main'; +const IS_WIN = process.platform === 'win32'; +const ESLINT_PATH = IS_WIN ? 'node_modules\\.bin\\eslint.cmd' : 'node_modules/.bin/eslint'; + +function main (args) { + let fix = false; + while (args.length > 0) { + switch (args[0]) { + case '-f': + case '--fix': + fix = true; + break; + default: + } + args.shift(); + } + + // Check js files that change on unstaged file + const fileUnStaged = spawn( + 'git', + ['diff', '--name-only', '--diff-filter=d', FORMAT_START, filesToCheck], + { + encoding: 'utf-8' + } + ); + + // Check js files that change on staged file + const fileStaged = spawn( + 'git', + ['diff', '--name-only', '--cached', '--diff-filter=d', FORMAT_START, filesToCheck], + { + encoding: 'utf-8' + } + ); + + const options = [ + ...fileStaged.stdout.split('\n').filter((f) => f !== ''), + ...fileUnStaged.stdout.split('\n').filter((f) => f !== '') + ]; + + if (fix) { + options.push('--fix'); + } + + const result = spawn(ESLINT_PATH, [...options], { + encoding: 'utf-8' + }); + + if (result.error && result.error.errno === 'ENOENT') { + console.error('Eslint not found! Eslint is supposed to be found at ', ESLINT_PATH); + return 2; + } + + if (result.status === 1) { + console.error('Eslint error:', result.stdout); + const fixCmd = 'npm run lint:fix'; + console.error(`ERROR: please run "${fixCmd}" to format changes in your commit + Note that when running the command locally, please keep your local + main branch and working branch up to date with nodejs/node-addon-api + to exclude un-related complains. + Or you can run "env FORMAT_START=upstream/main ${fixCmd}". + Also fix JS files by yourself if necessary.`); + return 1; + } + + if (result.stderr) { + console.error('Error running eslint:', result.stderr); + return 2; + } +} + +if (require.main === module) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/backend/node_modules/sharp/package.json b/backend/node_modules/sharp/package.json index 0c0d0098..b4d3a289 100644 --- a/backend/node_modules/sharp/package.json +++ b/backend/node_modules/sharp/package.json @@ -1,9 +1,9 @@ { "name": "sharp", "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF, AVIF and TIFF images", - "version": "0.34.5", + "version": "0.32.6", "author": "Lovell Fuller ", - "homepage": "https://sharp.pixelplumbing.com", + "homepage": "https://github.com/lovell/sharp", "contributors": [ "Pierre Inglebert ", "Jonathan Ong ", @@ -82,43 +82,36 @@ "Joris Dugué ", "Chris Banks ", "Ompal Singh ", - "Brodan ", + "Brodan ", "Brahim Ait elhaj ", "Mart Jansink ", - "Lachlan Newman ", - "Dennis Beatty ", - "Ingvar Stepanyan ", - "Don Denton " + "Lachlan Newman " ], "scripts": { - "build": "node install/build.js", - "install": "node install/check.js || npm run build", - "clean": "rm -rf src/build/ .nyc_output/ coverage/ test/fixtures/output.*", - "test": "npm run lint && npm run test-unit", - "lint": "npm run lint-cpp && npm run lint-js && npm run lint-types", - "lint-cpp": "cpplint --quiet src/*.h src/*.cc", - "lint-js": "biome lint", - "lint-types": "tsd --files ./test/types/sharp.test-d.ts", + "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)", + "clean": "rm -rf node_modules/ build/ vendor/ .nyc_output/ coverage/ test/fixtures/output.*", + "test": "npm run test-lint && npm run test-unit && npm run test-licensing && npm run test-types", + "test-lint": "semistandard && cpplint", + "test-unit": "nyc --reporter=lcov --reporter=text --check-coverage --branches=100 mocha", + "test-licensing": "license-checker --production --summary --onlyAllow=\"Apache-2.0;BSD;ISC;MIT\"", "test-leak": "./test/leak/leak.sh", - "test-unit": "node --experimental-test-coverage test/unit.mjs", - "package-from-local-build": "node npm/from-local-build.js", - "package-release-notes": "node npm/release-notes.js", - "docs-build": "node docs/build.mjs", - "docs-serve": "cd docs && npm start", - "docs-publish": "cd docs && npm run build && npx firebase-tools deploy --project pixelplumbing --only hosting:pixelplumbing-sharp" + "test-types": "tsd", + "docs-build": "node docs/build && node docs/search-index/build", + "docs-serve": "cd docs && npx serve", + "docs-publish": "cd docs && npx firebase-tools deploy --project pixelplumbing --only hosting:pixelplumbing-sharp" }, - "type": "commonjs", "main": "lib/index.js", "types": "lib/index.d.ts", "files": [ - "install", - "lib", - "src/*.{cc,h,gyp}" + "binding.gyp", + "install/**", + "lib/**", + "src/**" ], "repository": { "type": "git", - "url": "git://github.com/lovell/sharp.git" + "url": "git://github.com/lovell/sharp" }, "keywords": [ "jpeg", @@ -139,64 +132,73 @@ "vips" ], "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" }, "devDependencies": { - "@biomejs/biome": "^2.3.4", - "@cpplint/cli": "^0.1.0", - "@emnapi/runtime": "^1.7.0", - "@img/sharp-libvips-dev": "1.2.4", - "@img/sharp-libvips-dev-wasm32": "1.2.4", - "@img/sharp-libvips-win32-arm64": "1.2.4", - "@img/sharp-libvips-win32-ia32": "1.2.4", - "@img/sharp-libvips-win32-x64": "1.2.4", "@types/node": "*", - "emnapi": "^1.7.0", - "exif-reader": "^2.0.2", + "async": "^3.2.4", + "cc": "^3.0.1", + "exif-reader": "^1.2.0", "extract-zip": "^2.0.1", "icc": "^3.0.0", - "jsdoc-to-markdown": "^9.1.3", - "node-addon-api": "^8.5.0", - "node-gyp": "^11.5.0", - "tar-fs": "^3.1.1", - "tsd": "^0.33.0" + "jsdoc-to-markdown": "^8.0.0", + "license-checker": "^25.0.1", + "mocha": "^10.2.0", + "mock-fs": "^5.2.0", + "nyc": "^15.1.0", + "prebuild": "^12.0.0", + "semistandard": "^16.0.1", + "tsd": "^0.29.0" }, "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "config": { - "libvips": ">=8.17.3" + "libvips": "8.14.5", + "integrity": { + "darwin-arm64v8": "sha512-1QZzICfCJd4wAO0P6qmYI5e5VFMt9iCE4QgefI8VMMbdSzjIXA9L/ARN6pkMQPZ3h20Y9RtJ2W1skgCsvCIccw==", + "darwin-x64": "sha512-sMIKMYXsdU9FlIfztj6Kt/SfHlhlDpP0Ups7ftVFqwjaszmYmpI9y/d/q3mLb4jrzuSiSUEislSWCwBnW7MPTw==", + "linux-arm64v8": "sha512-CD8owELzkDumaom+O3jJ8fKamILAQdj+//KK/VNcHK3sngUcFpdjx36C8okwbux9sml/T7GTB/gzpvReDrAejQ==", + "linux-armv6": "sha512-wk6IPHatDFVWKJy7lI1TJezHGHPQut1wF2bwx256KlZwXUQU3fcVcMpV1zxXjgLFewHq2+uhyMkoSGBPahWzlA==", + "linux-armv7": "sha512-HEZC9KYtkmBK5rUR2MqBhrVarnQVZ/TwLUeLkKq0XuoM2pc/eXI6N0Fh5NGEFwdXI2XE8g1ySf+OYS6DDi+xCQ==", + "linux-x64": "sha512-SlFWrITSW5XVUkaFPQOySAaSGXnhkGJCj8X2wGYYta9hk5piZldQyMp4zwy0z6UeRu1qKTKtZvmq28W3Gnh9xA==", + "linuxmusl-arm64v8": "sha512-ga9iX7WUva3sG/VsKkOD318InLlCfPIztvzCZKZ2/+izQXRbQi8VoXWMHgEN4KHACv45FTl7mJ/8CRqUzhS8wQ==", + "linuxmusl-x64": "sha512-yeaHnpfee1hrZLok2l4eFceHzlfq8gN3QOu0R4Mh8iMK5O5vAUu97bdtxeZZeJJvHw8tfh2/msGi0qysxKN8bw==", + "win32-arm64v8": "sha512-kR91hy9w1+GEXK56hLh51+hBCBo7T+ijM4Slkmvb/2PsYZySq5H7s61n99iDYl6kTJP2y9sW5Xcvm3uuXDaDgg==", + "win32-ia32": "sha512-HrnofEbzHNpHJ0vVnjsTj5yfgVdcqdWshXuwFO2zc8xlEjA83BvXZ0lVj9MxPxkxJ2ta+/UlLr+CFzc5bOceMw==", + "win32-x64": "sha512-BwKckinJZ0Fu/EcunqiLPwOLEBWp4xf8GV7nvmVuKKz5f6B+GxoA2k9aa2wueqv4r4RJVgV/aWXZWFKOIjre/Q==" + }, + "runtime": "napi", + "target": 7 + }, + "engines": { + "node": ">=14.15.0" }, "funding": { "url": "https://opencollective.com/libvips" + }, + "binary": { + "napi_versions": [ + 7 + ] + }, + "semistandard": { + "env": [ + "mocha" + ] + }, + "cc": { + "linelength": "120", + "filter": [ + "build/include" + ] + }, + "tsd": { + "directory": "test/types/" } } diff --git a/backend/node_modules/sharp/src/binding.gyp b/backend/node_modules/sharp/src/binding.gyp deleted file mode 100644 index c4ec70cc..00000000 --- a/backend/node_modules/sharp/src/binding.gyp +++ /dev/null @@ -1,298 +0,0 @@ -# Copyright 2013 Lovell Fuller and others. -# SPDX-License-Identifier: Apache-2.0 - -{ - 'variables': { - 'vips_version': ' #include -#include -#include -#include #include -#include -#include +#include #include +#include +#include +#include // NOLINT(build/c++11) #include #include -#include "./common.h" +#include "common.h" using vips::VImage; @@ -79,7 +75,7 @@ namespace sharp { Napi::Buffer buffer = input.Get("buffer").As>(); descriptor->bufferLength = buffer.Length(); descriptor->buffer = buffer.Data(); - descriptor->isBuffer = true; + descriptor->isBuffer = TRUE; } descriptor->failOn = AttrAsEnum(input, "failOn", VIPS_TYPE_FAIL_ON); // Density for vector-based input @@ -97,7 +93,6 @@ namespace sharp { descriptor->rawWidth = AttrAsUint32(input, "rawWidth"); descriptor->rawHeight = AttrAsUint32(input, "rawHeight"); descriptor->rawPremultiplied = AttrAsBool(input, "rawPremultiplied"); - descriptor->rawPageHeight = AttrAsUint32(input, "rawPageHeight"); } // Multi-page input (GIF, TIFF, PDF) if (HasAttr(input, "pages")) { @@ -106,35 +101,19 @@ namespace sharp { if (HasAttr(input, "page")) { descriptor->page = AttrAsUint32(input, "page"); } - // SVG - if (HasAttr(input, "svgStylesheet")) { - descriptor->svgStylesheet = AttrAsStr(input, "svgStylesheet"); - } - if (HasAttr(input, "svgHighBitdepth")) { - descriptor->svgHighBitdepth = AttrAsBool(input, "svgHighBitdepth"); - } // Multi-level input (OpenSlide) - if (HasAttr(input, "openSlideLevel")) { - descriptor->openSlideLevel = AttrAsUint32(input, "openSlideLevel"); + if (HasAttr(input, "level")) { + descriptor->level = AttrAsUint32(input, "level"); } // subIFD (OME-TIFF) if (HasAttr(input, "subifd")) { - descriptor->tiffSubifd = AttrAsInt32(input, "tiffSubifd"); - } - // // PDF background color - if (HasAttr(input, "pdfBackground")) { - descriptor->pdfBackground = AttrAsVectorOfDouble(input, "pdfBackground"); - } - // Use JPEG 2000 oneshot mode? - if (HasAttr(input, "jp2Oneshot")) { - descriptor->jp2Oneshot = AttrAsBool(input, "jp2Oneshot"); + descriptor->subifd = AttrAsInt32(input, "subifd"); } // Create new image if (HasAttr(input, "createChannels")) { descriptor->createChannels = AttrAsUint32(input, "createChannels"); descriptor->createWidth = AttrAsUint32(input, "createWidth"); descriptor->createHeight = AttrAsUint32(input, "createHeight"); - descriptor->createPageHeight = AttrAsUint32(input, "createPageHeight"); if (HasAttr(input, "createNoiseType")) { descriptor->createNoiseType = AttrAsStr(input, "createNoiseType"); descriptor->createNoiseMean = AttrAsDouble(input, "createNoiseMean"); @@ -177,42 +156,20 @@ namespace sharp { descriptor->textWrap = AttrAsEnum(input, "textWrap", VIPS_TYPE_TEXT_WRAP); } } - // Join images together - if (HasAttr(input, "joinAnimated")) { - descriptor->joinAnimated = AttrAsBool(input, "joinAnimated"); - } - if (HasAttr(input, "joinAcross")) { - descriptor->joinAcross = AttrAsUint32(input, "joinAcross"); - } - if (HasAttr(input, "joinShim")) { - descriptor->joinShim = AttrAsUint32(input, "joinShim"); - } - if (HasAttr(input, "joinBackground")) { - descriptor->joinBackground = AttrAsVectorOfDouble(input, "joinBackground"); - } - if (HasAttr(input, "joinHalign")) { - descriptor->joinHalign = AttrAsEnum(input, "joinHalign", VIPS_TYPE_ALIGN); - } - if (HasAttr(input, "joinValign")) { - descriptor->joinValign = AttrAsEnum(input, "joinValign", VIPS_TYPE_ALIGN); - } // Limit input images to a given number of pixels, where pixels = width * height descriptor->limitInputPixels = static_cast(AttrAsInt64(input, "limitInputPixels")); - if (HasAttr(input, "access")) { - descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; - } + // Allow switch from random to sequential access + descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; // Remove safety features and allow unlimited input descriptor->unlimited = AttrAsBool(input, "unlimited"); - // Use the EXIF orientation to auto orient the image - descriptor->autoOrient = AttrAsBool(input, "autoOrient"); return descriptor; } // How many tasks are in the queue? - std::atomic counterQueue{0}; + volatile int counterQueue = 0; // How many tasks are being processed? - std::atomic counterProcess{0}; + volatile int counterProcess = 0; // Filename extension checkers static bool EndsWith(std::string const &str, std::string const &end) { @@ -287,8 +244,6 @@ namespace sharp { case ImageType::FITS: id = "fits"; break; case ImageType::EXR: id = "exr"; break; case ImageType::JXL: id = "jxl"; break; - case ImageType::RAD: id = "rad"; break; - case ImageType::DCRAW: id = "dcraw"; break; case ImageType::VIPS: id = "vips"; break; case ImageType::RAW: id = "raw"; break; case ImageType::UNKNOWN: id = "unknown"; break; @@ -335,10 +290,6 @@ namespace sharp { { "VipsForeignLoadOpenexr", ImageType::EXR }, { "VipsForeignLoadJxlFile", ImageType::JXL }, { "VipsForeignLoadJxlBuffer", ImageType::JXL }, - { "VipsForeignLoadRadFile", ImageType::RAD }, - { "VipsForeignLoadRadBuffer", ImageType::RAD }, - { "VipsForeignLoadDcRawFile", ImageType::DCRAW }, - { "VipsForeignLoadDcRawBuffer", ImageType::DCRAW }, { "VipsForeignLoadVips", ImageType::VIPS }, { "VipsForeignLoadVipsFile", ImageType::VIPS }, { "VipsForeignLoadRaw", ImageType::RAW } @@ -400,52 +351,9 @@ namespace sharp { imageType == ImageType::JPEG || imageType == ImageType::PNG || imageType == ImageType::SVG || - imageType == ImageType::TIFF || imageType == ImageType::HEIF; } - /* - Format-specific options builder - */ - vips::VOption* GetOptionsForImageType(ImageType imageType, InputDescriptor *descriptor) { - vips::VOption *option = VImage::option() - ->set("access", descriptor->access) - ->set("fail_on", descriptor->failOn); - if (descriptor->unlimited && ImageTypeSupportsUnlimited(imageType)) { - option->set("unlimited", true); - } - if (ImageTypeSupportsPage(imageType)) { - option->set("n", descriptor->pages); - option->set("page", descriptor->page); - } - switch (imageType) { - case ImageType::SVG: - option->set("dpi", descriptor->density) - ->set("stylesheet", descriptor->svgStylesheet.data()) - ->set("high_bitdepth", descriptor->svgHighBitdepth); - break; - case ImageType::TIFF: - option->set("subifd", descriptor->tiffSubifd); - break; - case ImageType::PDF: - option->set("dpi", descriptor->density) - ->set("background", descriptor->pdfBackground); - break; - case ImageType::OPENSLIDE: - option->set("level", descriptor->openSlideLevel); - break; - case ImageType::JP2: - option->set("oneshot", descriptor->jp2Oneshot); - break; - case ImageType::MAGICK: - option->set("density", std::to_string(descriptor->density).data()); - break; - default: - break; - } - return option; - } - /* Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data) */ @@ -455,17 +363,12 @@ namespace sharp { if (descriptor->isBuffer) { if (descriptor->rawChannels > 0) { // Raw, uncompressed pixel data - bool const is8bit = vips_band_format_is8bit(descriptor->rawDepth); image = VImage::new_from_memory(descriptor->buffer, descriptor->bufferLength, descriptor->rawWidth, descriptor->rawHeight, descriptor->rawChannels, descriptor->rawDepth); if (descriptor->rawChannels < 3) { - image.get_image()->Type = is8bit ? VIPS_INTERPRETATION_B_W : VIPS_INTERPRETATION_GREY16; + image.get_image()->Type = VIPS_INTERPRETATION_B_W; } else { - image.get_image()->Type = is8bit ? VIPS_INTERPRETATION_sRGB : VIPS_INTERPRETATION_RGB16; - } - if (descriptor->rawPageHeight > 0) { - image.set(VIPS_META_PAGE_HEIGHT, descriptor->rawPageHeight); - image.set(VIPS_META_N_PAGES, static_cast(descriptor->rawHeight / descriptor->rawPageHeight)); + image.get_image()->Type = VIPS_INTERPRETATION_sRGB; } if (descriptor->rawPremultiplied) { image = image.unpremultiply(); @@ -476,7 +379,28 @@ namespace sharp { imageType = DetermineImageType(descriptor->buffer, descriptor->bufferLength); if (imageType != ImageType::UNKNOWN) { try { - vips::VOption *option = GetOptionsForImageType(imageType, descriptor); + vips::VOption *option = VImage::option() + ->set("access", descriptor->access) + ->set("fail_on", descriptor->failOn); + if (descriptor->unlimited && ImageTypeSupportsUnlimited(imageType)) { + option->set("unlimited", TRUE); + } + if (imageType == ImageType::SVG || imageType == ImageType::PDF) { + option->set("dpi", descriptor->density); + } + if (imageType == ImageType::MAGICK) { + option->set("density", std::to_string(descriptor->density).data()); + } + if (ImageTypeSupportsPage(imageType)) { + option->set("n", descriptor->pages); + option->set("page", descriptor->page); + } + if (imageType == ImageType::OPENSLIDE) { + option->set("level", descriptor->level); + } + if (imageType == ImageType::TIFF) { + option->set("subifd", descriptor->subifd); + } image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option); if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { image = SetDensity(image, descriptor->density); @@ -516,10 +440,6 @@ namespace sharp { channels < 3 ? VIPS_INTERPRETATION_B_W : VIPS_INTERPRETATION_sRGB)) .new_from_image(background); } - if (descriptor->createPageHeight > 0) { - image.set(VIPS_META_PAGE_HEIGHT, descriptor->createPageHeight); - image.set(VIPS_META_N_PAGES, static_cast(descriptor->createHeight / descriptor->createPageHeight)); - } image = image.cast(VIPS_FORMAT_UCHAR); imageType = ImageType::RAW; } else if (descriptor->textValue.length() > 0) { @@ -563,7 +483,28 @@ namespace sharp { } if (imageType != ImageType::UNKNOWN) { try { - vips::VOption *option = GetOptionsForImageType(imageType, descriptor); + vips::VOption *option = VImage::option() + ->set("access", descriptor->access) + ->set("fail_on", descriptor->failOn); + if (descriptor->unlimited && ImageTypeSupportsUnlimited(imageType)) { + option->set("unlimited", TRUE); + } + if (imageType == ImageType::SVG || imageType == ImageType::PDF) { + option->set("dpi", descriptor->density); + } + if (imageType == ImageType::MAGICK) { + option->set("density", std::to_string(descriptor->density).data()); + } + if (ImageTypeSupportsPage(imageType)) { + option->set("n", descriptor->pages); + option->set("page", descriptor->page); + } + if (imageType == ImageType::OPENSLIDE) { + option->set("level", descriptor->level); + } + if (imageType == ImageType::TIFF) { + option->set("subifd", descriptor->subifd); + } image = VImage::new_from_file(descriptor->file.data(), option); if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { image = SetDensity(image, descriptor->density); @@ -589,54 +530,15 @@ namespace sharp { Does this image have an embedded profile? */ bool HasProfile(VImage image) { - return image.get_typeof(VIPS_META_ICC_NAME) == VIPS_TYPE_BLOB; + return (image.get_typeof(VIPS_META_ICC_NAME) != 0) ? TRUE : FALSE; } /* - Get copy of embedded profile. + Does this image have an alpha channel? + Uses colour space interpretation with number of channels to guess this. */ - std::pair GetProfile(VImage image) { - std::pair icc(nullptr, 0); - if (HasProfile(image)) { - size_t length; - const void *data = image.get_blob(VIPS_META_ICC_NAME, &length); - icc.first = static_cast(g_malloc(length)); - icc.second = length; - memcpy(icc.first, data, length); - } - return icc; - } - - /* - Set embedded profile. - */ - VImage SetProfile(VImage image, std::pair icc) { - if (icc.first != nullptr) { - image = image.copy(); - image.set(VIPS_META_ICC_NAME, reinterpret_cast(vips_area_free_cb), icc.first, icc.second); - } - return image; - } - - static void* RemoveExifCallback(VipsImage *image, char const *field, GValue *value, void *data) { - std::vector *fieldNames = static_cast *>(data); - std::string fieldName(field); - if (fieldName.substr(0, 8) == ("exif-ifd")) { - fieldNames->push_back(fieldName); - } - return nullptr; - } - - /* - Remove all EXIF-related image fields. - */ - VImage RemoveExif(VImage image) { - std::vector fieldNames; - vips_image_map(image.get_image(), static_cast(RemoveExifCallback), &fieldNames); - for (const auto& f : fieldNames) { - image.remove(f.data()); - } - return image; + bool HasAlpha(VImage image) { + return image.has_alpha(); } /* @@ -674,21 +576,22 @@ namespace sharp { */ VImage SetAnimationProperties(VImage image, int nPages, int pageHeight, std::vector delay, int loop) { bool hasDelay = !delay.empty(); + + // Avoid a copy if none of the animation properties are needed. + if (nPages == 1 && !hasDelay && loop == -1) return image; + + if (delay.size() == 1) { + // We have just one delay, repeat that value for all frames. + delay.insert(delay.end(), nPages - 1, delay[0]); + } + + // Attaching metadata, need to copy the image. VImage copy = image.copy(); // Only set page-height if we have more than one page, or this could // accidentally turn into an animated image later. if (nPages > 1) copy.set(VIPS_META_PAGE_HEIGHT, pageHeight); - if (hasDelay) { - if (delay.size() == 1) { - // We have just one delay, repeat that value for all frames. - delay.insert(delay.end(), nPages - 1, delay[0]); - } - copy.set("delay", delay); - } - if (nPages == 1 && !hasDelay && loop == -1) { - loop = 1; - } + if (hasDelay) copy.set("delay", delay); if (loop != -1) copy.set("loop", loop); return copy; @@ -817,7 +720,7 @@ namespace sharp { int *timeout = VIPS_NEW(im, int); *timeout = seconds; g_signal_connect(im, "eval", G_CALLBACK(VipsProgressCallBack), timeout); - vips_image_set_progress(im, true); + vips_image_set_progress(im, TRUE); } } } @@ -827,7 +730,7 @@ namespace sharp { */ void VipsProgressCallBack(VipsImage *im, VipsProgress *progress, int *timeout) { if (*timeout > 0 && progress->run >= *timeout) { - vips_image_set_kill(im, true); + vips_image_set_kill(im, TRUE); vips_error("timeout", "%d%% complete", progress->percent); *timeout = 0; } @@ -974,6 +877,14 @@ namespace sharp { return interpretation == VIPS_INTERPRETATION_RGB16 || interpretation == VIPS_INTERPRETATION_GREY16; } + /* + Return the image alpha maximum. Useful for combining alpha bands. scRGB + images are 0 - 1 for image data, but the alpha is 0 - 255. + */ + double MaximumImageAlpha(VipsInterpretation const interpretation) { + return Is16Bit(interpretation) ? 65535.0 : 255.0; + } + /* Convert RGBA value to another colourspace */ @@ -1016,25 +927,25 @@ namespace sharp { 0.0722 * colour[2]) }; } - // Add alpha channel(s) to alphaColour colour - if (colour[3] < 255.0 || image.has_alpha()) { - int extraBands = image.bands() > 4 ? image.bands() - 3 : 1; - alphaColour.insert(alphaColour.end(), extraBands, colour[3] * multiplier); + // Add alpha channel to alphaColour colour + if (colour[3] < 255.0 || HasAlpha(image)) { + alphaColour.push_back(colour[3] * multiplier); } // Ensure alphaColour colour uses correct colourspace alphaColour = sharp::GetRgbaAsColourspace(alphaColour, image.interpretation(), premultiply); // Add non-transparent alpha channel, if required - if (colour[3] < 255.0 && !image.has_alpha()) { - image = image.bandjoin_const({ 255 * multiplier }); + if (colour[3] < 255.0 && !HasAlpha(image)) { + image = image.bandjoin( + VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier).cast(image.format())); } return std::make_tuple(image, alphaColour); } /* - Removes alpha channels, if any. + Removes alpha channel, if any. */ VImage RemoveAlpha(VImage image) { - while (image.bands() > 1 && image.has_alpha()) { + if (HasAlpha(image)) { image = image.extract_band(0, VImage::option()->set("n", image.bands() - 1)); } return image; @@ -1044,8 +955,10 @@ namespace sharp { Ensures alpha channel, if missing. */ VImage EnsureAlpha(VImage image, double const value) { - if (!image.has_alpha()) { - image = image.bandjoin_const({ value * vips_interpretation_max_alpha(image.interpretation()) }); + if (!HasAlpha(image)) { + std::vector alpha; + alpha.push_back(value * sharp::MaximumImageAlpha(image.interpretation())); + image = image.bandjoin_const(alpha); } return image; } @@ -1120,10 +1033,9 @@ namespace sharp { /* Ensure decoding remains sequential. */ - VImage StaySequential(VImage image, bool condition) { - if (vips_image_is_sequential(image.get_image()) && condition) { - image = image.copy_memory().copy(); - image.remove(VIPS_META_SEQUENTIAL); + VImage StaySequential(VImage image, VipsAccess access, bool condition) { + if (access == VIPS_ACCESS_SEQUENTIAL && condition) { + return image.copy_memory(); } return image; } diff --git a/backend/node_modules/sharp/src/common.h b/backend/node_modules/sharp/src/common.h index c15755bb..a9425945 100644 --- a/backend/node_modules/sharp/src/common.h +++ b/backend/node_modules/sharp/src/common.h @@ -1,15 +1,11 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #ifndef SRC_COMMON_H_ #define SRC_COMMON_H_ -#include #include #include -#include #include #include @@ -18,14 +14,18 @@ // Verify platform and compiler compatibility #if (VIPS_MAJOR_VERSION < 8) || \ - (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 17) || \ - (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 17 && VIPS_MICRO_VERSION < 3) -#error "libvips version 8.17.3+ is required - please see https://sharp.pixelplumbing.com/install" + (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 14) || \ + (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 14 && VIPS_MICRO_VERSION < 5) +#error "libvips version 8.14.5+ is required - please see https://sharp.pixelplumbing.com/install" #endif -#if defined(__has_include) -#if !__has_include() -#error "C++17 compiler required - please see https://sharp.pixelplumbing.com/install" +#if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6))) +#error "GCC version 4.6+ is required for C++11 features - please see https://sharp.pixelplumbing.com/install" +#endif + +#if (defined(__clang__) && defined(__has_feature)) +#if (!__has_feature(cxx_range_for)) +#error "clang version 3.0+ is required for C++11 features - please see https://sharp.pixelplumbing.com/install" #endif #endif @@ -33,10 +33,9 @@ using vips::VImage; namespace sharp { - struct InputDescriptor { + struct InputDescriptor { // NOLINT(runtime/indentation_namespace) std::string name; std::string file; - bool autoOrient; char *buffer; VipsFailOn failOn; uint64_t limitInputPixels; @@ -51,13 +50,13 @@ namespace sharp { int rawWidth; int rawHeight; bool rawPremultiplied; - int rawPageHeight; int pages; int page; + int level; + int subifd; int createChannels; int createWidth; int createHeight; - int createPageHeight; std::vector createBackground; std::string createNoiseType; double createNoiseMean; @@ -74,65 +73,41 @@ namespace sharp { int textSpacing; VipsTextWrap textWrap; int textAutofitDpi; - bool joinAnimated; - int joinAcross; - int joinShim; - std::vector joinBackground; - VipsAlign joinHalign; - VipsAlign joinValign; - std::string svgStylesheet; - bool svgHighBitdepth; - int tiffSubifd; - int openSlideLevel; - std::vector pdfBackground; - bool jp2Oneshot; InputDescriptor(): - autoOrient(false), buffer(nullptr), failOn(VIPS_FAIL_ON_WARNING), limitInputPixels(0x3FFF * 0x3FFF), - unlimited(false), - access(VIPS_ACCESS_SEQUENTIAL), + unlimited(FALSE), + access(VIPS_ACCESS_RANDOM), bufferLength(0), - isBuffer(false), + isBuffer(FALSE), density(72.0), - ignoreIcc(false), + ignoreIcc(FALSE), rawDepth(VIPS_FORMAT_UCHAR), rawChannels(0), rawWidth(0), rawHeight(0), rawPremultiplied(false), - rawPageHeight(0), pages(1), page(0), + level(0), + subifd(-1), createChannels(0), createWidth(0), createHeight(0), - createPageHeight(0), createBackground{ 0.0, 0.0, 0.0, 255.0 }, createNoiseMean(0.0), createNoiseSigma(0.0), textWidth(0), textHeight(0), textAlign(VIPS_ALIGN_LOW), - textJustify(false), + textJustify(FALSE), textDpi(72), - textRgba(false), + textRgba(FALSE), textSpacing(0), textWrap(VIPS_TEXT_WRAP_WORD), - textAutofitDpi(0), - joinAnimated(false), - joinAcross(1), - joinShim(0), - joinBackground{ 0.0, 0.0, 0.0, 255.0 }, - joinHalign(VIPS_ALIGN_LOW), - joinValign(VIPS_ALIGN_LOW), - svgHighBitdepth(false), - tiffSubifd(-1), - openSlideLevel(0), - pdfBackground{ 255.0, 255.0, 255.0, 255.0 }, - jp2Oneshot(false) {} + textAutofitDpi(0) {} }; // Convenience methods to access the attributes of a Napi::Object @@ -171,8 +146,6 @@ namespace sharp { FITS, EXR, JXL, - RAD, - DCRAW, VIPS, RAW, UNKNOWN, @@ -188,10 +161,10 @@ namespace sharp { }; // How many tasks are in the queue? - extern std::atomic counterQueue; + extern volatile int counterQueue; // How many tasks are being processed? - extern std::atomic counterProcess; + extern volatile int counterProcess; // Filename extension checkers bool IsJpeg(std::string const &str); @@ -229,9 +202,14 @@ namespace sharp { ImageType DetermineImageType(char const *file); /* - Format-specific options builder + Does this image type support multiple pages? */ - vips::VOption* GetOptionsForImageType(ImageType imageType, InputDescriptor *descriptor); + bool ImageTypeSupportsPage(ImageType imageType); + + /* + Does this image type support removal of safety limits? + */ + bool ImageTypeSupportsUnlimited(ImageType imageType); /* Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data) @@ -244,19 +222,10 @@ namespace sharp { bool HasProfile(VImage image); /* - Get copy of embedded profile. + Does this image have an alpha channel? + Uses colour space interpretation with number of channels to guess this. */ - std::pair GetProfile(VImage image); - - /* - Set embedded profile. - */ - VImage SetProfile(VImage image, std::pair icc); - - /* - Remove all EXIF-related image fields. - */ - VImage RemoveExif(VImage image); + bool HasAlpha(VImage image); /* Get EXIF Orientation of image, if any. @@ -365,6 +334,12 @@ namespace sharp { */ bool Is16Bit(VipsInterpretation const interpretation); + /* + Return the image alpha maximum. Useful for combining alpha bands. scRGB + images are 0 - 1 for image data, but the alpha is 0 - 255. + */ + double MaximumImageAlpha(VipsInterpretation const interpretation); + /* Convert RGBA value to another colourspace */ @@ -377,7 +352,7 @@ namespace sharp { std::tuple> ApplyAlpha(VImage image, std::vector colour, bool premultiply); /* - Removes alpha channels, if any. + Removes alpha channel, if any. */ VImage RemoveAlpha(VImage image); @@ -395,7 +370,7 @@ namespace sharp { /* Ensure decoding remains sequential. */ - VImage StaySequential(VImage image, bool condition = true); + VImage StaySequential(VImage image, VipsAccess access, bool condition = TRUE); } // namespace sharp diff --git a/backend/node_modules/sharp/src/libvips/cplusplus/VConnection.cpp b/backend/node_modules/sharp/src/libvips/cplusplus/VConnection.cpp new file mode 100644 index 00000000..55451415 --- /dev/null +++ b/backend/node_modules/sharp/src/libvips/cplusplus/VConnection.cpp @@ -0,0 +1,151 @@ +/* Object part of the VSource and VTarget class + */ + +/* + + Copyright (C) 1991-2001 The National Gallery + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + + */ + +/* + + These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk + + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include + +#include + +/* +#define VIPS_DEBUG +#define VIPS_DEBUG_VERBOSE + */ + +VIPS_NAMESPACE_START + +VSource +VSource::new_from_descriptor( int descriptor ) +{ + VipsSource *input; + + if( !(input = vips_source_new_from_descriptor( descriptor )) ) + throw VError(); + + VSource out( input ); + + return( out ); +} + +VSource +VSource::new_from_file( const char *filename ) +{ + VipsSource *input; + + if( !(input = vips_source_new_from_file( filename )) ) + throw VError(); + + VSource out( input ); + + return( out ); +} + +VSource +VSource::new_from_blob( VipsBlob *blob ) +{ + VipsSource *input; + + if( !(input = vips_source_new_from_blob( blob )) ) + throw VError(); + + VSource out( input ); + + return( out ); +} + +VSource +VSource::new_from_memory( const void *data, + size_t size ) +{ + VipsSource *input; + + if( !(input = vips_source_new_from_memory( data, size )) ) + throw VError(); + + VSource out( input ); + + return( out ); +} + +VSource +VSource::new_from_options( const char *options ) +{ + VipsSource *input; + + if( !(input = vips_source_new_from_options( options )) ) + throw VError(); + + VSource out( input ); + + return( out ); +} + +VTarget +VTarget::new_to_descriptor( int descriptor ) +{ + VipsTarget *output; + + if( !(output = vips_target_new_to_descriptor( descriptor )) ) + throw VError(); + + VTarget out( output ); + + return( out ); +} + +VTarget +VTarget::new_to_file( const char *filename ) +{ + VipsTarget *output; + + if( !(output = vips_target_new_to_file( filename )) ) + throw VError(); + + VTarget out( output ); + + return( out ); +} + +VTarget +VTarget::new_to_memory() +{ + VipsTarget *output; + + if( !(output = vips_target_new_to_memory()) ) + throw VError(); + + VTarget out( output ); + + return( out ); +} + +VIPS_NAMESPACE_END diff --git a/backend/node_modules/sharp/src/libvips/cplusplus/VError.cpp b/backend/node_modules/sharp/src/libvips/cplusplus/VError.cpp new file mode 100644 index 00000000..3be49068 --- /dev/null +++ b/backend/node_modules/sharp/src/libvips/cplusplus/VError.cpp @@ -0,0 +1,49 @@ +// Code for error type + +/* + + Copyright (C) 1991-2001 The National Gallery + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + + */ + +/* + + These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk + + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include + +VIPS_NAMESPACE_START + +std::ostream &operator<<( std::ostream &file, const VError &err ) +{ + err.ostream_print( file ); + return( file ); +} + +void VError::ostream_print( std::ostream &file ) const +{ + file << _what; +} + +VIPS_NAMESPACE_END diff --git a/backend/node_modules/sharp/src/libvips/cplusplus/VImage.cpp b/backend/node_modules/sharp/src/libvips/cplusplus/VImage.cpp new file mode 100644 index 00000000..72c3fa9d --- /dev/null +++ b/backend/node_modules/sharp/src/libvips/cplusplus/VImage.cpp @@ -0,0 +1,1548 @@ +/* Object part of VImage class + * + * 30/12/14 + * - allow set enum value from string + * 10/6/16 + * - missing implementation of VImage::write() + * 11/6/16 + * - added arithmetic assignment overloads, += etc. + */ + +/* + + Copyright (C) 1991-2001 The National Gallery + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + + */ + +/* + + These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk + + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include + +#include + +/* +#define VIPS_DEBUG +#define VIPS_DEBUG_VERBOSE + */ + +VIPS_NAMESPACE_START + +/** + * \namespace vips + * + * General docs for the vips namespace. + */ + +std::vector +to_vectorv( int n, ... ) +{ + std::vector vector( n ); + va_list ap; + + va_start( ap, n ); + for( int i = 0; i < n; i++ ) + vector[i] = va_arg( ap, double ); + va_end( ap ); + + return( vector ); +} + +std::vector +to_vector( double value ) +{ + return( to_vectorv( 1, value ) ); +} + +std::vector +to_vector( int n, double array[] ) +{ + std::vector vector( n ); + + for( int i = 0; i < n; i++ ) + vector[i] = array[i]; + + return( vector ); +} + +std::vector +negate( std::vector vector ) +{ + std::vector new_vector( vector.size() ); + + for( std::vector::size_type i = 0; i < vector.size(); i++ ) + new_vector[i] = vector[i] * -1; + + return( new_vector ); +} + +std::vector +invert( std::vector vector ) +{ + std::vector new_vector( vector.size() ); + + for( std::vector::size_type i = 0; i < vector.size(); i++ ) + new_vector[i] = 1.0 / vector[i]; + + return( new_vector ); +} + +VOption::~VOption() +{ + std::list::iterator i; + + for( i = options.begin(); i != options.end(); ++i ) + delete *i; +} + +// input bool +VOption * +VOption::set( const char *name, bool value ) +{ + Pair *pair = new Pair( name ); + + pair->input = true; + g_value_init( &pair->value, G_TYPE_BOOLEAN ); + g_value_set_boolean( &pair->value, value ); + options.push_back( pair ); + + return( this ); +} + +// input int ... this path is used for enums as well +VOption * +VOption::set( const char *name, int value ) +{ + Pair *pair = new Pair( name ); + + pair->input = true; + g_value_init( &pair->value, G_TYPE_INT ); + g_value_set_int( &pair->value, value ); + options.push_back( pair ); + + return( this ); +} + +// input guint64 +VOption * +VOption::set( const char *name, guint64 value ) +{ + Pair *pair = new Pair( name ); + + pair->input = true; + g_value_init( &pair->value, G_TYPE_UINT64 ); + g_value_set_uint64( &pair->value, value ); + options.push_back( pair ); + + return( this ); +} + +// input double +VOption * +VOption::set( const char *name, double value ) +{ + Pair *pair = new Pair( name ); + + pair->input = true; + g_value_init( &pair->value, G_TYPE_DOUBLE ); + g_value_set_double( &pair->value, value ); + options.push_back( pair ); + + return( this ); +} + +VOption * +VOption::set( const char *name, const char *value ) +{ + Pair *pair = new Pair( name ); + + pair->input = true; + g_value_init( &pair->value, G_TYPE_STRING ); + g_value_set_string( &pair->value, value ); + options.push_back( pair ); + + return( this ); +} + +// input vips object (image, source, target, etc. etc.) +VOption * +VOption::set( const char *name, const VObject value ) +{ + Pair *pair = new Pair( name ); + VipsObject *object = value.get_object(); + GType type = G_OBJECT_TYPE( object ); + + pair->input = true; + g_value_init( &pair->value, type ); + g_value_set_object( &pair->value, object ); + options.push_back( pair ); + + return( this ); +} + +// input int array +VOption * +VOption::set( const char *name, std::vector value ) +{ + Pair *pair = new Pair( name ); + + int *array; + + pair->input = true; + + g_value_init( &pair->value, VIPS_TYPE_ARRAY_INT ); + vips_value_set_array_int( &pair->value, NULL, + static_cast< int >( value.size() ) ); + array = vips_value_get_array_int( &pair->value, NULL ); + + for( std::vector::size_type i = 0; i < value.size(); i++ ) + array[i] = value[i]; + + options.push_back( pair ); + + return( this ); +} + +// input double array +VOption * +VOption::set( const char *name, std::vector value ) +{ + Pair *pair = new Pair( name ); + + double *array; + + pair->input = true; + + g_value_init( &pair->value, VIPS_TYPE_ARRAY_DOUBLE ); + vips_value_set_array_double( &pair->value, NULL, + static_cast< int >( value.size() ) ); + array = vips_value_get_array_double( &pair->value, NULL ); + + for( std::vector::size_type i = 0; i < value.size(); i++ ) + array[i] = value[i]; + + options.push_back( pair ); + + return( this ); +} + +// input image array +VOption * +VOption::set( const char *name, std::vector value ) +{ + Pair *pair = new Pair( name ); + + VipsImage **array; + + pair->input = true; + + g_value_init( &pair->value, VIPS_TYPE_ARRAY_IMAGE ); + vips_value_set_array_image( &pair->value, + static_cast< int >( value.size() ) ); + array = vips_value_get_array_image( &pair->value, NULL ); + + for( std::vector::size_type i = 0; i < value.size(); i++ ) { + VipsImage *vips_image = value[i].get_image(); + + array[i] = vips_image; + g_object_ref( vips_image ); + } + + options.push_back( pair ); + + return( this ); +} + +// input blob +VOption * +VOption::set( const char *name, VipsBlob *value ) +{ + Pair *pair = new Pair( name ); + + pair->input = true; + g_value_init( &pair->value, VIPS_TYPE_BLOB ); + g_value_set_boxed( &pair->value, value ); + options.push_back( pair ); + + return( this ); +} + +// output bool +VOption * +VOption::set( const char *name, bool *value ) +{ + Pair *pair = new Pair( name ); + + pair->input = false; + pair->vbool = value; + g_value_init( &pair->value, G_TYPE_BOOLEAN ); + + options.push_back( pair ); + + return( this ); +} + +// output int +VOption * +VOption::set( const char *name, int *value ) +{ + Pair *pair = new Pair( name ); + + pair->input = false; + pair->vint = value; + g_value_init( &pair->value, G_TYPE_INT ); + + options.push_back( pair ); + + return( this ); +} + +// output double +VOption * +VOption::set( const char *name, double *value ) +{ + Pair *pair = new Pair( name ); + + pair->input = false; + pair->vdouble = value; + g_value_init( &pair->value, G_TYPE_DOUBLE ); + + options.push_back( pair ); + + return( this ); +} + +// output image +VOption * +VOption::set( const char *name, VImage *value ) +{ + Pair *pair = new Pair( name ); + + pair->input = false; + pair->vimage = value; + g_value_init( &pair->value, VIPS_TYPE_IMAGE ); + + options.push_back( pair ); + + return( this ); +} + +// output doublearray +VOption * +VOption::set( const char *name, std::vector *value ) +{ + Pair *pair = new Pair( name ); + + pair->input = false; + pair->vvector = value; + g_value_init( &pair->value, VIPS_TYPE_ARRAY_DOUBLE ); + + options.push_back( pair ); + + return( this ); +} + +// output blob +VOption * +VOption::set( const char *name, VipsBlob **value ) +{ + Pair *pair = new Pair( name ); + + pair->input = false; + pair->vblob = value; + g_value_init( &pair->value, VIPS_TYPE_BLOB ); + + options.push_back( pair ); + + return( this ); +} + +// just g_object_set_property(), except we allow set enum from string +static void +set_property( VipsObject *object, const char *name, const GValue *value ) +{ + VipsObjectClass *object_class = VIPS_OBJECT_GET_CLASS( object ); + GType type = G_VALUE_TYPE( value ); + + GParamSpec *pspec; + VipsArgumentClass *argument_class; + VipsArgumentInstance *argument_instance; + + if( vips_object_get_argument( object, name, + &pspec, &argument_class, &argument_instance ) ) { + g_warning( "%s", vips_error_buffer() ); + vips_error_clear(); + return; + } + + if( G_IS_PARAM_SPEC_ENUM( pspec ) && + type == G_TYPE_STRING ) { + GType pspec_type = G_PARAM_SPEC_VALUE_TYPE( pspec ); + + int enum_value; + GValue value2 = { 0 }; + + if( (enum_value = vips_enum_from_nick( object_class->nickname, + pspec_type, g_value_get_string( value ) )) < 0 ) { + g_warning( "%s", vips_error_buffer() ); + vips_error_clear(); + return; + } + + g_value_init( &value2, pspec_type ); + g_value_set_enum( &value2, enum_value ); + g_object_set_property( G_OBJECT( object ), name, &value2 ); + g_value_unset( &value2 ); + } + else + g_object_set_property( G_OBJECT( object ), name, value ); +} + +// walk the options and set props on the operation +void +VOption::set_operation( VipsOperation *operation ) +{ + std::list::iterator i; + + for( i = options.begin(); i != options.end(); ++i ) + if( (*i)->input ) { +#ifdef VIPS_DEBUG_VERBOSE + printf( "set_operation: " ); + vips_object_print_name( VIPS_OBJECT( operation ) ); + char *str_value = + g_strdup_value_contents( &(*i)->value ); + printf( ".%s = %s\n", (*i)->name, str_value ); + g_free( str_value ); +#endif /*VIPS_DEBUG_VERBOSE*/ + + set_property( VIPS_OBJECT( operation ), + (*i)->name, &(*i)->value ); + } +} + +// walk the options and fetch any requested outputs +void +VOption::get_operation( VipsOperation *operation ) +{ + std::list::iterator i; + + for( i = options.begin(); i != options.end(); ++i ) + if( ! (*i)->input ) { + const char *name = (*i)->name; + + g_object_get_property( G_OBJECT( operation ), + name, &(*i)->value ); + +#ifdef VIPS_DEBUG_VERBOSE + printf( "get_operation: " ); + vips_object_print_name( VIPS_OBJECT( operation ) ); + char *str_value = g_strdup_value_contents( + &(*i)->value ); + printf( ".%s = %s\n", name, str_value ); + g_free( str_value ); +#endif /*VIPS_DEBUG_VERBOSE*/ + + GValue *value = &(*i)->value; + GType type = G_VALUE_TYPE( value ); + + if( type == VIPS_TYPE_IMAGE ) { + // rebox object + VipsImage *image = VIPS_IMAGE( + g_value_get_object( value ) ); + *((*i)->vimage) = VImage( image ); + } + else if( type == G_TYPE_INT ) + *((*i)->vint) = g_value_get_int( value ); + else if( type == G_TYPE_BOOLEAN ) + *((*i)->vbool) = g_value_get_boolean( value ); + else if( type == G_TYPE_DOUBLE ) + *((*i)->vdouble) = g_value_get_double( value ); + else if( type == VIPS_TYPE_ARRAY_DOUBLE ) { + int length; + double *array = + vips_value_get_array_double( value, + &length ); + + ((*i)->vvector)->resize( length ); + for( int j = 0; j < length; j++ ) + (*((*i)->vvector))[j] = array[j]; + } + else if( type == VIPS_TYPE_BLOB ) { + // our caller gets a reference + *((*i)->vblob) = + (VipsBlob *) g_value_dup_boxed( value ); + } + } +} + +void +VImage::call_option_string( const char *operation_name, + const char *option_string, VOption *options ) +{ + VipsOperation *operation; + + VIPS_DEBUG_MSG( "call_option_string: starting for %s ...\n", + operation_name ); + + if( !(operation = vips_operation_new( operation_name )) ) { + delete options; + throw( VError() ); + } + + /* Set str options before vargs options, so the user can't + * override things we set deliberately. + */ + if( option_string && + vips_object_set_from_string( VIPS_OBJECT( operation ), + option_string ) ) { + vips_object_unref_outputs( VIPS_OBJECT( operation ) ); + g_object_unref( operation ); + delete options; + throw( VError() ); + } + + if( options ) + options->set_operation( operation ); + + /* Build from cache. + */ + if( vips_cache_operation_buildp( &operation ) ) { + vips_object_unref_outputs( VIPS_OBJECT( operation ) ); + g_object_unref( operation ); + delete options; + throw( VError() ); + } + + /* Walk args again, writing output. + */ + if( options ) + options->get_operation( operation ); + + /* We're done with options! + */ + delete options; + + /* The operation we have built should now have been reffed by + * one of its arguments or have finished its work. Either + * way, we can unref. + */ + g_object_unref( operation ); +} + +void +VImage::call( const char *operation_name, VOption *options ) +{ + call_option_string( operation_name, NULL, options ); +} + +VImage +VImage::new_from_file( const char *name, VOption *options ) +{ + char filename[VIPS_PATH_MAX]; + char option_string[VIPS_PATH_MAX]; + const char *operation_name; + + VImage out; + + vips__filename_split8( name, filename, option_string ); + if( !(operation_name = vips_foreign_find_load( filename )) ) { + delete options; + throw VError(); + } + + call_option_string( operation_name, option_string, + (options ? options : VImage::option())-> + set( "filename", filename )-> + set( "out", &out ) ); + + return( out ); +} + +VImage +VImage::new_from_buffer( const void *buf, size_t len, const char *option_string, + VOption *options ) +{ + const char *operation_name; + VipsBlob *blob; + VImage out; + + if( !(operation_name = vips_foreign_find_load_buffer( buf, len )) ) { + delete options; + throw( VError() ); + } + + /* We don't take a copy of the data or free it. + */ + blob = vips_blob_new( NULL, buf, len ); + options = (options ? options : VImage::option())-> + set( "buffer", blob )-> + set( "out", &out ); + vips_area_unref( VIPS_AREA( blob ) ); + + call_option_string( operation_name, option_string, options ); + + return( out ); +} + +VImage +VImage::new_from_buffer( const std::string &buf, const char *option_string, + VOption *options ) +{ + return( new_from_buffer( buf.c_str(), buf.size(), + option_string, options ) ); +} + +VImage +VImage::new_from_source( VSource source, const char *option_string, + VOption *options ) +{ + const char *operation_name; + VImage out; + + if( !(operation_name = vips_foreign_find_load_source( + source.get_source() )) ) { + delete options; + throw( VError() ); + } + + options = (options ? options : VImage::option())-> + set( "source", source )-> + set( "out", &out ); + + call_option_string( operation_name, option_string, options ); + + return( out ); +} + +VImage +VImage::new_from_memory_steal( void *data, size_t size, + int width, int height, int bands, VipsBandFormat format ) +{ + VipsImage *image; + + if( !(image = vips_image_new_from_memory( data, size, + width, height, bands, format )) ) + throw( VError() ); + + g_signal_connect( image, "postclose", + G_CALLBACK( vips_image_free_buffer ), data); + + return( VImage( image ) ); +} + +VImage +VImage::new_matrix( int width, int height ) +{ + return( VImage( vips_image_new_matrix( width, height ) ) ); +} + +VImage +VImage::new_matrixv( int width, int height, ... ) +{ + VImage matrix = new_matrix( width, height ); + VipsImage *vips_matrix = matrix.get_image(); + + va_list ap; + + va_start( ap, height ); + for( int y = 0; y < height; y++ ) + for( int x = 0; x < width; x++ ) + *VIPS_MATRIX( vips_matrix, x, y ) = + va_arg( ap, double ); + va_end( ap ); + + return( matrix ); +} + +VImage +VImage::write( VImage out ) const +{ + if( vips_image_write( this->get_image(), out.get_image() ) ) + throw VError(); + + return( out ); +} + +void +VImage::write_to_file( const char *name, VOption *options ) const +{ + char filename[VIPS_PATH_MAX]; + char option_string[VIPS_PATH_MAX]; + const char *operation_name; + + vips__filename_split8( name, filename, option_string ); + if( !(operation_name = vips_foreign_find_save( filename )) ) { + delete options; + throw VError(); + } + + call_option_string( operation_name, option_string, + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +void +VImage::write_to_buffer( const char *suffix, void **buf, size_t *size, + VOption *options ) const +{ + char filename[VIPS_PATH_MAX]; + char option_string[VIPS_PATH_MAX]; + const char *operation_name; + VipsBlob *blob; + + /* Save with the new target API if we can. Fall back to the older + * mechanism in case the saver we need has not been converted yet. + * + * We need to hide any errors from this first phase. + */ + vips__filename_split8( suffix, filename, option_string ); + + vips_error_freeze(); + operation_name = vips_foreign_find_save_target( filename ); + vips_error_thaw(); + + if( operation_name ) { + VTarget target = VTarget::new_to_memory(); + + call_option_string( operation_name, option_string, + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); + + g_object_get( target.get_target(), "blob", &blob, (void *) NULL ); + } + else if( (operation_name = vips_foreign_find_save_buffer( filename )) ) { + call_option_string( operation_name, option_string, + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &blob ) ); + } + else { + delete options; + throw VError(); + } + + if( blob ) { + if( buf ) { + *buf = VIPS_AREA( blob )->data; + VIPS_AREA( blob )->free_fn = NULL; + } + if( size ) + *size = VIPS_AREA( blob )->length; + + vips_area_unref( VIPS_AREA( blob ) ); + } +} + +void +VImage::write_to_target( const char *suffix, VTarget target, + VOption *options ) const +{ + char filename[VIPS_PATH_MAX]; + char option_string[VIPS_PATH_MAX]; + const char *operation_name; + + vips__filename_split8( suffix, filename, option_string ); + if( !(operation_name = vips_foreign_find_save_target( filename )) ) { + delete options; + throw VError(); + } + + call_option_string( operation_name, option_string, + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VRegion +VImage::region() const +{ + return VRegion::new_from_image( *this ); +} + +VRegion +VImage::region( VipsRect *rect ) const +{ + VRegion region = VRegion::new_from_image( *this ); + + region.prepare( rect ); + + return region; +} + +VRegion +VImage::region( int left, int top, int width, int height ) const +{ + VRegion region = VRegion::new_from_image( *this ); + + region.prepare( left, top, width, height ); + + return region; +} + +#include "vips-operators.cpp" + +std::vector +VImage::bandsplit( VOption *options ) const +{ + std::vector b; + b.reserve(bands()); + + for( int i = 0; i < bands(); i++ ) + b.push_back( extract_band( i ) ); + + return( b ); +} + +VImage +VImage::bandjoin( VImage other, VOption *options ) const +{ + VImage v[2] = { *this, other }; + std::vector vec( v, v + VIPS_NUMBER( v ) ); + + return( bandjoin( vec, options ) ); +} + +VImage +VImage::composite( VImage other, VipsBlendMode mode, VOption *options ) const +{ + VImage v[2] = { *this, other }; + std::vector ivec( v, v + VIPS_NUMBER( v ) ); + int m[1] = { static_cast( mode ) }; + std::vector mvec( m, m + VIPS_NUMBER( m ) ); + + return( composite( ivec, mvec, options ) ); +} + +std::complex +VImage::minpos( VOption *options ) const +{ + double x, y; + + (void) min( + (options ? options : VImage::option()) -> + set( "x", &x ) -> + set( "y", &y ) ); + + return( std::complex( x, y ) ); +} + +std::complex +VImage::maxpos( VOption *options ) const +{ + double x, y; + + (void) max( + (options ? options : VImage::option()) -> + set( "x", &x ) -> + set( "y", &y ) ); + + return( std::complex( x, y ) ); +} + +// Operator overloads + +VImage +VImage::operator[]( int index ) const +{ + return( this->extract_band( index ) ); +} + +std::vector +VImage::operator()( int x, int y ) const +{ + return( this->getpoint( x, y ) ); +} + +VImage +operator+( const VImage a, const VImage b ) +{ + return( a.add( b ) ); +} + +VImage +operator+( double a, const VImage b ) +{ + return( b.linear( 1.0, a ) ); +} + +VImage +operator+( const VImage a, double b ) +{ + return( a.linear( 1.0, b ) ); +} + +VImage +operator+( const std::vector a, const VImage b ) +{ + return( b.linear( 1.0, a ) ); +} + +VImage +operator+( const VImage a, const std::vector b ) +{ + return( a.linear( 1.0, b ) ); +} + +VImage & +operator+=( VImage &a, const VImage b ) +{ + return( a = a + b ); +} + +VImage & +operator+=( VImage &a, const double b ) +{ + return( a = a + b ); +} + +VImage & +operator+=( VImage &a, const std::vector b ) +{ + return( a = a + b ); +} + +VImage +operator-( const VImage a, const VImage b ) +{ + return( a.subtract( b ) ); +} + +VImage +operator-( double a, const VImage b ) +{ + return( b.linear( -1.0, a ) ); +} + +VImage +operator-( const VImage a, double b ) +{ + return( a.linear( 1.0, -b ) ); +} + +VImage +operator-( const std::vector a, const VImage b ) +{ + return( b.linear( -1.0, a ) ); +} + +VImage +operator-( const VImage a, const std::vector b ) +{ + return( a.linear( 1.0, vips::negate( b ) ) ); +} + +VImage & +operator-=( VImage &a, const VImage b ) +{ + return( a = a - b ); +} + +VImage & +operator-=( VImage &a, const double b ) +{ + return( a = a - b ); +} + +VImage & +operator-=( VImage &a, const std::vector b ) +{ + return( a = a - b ); +} + +VImage +operator-( const VImage a ) +{ + return( a * -1 ); +} + +VImage +operator*( const VImage a, const VImage b ) +{ + return( a.multiply( b ) ); +} + +VImage +operator*( double a, const VImage b ) +{ + return( b.linear( a, 0.0 ) ); +} + +VImage +operator*( const VImage a, double b ) +{ + return( a.linear( b, 0.0 ) ); +} + +VImage +operator*( const std::vector a, const VImage b ) +{ + return( b.linear( a, 0.0 ) ); +} + +VImage +operator*( const VImage a, const std::vector b ) +{ + return( a.linear( b, 0.0 ) ); +} + +VImage & +operator*=( VImage &a, const VImage b ) +{ + return( a = a * b ); +} + +VImage & +operator*=( VImage &a, const double b ) +{ + return( a = a * b ); +} + +VImage & +operator*=( VImage &a, const std::vector b ) +{ + return( a = a * b ); +} + +VImage +operator/( const VImage a, const VImage b ) +{ + return( a.divide( b ) ); +} + +VImage +operator/( double a, const VImage b ) +{ + return( b.pow( -1.0 ).linear( a, 0.0 ) ); +} + +VImage +operator/( const VImage a, double b ) +{ + return( a.linear( 1.0 / b, 0.0 ) ); +} + +VImage +operator/( const std::vector a, const VImage b ) +{ + return( b.pow( -1.0 ).linear( a, 0.0 ) ); +} + +VImage +operator/( const VImage a, const std::vector b ) +{ + return( a.linear( vips::invert( b ), 0.0 ) ); +} + +VImage & +operator/=( VImage &a, const VImage b ) +{ + return( a = a / b ); +} + +VImage & +operator/=( VImage &a, const double b ) +{ + return( a = a / b ); +} + +VImage & +operator/=( VImage &a, const std::vector b ) +{ + return( a = a / b ); +} + +VImage +operator%( const VImage a, const VImage b ) +{ + return( a.remainder( b ) ); +} + +VImage +operator%( const VImage a, const double b ) +{ + return( a.remainder_const( to_vector( b ) ) ); +} + +VImage +operator%( const VImage a, const std::vector b ) +{ + return( a.remainder_const( b ) ); +} + +VImage & +operator%=( VImage &a, const VImage b ) +{ + return( a = a % b ); +} + +VImage & +operator%=( VImage &a, const double b ) +{ + return( a = a % b ); +} + +VImage & +operator%=( VImage &a, const std::vector b ) +{ + return( a = a % b ); +} + +VImage +operator<( const VImage a, const VImage b ) +{ + return( a.relational( b, VIPS_OPERATION_RELATIONAL_LESS ) ); +} + +VImage +operator<( const double a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_MORE, + to_vector( a ) ) ); +} + +VImage +operator<( const VImage a, const double b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_LESS, + to_vector( b ) ) ); +} + +VImage +operator<( const std::vector a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_MORE, + a ) ); +} + +VImage +operator<( const VImage a, const std::vector b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_LESS, + b ) ); +} + +VImage +operator<=( const VImage a, const VImage b ) +{ + return( a.relational( b, VIPS_OPERATION_RELATIONAL_LESSEQ ) ); +} + +VImage +operator<=( const double a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_MOREEQ, + to_vector( a ) ) ); +} + +VImage +operator<=( const VImage a, const double b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_LESSEQ, + to_vector( b ) ) ); +} + +VImage +operator<=( const std::vector a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_MOREEQ, + a ) ); +} + +VImage +operator<=( const VImage a, const std::vector b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_LESSEQ, + b ) ); +} + +VImage +operator>( const VImage a, const VImage b ) +{ + return( a.relational( b, VIPS_OPERATION_RELATIONAL_MORE ) ); +} + +VImage +operator>( const double a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_LESS, + to_vector( a ) ) ); +} + +VImage +operator>( const VImage a, const double b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_MORE, + to_vector( b ) ) ); +} + +VImage +operator>( const std::vector a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_LESS, + a ) ); +} + +VImage +operator>( const VImage a, const std::vector b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_MORE, + b ) ); +} + +VImage +operator>=( const VImage a, const VImage b ) +{ + return( a.relational( b, VIPS_OPERATION_RELATIONAL_MOREEQ ) ); +} + +VImage +operator>=( const double a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_LESSEQ, + to_vector( a ) ) ); +} + +VImage +operator>=( const VImage a, const double b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_MOREEQ, + to_vector( b ) ) ); +} + +VImage +operator>=( const std::vector a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_LESSEQ, + a ) ); +} + +VImage +operator>=( const VImage a, const std::vector b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_MOREEQ, + b ) ); +} + +VImage +operator==( const VImage a, const VImage b ) +{ + return( a.relational( b, VIPS_OPERATION_RELATIONAL_EQUAL ) ); +} + +VImage +operator==( const double a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_EQUAL, + to_vector( a ) ) ); +} + +VImage +operator==( const VImage a, const double b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_EQUAL, + to_vector( b ) ) ); +} + +VImage +operator==( const std::vector a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_EQUAL, + a ) ); +} + +VImage +operator==( const VImage a, const std::vector b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_EQUAL, + b ) ); +} + +VImage +operator!=( const VImage a, const VImage b ) +{ + return( a.relational( b, VIPS_OPERATION_RELATIONAL_NOTEQ ) ); +} + +VImage +operator!=( const double a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_NOTEQ, + to_vector( a ) ) ); +} + +VImage +operator!=( const VImage a, const double b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_NOTEQ, + to_vector( b ) ) ); +} + +VImage +operator!=( const std::vector a, const VImage b ) +{ + return( b.relational_const( VIPS_OPERATION_RELATIONAL_NOTEQ, + a ) ); +} + +VImage +operator!=( const VImage a, const std::vector b ) +{ + return( a.relational_const( VIPS_OPERATION_RELATIONAL_NOTEQ, + b ) ); +} + +VImage +operator&( const VImage a, const VImage b ) +{ + return( a.boolean( b, VIPS_OPERATION_BOOLEAN_AND ) ); +} + +VImage +operator&( const double a, const VImage b ) +{ + return( b.boolean_const( VIPS_OPERATION_BOOLEAN_AND, + to_vector( a ) ) ); +} + +VImage +operator&( const VImage a, const double b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_AND, + to_vector( b ) ) ); +} + +VImage +operator&( const std::vector a, const VImage b ) +{ + return( b.boolean_const( VIPS_OPERATION_BOOLEAN_AND, a ) ); +} + +VImage +operator&( const VImage a, const std::vector b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_AND, b ) ); +} + +VImage & +operator&=( VImage &a, const VImage b ) +{ + return( a = a & b ); +} + +VImage & +operator&=( VImage &a, const double b ) +{ + return( a = a & b ); +} + +VImage & +operator&=( VImage &a, const std::vector b ) +{ + return( a = a & b ); +} + +VImage +operator|( const VImage a, const VImage b ) +{ + return( a.boolean( b, VIPS_OPERATION_BOOLEAN_OR ) ); +} + +VImage +operator|( const double a, const VImage b ) +{ + return( b.boolean_const( VIPS_OPERATION_BOOLEAN_OR, + to_vector( a ) ) ); +} + +VImage +operator|( const VImage a, const double b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_OR, + to_vector( b ) ) ); +} + +VImage +operator|( const std::vector a, const VImage b ) +{ + return( b.boolean_const( VIPS_OPERATION_BOOLEAN_OR, + a ) ); +} + +VImage +operator|( const VImage a, const std::vector b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_OR, + b ) ); +} + +VImage & +operator|=( VImage &a, const VImage b ) +{ + return( a = a | b ); +} + +VImage & +operator|=( VImage &a, const double b ) +{ + return( a = a | b ); +} + +VImage & +operator|=( VImage &a, const std::vector b ) +{ + return( a = a | b ); +} + +VImage +operator^( const VImage a, const VImage b ) +{ + return( a.boolean( b, VIPS_OPERATION_BOOLEAN_EOR ) ); +} + +VImage +operator^( const double a, const VImage b ) +{ + return( b.boolean_const( VIPS_OPERATION_BOOLEAN_EOR, + to_vector( a ) ) ); +} + +VImage +operator^( const VImage a, const double b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_EOR, + to_vector( b ) ) ); +} + +VImage +operator^( const std::vector a, const VImage b ) +{ + return( b.boolean_const( VIPS_OPERATION_BOOLEAN_EOR, + a ) ); +} + +VImage +operator^( const VImage a, const std::vector b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_EOR, + b ) ); +} + +VImage & +operator^=( VImage &a, const VImage b ) +{ + return( a = a ^ b ); +} + +VImage & +operator^=( VImage &a, const double b ) +{ + return( a = a ^ b ); +} + +VImage & +operator^=( VImage &a, const std::vector b ) +{ + return( a = a ^ b ); +} + +VImage +operator<<( const VImage a, const VImage b ) +{ + return( a.boolean( b, VIPS_OPERATION_BOOLEAN_LSHIFT ) ); +} + +VImage +operator<<( const VImage a, const double b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_LSHIFT, + to_vector( b ) ) ); +} + +VImage +operator<<( const VImage a, const std::vector b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_LSHIFT, + b ) ); +} + +VImage & +operator<<=( VImage &a, const VImage b ) +{ + return( a = a << b ); +} + +VImage & +operator<<=( VImage &a, const double b ) +{ + return( a = a << b ); +} + +VImage & +operator<<=( VImage &a, const std::vector b ) +{ + return( a = a << b ); +} + +VImage +operator>>( const VImage a, const VImage b ) +{ + return( a.boolean( b, VIPS_OPERATION_BOOLEAN_RSHIFT ) ); +} + +VImage +operator>>( const VImage a, const double b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_RSHIFT, + to_vector( b ) ) ); +} + +VImage +operator>>( const VImage a, const std::vector b ) +{ + return( a.boolean_const( VIPS_OPERATION_BOOLEAN_RSHIFT, + b ) ); +} + +VImage & +operator>>=( VImage &a, const VImage b ) +{ + return( a = a << b ); +} + +VImage & +operator>>=( VImage &a, const double b ) +{ + return( a = a << b ); +} + +VImage & +operator>>=( VImage &a, const std::vector b ) +{ + return( a = a << b ); +} + +VIPS_NAMESPACE_END diff --git a/backend/node_modules/sharp/src/libvips/cplusplus/VInterpolate.cpp b/backend/node_modules/sharp/src/libvips/cplusplus/VInterpolate.cpp new file mode 100644 index 00000000..ed1e2bd9 --- /dev/null +++ b/backend/node_modules/sharp/src/libvips/cplusplus/VInterpolate.cpp @@ -0,0 +1,62 @@ +/* Object part of VInterpolate class + */ + +/* + + Copyright (C) 1991-2001 The National Gallery + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + + */ + +/* + + These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk + + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include + +#include + +/* +#define VIPS_DEBUG +#define VIPS_DEBUG_VERBOSE + */ + +VIPS_NAMESPACE_START + +VInterpolate +VInterpolate::new_from_name( const char *name, VOption *options ) +{ + VipsInterpolate *interp; + + if( !(interp = vips_interpolate_new( name )) ) { + delete options; + throw VError(); + } + delete options; + + VInterpolate out( interp ); + + return( out ); +} + +VIPS_NAMESPACE_END diff --git a/backend/node_modules/sharp/src/libvips/cplusplus/VRegion.cpp b/backend/node_modules/sharp/src/libvips/cplusplus/VRegion.cpp new file mode 100644 index 00000000..4a042821 --- /dev/null +++ b/backend/node_modules/sharp/src/libvips/cplusplus/VRegion.cpp @@ -0,0 +1,27 @@ +// Object part of VRegion class + +#ifdef HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include + +#include + +VIPS_NAMESPACE_START + +VRegion +VRegion::new_from_image( VImage image ) +{ + VipsRegion *region; + + if( !(region = vips_region_new( image.get_image() )) ) { + throw VError(); + } + + VRegion out( region ); + + return( out ); +} + +VIPS_NAMESPACE_END diff --git a/backend/node_modules/sharp/src/libvips/cplusplus/vips-operators.cpp b/backend/node_modules/sharp/src/libvips/cplusplus/vips-operators.cpp new file mode 100644 index 00000000..56d46881 --- /dev/null +++ b/backend/node_modules/sharp/src/libvips/cplusplus/vips-operators.cpp @@ -0,0 +1,3760 @@ +// bodies for vips operations +// this file is generated automatically, do not edit! + +VImage VImage::CMC2LCh( VOption *options ) const +{ + VImage out; + + call( "CMC2LCh", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::CMYK2XYZ( VOption *options ) const +{ + VImage out; + + call( "CMYK2XYZ", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::HSV2sRGB( VOption *options ) const +{ + VImage out; + + call( "HSV2sRGB", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::LCh2CMC( VOption *options ) const +{ + VImage out; + + call( "LCh2CMC", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::LCh2Lab( VOption *options ) const +{ + VImage out; + + call( "LCh2Lab", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::Lab2LCh( VOption *options ) const +{ + VImage out; + + call( "Lab2LCh", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::Lab2LabQ( VOption *options ) const +{ + VImage out; + + call( "Lab2LabQ", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::Lab2LabS( VOption *options ) const +{ + VImage out; + + call( "Lab2LabS", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::Lab2XYZ( VOption *options ) const +{ + VImage out; + + call( "Lab2XYZ", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::LabQ2Lab( VOption *options ) const +{ + VImage out; + + call( "LabQ2Lab", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::LabQ2LabS( VOption *options ) const +{ + VImage out; + + call( "LabQ2LabS", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::LabQ2sRGB( VOption *options ) const +{ + VImage out; + + call( "LabQ2sRGB", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::LabS2Lab( VOption *options ) const +{ + VImage out; + + call( "LabS2Lab", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::LabS2LabQ( VOption *options ) const +{ + VImage out; + + call( "LabS2LabQ", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::XYZ2CMYK( VOption *options ) const +{ + VImage out; + + call( "XYZ2CMYK", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::XYZ2Lab( VOption *options ) const +{ + VImage out; + + call( "XYZ2Lab", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::XYZ2Yxy( VOption *options ) const +{ + VImage out; + + call( "XYZ2Yxy", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::XYZ2scRGB( VOption *options ) const +{ + VImage out; + + call( "XYZ2scRGB", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::Yxy2XYZ( VOption *options ) const +{ + VImage out; + + call( "Yxy2XYZ", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::abs( VOption *options ) const +{ + VImage out; + + call( "abs", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::add( VImage right, VOption *options ) const +{ + VImage out; + + call( "add", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +VImage VImage::affine( std::vector matrix, VOption *options ) const +{ + VImage out; + + call( "affine", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "matrix", matrix ) ); + + return( out ); +} + +VImage VImage::analyzeload( const char *filename, VOption *options ) +{ + VImage out; + + call( "analyzeload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::arrayjoin( std::vector in, VOption *options ) +{ + VImage out; + + call( "arrayjoin", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "in", in ) ); + + return( out ); +} + +VImage VImage::autorot( VOption *options ) const +{ + VImage out; + + call( "autorot", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +double VImage::avg( VOption *options ) const +{ + double out; + + call( "avg", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::bandbool( VipsOperationBoolean boolean, VOption *options ) const +{ + VImage out; + + call( "bandbool", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "boolean", boolean ) ); + + return( out ); +} + +VImage VImage::bandfold( VOption *options ) const +{ + VImage out; + + call( "bandfold", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::bandjoin( std::vector in, VOption *options ) +{ + VImage out; + + call( "bandjoin", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "in", in ) ); + + return( out ); +} + +VImage VImage::bandjoin_const( std::vector c, VOption *options ) const +{ + VImage out; + + call( "bandjoin_const", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "c", c ) ); + + return( out ); +} + +VImage VImage::bandmean( VOption *options ) const +{ + VImage out; + + call( "bandmean", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::bandrank( std::vector in, VOption *options ) +{ + VImage out; + + call( "bandrank", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "in", in ) ); + + return( out ); +} + +VImage VImage::bandunfold( VOption *options ) const +{ + VImage out; + + call( "bandunfold", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::black( int width, int height, VOption *options ) +{ + VImage out; + + call( "black", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::boolean( VImage right, VipsOperationBoolean boolean, VOption *options ) const +{ + VImage out; + + call( "boolean", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right )-> + set( "boolean", boolean ) ); + + return( out ); +} + +VImage VImage::boolean_const( VipsOperationBoolean boolean, std::vector c, VOption *options ) const +{ + VImage out; + + call( "boolean_const", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "boolean", boolean )-> + set( "c", c ) ); + + return( out ); +} + +VImage VImage::buildlut( VOption *options ) const +{ + VImage out; + + call( "buildlut", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::byteswap( VOption *options ) const +{ + VImage out; + + call( "byteswap", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::cache( VOption *options ) const +{ + VImage out; + + call( "cache", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::canny( VOption *options ) const +{ + VImage out; + + call( "canny", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::case_image( std::vector cases, VOption *options ) const +{ + VImage out; + + call( "case", + (options ? options : VImage::option())-> + set( "index", *this )-> + set( "out", &out )-> + set( "cases", cases ) ); + + return( out ); +} + +VImage VImage::cast( VipsBandFormat format, VOption *options ) const +{ + VImage out; + + call( "cast", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "format", format ) ); + + return( out ); +} + +VImage VImage::colourspace( VipsInterpretation space, VOption *options ) const +{ + VImage out; + + call( "colourspace", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "space", space ) ); + + return( out ); +} + +VImage VImage::compass( VImage mask, VOption *options ) const +{ + VImage out; + + call( "compass", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::complex( VipsOperationComplex cmplx, VOption *options ) const +{ + VImage out; + + call( "complex", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "cmplx", cmplx ) ); + + return( out ); +} + +VImage VImage::complex2( VImage right, VipsOperationComplex2 cmplx, VOption *options ) const +{ + VImage out; + + call( "complex2", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right )-> + set( "cmplx", cmplx ) ); + + return( out ); +} + +VImage VImage::complexform( VImage right, VOption *options ) const +{ + VImage out; + + call( "complexform", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +VImage VImage::complexget( VipsOperationComplexget get, VOption *options ) const +{ + VImage out; + + call( "complexget", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "get", get ) ); + + return( out ); +} + +VImage VImage::composite( std::vector in, std::vector mode, VOption *options ) +{ + VImage out; + + call( "composite", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "in", in )-> + set( "mode", mode ) ); + + return( out ); +} + +VImage VImage::composite2( VImage overlay, VipsBlendMode mode, VOption *options ) const +{ + VImage out; + + call( "composite2", + (options ? options : VImage::option())-> + set( "base", *this )-> + set( "out", &out )-> + set( "overlay", overlay )-> + set( "mode", mode ) ); + + return( out ); +} + +VImage VImage::conv( VImage mask, VOption *options ) const +{ + VImage out; + + call( "conv", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::conva( VImage mask, VOption *options ) const +{ + VImage out; + + call( "conva", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::convasep( VImage mask, VOption *options ) const +{ + VImage out; + + call( "convasep", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::convf( VImage mask, VOption *options ) const +{ + VImage out; + + call( "convf", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::convi( VImage mask, VOption *options ) const +{ + VImage out; + + call( "convi", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::convsep( VImage mask, VOption *options ) const +{ + VImage out; + + call( "convsep", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::copy( VOption *options ) const +{ + VImage out; + + call( "copy", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +double VImage::countlines( VipsDirection direction, VOption *options ) const +{ + double nolines; + + call( "countlines", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "nolines", &nolines )-> + set( "direction", direction ) ); + + return( nolines ); +} + +VImage VImage::crop( int left, int top, int width, int height, VOption *options ) const +{ + VImage out; + + call( "crop", + (options ? options : VImage::option())-> + set( "input", *this )-> + set( "out", &out )-> + set( "left", left )-> + set( "top", top )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::csvload( const char *filename, VOption *options ) +{ + VImage out; + + call( "csvload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::csvload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "csvload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::csvsave( const char *filename, VOption *options ) const +{ + call( "csvsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +void VImage::csvsave_target( VTarget target, VOption *options ) const +{ + call( "csvsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::dE00( VImage right, VOption *options ) const +{ + VImage out; + + call( "dE00", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +VImage VImage::dE76( VImage right, VOption *options ) const +{ + VImage out; + + call( "dE76", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +VImage VImage::dECMC( VImage right, VOption *options ) const +{ + VImage out; + + call( "dECMC", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +double VImage::deviate( VOption *options ) const +{ + double out; + + call( "deviate", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::divide( VImage right, VOption *options ) const +{ + VImage out; + + call( "divide", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +void VImage::draw_circle( std::vector ink, int cx, int cy, int radius, VOption *options ) const +{ + call( "draw_circle", + (options ? options : VImage::option())-> + set( "image", *this )-> + set( "ink", ink )-> + set( "cx", cx )-> + set( "cy", cy )-> + set( "radius", radius ) ); +} + +void VImage::draw_flood( std::vector ink, int x, int y, VOption *options ) const +{ + call( "draw_flood", + (options ? options : VImage::option())-> + set( "image", *this )-> + set( "ink", ink )-> + set( "x", x )-> + set( "y", y ) ); +} + +void VImage::draw_image( VImage sub, int x, int y, VOption *options ) const +{ + call( "draw_image", + (options ? options : VImage::option())-> + set( "image", *this )-> + set( "sub", sub )-> + set( "x", x )-> + set( "y", y ) ); +} + +void VImage::draw_line( std::vector ink, int x1, int y1, int x2, int y2, VOption *options ) const +{ + call( "draw_line", + (options ? options : VImage::option())-> + set( "image", *this )-> + set( "ink", ink )-> + set( "x1", x1 )-> + set( "y1", y1 )-> + set( "x2", x2 )-> + set( "y2", y2 ) ); +} + +void VImage::draw_mask( std::vector ink, VImage mask, int x, int y, VOption *options ) const +{ + call( "draw_mask", + (options ? options : VImage::option())-> + set( "image", *this )-> + set( "ink", ink )-> + set( "mask", mask )-> + set( "x", x )-> + set( "y", y ) ); +} + +void VImage::draw_rect( std::vector ink, int left, int top, int width, int height, VOption *options ) const +{ + call( "draw_rect", + (options ? options : VImage::option())-> + set( "image", *this )-> + set( "ink", ink )-> + set( "left", left )-> + set( "top", top )-> + set( "width", width )-> + set( "height", height ) ); +} + +void VImage::draw_smudge( int left, int top, int width, int height, VOption *options ) const +{ + call( "draw_smudge", + (options ? options : VImage::option())-> + set( "image", *this )-> + set( "left", left )-> + set( "top", top )-> + set( "width", width )-> + set( "height", height ) ); +} + +void VImage::dzsave( const char *filename, VOption *options ) const +{ + call( "dzsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::dzsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "dzsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::dzsave_target( VTarget target, VOption *options ) const +{ + call( "dzsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::embed( int x, int y, int width, int height, VOption *options ) const +{ + VImage out; + + call( "embed", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "x", x )-> + set( "y", y )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::extract_area( int left, int top, int width, int height, VOption *options ) const +{ + VImage out; + + call( "extract_area", + (options ? options : VImage::option())-> + set( "input", *this )-> + set( "out", &out )-> + set( "left", left )-> + set( "top", top )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::extract_band( int band, VOption *options ) const +{ + VImage out; + + call( "extract_band", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "band", band ) ); + + return( out ); +} + +VImage VImage::eye( int width, int height, VOption *options ) +{ + VImage out; + + call( "eye", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::falsecolour( VOption *options ) const +{ + VImage out; + + call( "falsecolour", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::fastcor( VImage ref, VOption *options ) const +{ + VImage out; + + call( "fastcor", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "ref", ref ) ); + + return( out ); +} + +VImage VImage::fill_nearest( VOption *options ) const +{ + VImage out; + + call( "fill_nearest", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +int VImage::find_trim( int *top, int *width, int *height, VOption *options ) const +{ + int left; + + call( "find_trim", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "left", &left )-> + set( "top", top )-> + set( "width", width )-> + set( "height", height ) ); + + return( left ); +} + +VImage VImage::fitsload( const char *filename, VOption *options ) +{ + VImage out; + + call( "fitsload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::fitsload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "fitsload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::fitssave( const char *filename, VOption *options ) const +{ + call( "fitssave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VImage VImage::flatten( VOption *options ) const +{ + VImage out; + + call( "flatten", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::flip( VipsDirection direction, VOption *options ) const +{ + VImage out; + + call( "flip", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "direction", direction ) ); + + return( out ); +} + +VImage VImage::float2rad( VOption *options ) const +{ + VImage out; + + call( "float2rad", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::fractsurf( int width, int height, double fractal_dimension, VOption *options ) +{ + VImage out; + + call( "fractsurf", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "fractal_dimension", fractal_dimension ) ); + + return( out ); +} + +VImage VImage::freqmult( VImage mask, VOption *options ) const +{ + VImage out; + + call( "freqmult", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask ) ); + + return( out ); +} + +VImage VImage::fwfft( VOption *options ) const +{ + VImage out; + + call( "fwfft", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::gamma( VOption *options ) const +{ + VImage out; + + call( "gamma", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::gaussblur( double sigma, VOption *options ) const +{ + VImage out; + + call( "gaussblur", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "sigma", sigma ) ); + + return( out ); +} + +VImage VImage::gaussmat( double sigma, double min_ampl, VOption *options ) +{ + VImage out; + + call( "gaussmat", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "sigma", sigma )-> + set( "min_ampl", min_ampl ) ); + + return( out ); +} + +VImage VImage::gaussnoise( int width, int height, VOption *options ) +{ + VImage out; + + call( "gaussnoise", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +std::vector VImage::getpoint( int x, int y, VOption *options ) const +{ + std::vector out_array; + + call( "getpoint", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out_array", &out_array )-> + set( "x", x )-> + set( "y", y ) ); + + return( out_array ); +} + +VImage VImage::gifload( const char *filename, VOption *options ) +{ + VImage out; + + call( "gifload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::gifload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "gifload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::gifload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "gifload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::gifsave( const char *filename, VOption *options ) const +{ + call( "gifsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::gifsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "gifsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::gifsave_target( VTarget target, VOption *options ) const +{ + call( "gifsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::globalbalance( VOption *options ) const +{ + VImage out; + + call( "globalbalance", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::gravity( VipsCompassDirection direction, int width, int height, VOption *options ) const +{ + VImage out; + + call( "gravity", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "direction", direction )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::grey( int width, int height, VOption *options ) +{ + VImage out; + + call( "grey", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::grid( int tile_height, int across, int down, VOption *options ) const +{ + VImage out; + + call( "grid", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "tile_height", tile_height )-> + set( "across", across )-> + set( "down", down ) ); + + return( out ); +} + +VImage VImage::heifload( const char *filename, VOption *options ) +{ + VImage out; + + call( "heifload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::heifload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "heifload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::heifload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "heifload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::heifsave( const char *filename, VOption *options ) const +{ + call( "heifsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::heifsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "heifsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::heifsave_target( VTarget target, VOption *options ) const +{ + call( "heifsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::hist_cum( VOption *options ) const +{ + VImage out; + + call( "hist_cum", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +double VImage::hist_entropy( VOption *options ) const +{ + double out; + + call( "hist_entropy", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::hist_equal( VOption *options ) const +{ + VImage out; + + call( "hist_equal", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::hist_find( VOption *options ) const +{ + VImage out; + + call( "hist_find", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::hist_find_indexed( VImage index, VOption *options ) const +{ + VImage out; + + call( "hist_find_indexed", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "index", index ) ); + + return( out ); +} + +VImage VImage::hist_find_ndim( VOption *options ) const +{ + VImage out; + + call( "hist_find_ndim", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +bool VImage::hist_ismonotonic( VOption *options ) const +{ + bool monotonic; + + call( "hist_ismonotonic", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "monotonic", &monotonic ) ); + + return( monotonic ); +} + +VImage VImage::hist_local( int width, int height, VOption *options ) const +{ + VImage out; + + call( "hist_local", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::hist_match( VImage ref, VOption *options ) const +{ + VImage out; + + call( "hist_match", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "ref", ref ) ); + + return( out ); +} + +VImage VImage::hist_norm( VOption *options ) const +{ + VImage out; + + call( "hist_norm", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::hist_plot( VOption *options ) const +{ + VImage out; + + call( "hist_plot", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::hough_circle( VOption *options ) const +{ + VImage out; + + call( "hough_circle", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::hough_line( VOption *options ) const +{ + VImage out; + + call( "hough_line", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::icc_export( VOption *options ) const +{ + VImage out; + + call( "icc_export", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::icc_import( VOption *options ) const +{ + VImage out; + + call( "icc_import", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::icc_transform( const char *output_profile, VOption *options ) const +{ + VImage out; + + call( "icc_transform", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "output_profile", output_profile ) ); + + return( out ); +} + +VImage VImage::identity( VOption *options ) +{ + VImage out; + + call( "identity", + (options ? options : VImage::option())-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::ifthenelse( VImage in1, VImage in2, VOption *options ) const +{ + VImage out; + + call( "ifthenelse", + (options ? options : VImage::option())-> + set( "cond", *this )-> + set( "out", &out )-> + set( "in1", in1 )-> + set( "in2", in2 ) ); + + return( out ); +} + +VImage VImage::insert( VImage sub, int x, int y, VOption *options ) const +{ + VImage out; + + call( "insert", + (options ? options : VImage::option())-> + set( "main", *this )-> + set( "out", &out )-> + set( "sub", sub )-> + set( "x", x )-> + set( "y", y ) ); + + return( out ); +} + +VImage VImage::invert( VOption *options ) const +{ + VImage out; + + call( "invert", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::invertlut( VOption *options ) const +{ + VImage out; + + call( "invertlut", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::invfft( VOption *options ) const +{ + VImage out; + + call( "invfft", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::join( VImage in2, VipsDirection direction, VOption *options ) const +{ + VImage out; + + call( "join", + (options ? options : VImage::option())-> + set( "in1", *this )-> + set( "out", &out )-> + set( "in2", in2 )-> + set( "direction", direction ) ); + + return( out ); +} + +VImage VImage::jp2kload( const char *filename, VOption *options ) +{ + VImage out; + + call( "jp2kload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::jp2kload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "jp2kload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::jp2kload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "jp2kload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::jp2ksave( const char *filename, VOption *options ) const +{ + call( "jp2ksave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::jp2ksave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "jp2ksave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::jp2ksave_target( VTarget target, VOption *options ) const +{ + call( "jp2ksave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::jpegload( const char *filename, VOption *options ) +{ + VImage out; + + call( "jpegload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::jpegload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "jpegload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::jpegload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "jpegload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::jpegsave( const char *filename, VOption *options ) const +{ + call( "jpegsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::jpegsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "jpegsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::jpegsave_mime( VOption *options ) const +{ + call( "jpegsave_mime", + (options ? options : VImage::option())-> + set( "in", *this ) ); +} + +void VImage::jpegsave_target( VTarget target, VOption *options ) const +{ + call( "jpegsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::jxlload( const char *filename, VOption *options ) +{ + VImage out; + + call( "jxlload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::jxlload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "jxlload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::jxlload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "jxlload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::jxlsave( const char *filename, VOption *options ) const +{ + call( "jxlsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::jxlsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "jxlsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::jxlsave_target( VTarget target, VOption *options ) const +{ + call( "jxlsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::labelregions( VOption *options ) const +{ + VImage mask; + + call( "labelregions", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "mask", &mask ) ); + + return( mask ); +} + +VImage VImage::linear( std::vector a, std::vector b, VOption *options ) const +{ + VImage out; + + call( "linear", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "a", a )-> + set( "b", b ) ); + + return( out ); +} + +VImage VImage::linecache( VOption *options ) const +{ + VImage out; + + call( "linecache", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::logmat( double sigma, double min_ampl, VOption *options ) +{ + VImage out; + + call( "logmat", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "sigma", sigma )-> + set( "min_ampl", min_ampl ) ); + + return( out ); +} + +VImage VImage::magickload( const char *filename, VOption *options ) +{ + VImage out; + + call( "magickload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::magickload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "magickload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +void VImage::magicksave( const char *filename, VOption *options ) const +{ + call( "magicksave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::magicksave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "magicksave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +VImage VImage::mapim( VImage index, VOption *options ) const +{ + VImage out; + + call( "mapim", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "index", index ) ); + + return( out ); +} + +VImage VImage::maplut( VImage lut, VOption *options ) const +{ + VImage out; + + call( "maplut", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "lut", lut ) ); + + return( out ); +} + +VImage VImage::mask_butterworth( int width, int height, double order, double frequency_cutoff, double amplitude_cutoff, VOption *options ) +{ + VImage out; + + call( "mask_butterworth", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "order", order )-> + set( "frequency_cutoff", frequency_cutoff )-> + set( "amplitude_cutoff", amplitude_cutoff ) ); + + return( out ); +} + +VImage VImage::mask_butterworth_band( int width, int height, double order, double frequency_cutoff_x, double frequency_cutoff_y, double radius, double amplitude_cutoff, VOption *options ) +{ + VImage out; + + call( "mask_butterworth_band", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "order", order )-> + set( "frequency_cutoff_x", frequency_cutoff_x )-> + set( "frequency_cutoff_y", frequency_cutoff_y )-> + set( "radius", radius )-> + set( "amplitude_cutoff", amplitude_cutoff ) ); + + return( out ); +} + +VImage VImage::mask_butterworth_ring( int width, int height, double order, double frequency_cutoff, double amplitude_cutoff, double ringwidth, VOption *options ) +{ + VImage out; + + call( "mask_butterworth_ring", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "order", order )-> + set( "frequency_cutoff", frequency_cutoff )-> + set( "amplitude_cutoff", amplitude_cutoff )-> + set( "ringwidth", ringwidth ) ); + + return( out ); +} + +VImage VImage::mask_fractal( int width, int height, double fractal_dimension, VOption *options ) +{ + VImage out; + + call( "mask_fractal", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "fractal_dimension", fractal_dimension ) ); + + return( out ); +} + +VImage VImage::mask_gaussian( int width, int height, double frequency_cutoff, double amplitude_cutoff, VOption *options ) +{ + VImage out; + + call( "mask_gaussian", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "frequency_cutoff", frequency_cutoff )-> + set( "amplitude_cutoff", amplitude_cutoff ) ); + + return( out ); +} + +VImage VImage::mask_gaussian_band( int width, int height, double frequency_cutoff_x, double frequency_cutoff_y, double radius, double amplitude_cutoff, VOption *options ) +{ + VImage out; + + call( "mask_gaussian_band", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "frequency_cutoff_x", frequency_cutoff_x )-> + set( "frequency_cutoff_y", frequency_cutoff_y )-> + set( "radius", radius )-> + set( "amplitude_cutoff", amplitude_cutoff ) ); + + return( out ); +} + +VImage VImage::mask_gaussian_ring( int width, int height, double frequency_cutoff, double amplitude_cutoff, double ringwidth, VOption *options ) +{ + VImage out; + + call( "mask_gaussian_ring", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "frequency_cutoff", frequency_cutoff )-> + set( "amplitude_cutoff", amplitude_cutoff )-> + set( "ringwidth", ringwidth ) ); + + return( out ); +} + +VImage VImage::mask_ideal( int width, int height, double frequency_cutoff, VOption *options ) +{ + VImage out; + + call( "mask_ideal", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "frequency_cutoff", frequency_cutoff ) ); + + return( out ); +} + +VImage VImage::mask_ideal_band( int width, int height, double frequency_cutoff_x, double frequency_cutoff_y, double radius, VOption *options ) +{ + VImage out; + + call( "mask_ideal_band", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "frequency_cutoff_x", frequency_cutoff_x )-> + set( "frequency_cutoff_y", frequency_cutoff_y )-> + set( "radius", radius ) ); + + return( out ); +} + +VImage VImage::mask_ideal_ring( int width, int height, double frequency_cutoff, double ringwidth, VOption *options ) +{ + VImage out; + + call( "mask_ideal_ring", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "frequency_cutoff", frequency_cutoff )-> + set( "ringwidth", ringwidth ) ); + + return( out ); +} + +VImage VImage::match( VImage sec, int xr1, int yr1, int xs1, int ys1, int xr2, int yr2, int xs2, int ys2, VOption *options ) const +{ + VImage out; + + call( "match", + (options ? options : VImage::option())-> + set( "ref", *this )-> + set( "out", &out )-> + set( "sec", sec )-> + set( "xr1", xr1 )-> + set( "yr1", yr1 )-> + set( "xs1", xs1 )-> + set( "ys1", ys1 )-> + set( "xr2", xr2 )-> + set( "yr2", yr2 )-> + set( "xs2", xs2 )-> + set( "ys2", ys2 ) ); + + return( out ); +} + +VImage VImage::math( VipsOperationMath math, VOption *options ) const +{ + VImage out; + + call( "math", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "math", math ) ); + + return( out ); +} + +VImage VImage::math2( VImage right, VipsOperationMath2 math2, VOption *options ) const +{ + VImage out; + + call( "math2", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right )-> + set( "math2", math2 ) ); + + return( out ); +} + +VImage VImage::math2_const( VipsOperationMath2 math2, std::vector c, VOption *options ) const +{ + VImage out; + + call( "math2_const", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "math2", math2 )-> + set( "c", c ) ); + + return( out ); +} + +VImage VImage::matload( const char *filename, VOption *options ) +{ + VImage out; + + call( "matload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::matrixinvert( VOption *options ) const +{ + VImage out; + + call( "matrixinvert", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::matrixload( const char *filename, VOption *options ) +{ + VImage out; + + call( "matrixload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::matrixload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "matrixload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::matrixprint( VOption *options ) const +{ + call( "matrixprint", + (options ? options : VImage::option())-> + set( "in", *this ) ); +} + +void VImage::matrixsave( const char *filename, VOption *options ) const +{ + call( "matrixsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +void VImage::matrixsave_target( VTarget target, VOption *options ) const +{ + call( "matrixsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +double VImage::max( VOption *options ) const +{ + double out; + + call( "max", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::measure( int h, int v, VOption *options ) const +{ + VImage out; + + call( "measure", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "h", h )-> + set( "v", v ) ); + + return( out ); +} + +VImage VImage::merge( VImage sec, VipsDirection direction, int dx, int dy, VOption *options ) const +{ + VImage out; + + call( "merge", + (options ? options : VImage::option())-> + set( "ref", *this )-> + set( "out", &out )-> + set( "sec", sec )-> + set( "direction", direction )-> + set( "dx", dx )-> + set( "dy", dy ) ); + + return( out ); +} + +double VImage::min( VOption *options ) const +{ + double out; + + call( "min", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::morph( VImage mask, VipsOperationMorphology morph, VOption *options ) const +{ + VImage out; + + call( "morph", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "mask", mask )-> + set( "morph", morph ) ); + + return( out ); +} + +VImage VImage::mosaic( VImage sec, VipsDirection direction, int xref, int yref, int xsec, int ysec, VOption *options ) const +{ + VImage out; + + call( "mosaic", + (options ? options : VImage::option())-> + set( "ref", *this )-> + set( "out", &out )-> + set( "sec", sec )-> + set( "direction", direction )-> + set( "xref", xref )-> + set( "yref", yref )-> + set( "xsec", xsec )-> + set( "ysec", ysec ) ); + + return( out ); +} + +VImage VImage::mosaic1( VImage sec, VipsDirection direction, int xr1, int yr1, int xs1, int ys1, int xr2, int yr2, int xs2, int ys2, VOption *options ) const +{ + VImage out; + + call( "mosaic1", + (options ? options : VImage::option())-> + set( "ref", *this )-> + set( "out", &out )-> + set( "sec", sec )-> + set( "direction", direction )-> + set( "xr1", xr1 )-> + set( "yr1", yr1 )-> + set( "xs1", xs1 )-> + set( "ys1", ys1 )-> + set( "xr2", xr2 )-> + set( "yr2", yr2 )-> + set( "xs2", xs2 )-> + set( "ys2", ys2 ) ); + + return( out ); +} + +VImage VImage::msb( VOption *options ) const +{ + VImage out; + + call( "msb", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::multiply( VImage right, VOption *options ) const +{ + VImage out; + + call( "multiply", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +VImage VImage::niftiload( const char *filename, VOption *options ) +{ + VImage out; + + call( "niftiload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::niftiload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "niftiload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::niftisave( const char *filename, VOption *options ) const +{ + call( "niftisave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VImage VImage::openexrload( const char *filename, VOption *options ) +{ + VImage out; + + call( "openexrload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::openslideload( const char *filename, VOption *options ) +{ + VImage out; + + call( "openslideload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::openslideload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "openslideload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +VImage VImage::pdfload( const char *filename, VOption *options ) +{ + VImage out; + + call( "pdfload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::pdfload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "pdfload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::pdfload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "pdfload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +int VImage::percent( double percent, VOption *options ) const +{ + int threshold; + + call( "percent", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "threshold", &threshold )-> + set( "percent", percent ) ); + + return( threshold ); +} + +VImage VImage::perlin( int width, int height, VOption *options ) +{ + VImage out; + + call( "perlin", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::phasecor( VImage in2, VOption *options ) const +{ + VImage out; + + call( "phasecor", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "in2", in2 ) ); + + return( out ); +} + +VImage VImage::pngload( const char *filename, VOption *options ) +{ + VImage out; + + call( "pngload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::pngload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "pngload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::pngload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "pngload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::pngsave( const char *filename, VOption *options ) const +{ + call( "pngsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::pngsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "pngsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::pngsave_target( VTarget target, VOption *options ) const +{ + call( "pngsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::ppmload( const char *filename, VOption *options ) +{ + VImage out; + + call( "ppmload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::ppmload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "ppmload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::ppmsave( const char *filename, VOption *options ) const +{ + call( "ppmsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +void VImage::ppmsave_target( VTarget target, VOption *options ) const +{ + call( "ppmsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::premultiply( VOption *options ) const +{ + VImage out; + + call( "premultiply", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::profile( VImage *rows, VOption *options ) const +{ + VImage columns; + + call( "profile", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "columns", &columns )-> + set( "rows", rows ) ); + + return( columns ); +} + +VipsBlob *VImage::profile_load( const char *name, VOption *options ) +{ + VipsBlob *profile; + + call( "profile_load", + (options ? options : VImage::option())-> + set( "profile", &profile )-> + set( "name", name ) ); + + return( profile ); +} + +VImage VImage::project( VImage *rows, VOption *options ) const +{ + VImage columns; + + call( "project", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "columns", &columns )-> + set( "rows", rows ) ); + + return( columns ); +} + +VImage VImage::quadratic( VImage coeff, VOption *options ) const +{ + VImage out; + + call( "quadratic", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "coeff", coeff ) ); + + return( out ); +} + +VImage VImage::rad2float( VOption *options ) const +{ + VImage out; + + call( "rad2float", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::radload( const char *filename, VOption *options ) +{ + VImage out; + + call( "radload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::radload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "radload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::radload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "radload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::radsave( const char *filename, VOption *options ) const +{ + call( "radsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::radsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "radsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::radsave_target( VTarget target, VOption *options ) const +{ + call( "radsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::rank( int width, int height, int index, VOption *options ) const +{ + VImage out; + + call( "rank", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height )-> + set( "index", index ) ); + + return( out ); +} + +VImage VImage::rawload( const char *filename, int width, int height, int bands, VOption *options ) +{ + VImage out; + + call( "rawload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename )-> + set( "width", width )-> + set( "height", height )-> + set( "bands", bands ) ); + + return( out ); +} + +void VImage::rawsave( const char *filename, VOption *options ) const +{ + call( "rawsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +void VImage::rawsave_fd( int fd, VOption *options ) const +{ + call( "rawsave_fd", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "fd", fd ) ); +} + +VImage VImage::recomb( VImage m, VOption *options ) const +{ + VImage out; + + call( "recomb", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "m", m ) ); + + return( out ); +} + +VImage VImage::reduce( double hshrink, double vshrink, VOption *options ) const +{ + VImage out; + + call( "reduce", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "hshrink", hshrink )-> + set( "vshrink", vshrink ) ); + + return( out ); +} + +VImage VImage::reduceh( double hshrink, VOption *options ) const +{ + VImage out; + + call( "reduceh", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "hshrink", hshrink ) ); + + return( out ); +} + +VImage VImage::reducev( double vshrink, VOption *options ) const +{ + VImage out; + + call( "reducev", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "vshrink", vshrink ) ); + + return( out ); +} + +VImage VImage::relational( VImage right, VipsOperationRelational relational, VOption *options ) const +{ + VImage out; + + call( "relational", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right )-> + set( "relational", relational ) ); + + return( out ); +} + +VImage VImage::relational_const( VipsOperationRelational relational, std::vector c, VOption *options ) const +{ + VImage out; + + call( "relational_const", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "relational", relational )-> + set( "c", c ) ); + + return( out ); +} + +VImage VImage::remainder( VImage right, VOption *options ) const +{ + VImage out; + + call( "remainder", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +VImage VImage::remainder_const( std::vector c, VOption *options ) const +{ + VImage out; + + call( "remainder_const", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "c", c ) ); + + return( out ); +} + +VImage VImage::replicate( int across, int down, VOption *options ) const +{ + VImage out; + + call( "replicate", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "across", across )-> + set( "down", down ) ); + + return( out ); +} + +VImage VImage::resize( double scale, VOption *options ) const +{ + VImage out; + + call( "resize", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "scale", scale ) ); + + return( out ); +} + +VImage VImage::rot( VipsAngle angle, VOption *options ) const +{ + VImage out; + + call( "rot", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "angle", angle ) ); + + return( out ); +} + +VImage VImage::rot45( VOption *options ) const +{ + VImage out; + + call( "rot45", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::rotate( double angle, VOption *options ) const +{ + VImage out; + + call( "rotate", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "angle", angle ) ); + + return( out ); +} + +VImage VImage::round( VipsOperationRound round, VOption *options ) const +{ + VImage out; + + call( "round", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "round", round ) ); + + return( out ); +} + +VImage VImage::sRGB2HSV( VOption *options ) const +{ + VImage out; + + call( "sRGB2HSV", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::sRGB2scRGB( VOption *options ) const +{ + VImage out; + + call( "sRGB2scRGB", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::scRGB2BW( VOption *options ) const +{ + VImage out; + + call( "scRGB2BW", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::scRGB2XYZ( VOption *options ) const +{ + VImage out; + + call( "scRGB2XYZ", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::scRGB2sRGB( VOption *options ) const +{ + VImage out; + + call( "scRGB2sRGB", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::scale( VOption *options ) const +{ + VImage out; + + call( "scale", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::sequential( VOption *options ) const +{ + VImage out; + + call( "sequential", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::sharpen( VOption *options ) const +{ + VImage out; + + call( "sharpen", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::shrink( double hshrink, double vshrink, VOption *options ) const +{ + VImage out; + + call( "shrink", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "hshrink", hshrink )-> + set( "vshrink", vshrink ) ); + + return( out ); +} + +VImage VImage::shrinkh( int hshrink, VOption *options ) const +{ + VImage out; + + call( "shrinkh", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "hshrink", hshrink ) ); + + return( out ); +} + +VImage VImage::shrinkv( int vshrink, VOption *options ) const +{ + VImage out; + + call( "shrinkv", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "vshrink", vshrink ) ); + + return( out ); +} + +VImage VImage::sign( VOption *options ) const +{ + VImage out; + + call( "sign", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::similarity( VOption *options ) const +{ + VImage out; + + call( "similarity", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::sines( int width, int height, VOption *options ) +{ + VImage out; + + call( "sines", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::smartcrop( int width, int height, VOption *options ) const +{ + VImage out; + + call( "smartcrop", + (options ? options : VImage::option())-> + set( "input", *this )-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::sobel( VOption *options ) const +{ + VImage out; + + call( "sobel", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::spcor( VImage ref, VOption *options ) const +{ + VImage out; + + call( "spcor", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "ref", ref ) ); + + return( out ); +} + +VImage VImage::spectrum( VOption *options ) const +{ + VImage out; + + call( "spectrum", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::stats( VOption *options ) const +{ + VImage out; + + call( "stats", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::stdif( int width, int height, VOption *options ) const +{ + VImage out; + + call( "stdif", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::subsample( int xfac, int yfac, VOption *options ) const +{ + VImage out; + + call( "subsample", + (options ? options : VImage::option())-> + set( "input", *this )-> + set( "out", &out )-> + set( "xfac", xfac )-> + set( "yfac", yfac ) ); + + return( out ); +} + +VImage VImage::subtract( VImage right, VOption *options ) const +{ + VImage out; + + call( "subtract", + (options ? options : VImage::option())-> + set( "left", *this )-> + set( "out", &out )-> + set( "right", right ) ); + + return( out ); +} + +VImage VImage::sum( std::vector in, VOption *options ) +{ + VImage out; + + call( "sum", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "in", in ) ); + + return( out ); +} + +VImage VImage::svgload( const char *filename, VOption *options ) +{ + VImage out; + + call( "svgload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::svgload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "svgload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::svgload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "svgload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +VImage VImage::switch_image( std::vector tests, VOption *options ) +{ + VImage out; + + call( "switch", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "tests", tests ) ); + + return( out ); +} + +void VImage::system( const char *cmd_format, VOption *options ) +{ + call( "system", + (options ? options : VImage::option())-> + set( "cmd_format", cmd_format ) ); +} + +VImage VImage::text( const char *text, VOption *options ) +{ + VImage out; + + call( "text", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "text", text ) ); + + return( out ); +} + +VImage VImage::thumbnail( const char *filename, int width, VOption *options ) +{ + VImage out; + + call( "thumbnail", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename )-> + set( "width", width ) ); + + return( out ); +} + +VImage VImage::thumbnail_buffer( VipsBlob *buffer, int width, VOption *options ) +{ + VImage out; + + call( "thumbnail_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer )-> + set( "width", width ) ); + + return( out ); +} + +VImage VImage::thumbnail_image( int width, VOption *options ) const +{ + VImage out; + + call( "thumbnail_image", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out )-> + set( "width", width ) ); + + return( out ); +} + +VImage VImage::thumbnail_source( VSource source, int width, VOption *options ) +{ + VImage out; + + call( "thumbnail_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source )-> + set( "width", width ) ); + + return( out ); +} + +VImage VImage::tiffload( const char *filename, VOption *options ) +{ + VImage out; + + call( "tiffload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::tiffload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "tiffload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::tiffload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "tiffload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::tiffsave( const char *filename, VOption *options ) const +{ + call( "tiffsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::tiffsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "tiffsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::tiffsave_target( VTarget target, VOption *options ) const +{ + call( "tiffsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::tilecache( VOption *options ) const +{ + VImage out; + + call( "tilecache", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::tonelut( VOption *options ) +{ + VImage out; + + call( "tonelut", + (options ? options : VImage::option())-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::transpose3d( VOption *options ) const +{ + VImage out; + + call( "transpose3d", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::unpremultiply( VOption *options ) const +{ + VImage out; + + call( "unpremultiply", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::vipsload( const char *filename, VOption *options ) +{ + VImage out; + + call( "vipsload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::vipsload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "vipsload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::vipssave( const char *filename, VOption *options ) const +{ + call( "vipssave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +void VImage::vipssave_target( VTarget target, VOption *options ) const +{ + call( "vipssave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::webpload( const char *filename, VOption *options ) +{ + VImage out; + + call( "webpload", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "filename", filename ) ); + + return( out ); +} + +VImage VImage::webpload_buffer( VipsBlob *buffer, VOption *options ) +{ + VImage out; + + call( "webpload_buffer", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "buffer", buffer ) ); + + return( out ); +} + +VImage VImage::webpload_source( VSource source, VOption *options ) +{ + VImage out; + + call( "webpload_source", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "source", source ) ); + + return( out ); +} + +void VImage::webpsave( const char *filename, VOption *options ) const +{ + call( "webpsave", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "filename", filename ) ); +} + +VipsBlob *VImage::webpsave_buffer( VOption *options ) const +{ + VipsBlob *buffer; + + call( "webpsave_buffer", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "buffer", &buffer ) ); + + return( buffer ); +} + +void VImage::webpsave_mime( VOption *options ) const +{ + call( "webpsave_mime", + (options ? options : VImage::option())-> + set( "in", *this ) ); +} + +void VImage::webpsave_target( VTarget target, VOption *options ) const +{ + call( "webpsave_target", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "target", target ) ); +} + +VImage VImage::worley( int width, int height, VOption *options ) +{ + VImage out; + + call( "worley", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::wrap( VOption *options ) const +{ + VImage out; + + call( "wrap", + (options ? options : VImage::option())-> + set( "in", *this )-> + set( "out", &out ) ); + + return( out ); +} + +VImage VImage::xyz( int width, int height, VOption *options ) +{ + VImage out; + + call( "xyz", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::zone( int width, int height, VOption *options ) +{ + VImage out; + + call( "zone", + (options ? options : VImage::option())-> + set( "out", &out )-> + set( "width", width )-> + set( "height", height ) ); + + return( out ); +} + +VImage VImage::zoom( int xfac, int yfac, VOption *options ) const +{ + VImage out; + + call( "zoom", + (options ? options : VImage::option())-> + set( "input", *this )-> + set( "out", &out )-> + set( "xfac", xfac )-> + set( "yfac", yfac ) ); + + return( out ); +} diff --git a/backend/node_modules/sharp/src/metadata.cc b/backend/node_modules/sharp/src/metadata.cc index 2fde7bf6..6bb861f4 100644 --- a/backend/node_modules/sharp/src/metadata.cc +++ b/backend/node_modules/sharp/src/metadata.cc @@ -1,21 +1,14 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -#include #include -#include -#include #include #include #include -#include "./common.h" -#include "./metadata.h" - -static void* readPNGComment(VipsImage *image, const char *field, GValue *value, void *p); +#include "common.h" +#include "metadata.h" class MetadataWorker : public Napi::AsyncWorker { public: @@ -25,7 +18,7 @@ class MetadataWorker : public Napi::AsyncWorker { void Execute() { // Decrement queued task counter - sharp::counterQueue--; + g_atomic_int_dec_and_test(&sharp::counterQueue); vips::VImage image; sharp::ImageType imageType = sharp::ImageType::UNKNOWN; @@ -52,11 +45,8 @@ class MetadataWorker : public Napi::AsyncWorker { if (image.get_typeof("interlaced") == G_TYPE_INT) { baton->isProgressive = image.get_int("interlaced") == 1; } - if (image.get_typeof(VIPS_META_PALETTE) == G_TYPE_INT) { - baton->isPalette = image.get_int(VIPS_META_PALETTE); - } - if (image.get_typeof(VIPS_META_BITS_PER_SAMPLE) == G_TYPE_INT) { - baton->bitsPerSample = image.get_int(VIPS_META_BITS_PER_SAMPLE); + if (image.get_typeof("palette-bit-depth") == G_TYPE_INT) { + baton->paletteBitDepth = image.get_int("palette-bit-depth"); } if (image.get_typeof(VIPS_META_N_PAGES) == G_TYPE_INT) { baton->pages = image.get_int(VIPS_META_N_PAGES); @@ -99,7 +89,7 @@ class MetadataWorker : public Napi::AsyncWorker { baton->background = image.get_array_double("background"); } // Derived attributes - baton->hasAlpha = image.has_alpha(); + baton->hasAlpha = sharp::HasAlpha(image); baton->orientation = sharp::ExifOrientation(image); // EXIF if (image.get_typeof(VIPS_META_EXIF_NAME) == VIPS_TYPE_BLOB) { @@ -141,8 +131,6 @@ class MetadataWorker : public Napi::AsyncWorker { memcpy(baton->tifftagPhotoshop, tifftagPhotoshop, tifftagPhotoshopLength); baton->tifftagPhotoshopLength = tifftagPhotoshopLength; } - // PNG comments - vips_image_map(image.get_image(), readPNGComment, &baton->comments); } // Clean up @@ -157,7 +145,7 @@ class MetadataWorker : public Napi::AsyncWorker { // Handle warnings std::string warning = sharp::VipsWarningPop(); while (!warning.empty()) { - debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) }); + debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) }); warning = sharp::VipsWarningPop(); } @@ -179,13 +167,8 @@ class MetadataWorker : public Napi::AsyncWorker { info.Set("chromaSubsampling", baton->chromaSubsampling); } info.Set("isProgressive", baton->isProgressive); - info.Set("isPalette", baton->isPalette); - if (baton->bitsPerSample > 0) { - info.Set("bitsPerSample", baton->bitsPerSample); - if (baton->isPalette) { - // Deprecated, remove with libvips 8.17.0 - info.Set("paletteBitDepth", baton->bitsPerSample); - } + if (baton->paletteBitDepth > 0) { + info.Set("paletteBitDepth", baton->paletteBitDepth); } if (baton->pages > 0) { info.Set("pages", baton->pages); @@ -219,10 +202,10 @@ class MetadataWorker : public Napi::AsyncWorker { if (!baton->levels.empty()) { int i = 0; Napi::Array levels = Napi::Array::New(env, static_cast(baton->levels.size())); - for (const auto& [width, height] : baton->levels) { + for (std::pair const &l : baton->levels) { Napi::Object level = Napi::Object::New(env); - level.Set("width", width); - level.Set("height", height); + level.Set("width", l.first); + level.Set("height", l.second); levels.Set(i++, level); } info.Set("levels", levels); @@ -231,30 +214,21 @@ class MetadataWorker : public Napi::AsyncWorker { info.Set("subifds", baton->subifds); } if (!baton->background.empty()) { - Napi::Object background = Napi::Object::New(env); if (baton->background.size() == 3) { + Napi::Object background = Napi::Object::New(env); background.Set("r", baton->background[0]); background.Set("g", baton->background[1]); background.Set("b", baton->background[2]); + info.Set("background", background); } else { - background.Set("gray", round(baton->background[0] * 100 / 255)); + info.Set("background", baton->background[0]); } - info.Set("background", background); } info.Set("hasProfile", baton->hasProfile); info.Set("hasAlpha", baton->hasAlpha); if (baton->orientation > 0) { info.Set("orientation", baton->orientation); } - Napi::Object autoOrient = Napi::Object::New(env); - info.Set("autoOrient", autoOrient); - if (baton->orientation >= 5) { - autoOrient.Set("width", baton->height); - autoOrient.Set("height", baton->width); - } else { - autoOrient.Set("width", baton->width); - autoOrient.Set("height", baton->height); - } if (baton->exifLength > 0) { info.Set("exif", Napi::Buffer::NewOrCopy(env, baton->exif, baton->exifLength, sharp::FreeCallback)); } @@ -265,10 +239,6 @@ class MetadataWorker : public Napi::AsyncWorker { info.Set("iptc", Napi::Buffer::NewOrCopy(env, baton->iptc, baton->iptcLength, sharp::FreeCallback)); } if (baton->xmpLength > 0) { - if (g_utf8_validate(static_cast(baton->xmp), baton->xmpLength, nullptr)) { - info.Set("xmpAsString", - Napi::String::New(env, static_cast(baton->xmp), baton->xmpLength)); - } info.Set("xmp", Napi::Buffer::NewOrCopy(env, baton->xmp, baton->xmpLength, sharp::FreeCallback)); } if (baton->tifftagPhotoshopLength > 0) { @@ -276,20 +246,9 @@ class MetadataWorker : public Napi::AsyncWorker { Napi::Buffer::NewOrCopy(env, baton->tifftagPhotoshop, baton->tifftagPhotoshopLength, sharp::FreeCallback)); } - if (baton->comments.size() > 0) { - int i = 0; - Napi::Array comments = Napi::Array::New(env, baton->comments.size()); - for (const auto& [keyword, text] : baton->comments) { - Napi::Object comment = Napi::Object::New(env); - comment.Set("keyword", keyword); - comment.Set("text", text); - comments.Set(i++, comment); - } - info.Set("comments", comments); - } - Callback().Call(Receiver().Value(), { env.Null(), info }); + Callback().MakeCallback(Receiver().Value(), { env.Null(), info }); } else { - Callback().Call(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); + Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); } delete baton->input; @@ -322,25 +281,7 @@ Napi::Value metadata(const Napi::CallbackInfo& info) { worker->Queue(); // Increment queued task counter - sharp::counterQueue++; + g_atomic_int_inc(&sharp::counterQueue); return info.Env().Undefined(); } - -const char *PNG_COMMENT_START = "png-comment-"; -const int PNG_COMMENT_START_LEN = strlen(PNG_COMMENT_START); - -static void* readPNGComment(VipsImage *image, const char *field, GValue *value, void *p) { - MetadataComments *comments = static_cast(p); - - if (vips_isprefix(PNG_COMMENT_START, field)) { - const char *keyword = strchr(field + PNG_COMMENT_START_LEN, '-'); - const char *str; - if (keyword != NULL && !vips_image_get_string(image, field, &str)) { - keyword++; // Skip the hyphen - comments->push_back(std::make_pair(keyword, str)); - } - } - - return NULL; -} diff --git a/backend/node_modules/sharp/src/metadata.h b/backend/node_modules/sharp/src/metadata.h index 6f02d452..3030ae29 100644 --- a/backend/node_modules/sharp/src/metadata.h +++ b/backend/node_modules/sharp/src/metadata.h @@ -1,19 +1,14 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #ifndef SRC_METADATA_H_ #define SRC_METADATA_H_ #include -#include #include #include "./common.h" -typedef std::vector> MetadataComments; - struct MetadataBaton { // Input sharp::InputDescriptor *input; @@ -27,8 +22,7 @@ struct MetadataBaton { int density; std::string chromaSubsampling; bool isProgressive; - bool isPalette; - int bitsPerSample; + int paletteBitDepth; int pages; int pageHeight; int loop; @@ -53,7 +47,6 @@ struct MetadataBaton { size_t xmpLength; char *tifftagPhotoshop; size_t tifftagPhotoshopLength; - MetadataComments comments; std::string err; MetadataBaton(): @@ -63,8 +56,7 @@ struct MetadataBaton { channels(0), density(0), isProgressive(false), - isPalette(false), - bitsPerSample(0), + paletteBitDepth(0), pages(0), pageHeight(0), loop(-1), diff --git a/backend/node_modules/sharp/src/operations.cc b/backend/node_modules/sharp/src/operations.cc index daeba5ab..e59157ff 100644 --- a/backend/node_modules/sharp/src/operations.cc +++ b/backend/node_modules/sharp/src/operations.cc @@ -1,7 +1,5 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #include #include @@ -10,52 +8,38 @@ #include #include -#include "./common.h" -#include "./operations.h" +#include "common.h" +#include "operations.h" using vips::VImage; using vips::VError; namespace sharp { /* - * Tint an image using the provided RGB. + * Tint an image using the specified chroma, preserving the original image luminance */ - VImage Tint(VImage image, std::vector const tint) { - std::vector const tintLab = (VImage::black(1, 1) + tint) - .colourspace(VIPS_INTERPRETATION_LAB, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB)) - .getpoint(0, 0); - // LAB identity function - VImage identityLab = VImage::identity(VImage::option()->set("bands", 3)) - .colourspace(VIPS_INTERPRETATION_LAB, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB)); - // Scale luminance range, 0.0 to 1.0 - VImage l = identityLab[0] / 100; - // Weighting functions - VImage weightL = 1.0 - 4.0 * ((l - 0.5) * (l - 0.5)); - VImage weightAB = (weightL * tintLab).extract_band(1, VImage::option()->set("n", 2)); - identityLab = identityLab[0].bandjoin(weightAB); - // Convert lookup table to sRGB - VImage lut = identityLab.colourspace(VIPS_INTERPRETATION_sRGB, - VImage::option()->set("source_space", VIPS_INTERPRETATION_LAB)); - // Original colourspace + VImage Tint(VImage image, double const a, double const b) { + // Get original colourspace VipsInterpretation typeBeforeTint = image.interpretation(); if (typeBeforeTint == VIPS_INTERPRETATION_RGB) { typeBeforeTint = VIPS_INTERPRETATION_sRGB; } - // Apply lookup table - if (image.has_alpha()) { + // Extract luminance + VImage luminance = image.colourspace(VIPS_INTERPRETATION_LAB)[0]; + // Create the tinted version by combining the L from the original and the chroma from the tint + std::vector chroma {a, b}; + VImage tinted = luminance + .bandjoin(chroma) + .copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_LAB)) + .colourspace(typeBeforeTint); + // Attach original alpha channel, if any + if (HasAlpha(image)) { + // Extract original alpha channel VImage alpha = image[image.bands() - 1]; - image = RemoveAlpha(image) - .colourspace(VIPS_INTERPRETATION_B_W) - .maplut(lut) - .colourspace(typeBeforeTint) - .bandjoin(alpha); - } else { - image = image - .colourspace(VIPS_INTERPRETATION_B_W) - .maplut(lut) - .colourspace(typeBeforeTint); + // Join alpha channel to normalised image + tinted = tinted.bandjoin(alpha); } - return image; + return tinted; } /* @@ -85,7 +69,7 @@ namespace sharp { // Scale luminance, join to chroma, convert back to original colourspace VImage normalized = luminance.linear(f, a).bandjoin(chroma).colourspace(typeBeforeNormalize); // Attach original alpha channel, if any - if (image.has_alpha()) { + if (HasAlpha(image)) { // Extract original alpha channel VImage alpha = image[image.bands() - 1]; // Join alpha channel to normalised image @@ -108,7 +92,7 @@ namespace sharp { * Gamma encoding/decoding */ VImage Gamma(VImage image, double const exponent) { - if (image.has_alpha()) { + if (HasAlpha(image)) { // Separate alpha channel VImage alpha = image[image.bands() - 1]; return RemoveAlpha(image).gamma(VImage::option()->set("exponent", exponent)).bandjoin(alpha); @@ -134,7 +118,7 @@ namespace sharp { * Produce the "negative" of the image. */ VImage Negate(VImage image, bool const negateAlpha) { - if (image.has_alpha() && !negateAlpha) { + if (HasAlpha(image) && !negateAlpha) { // Separate alpha channel VImage alpha = image[image.bands() - 1]; return RemoveAlpha(image).invert().bandjoin(alpha); @@ -146,7 +130,7 @@ namespace sharp { /* * Gaussian blur. Use sigma of -1.0 for fast blur. */ - VImage Blur(VImage image, double const sigma, VipsPrecision precision, double const minAmpl) { + VImage Blur(VImage image, double const sigma) { if (sigma == -1.0) { // Fast, mild blur - averages neighbouring pixels VImage blur = VImage::new_matrixv(3, 3, @@ -157,9 +141,7 @@ namespace sharp { return image.conv(blur); } else { // Slower, accurate Gaussian blur - return StaySequential(image).gaussblur(sigma, VImage::option() - ->set("precision", precision) - ->set("min_ampl", minAmpl)); + return image.gaussblur(sigma); } } @@ -168,10 +150,10 @@ namespace sharp { */ VImage Convolve(VImage image, int const width, int const height, double const scale, double const offset, - std::vector const &kernel_v + std::unique_ptr const &kernel_v ) { VImage kernel = VImage::new_from_memory( - static_cast(const_cast(kernel_v.data())), + kernel_v.get(), width * height * sizeof(double), width, height, @@ -187,27 +169,25 @@ namespace sharp { * Recomb with a Matrix of the given bands/channel size. * Eg. RGB will be a 3x3 matrix. */ - VImage Recomb(VImage image, std::vector const& matrix) { - double* m = const_cast(matrix.data()); + VImage Recomb(VImage image, std::unique_ptr const &matrix) { + double *m = matrix.get(); image = image.colourspace(VIPS_INTERPRETATION_sRGB); - if (matrix.size() == 9) { - return image - .recomb(image.bands() == 3 - ? VImage::new_matrix(3, 3, m, 9) - : VImage::new_matrixv(4, 4, - m[0], m[1], m[2], 0.0, - m[3], m[4], m[5], 0.0, - m[6], m[7], m[8], 0.0, - 0.0, 0.0, 0.0, 1.0)); - } else { - return image.recomb(VImage::new_matrix(4, 4, m, 16)); - } + return image + .recomb(image.bands() == 3 + ? VImage::new_from_memory( + m, 9 * sizeof(double), 3, 3, 1, VIPS_FORMAT_DOUBLE + ) + : VImage::new_matrixv(4, 4, + m[0], m[1], m[2], 0.0, + m[3], m[4], m[5], 0.0, + m[6], m[7], m[8], 0.0, + 0.0, 0.0, 0.0, 1.0)); } VImage Modulate(VImage image, double const brightness, double const saturation, int const hue, double const lightness) { VipsInterpretation colourspaceBeforeModulate = image.interpretation(); - if (image.has_alpha()) { + if (HasAlpha(image)) { // Separate alpha channel VImage alpha = image[image.bands() - 1]; return RemoveAlpha(image) @@ -285,7 +265,7 @@ namespace sharp { /* Trim an image */ - VImage Trim(VImage image, std::vector background, double threshold, bool const lineArt) { + VImage Trim(VImage image, std::vector background, double threshold) { if (image.width() < 3 && image.height() < 3) { throw VError("Image to trim must be at least 3x3 pixels"); } @@ -299,7 +279,7 @@ namespace sharp { threshold *= 256.0; } std::vector backgroundAlpha({ background.back() }); - if (image.has_alpha()) { + if (HasAlpha(image)) { background.pop_back(); } else { background.resize(image.bands()); @@ -307,15 +287,13 @@ namespace sharp { int left, top, width, height; left = image.find_trim(&top, &width, &height, VImage::option() ->set("background", background) - ->set("line_art", lineArt) ->set("threshold", threshold)); - if (image.has_alpha()) { + if (HasAlpha(image)) { // Search alpha channel (A) int leftA, topA, widthA, heightA; VImage alpha = image[image.bands() - 1]; leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option() ->set("background", backgroundAlpha) - ->set("line_art", lineArt) ->set("threshold", threshold)); if (widthA > 0 && heightA > 0) { if (width > 0 && height > 0) { @@ -346,7 +324,7 @@ namespace sharp { throw VError("Band expansion using linear is unsupported"); } bool const uchar = !Is16Bit(image.interpretation()); - if (image.has_alpha() && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) { + if (HasAlpha(image) && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) { // Separate alpha channel VImage alpha = image[bands - 1]; return RemoveAlpha(image).linear(a, b, VImage::option()->set("uchar", uchar)).bandjoin(alpha); @@ -359,7 +337,7 @@ namespace sharp { * Unflatten */ VImage Unflatten(VImage image) { - if (image.has_alpha()) { + if (HasAlpha(image)) { VImage alpha = image[image.bands() - 1]; VImage noAlpha = RemoveAlpha(image); return noAlpha.bandjoin(alpha & (noAlpha.colourspace(VIPS_INTERPRETATION_B_W) < 255)); @@ -392,7 +370,6 @@ namespace sharp { pages.reserve(nPages); // Split the image into cropped frames - image = StaySequential(image); for (int i = 0; i < nPages; i++) { pages.push_back( image.extract_area(left, *pageHeight * i + top, width, height)); @@ -474,26 +451,4 @@ namespace sharp { } } - /* - * Dilate an image - */ - VImage Dilate(VImage image, int const width) { - int const maskWidth = 2 * width + 1; - VImage mask = VImage::new_matrix(maskWidth, maskWidth); - return image.morph( - mask, - VIPS_OPERATION_MORPHOLOGY_DILATE).invert(); - } - - /* - * Erode an image - */ - VImage Erode(VImage image, int const width) { - int const maskWidth = 2 * width + 1; - VImage mask = VImage::new_matrix(maskWidth, maskWidth); - return image.morph( - mask, - VIPS_OPERATION_MORPHOLOGY_ERODE).invert(); - } - } // namespace sharp diff --git a/backend/node_modules/sharp/src/operations.h b/backend/node_modules/sharp/src/operations.h index c281c02c..10a0c6ce 100644 --- a/backend/node_modules/sharp/src/operations.h +++ b/backend/node_modules/sharp/src/operations.h @@ -1,7 +1,5 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #ifndef SRC_OPERATIONS_H_ #define SRC_OPERATIONS_H_ @@ -10,7 +8,6 @@ #include #include #include -#include #include using vips::VImage; @@ -18,9 +15,9 @@ using vips::VImage; namespace sharp { /* - * Tint an image using the provided RGB. + * Tint an image using the specified chroma, preserving the original image luminance */ - VImage Tint(VImage image, std::vector const tint); + VImage Tint(VImage image, double const a, double const b); /* * Stretch luminance to cover full dynamic range. @@ -50,13 +47,13 @@ namespace sharp { /* * Gaussian blur. Use sigma of -1.0 for fast blur. */ - VImage Blur(VImage image, double const sigma, VipsPrecision precision, double const minAmpl); + VImage Blur(VImage image, double const sigma); /* * Convolution with a kernel. */ VImage Convolve(VImage image, int const width, int const height, - double const scale, double const offset, std::vector const &kernel_v); + double const scale, double const offset, std::unique_ptr const &kernel_v); /* * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. @@ -82,7 +79,7 @@ namespace sharp { /* Trim an image */ - VImage Trim(VImage image, std::vector background, double threshold, bool const lineArt); + VImage Trim(VImage image, std::vector background, double const threshold); /* * Linear adjustment (a * in + b) @@ -98,7 +95,7 @@ namespace sharp { * Recomb with a Matrix of the given bands/channel size. * Eg. RGB will be a 3x3 matrix. */ - VImage Recomb(VImage image, std::vector const &matrix); + VImage Recomb(VImage image, std::unique_ptr const &matrix); /* * Modulate brightness, saturation, hue and lightness @@ -123,15 +120,6 @@ namespace sharp { VImage EmbedMultiPage(VImage image, int left, int top, int width, int height, VipsExtend extendWith, std::vector background, int nPages, int *pageHeight); - /* - * Dilate an image - */ - VImage Dilate(VImage image, int const maskWidth); - - /* - * Erode an image - */ - VImage Erode(VImage image, int const maskWidth); } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/backend/node_modules/sharp/src/pipeline.cc b/backend/node_modules/sharp/src/pipeline.cc index 5f0a3bb0..5596d86f 100644 --- a/backend/node_modules/sharp/src/pipeline.cc +++ b/backend/node_modules/sharp/src/pipeline.cc @@ -1,11 +1,8 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #include #include -#include // NOLINT(build/c++17) #include #include #include @@ -19,9 +16,20 @@ #include #include -#include "./common.h" -#include "./operations.h" -#include "./pipeline.h" +#include "common.h" +#include "operations.h" +#include "pipeline.h" + +#ifdef _WIN32 +#define STAT64_STRUCT __stat64 +#define STAT64_FUNCTION _stat64 +#elif defined(_LARGEFILE64_SOURCE) +#define STAT64_STRUCT stat64 +#define STAT64_FUNCTION stat64 +#else +#define STAT64_STRUCT stat +#define STAT64_FUNCTION stat +#endif class PipelineWorker : public Napi::AsyncWorker { public: @@ -36,49 +44,17 @@ class PipelineWorker : public Napi::AsyncWorker { // libuv worker void Execute() { // Decrement queued task counter - sharp::counterQueue--; + g_atomic_int_dec_and_test(&sharp::counterQueue); // Increment processing task counter - sharp::counterProcess++; + g_atomic_int_inc(&sharp::counterProcess); try { // Open input vips::VImage image; sharp::ImageType inputImageType; - if (baton->join.empty()) { - std::tie(image, inputImageType) = sharp::OpenInput(baton->input); - } else { - std::vector images; - bool hasAlpha = false; - for (auto &join : baton->join) { - std::tie(image, inputImageType) = sharp::OpenInput(join); - image = sharp::EnsureColourspace(image, baton->colourspacePipeline); - images.push_back(image); - hasAlpha |= image.has_alpha(); - } - if (hasAlpha) { - for (auto &image : images) { - if (!image.has_alpha()) { - image = sharp::EnsureAlpha(image, 1); - } - } - } else { - baton->input->joinBackground.pop_back(); - } - inputImageType = sharp::ImageType::PNG; - image = VImage::arrayjoin(images, VImage::option() - ->set("across", baton->input->joinAcross) - ->set("shim", baton->input->joinShim) - ->set("background", baton->input->joinBackground) - ->set("halign", baton->input->joinHalign) - ->set("valign", baton->input->joinValign)); - if (baton->input->joinAnimated) { - image = image.copy(); - image.set(VIPS_META_N_PAGES, static_cast(images.size())); - image.set(VIPS_META_PAGE_HEIGHT, static_cast(image.height() / images.size())); - } - } + std::tie(image, inputImageType) = sharp::OpenInput(baton->input); VipsAccess access = baton->input->access; - image = sharp::EnsureColourspace(image, baton->colourspacePipeline); + image = sharp::EnsureColourspace(image, baton->colourspaceInput); int nPages = baton->input->pages; if (nPages == -1) { @@ -94,66 +70,66 @@ class PipelineWorker : public Napi::AsyncWorker { // Calculate angle of rotation VipsAngle rotation = VIPS_ANGLE_D0; VipsAngle autoRotation = VIPS_ANGLE_D0; - bool autoFlop = false; + bool autoFlip = FALSE; + bool autoFlop = FALSE; - if (baton->input->autoOrient) { + if (baton->useExifOrientation) { // Rotate and flip image according to Exif orientation - std::tie(autoRotation, autoFlop) = CalculateExifRotationAndFlop(sharp::ExifOrientation(image)); + std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image)); + image = sharp::RemoveExifOrientation(image); + } else { + rotation = CalculateAngleRotation(baton->angle); } - rotation = CalculateAngleRotation(baton->angle); + // Rotate pre-extract + bool const shouldRotateBefore = baton->rotateBeforePreExtract && + (rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 || + autoFlip || baton->flip || autoFlop || baton->flop || + baton->rotationAngle != 0.0); - bool const shouldRotateBefore = baton->rotateBefore && - (rotation != VIPS_ANGLE_D0 || baton->flip || baton->flop || baton->rotationAngle != 0.0); - bool const shouldOrientBefore = (shouldRotateBefore || baton->orientBefore) && - (autoRotation != VIPS_ANGLE_D0 || autoFlop); + if (shouldRotateBefore) { + image = sharp::StaySequential(image, access, + rotation != VIPS_ANGLE_D0 || + autoRotation != VIPS_ANGLE_D0 || + autoFlip || + baton->flip || + baton->rotationAngle != 0.0); - if (shouldOrientBefore) { - image = sharp::StaySequential(image, autoRotation != VIPS_ANGLE_D0); if (autoRotation != VIPS_ANGLE_D0) { - if (autoRotation != VIPS_ANGLE_D180) { - MultiPageUnsupported(nPages, "Rotate"); - } image = image.rot(autoRotation); autoRotation = VIPS_ANGLE_D0; } + if (autoFlip) { + image = image.flip(VIPS_DIRECTION_VERTICAL); + autoFlip = FALSE; + } else if (baton->flip) { + image = image.flip(VIPS_DIRECTION_VERTICAL); + baton->flip = FALSE; + } if (autoFlop) { image = image.flip(VIPS_DIRECTION_HORIZONTAL); - autoFlop = false; - } - } - - if (shouldRotateBefore) { - image = sharp::StaySequential(image, rotation != VIPS_ANGLE_D0 || baton->flip || baton->rotationAngle != 0.0); - if (baton->flip) { - image = image.flip(VIPS_DIRECTION_VERTICAL); - baton->flip = false; - } - if (baton->flop) { + autoFlop = FALSE; + } else if (baton->flop) { image = image.flip(VIPS_DIRECTION_HORIZONTAL); - baton->flop = false; + baton->flop = FALSE; } if (rotation != VIPS_ANGLE_D0) { - if (rotation != VIPS_ANGLE_D180) { - MultiPageUnsupported(nPages, "Rotate"); - } image = image.rot(rotation); rotation = VIPS_ANGLE_D0; } if (baton->rotationAngle != 0.0) { MultiPageUnsupported(nPages, "Rotate"); std::vector background; - std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, false); + std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE); image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)).copy_memory(); - baton->rotationAngle = 0.0; } } // Trim - if (baton->trimThreshold >= 0.0) { + if (baton->trimThreshold > 0.0) { MultiPageUnsupported(nPages, "Trim"); - image = sharp::StaySequential(image); - image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt); + image = sharp::StaySequential(image, access); + image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold); baton->trimOffsetLeft = image.xoffset(); baton->trimOffsetTop = image.yoffset(); } @@ -184,7 +160,8 @@ class PipelineWorker : public Napi::AsyncWorker { // When auto-rotating by 90 or 270 degrees, swap the target width and // height to ensure the behavior aligns with how it would have been if // the rotation had taken place *before* resizing. - if (autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270) { + if (!baton->rotateBeforePreExtract && + (autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270)) { std::swap(targetResizeWidth, targetResizeHeight); } @@ -205,8 +182,8 @@ class PipelineWorker : public Napi::AsyncWorker { // - trimming or pre-resize extract isn't required; // - input colourspace is not specified; bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) && - baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold < 0.0 && - baton->colourspacePipeline == VIPS_INTERPRETATION_LAST && !(shouldOrientBefore || shouldRotateBefore); + baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 && + baton->colourspaceInput == VIPS_INTERPRETATION_LAST && !shouldRotateBefore; if (shouldPreShrink) { // The common part of the shrink: the bit by which both axes must be shrunk @@ -240,7 +217,11 @@ class PipelineWorker : public Napi::AsyncWorker { // factor for jpegload*, a double scale factor for webpload*, // pdfload* and svgload* if (jpegShrinkOnLoad > 1) { - vips::VOption *option = GetOptionsForImageType(inputImageType, baton->input)->set("shrink", jpegShrinkOnLoad); + vips::VOption *option = VImage::option() + ->set("access", access) + ->set("shrink", jpegShrinkOnLoad) + ->set("unlimited", baton->input->unlimited) + ->set("fail_on", baton->input->failOn); if (baton->input->buffer != nullptr) { // Reload JPEG buffer VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength); @@ -251,8 +232,14 @@ class PipelineWorker : public Napi::AsyncWorker { image = VImage::jpegload(const_cast(baton->input->file.data()), option); } } else if (scale != 1.0) { - vips::VOption *option = GetOptionsForImageType(inputImageType, baton->input)->set("scale", scale); + vips::VOption *option = VImage::option() + ->set("access", access) + ->set("scale", scale) + ->set("fail_on", baton->input->failOn); if (inputImageType == sharp::ImageType::WEBP) { + option->set("n", baton->input->pages); + option->set("page", baton->input->page); + if (baton->input->buffer != nullptr) { // Reload WebP buffer VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength); @@ -263,6 +250,9 @@ class PipelineWorker : public Napi::AsyncWorker { image = VImage::webpload(const_cast(baton->input->file.data()), option); } } else if (inputImageType == sharp::ImageType::SVG) { + option->set("unlimited", baton->input->unlimited); + option->set("dpi", baton->input->density); + if (baton->input->buffer != nullptr) { // Reload SVG buffer VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength); @@ -277,6 +267,10 @@ class PipelineWorker : public Napi::AsyncWorker { throw vips::VError("Input SVG image will exceed 32767x32767 pixel limit when scaled"); } } else if (inputImageType == sharp::ImageType::PDF) { + option->set("n", baton->input->pages); + option->set("page", baton->input->page); + option->set("dpi", baton->input->density); + if (baton->input->buffer != nullptr) { // Reload PDF buffer VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength); @@ -286,6 +280,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Reload PDF file image = VImage::pdfload(const_cast(baton->input->file.data()), option); } + sharp::SetDensity(image, baton->input->density); } } else { @@ -293,9 +288,6 @@ class PipelineWorker : public Napi::AsyncWorker { throw vips::VError("Input SVG image exceeds 32767x32767 pixel limit"); } } - if (baton->input->autoOrient) { - image = sharp::RemoveExifOrientation(image); - } // Any pre-shrinking may already have been done inputWidth = image.width(); @@ -323,24 +315,17 @@ class PipelineWorker : public Napi::AsyncWorker { } // Ensure we're using a device-independent colour space - std::pair inputProfile(nullptr, 0); - if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) && baton->withIccProfile.empty()) { - // Cache input profile for use with output - inputProfile = sharp::GetProfile(image); - baton->input->ignoreIcc = true; - } char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb"; if ( sharp::HasProfile(image) && image.interpretation() != VIPS_INTERPRETATION_LABS && image.interpretation() != VIPS_INTERPRETATION_GREY16 && - baton->colourspacePipeline != VIPS_INTERPRETATION_CMYK && !baton->input->ignoreIcc ) { // Convert to sRGB/P3 using embedded profile try { image = image.icc_transform(processingProfile, VImage::option() - ->set("embedded", true) + ->set("embedded", TRUE) ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8) ->set("intent", VIPS_INTENT_PERCEPTUAL)); } catch(...) { @@ -348,7 +333,7 @@ class PipelineWorker : public Napi::AsyncWorker { } } else if ( image.interpretation() == VIPS_INTERPRETATION_CMYK && - baton->colourspacePipeline != VIPS_INTERPRETATION_CMYK + baton->colourspaceInput != VIPS_INTERPRETATION_CMYK ) { image = image.icc_transform(processingProfile, VImage::option() ->set("input_profile", "cmyk") @@ -356,10 +341,15 @@ class PipelineWorker : public Napi::AsyncWorker { } // Flatten image to remove alpha channel - if (baton->flatten && image.has_alpha()) { + if (baton->flatten && sharp::HasAlpha(image)) { image = sharp::Flatten(image, baton->flattenBackground); } + // Negate the colours in the image + if (baton->negate) { + image = sharp::Negate(image, baton->negateAlpha); + } + // Gamma encoding (darken) if (baton->gamma >= 1 && baton->gamma <= 3) { image = sharp::Gamma(image, 1.0 / baton->gamma); @@ -376,12 +366,12 @@ class PipelineWorker : public Napi::AsyncWorker { bool const shouldSharpen = baton->sharpenSigma != 0.0; bool const shouldComposite = !baton->composite.empty(); - if (shouldComposite && !image.has_alpha()) { + if (shouldComposite && !sharp::HasAlpha(image)) { image = sharp::EnsureAlpha(image, 1); } VipsBandFormat premultiplyFormat = image.format(); - bool const shouldPremultiplyAlpha = image.has_alpha() && + bool const shouldPremultiplyAlpha = sharp::HasAlpha(image) && (shouldResize || shouldBlur || shouldConv || shouldSharpen); if (shouldPremultiplyAlpha) { @@ -395,30 +385,25 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("kernel", baton->kernel)); } - image = sharp::StaySequential(image, + image = sharp::StaySequential(image, access, autoRotation != VIPS_ANGLE_D0 || baton->flip || + autoFlip || rotation != VIPS_ANGLE_D0); // Auto-rotate post-extract if (autoRotation != VIPS_ANGLE_D0) { - if (autoRotation != VIPS_ANGLE_D180) { - MultiPageUnsupported(nPages, "Rotate"); - } image = image.rot(autoRotation); } // Mirror vertically (up-down) about the x-axis - if (baton->flip) { + if (baton->flip || autoFlip) { image = image.flip(VIPS_DIRECTION_VERTICAL); } // Mirror horizontally (left-right) about the y-axis - if (baton->flop != autoFlop) { + if (baton->flop || autoFlop) { image = image.flip(VIPS_DIRECTION_HORIZONTAL); } // Rotate post-extract 90-angle if (rotation != VIPS_ANGLE_D0) { - if (rotation != VIPS_ANGLE_D180) { - MultiPageUnsupported(nPages, "Rotate"); - } image = image.rot(rotation); } @@ -430,7 +415,7 @@ class PipelineWorker : public Napi::AsyncWorker { for (unsigned int i = 0; i < baton->joinChannelIn.size(); i++) { baton->joinChannelIn[i]->access = access; std::tie(joinImage, joinImageType) = sharp::OpenInput(baton->joinChannelIn[i]); - joinImage = sharp::EnsureColourspace(joinImage, baton->colourspacePipeline); + joinImage = sharp::EnsureColourspace(joinImage, baton->colourspaceInput); image = image.bandjoin(joinImage); } image = image.copy(VImage::option()->set("interpretation", baton->colourspace)); @@ -455,10 +440,12 @@ class PipelineWorker : public Napi::AsyncWorker { std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha); // Embed - const auto& [left, top] = sharp::CalculateEmbedPosition( + int left; + int top; + std::tie(left, top) = sharp::CalculateEmbedPosition( inputWidth, inputHeight, baton->width, baton->height, baton->position); - const int width = std::max(inputWidth, baton->width); - const int height = std::max(inputHeight, baton->height); + int width = std::max(inputWidth, baton->width); + int height = std::max(inputHeight, baton->height); image = nPages > 1 ? sharp::EmbedMultiPage(image, @@ -477,10 +464,13 @@ class PipelineWorker : public Napi::AsyncWorker { // Crop if (baton->position < 9) { // Gravity-based crop - const auto& [left, top] = sharp::CalculateCrop( + int left; + int top; + + std::tie(left, top) = sharp::CalculateCrop( inputWidth, inputHeight, baton->width, baton->height, baton->position); - const int width = std::min(inputWidth, baton->width); - const int height = std::min(inputHeight, baton->height); + int width = std::min(inputWidth, baton->width); + int height = std::min(inputHeight, baton->height); image = nPages > 1 ? sharp::CropMultiPage(image, @@ -492,10 +482,12 @@ class PipelineWorker : public Napi::AsyncWorker { // Attention-based or Entropy-based crop MultiPageUnsupported(nPages, "Resize strategy"); - image = sharp::StaySequential(image); + image = sharp::StaySequential(image, access); image = image.smartcrop(baton->width, baton->height, VImage::option() ->set("interesting", baton->position == 16 ? VIPS_INTERESTING_ENTROPY : VIPS_INTERESTING_ATTENTION) +#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 15) ->set("premultiplied", shouldPremultiplyAlpha) +#endif ->set("attention_x", &attention_x) ->set("attention_y", &attention_y)); baton->hasCropOffset = true; @@ -509,9 +501,9 @@ class PipelineWorker : public Napi::AsyncWorker { } // Rotate post-extract non-90 angle - if (!baton->rotateBefore && baton->rotationAngle != 0.0) { + if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) { MultiPageUnsupported(nPages, "Rotate"); - image = sharp::StaySequential(image); + image = sharp::StaySequential(image, access); std::vector background; std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha); image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)); @@ -535,7 +527,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Affine transform if (!baton->affineMatrix.empty()) { MultiPageUnsupported(nPages, "Affine"); - image = sharp::StaySequential(image); + image = sharp::StaySequential(image, access); std::vector background; std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha); vips::VInterpolate interp = vips::VInterpolate::new_from_name( @@ -558,7 +550,6 @@ class PipelineWorker : public Napi::AsyncWorker { std::vector background; std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha); - image = sharp::StaySequential(image, nPages > 1); image = nPages > 1 ? sharp::EmbedMultiPage(image, baton->extendLeft, baton->extendTop, baton->width, baton->height, @@ -567,7 +558,6 @@ class PipelineWorker : public Napi::AsyncWorker { VImage::option()->set("extend", baton->extendWith)->set("background", background)); } else { std::vector ignoredBackground(1); - image = sharp::StaySequential(image); image = nPages > 1 ? sharp::EmbedMultiPage(image, baton->extendLeft, baton->extendTop, baton->width, baton->height, @@ -587,19 +577,9 @@ class PipelineWorker : public Napi::AsyncWorker { image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale); } - // Dilate - must happen before blurring, due to the utility of dilating after thresholding - if (baton->dilateWidth != 0) { - image = sharp::Dilate(image, baton->dilateWidth); - } - - // Erode - must happen before blurring, due to the utility of eroding after thresholding - if (baton->erodeWidth != 0) { - image = sharp::Erode(image, baton->erodeWidth); - } - // Blur if (shouldBlur) { - image = sharp::Blur(image, baton->blurSigma, baton->precision, baton->minAmpl); + image = sharp::Blur(image, baton->blurSigma); } // Unflatten the image @@ -616,7 +596,7 @@ class PipelineWorker : public Napi::AsyncWorker { } // Recomb - if (!baton->recombMatrix.empty()) { + if (baton->recombMatrix != NULL) { image = sharp::Recomb(image, baton->recombMatrix); } @@ -646,25 +626,7 @@ class PipelineWorker : public Napi::AsyncWorker { sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN; composite->input->access = access; std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input); - - if (composite->input->autoOrient) { - // Respect EXIF Orientation - VipsAngle compositeAutoRotation = VIPS_ANGLE_D0; - bool compositeAutoFlop = false; - std::tie(compositeAutoRotation, compositeAutoFlop) = - CalculateExifRotationAndFlop(sharp::ExifOrientation(compositeImage)); - - compositeImage = sharp::RemoveExifOrientation(compositeImage); - compositeImage = sharp::StaySequential(compositeImage, compositeAutoRotation != VIPS_ANGLE_D0); - - if (compositeAutoRotation != VIPS_ANGLE_D0) { - compositeImage = compositeImage.rot(compositeAutoRotation); - } - if (compositeAutoFlop) { - compositeImage = compositeImage.flip(VIPS_DIRECTION_HORIZONTAL); - } - } - + compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspaceInput); // Verify within current dimensions if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) { throw vips::VError("Image to composite must have same dimensions or smaller"); @@ -691,7 +653,7 @@ class PipelineWorker : public Napi::AsyncWorker { if (across != 0 || down != 0) { int left; int top; - compositeImage = sharp::StaySequential(compositeImage).replicate(across, down); + compositeImage = sharp::StaySequential(compositeImage, access).replicate(across, down); if (composite->hasOffset) { std::tie(left, top) = sharp::CalculateCrop( compositeImage.width(), compositeImage.height(), image.width(), image.height(), @@ -705,8 +667,11 @@ class PipelineWorker : public Napi::AsyncWorker { // gravity was used for extract_area, set it back to its default value of 0 composite->gravity = 0; } - // Ensure image to composite is with unpremultiplied alpha - compositeImage = sharp::EnsureAlpha(compositeImage, 1); + // Ensure image to composite is sRGB with unpremultiplied alpha + compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB); + if (!sharp::HasAlpha(compositeImage)) { + compositeImage = sharp::EnsureAlpha(compositeImage, 1); + } if (composite->premultiplied) compositeImage = compositeImage.unpremultiply(); // Calculate position int left; @@ -730,12 +695,7 @@ class PipelineWorker : public Napi::AsyncWorker { xs.push_back(left); ys.push_back(top); } - image = VImage::composite(images, modes, VImage::option() - ->set("compositing_space", baton->colourspacePipeline == VIPS_INTERPRETATION_LAST - ? VIPS_INTERPRETATION_sRGB - : baton->colourspacePipeline) - ->set("x", xs) - ->set("y", ys)); + image = VImage::composite(images, modes, VImage::option()->set("x", xs)->set("y", ys)); image = sharp::RemoveGifPalette(image); } @@ -751,13 +711,13 @@ class PipelineWorker : public Napi::AsyncWorker { // Apply normalisation - stretch luminance to cover full dynamic range if (baton->normalise) { - image = sharp::StaySequential(image); + image = sharp::StaySequential(image, access); image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper); } // Apply contrast limiting adaptive histogram equalization (CLAHE) if (baton->claheWidth != 0 && baton->claheHeight != 0) { - image = sharp::StaySequential(image); + image = sharp::StaySequential(image, access); image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope); } @@ -767,7 +727,7 @@ class PipelineWorker : public Napi::AsyncWorker { sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN; baton->boolean->access = access; std::tie(booleanImage, booleanImageType) = sharp::OpenInput(baton->boolean); - booleanImage = sharp::EnsureColourspace(booleanImage, baton->colourspacePipeline); + booleanImage = sharp::EnsureColourspace(booleanImage, baton->colourspaceInput); image = sharp::Boolean(image, booleanImage, baton->booleanOp); image = sharp::RemoveGifPalette(image); } @@ -778,8 +738,8 @@ class PipelineWorker : public Napi::AsyncWorker { } // Tint the image - if (baton->tint[0] >= 0.0) { - image = sharp::Tint(image, baton->tint); + if (baton->tintA < 128.0 || baton->tintB < 128.0) { + image = sharp::Tint(image, baton->tintA, baton->tintB); } // Remove alpha channel, if any @@ -792,21 +752,26 @@ class PipelineWorker : public Napi::AsyncWorker { image = sharp::EnsureAlpha(image, baton->ensureAlpha); } - // Ensure output colour space + // Convert image to sRGB, if not already if (sharp::Is16Bit(image.interpretation())) { image = image.cast(VIPS_FORMAT_USHORT); } if (image.interpretation() != baton->colourspace) { + // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation())); - if (inputProfile.first != nullptr && baton->withIccProfile.empty()) { - image = sharp::SetProfile(image, inputProfile); + // Transform colours from embedded profile to output profile + if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) { + image = image.icc_transform("srgb", VImage::option() + ->set("embedded", TRUE) + ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8) + ->set("intent", VIPS_INTENT_PERCEPTUAL)); } } // Extract channel if (baton->extractChannel > -1) { if (baton->extractChannel >= image.bands()) { - if (baton->extractChannel == 3 && image.has_alpha()) { + if (baton->extractChannel == 3 && sharp::HasAlpha(image)) { baton->extractChannel = image.bands() - 1; } else { (baton->err) @@ -824,47 +789,31 @@ class PipelineWorker : public Napi::AsyncWorker { } // Apply output ICC profile - if (!baton->withIccProfile.empty()) { - try { - image = image.icc_transform(const_cast(baton->withIccProfile.data()), VImage::option() + if (baton->withMetadata) { + image = image.icc_transform( + baton->withMetadataIcc.empty() ? "srgb" : const_cast(baton->withMetadataIcc.data()), + VImage::option() ->set("input_profile", processingProfile) - ->set("embedded", true) + ->set("embedded", TRUE) ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8) ->set("intent", VIPS_INTENT_PERCEPTUAL)); - } catch(...) { - sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid profile", nullptr); - } } - - // Negate the colours in the image - if (baton->negate) { - image = sharp::Negate(image, baton->negateAlpha); - } - // Override EXIF Orientation tag - if (baton->withMetadataOrientation != -1) { + if (baton->withMetadata && baton->withMetadataOrientation != -1) { image = sharp::SetExifOrientation(image, baton->withMetadataOrientation); } // Override pixel density if (baton->withMetadataDensity > 0) { image = sharp::SetDensity(image, baton->withMetadataDensity); } - // EXIF key/value pairs - if (baton->keepMetadata & VIPS_FOREIGN_KEEP_EXIF) { + // Metadata key/value pairs, e.g. EXIF + if (!baton->withMetadataStrs.empty()) { image = image.copy(); - if (!baton->withExifMerge) { - image = sharp::RemoveExif(image); - } - for (const auto& [key, value] : baton->withExif) { - image.set(key.c_str(), value.c_str()); + for (const auto& s : baton->withMetadataStrs) { + image.set(s.first.data(), s.second.data()); } } - // XMP buffer - if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_XMP) && !baton->withXmp.empty()) { - image = image.copy(); - image.set(VIPS_META_XMP_NAME, nullptr, - const_cast(static_cast(baton->withXmp.c_str())), baton->withXmp.size()); - } + // Number of channels used in output image baton->channels = image.bands(); baton->width = image.width(); @@ -873,11 +822,6 @@ class PipelineWorker : public Napi::AsyncWorker { image = sharp::SetAnimationProperties( image, nPages, targetPageHeight, baton->delay, baton->loop); - if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) { - baton->pageHeightOut = image.get_int(VIPS_META_PAGE_HEIGHT); - baton->pagesOut = image.get_int(VIPS_META_N_PAGES); - } - // Output sharp::SetTimeout(image, baton->timeoutSeconds); if (baton->fileOut.empty()) { @@ -886,7 +830,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JPEG to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG); VipsArea *area = reinterpret_cast(image.jpegsave_buffer(VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->jpegQuality) ->set("interlace", baton->jpegProgressive) ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4" @@ -928,7 +872,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write PNG to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); VipsArea *area = reinterpret_cast(image.pngsave_buffer(VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("interlace", baton->pngProgressive) ->set("compression", baton->pngCompressionLevel) ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE) @@ -947,12 +891,11 @@ class PipelineWorker : public Napi::AsyncWorker { // Write WEBP to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP); VipsArea *area = reinterpret_cast(image.webpsave_buffer(VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->webpQuality) ->set("lossless", baton->webpLossless) ->set("near_lossless", baton->webpNearLossless) ->set("smart_subsample", baton->webpSmartSubsample) - ->set("smart_deblock", baton->webpSmartDeblock) ->set("preset", baton->webpPreset) ->set("effort", baton->webpEffort) ->set("min_size", baton->webpMinSize) @@ -968,14 +911,13 @@ class PipelineWorker : public Napi::AsyncWorker { // Write GIF to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); VipsArea *area = reinterpret_cast(image.gifsave_buffer(VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) ->set("reuse", baton->gifReuse) ->set("interlace", baton->gifProgressive) ->set("interframe_maxerror", baton->gifInterFrameMaxError) ->set("interpalette_maxerror", baton->gifInterPaletteMaxError) - ->set("keep_duplicate_frames", baton->gifKeepDuplicateFrames) ->set("dither", baton->gifDither))); baton->bufferOut = static_cast(area->data); baton->bufferOutLength = area->length; @@ -994,12 +936,10 @@ class PipelineWorker : public Napi::AsyncWorker { image = image.cast(VIPS_FORMAT_FLOAT); } VipsArea *area = reinterpret_cast(image.tiffsave_buffer(VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->tiffQuality) ->set("bitdepth", baton->tiffBitdepth) ->set("compression", baton->tiffCompression) - ->set("bigtiff", baton->tiffBigtiff) - ->set("miniswhite", baton->tiffMiniswhite) ->set("predictor", baton->tiffPredictor) ->set("pyramid", baton->tiffPyramid) ->set("tile", baton->tiffTile) @@ -1017,13 +957,13 @@ class PipelineWorker : public Napi::AsyncWorker { (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) { // Write HEIF to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF); - image = sharp::RemoveAnimationProperties(image); + image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR); VipsArea *area = reinterpret_cast(image.heifsave_buffer(VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->heifQuality) ->set("compression", baton->heifCompression) ->set("effort", baton->heifEffort) - ->set("bitdepth", baton->heifBitdepth) + ->set("bitdepth", 8) ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4" ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON) ->set("lossless", baton->heifLossless))); @@ -1035,10 +975,10 @@ class PipelineWorker : public Napi::AsyncWorker { } else if (baton->formatOut == "dz") { // Write DZ to buffer baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; - if (!image.has_alpha()) { + if (!sharp::HasAlpha(image)) { baton->tileBackground.pop_back(); } - image = sharp::StaySequential(image, baton->tileAngle != 0); + image = sharp::StaySequential(image, access, baton->tileAngle != 0); vips::VOption *options = BuildOptionsDZ(baton); VipsArea *area = reinterpret_cast(image.dzsave_buffer(options)); baton->bufferOut = static_cast(area->data); @@ -1051,7 +991,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JXL to buffer image = sharp::RemoveAnimationProperties(image); VipsArea *area = reinterpret_cast(image.jxlsave_buffer(VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("distance", baton->jxlDistance) ->set("tier", baton->jxlDecodingTier) ->set("effort", baton->jxlEffort) @@ -1084,7 +1024,6 @@ class PipelineWorker : public Napi::AsyncWorker { // Unsupported output format (baton->err).append("Unsupported output format "); if (baton->formatOut == "input") { - (baton->err).append("when trying to match input format of "); (baton->err).append(ImageTypeId(inputImageType)); } else { (baton->err).append(baton->formatOut); @@ -1113,7 +1052,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JPEG to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG); image.jpegsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->jpegQuality) ->set("interlace", baton->jpegProgressive) ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4" @@ -1143,7 +1082,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write PNG to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); image.pngsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("interlace", baton->pngProgressive) ->set("compression", baton->pngCompressionLevel) ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE) @@ -1158,12 +1097,11 @@ class PipelineWorker : public Napi::AsyncWorker { // Write WEBP to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP); image.webpsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->webpQuality) ->set("lossless", baton->webpLossless) ->set("near_lossless", baton->webpNearLossless) ->set("smart_subsample", baton->webpSmartSubsample) - ->set("smart_deblock", baton->webpSmartDeblock) ->set("preset", baton->webpPreset) ->set("effort", baton->webpEffort) ->set("min_size", baton->webpMinSize) @@ -1175,14 +1113,11 @@ class PipelineWorker : public Napi::AsyncWorker { // Write GIF to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); image.gifsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) ->set("reuse", baton->gifReuse) ->set("interlace", baton->gifProgressive) - ->set("interframe_maxerror", baton->gifInterFrameMaxError) - ->set("interpalette_maxerror", baton->gifInterPaletteMaxError) - ->set("keep_duplicate_frames", baton->gifKeepDuplicateFrames) ->set("dither", baton->gifDither)); baton->formatOut = "gif"; } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) || @@ -1197,12 +1132,10 @@ class PipelineWorker : public Napi::AsyncWorker { image = image.cast(VIPS_FORMAT_FLOAT); } image.tiffsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->tiffQuality) ->set("bitdepth", baton->tiffBitdepth) ->set("compression", baton->tiffCompression) - ->set("bigtiff", baton->tiffBigtiff) - ->set("miniswhite", baton->tiffMiniswhite) ->set("predictor", baton->tiffPredictor) ->set("pyramid", baton->tiffPyramid) ->set("tile", baton->tiffTile) @@ -1216,13 +1149,13 @@ class PipelineWorker : public Napi::AsyncWorker { (willMatchInput && inputImageType == sharp::ImageType::HEIF)) { // Write HEIF to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF); - image = sharp::RemoveAnimationProperties(image); + image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR); image.heifsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("Q", baton->heifQuality) ->set("compression", baton->heifCompression) ->set("effort", baton->heifEffort) - ->set("bitdepth", baton->heifBitdepth) + ->set("bitdepth", 8) ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4" ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON) ->set("lossless", baton->heifLossless)); @@ -1232,7 +1165,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JXL to file image = sharp::RemoveAnimationProperties(image); image.jxlsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("distance", baton->jxlDistance) ->set("tier", baton->jxlDecodingTier) ->set("effort", baton->jxlEffort) @@ -1243,10 +1176,10 @@ class PipelineWorker : public Napi::AsyncWorker { if (isDzZip) { baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; } - if (!image.has_alpha()) { + if (!sharp::HasAlpha(image)) { baton->tileBackground.pop_back(); } - image = sharp::StaySequential(image, baton->tileAngle != 0); + image = sharp::StaySequential(image, access, baton->tileAngle != 0); vips::VOption *options = BuildOptionsDZ(baton); image.dzsave(const_cast(baton->fileOut.data()), options); baton->formatOut = "dz"; @@ -1254,7 +1187,7 @@ class PipelineWorker : public Napi::AsyncWorker { (willMatchInput && inputImageType == sharp::ImageType::VIPS)) { // Write V to file image.vipssave(const_cast(baton->fileOut.data()), VImage::option() - ->set("keep", baton->keepMetadata)); + ->set("strip", !baton->withMetadata)); baton->formatOut = "v"; } else { // Unsupported output format @@ -1267,12 +1200,7 @@ class PipelineWorker : public Napi::AsyncWorker { if (what && what[0]) { (baton->err).append(what); } else { - if (baton->input->failOn == VIPS_FAIL_ON_WARNING) { - (baton->err).append("Warning treated as error due to failOn setting"); - baton->errUseWarning = true; - } else { - (baton->err).append("Unknown error"); - } + (baton->err).append("Unknown error"); } } // Clean up libvips' per-request data and threads @@ -1287,11 +1215,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Handle warnings std::string warning = sharp::VipsWarningPop(); while (!warning.empty()) { - if (baton->errUseWarning) { - (baton->err).append("\n").append(warning); - } else { - debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) }); - } + debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) }); warning = sharp::VipsWarningPop(); } @@ -1324,17 +1248,14 @@ class PipelineWorker : public Napi::AsyncWorker { info.Set("attentionX", static_cast(baton->attentionX)); info.Set("attentionY", static_cast(baton->attentionY)); } - if (baton->trimThreshold >= 0.0) { + if (baton->trimThreshold > 0.0) { info.Set("trimOffsetLeft", static_cast(baton->trimOffsetLeft)); info.Set("trimOffsetTop", static_cast(baton->trimOffsetTop)); } + if (baton->input->textAutofitDpi) { info.Set("textAutofitDpi", static_cast(baton->input->textAutofitDpi)); } - if (baton->pageHeightOut) { - info.Set("pageHeight", static_cast(baton->pageHeightOut)); - info.Set("pages", static_cast(baton->pagesOut)); - } if (baton->bufferOutLength > 0) { // Add buffer size to info @@ -1342,20 +1263,17 @@ class PipelineWorker : public Napi::AsyncWorker { // Pass ownership of output data to Buffer instance Napi::Buffer data = Napi::Buffer::NewOrCopy(env, static_cast(baton->bufferOut), baton->bufferOutLength, sharp::FreeCallback); - Callback().Call(Receiver().Value(), { env.Null(), data, info }); + Callback().MakeCallback(Receiver().Value(), { env.Null(), data, info }); } else { // Add file size to info - if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) { - try { - uint32_t const size = static_cast( - std::filesystem::file_size(std::filesystem::u8path(baton->fileOut))); - info.Set("size", size); - } catch (...) {} + struct STAT64_STRUCT st; + if (STAT64_FUNCTION(baton->fileOut.data(), &st) == 0) { + info.Set("size", static_cast(st.st_size)); } - Callback().Call(Receiver().Value(), { env.Null(), info }); + Callback().MakeCallback(Receiver().Value(), { env.Null(), info }); } } else { - Callback().Call(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); + Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); } // Delete baton @@ -1368,15 +1286,12 @@ class PipelineWorker : public Napi::AsyncWorker { for (sharp::InputDescriptor *input : baton->joinChannelIn) { delete input; } - for (sharp::InputDescriptor *input : baton->join) { - delete input; - } delete baton; // Decrement processing task counter - sharp::counterProcess--; - Napi::Number queueLength = Napi::Number::New(env, static_cast(sharp::counterQueue)); - queueListener.Call(Receiver().Value(), { queueLength }); + g_atomic_int_dec_and_test(&sharp::counterProcess); + Napi::Number queueLength = Napi::Number::New(env, static_cast(sharp::counterQueue)); + queueListener.MakeCallback(Receiver().Value(), { queueLength }); } private: @@ -1394,20 +1309,21 @@ class PipelineWorker : public Napi::AsyncWorker { Calculate the angle of rotation and need-to-flip for the given Exif orientation By default, returns zero, i.e. no rotation. */ - std::tuple - CalculateExifRotationAndFlop(int const exifOrientation) { + std::tuple + CalculateExifRotationAndFlip(int const exifOrientation) { VipsAngle rotate = VIPS_ANGLE_D0; - bool flop = false; + bool flip = FALSE; + bool flop = FALSE; switch (exifOrientation) { case 6: rotate = VIPS_ANGLE_D90; break; case 3: rotate = VIPS_ANGLE_D180; break; case 8: rotate = VIPS_ANGLE_D270; break; - case 2: flop = true; break; - case 7: flop = true; rotate = VIPS_ANGLE_D270; break; - case 4: flop = true; rotate = VIPS_ANGLE_D180; break; - case 5: flop = true; rotate = VIPS_ANGLE_D90; break; + case 2: flop = TRUE; break; // flop 1 + case 7: flip = TRUE; rotate = VIPS_ANGLE_D90; break; // flip 6 + case 4: flop = TRUE; rotate = VIPS_ANGLE_D180; break; // flop 3 + case 5: flip = TRUE; rotate = VIPS_ANGLE_D270; break; // flip 8 } - return std::make_tuple(rotate, flop); + return std::make_tuple(rotate, flip, flop); } /* @@ -1435,11 +1351,11 @@ class PipelineWorker : public Napi::AsyncWorker { std::string AssembleSuffixString(std::string extname, std::vector> options) { std::string argument; - for (const auto& [key, value] : options) { + for (auto const &option : options) { if (!argument.empty()) { argument += ","; } - argument += key + "=" + value; + argument += option.first + "=" + option.second; } return extname + "[" + argument + "]"; } @@ -1453,7 +1369,7 @@ class PipelineWorker : public Napi::AsyncWorker { std::string suffix; if (baton->tileFormat == "png") { std::vector> options { - {"interlace", baton->pngProgressive ? "true" : "false"}, + {"interlace", baton->pngProgressive ? "TRUE" : "FALSE"}, {"compression", std::to_string(baton->pngCompressionLevel)}, {"filter", baton->pngAdaptiveFiltering ? "all" : "none"} }; @@ -1462,32 +1378,31 @@ class PipelineWorker : public Napi::AsyncWorker { std::vector> options { {"Q", std::to_string(baton->webpQuality)}, {"alpha_q", std::to_string(baton->webpAlphaQuality)}, - {"lossless", baton->webpLossless ? "true" : "false"}, - {"near_lossless", baton->webpNearLossless ? "true" : "false"}, - {"smart_subsample", baton->webpSmartSubsample ? "true" : "false"}, - {"smart_deblock", baton->webpSmartDeblock ? "true" : "false"}, + {"lossless", baton->webpLossless ? "TRUE" : "FALSE"}, + {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"}, + {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"}, {"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)}, - {"min_size", baton->webpMinSize ? "true" : "false"}, - {"mixed", baton->webpMixed ? "true" : "false"}, + {"min_size", baton->webpMinSize ? "TRUE" : "FALSE"}, + {"mixed", baton->webpMixed ? "TRUE" : "FALSE"}, {"effort", std::to_string(baton->webpEffort)} }; suffix = AssembleSuffixString(".webp", options); } else { std::vector> options { {"Q", std::to_string(baton->jpegQuality)}, - {"interlace", baton->jpegProgressive ? "true" : "false"}, + {"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"}, {"subsample_mode", baton->jpegChromaSubsampling == "4:4:4" ? "off" : "on"}, - {"trellis_quant", baton->jpegTrellisQuantisation ? "true" : "false"}, + {"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"}, {"quant_table", std::to_string(baton->jpegQuantisationTable)}, - {"overshoot_deringing", baton->jpegOvershootDeringing ? "true": "false"}, - {"optimize_scans", baton->jpegOptimiseScans ? "true": "false"}, - {"optimize_coding", baton->jpegOptimiseCoding ? "true": "false"} + {"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"}, + {"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"}, + {"optimize_coding", baton->jpegOptimiseCoding ? "TRUE": "FALSE"} }; std::string extname = baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_DZ ? ".jpeg" : ".jpg"; suffix = AssembleSuffixString(extname, options); } vips::VOption *options = VImage::option() - ->set("keep", baton->keepMetadata) + ->set("strip", !baton->withMetadata) ->set("tile_size", baton->tileSize) ->set("overlap", baton->tileOverlap) ->set("container", baton->tileContainer) @@ -1527,14 +1442,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { // Input baton->input = sharp::CreateInputDescriptor(options.Get("input").As()); - // Join images together - if (sharp::HasAttr(options, "join")) { - Napi::Array join = options.Get("join").As(); - for (unsigned int i = 0; i < join.Length(); i++) { - baton->join.push_back( - sharp::CreateInputDescriptor(join.Get(i).As())); - } - } // Extract image options baton->topOffsetPre = sharp::AttrAsInt32(options, "topOffsetPre"); baton->leftOffsetPre = sharp::AttrAsInt32(options, "leftOffsetPre"); @@ -1597,8 +1504,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->negate = sharp::AttrAsBool(options, "negate"); baton->negateAlpha = sharp::AttrAsBool(options, "negateAlpha"); baton->blurSigma = sharp::AttrAsDouble(options, "blurSigma"); - baton->precision = sharp::AttrAsEnum(options, "precision", VIPS_TYPE_PRECISION); - baton->minAmpl = sharp::AttrAsDouble(options, "minAmpl"); baton->brightness = sharp::AttrAsDouble(options, "brightness"); baton->saturation = sharp::AttrAsDouble(options, "saturation"); baton->hue = sharp::AttrAsInt32(options, "hue"); @@ -1614,26 +1519,24 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale"); baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground"); baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold"); - baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt"); baton->gamma = sharp::AttrAsDouble(options, "gamma"); baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut"); baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA"); baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB"); - baton->dilateWidth = sharp::AttrAsUint32(options, "dilateWidth"); - baton->erodeWidth = sharp::AttrAsUint32(options, "erodeWidth"); baton->greyscale = sharp::AttrAsBool(options, "greyscale"); baton->normalise = sharp::AttrAsBool(options, "normalise"); baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower"); baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper"); - baton->tint = sharp::AttrAsVectorOfDouble(options, "tint"); + baton->tintA = sharp::AttrAsDouble(options, "tintA"); + baton->tintB = sharp::AttrAsDouble(options, "tintB"); baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth"); baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight"); baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope"); + baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation"); baton->angle = sharp::AttrAsInt32(options, "angle"); baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle"); baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground"); - baton->rotateBefore = sharp::AttrAsBool(options, "rotateBefore"); - baton->orientBefore = sharp::AttrAsBool(options, "orientBefore"); + baton->rotateBeforePreExtract = sharp::AttrAsBool(options, "rotateBeforePreExtract"); baton->flip = sharp::AttrAsBool(options, "flip"); baton->flop = sharp::AttrAsBool(options, "flop"); baton->extendTop = sharp::AttrAsInt32(options, "extendTop"); @@ -1666,24 +1569,23 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->convKernelScale = sharp::AttrAsDouble(kernel, "scale"); baton->convKernelOffset = sharp::AttrAsDouble(kernel, "offset"); size_t const kernelSize = static_cast(baton->convKernelWidth * baton->convKernelHeight); - baton->convKernel.resize(kernelSize); + baton->convKernel = std::unique_ptr(new double[kernelSize]); Napi::Array kdata = kernel.Get("kernel").As(); for (unsigned int i = 0; i < kernelSize; i++) { baton->convKernel[i] = sharp::AttrAsDouble(kdata, i); } } if (options.Has("recombMatrix")) { + baton->recombMatrix = std::unique_ptr(new double[9]); Napi::Array recombMatrix = options.Get("recombMatrix").As(); - unsigned int matrixElements = recombMatrix.Length(); - baton->recombMatrix.resize(matrixElements); - for (unsigned int i = 0; i < matrixElements; i++) { - baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i); + for (unsigned int i = 0; i < 9; i++) { + baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i); } } - baton->colourspacePipeline = sharp::AttrAsEnum( - options, "colourspacePipeline", VIPS_TYPE_INTERPRETATION); - if (baton->colourspacePipeline == VIPS_INTERPRETATION_ERROR) { - baton->colourspacePipeline = VIPS_INTERPRETATION_LAST; + baton->colourspaceInput = sharp::AttrAsEnum( + options, "colourspaceInput", VIPS_TYPE_INTERPRETATION); + if (baton->colourspaceInput == VIPS_INTERPRETATION_ERROR) { + baton->colourspaceInput = VIPS_INTERPRETATION_LAST; } baton->colourspace = sharp::AttrAsEnum(options, "colourspace", VIPS_TYPE_INTERPRETATION); if (baton->colourspace == VIPS_INTERPRETATION_ERROR) { @@ -1692,23 +1594,19 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { // Output baton->formatOut = sharp::AttrAsStr(options, "formatOut"); baton->fileOut = sharp::AttrAsStr(options, "fileOut"); - baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata"); + baton->withMetadata = sharp::AttrAsBool(options, "withMetadata"); baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation"); baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity"); - baton->withIccProfile = sharp::AttrAsStr(options, "withIccProfile"); - Napi::Object withExif = options.Get("withExif").As(); - Napi::Array withExifKeys = withExif.GetPropertyNames(); - for (unsigned int i = 0; i < withExifKeys.Length(); i++) { - std::string k = sharp::AttrAsStr(withExifKeys, i); - if (withExif.HasOwnProperty(k)) { - baton->withExif.insert(std::make_pair(k, sharp::AttrAsStr(withExif, k))); + baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc"); + Napi::Object mdStrs = options.Get("withMetadataStrs").As(); + Napi::Array mdStrKeys = mdStrs.GetPropertyNames(); + for (unsigned int i = 0; i < mdStrKeys.Length(); i++) { + std::string k = sharp::AttrAsStr(mdStrKeys, i); + if (mdStrs.HasOwnProperty(k)) { + baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k))); } } - baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge"); - baton->withXmp = sharp::AttrAsStr(options, "withXmp"); baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds"); - baton->loop = sharp::AttrAsUint32(options, "loop"); - baton->delay = sharp::AttrAsInt32Vector(options, "delay"); // Format-specific baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality"); baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive"); @@ -1736,7 +1634,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->webpLossless = sharp::AttrAsBool(options, "webpLossless"); baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless"); baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample"); - baton->webpSmartDeblock = sharp::AttrAsBool(options, "webpSmartDeblock"); baton->webpPreset = sharp::AttrAsEnum(options, "webpPreset", VIPS_TYPE_FOREIGN_WEBP_PRESET); baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort"); baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize"); @@ -1746,13 +1643,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->gifDither = sharp::AttrAsDouble(options, "gifDither"); baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError"); baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError"); - baton->gifKeepDuplicateFrames = sharp::AttrAsBool(options, "gifKeepDuplicateFrames"); baton->gifReuse = sharp::AttrAsBool(options, "gifReuse"); baton->gifProgressive = sharp::AttrAsBool(options, "gifProgressive"); baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality"); - baton->tiffBigtiff = sharp::AttrAsBool(options, "tiffBigtiff"); baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid"); - baton->tiffMiniswhite = sharp::AttrAsBool(options, "tiffMiniswhite"); baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth"); baton->tiffTile = sharp::AttrAsBool(options, "tiffTile"); baton->tiffTileWidth = sharp::AttrAsUint32(options, "tiffTileWidth"); @@ -1774,12 +1668,18 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { options, "heifCompression", VIPS_TYPE_FOREIGN_HEIF_COMPRESSION); baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort"); baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling"); - baton->heifBitdepth = sharp::AttrAsUint32(options, "heifBitdepth"); baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance"); baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier"); baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort"); baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless"); baton->rawDepth = sharp::AttrAsEnum(options, "rawDepth", VIPS_TYPE_BAND_FORMAT); + // Animated output properties + if (sharp::HasAttr(options, "loop")) { + baton->loop = sharp::AttrAsUint32(options, "loop"); + } + if (sharp::HasAttr(options, "delay")) { + baton->delay = sharp::AttrAsInt32Vector(options, "delay"); + } baton->tileSize = sharp::AttrAsUint32(options, "tileSize"); baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap"); baton->tileAngle = sharp::AttrAsInt32(options, "tileAngle"); @@ -1807,8 +1707,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { worker->Queue(); // Increment queued task counter - Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast(++sharp::counterQueue)); - queueListener.Call(info.This(), { queueLength }); + g_atomic_int_inc(&sharp::counterQueue); + Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast(sharp::counterQueue)); + queueListener.MakeCallback(info.This(), { queueLength }); return info.Env().Undefined(); } diff --git a/backend/node_modules/sharp/src/pipeline.h b/backend/node_modules/sharp/src/pipeline.h index ff946598..02c97c66 100644 --- a/backend/node_modules/sharp/src/pipeline.h +++ b/backend/node_modules/sharp/src/pipeline.h @@ -1,15 +1,13 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #ifndef SRC_PIPELINE_H_ #define SRC_PIPELINE_H_ #include #include -#include #include +#include #include #include @@ -41,13 +39,10 @@ struct Composite { struct PipelineBaton { sharp::InputDescriptor *input; - std::vector join; std::string formatOut; std::string fileOut; void *bufferOut; size_t bufferOutLength; - int pageHeightOut; - int pagesOut; std::vector composite; std::vector joinChannelIn; int topOffsetPre; @@ -74,15 +69,14 @@ struct PipelineBaton { bool premultiplied; bool tileCentre; bool fastShrinkOnLoad; - std::vector tint; + double tintA; + double tintB; bool flatten; std::vector flattenBackground; bool unflatten; bool negate; bool negateAlpha; double blurSigma; - VipsPrecision precision; - double minAmpl; double brightness; double saturation; int hue; @@ -98,13 +92,10 @@ struct PipelineBaton { bool thresholdGrayscale; std::vector trimBackground; double trimThreshold; - bool trimLineArt; int trimOffsetLeft; int trimOffsetTop; std::vector linearA; std::vector linearB; - int dilateWidth; - int erodeWidth; double gamma; double gammaOut; bool greyscale; @@ -114,11 +105,11 @@ struct PipelineBaton { int claheWidth; int claheHeight; int claheMaxSlope; + bool useExifOrientation; int angle; double rotationAngle; std::vector rotationBackground; - bool rotateBefore; - bool orientBefore; + bool rotateBeforePreExtract; bool flip; bool flop; int extendTop; @@ -162,7 +153,6 @@ struct PipelineBaton { bool webpNearLossless; bool webpLossless; bool webpSmartSubsample; - bool webpSmartDeblock; VipsForeignWebpPreset webpPreset; int webpEffort; bool webpMinSize; @@ -172,16 +162,13 @@ struct PipelineBaton { double gifDither; double gifInterFrameMaxError; double gifInterPaletteMaxError; - bool gifKeepDuplicateFrames; bool gifReuse; bool gifProgressive; int tiffQuality; VipsForeignTiffCompression tiffCompression; - bool tiffBigtiff; VipsForeignTiffPredictor tiffPredictor; bool tiffPyramid; int tiffBitdepth; - bool tiffMiniswhite; bool tiffTile; int tiffTileHeight; int tiffTileWidth; @@ -193,23 +180,19 @@ struct PipelineBaton { int heifEffort; std::string heifChromaSubsampling; bool heifLossless; - int heifBitdepth; double jxlDistance; int jxlDecodingTier; int jxlEffort; bool jxlLossless; VipsBandFormat rawDepth; std::string err; - bool errUseWarning; - int keepMetadata; + bool withMetadata; int withMetadataOrientation; double withMetadataDensity; - std::string withIccProfile; - std::unordered_map withExif; - bool withExifMerge; - std::string withXmp; + std::string withMetadataIcc; + std::unordered_map withMetadataStrs; int timeoutSeconds; - std::vector convKernel; + std::unique_ptr convKernel; int convKernelWidth; int convKernelHeight; double convKernelScale; @@ -220,7 +203,7 @@ struct PipelineBaton { int extractChannel; bool removeAlpha; double ensureAlpha; - VipsInterpretation colourspacePipeline; + VipsInterpretation colourspaceInput; VipsInterpretation colourspace; std::vector delay; int loop; @@ -235,13 +218,11 @@ struct PipelineBaton { VipsForeignDzDepth tileDepth; std::string tileId; std::string tileBasename; - std::vector recombMatrix; + std::unique_ptr recombMatrix; PipelineBaton(): input(nullptr), bufferOutLength(0), - pageHeightOut(0), - pagesOut(0), topOffsetPre(-1), topOffsetPost(-1), channels(0), @@ -256,7 +237,8 @@ struct PipelineBaton { attentionX(0), attentionY(0), premultiplied(false), - tint{ -1.0, 0.0, 0.0, 0.0 }, + tintA(128.0), + tintB(128.0), flatten(false), flattenBackground{ 0.0, 0.0, 0.0 }, unflatten(false), @@ -277,14 +259,11 @@ struct PipelineBaton { threshold(0), thresholdGrayscale(true), trimBackground{}, - trimThreshold(-1.0), - trimLineArt(false), + trimThreshold(0.0), trimOffsetLeft(0), trimOffsetTop(0), linearA{}, linearB{}, - dilateWidth(0), - erodeWidth(0), gamma(0.0), greyscale(false), normalise(false), @@ -293,6 +272,7 @@ struct PipelineBaton { claheWidth(0), claheHeight(0), claheMaxSlope(3), + useExifOrientation(false), angle(0), rotationAngle(0.0), rotationBackground{ 0.0, 0.0, 0.0, 255.0 }, @@ -339,7 +319,6 @@ struct PipelineBaton { webpNearLossless(false), webpLossless(false), webpSmartSubsample(false), - webpSmartDeblock(false), webpPreset(VIPS_FOREIGN_WEBP_PRESET_DEFAULT), webpEffort(4), webpMinSize(false), @@ -349,16 +328,13 @@ struct PipelineBaton { gifDither(1.0), gifInterFrameMaxError(0.0), gifInterPaletteMaxError(3.0), - gifKeepDuplicateFrames(false), gifReuse(true), gifProgressive(false), tiffQuality(80), tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG), - tiffBigtiff(false), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL), tiffPyramid(false), tiffBitdepth(8), - tiffMiniswhite(false), tiffTile(false), tiffTileHeight(256), tiffTileWidth(256), @@ -370,17 +346,14 @@ struct PipelineBaton { heifEffort(4), heifChromaSubsampling("4:4:4"), heifLossless(false), - heifBitdepth(8), jxlDistance(1.0), jxlDecodingTier(0), jxlEffort(7), jxlLossless(false), rawDepth(VIPS_FORMAT_UCHAR), - errUseWarning(false), - keepMetadata(0), + withMetadata(false), withMetadataOrientation(-1), withMetadataDensity(0.0), - withExifMerge(true), timeoutSeconds(0), convKernelWidth(0), convKernelHeight(0), @@ -392,7 +365,7 @@ struct PipelineBaton { extractChannel(-1), removeAlpha(false), ensureAlpha(-1.0), - colourspacePipeline(VIPS_INTERPRETATION_LAST), + colourspaceInput(VIPS_INTERPRETATION_LAST), colourspace(VIPS_INTERPRETATION_LAST), loop(-1), tileSize(256), diff --git a/backend/node_modules/sharp/src/sharp.cc b/backend/node_modules/sharp/src/sharp.cc index 7678975c..77e16b45 100644 --- a/backend/node_modules/sharp/src/sharp.cc +++ b/backend/node_modules/sharp/src/sharp.cc @@ -1,24 +1,23 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ - -#include +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #include #include -#include "./common.h" -#include "./metadata.h" -#include "./pipeline.h" -#include "./stats.h" -#include "./utilities.h" +#include "common.h" +#include "metadata.h" +#include "pipeline.h" +#include "utilities.h" +#include "stats.h" + +static void* sharp_vips_init(void*) { + vips_init("sharp"); + return nullptr; +} Napi::Object init(Napi::Env env, Napi::Object exports) { - static std::once_flag sharp_vips_init_once; - std::call_once(sharp_vips_init_once, []() { - vips_init("sharp"); - }); + static GOnce sharp_vips_init_once = G_ONCE_INIT; + g_once(&sharp_vips_init_once, static_cast(sharp_vips_init), nullptr); g_log_set_handler("VIPS", static_cast(G_LOG_LEVEL_WARNING), static_cast(sharp::VipsWarningCallback), nullptr); @@ -35,7 +34,6 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { exports.Set("block", Napi::Function::New(env, block)); exports.Set("_maxColourDistance", Napi::Function::New(env, _maxColourDistance)); exports.Set("_isUsingJemalloc", Napi::Function::New(env, _isUsingJemalloc)); - exports.Set("_isUsingX64V2", Napi::Function::New(env, _isUsingX64V2)); exports.Set("stats", Napi::Function::New(env, stats)); return exports; } diff --git a/backend/node_modules/sharp/src/stats.cc b/backend/node_modules/sharp/src/stats.cc index b1fd27a7..c6c0ccc0 100644 --- a/backend/node_modules/sharp/src/stats.cc +++ b/backend/node_modules/sharp/src/stats.cc @@ -1,18 +1,15 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 -#include #include -#include #include +#include #include #include -#include "./common.h" -#include "./stats.h" +#include "common.h" +#include "stats.h" class StatsWorker : public Napi::AsyncWorker { public: @@ -33,7 +30,7 @@ class StatsWorker : public Napi::AsyncWorker { void Execute() { // Decrement queued task counter - sharp::counterQueue--; + g_atomic_int_dec_and_test(&sharp::counterQueue); vips::VImage image; sharp::ImageType imageType = sharp::ImageType::UNKNOWN; @@ -61,9 +58,9 @@ class StatsWorker : public Napi::AsyncWorker { baton->channelStats.push_back(cStats); } // Image is not opaque when alpha layer is present and contains a non-mamixa value - if (image.has_alpha()) { + if (sharp::HasAlpha(image)) { double const minAlpha = static_cast(stats.getpoint(STAT_MIN_INDEX, bands).front()); - if (minAlpha != vips_interpretation_max_alpha(image.interpretation())) { + if (minAlpha != sharp::MaximumImageAlpha(image.interpretation())) { baton->isOpaque = false; } } @@ -109,7 +106,7 @@ class StatsWorker : public Napi::AsyncWorker { // Handle warnings std::string warning = sharp::VipsWarningPop(); while (!warning.empty()) { - debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) }); + debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) }); warning = sharp::VipsWarningPop(); } @@ -144,9 +141,9 @@ class StatsWorker : public Napi::AsyncWorker { dominant.Set("g", baton->dominantGreen); dominant.Set("b", baton->dominantBlue); info.Set("dominant", dominant); - Callback().Call(Receiver().Value(), { env.Null(), info }); + Callback().MakeCallback(Receiver().Value(), { env.Null(), info }); } else { - Callback().Call(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); + Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); } delete baton->input; @@ -180,7 +177,7 @@ Napi::Value stats(const Napi::CallbackInfo& info) { worker->Queue(); // Increment queued task counter - sharp::counterQueue++; + g_atomic_int_inc(&sharp::counterQueue); return info.Env().Undefined(); } diff --git a/backend/node_modules/sharp/src/stats.h b/backend/node_modules/sharp/src/stats.h index 88e13c60..c80e65fa 100644 --- a/backend/node_modules/sharp/src/stats.h +++ b/backend/node_modules/sharp/src/stats.h @@ -1,13 +1,10 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #ifndef SRC_STATS_H_ #define SRC_STATS_H_ #include -#include #include #include "./common.h" @@ -27,7 +24,7 @@ struct ChannelStats { ChannelStats(int minVal, int maxVal, double sumVal, double squaresSumVal, double meanVal, double stdevVal, int minXVal, int minYVal, int maxXVal, int maxYVal): - min(minVal), max(maxVal), sum(sumVal), squaresSum(squaresSumVal), // NOLINT(build/include_what_you_use) + min(minVal), max(maxVal), sum(sumVal), squaresSum(squaresSumVal), mean(meanVal), stdev(stdevVal), minX(minXVal), minY(minYVal), maxX(maxXVal), maxY(maxYVal) {} }; diff --git a/backend/node_modules/sharp/src/utilities.cc b/backend/node_modules/sharp/src/utilities.cc index 4154c08a..2ff4e53b 100644 --- a/backend/node_modules/sharp/src/utilities.cc +++ b/backend/node_modules/sharp/src/utilities.cc @@ -1,19 +1,16 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #include -#include #include #include #include #include -#include "./common.h" -#include "./operations.h" -#include "./utilities.h" +#include "common.h" +#include "operations.h" +#include "utilities.h" /* Get and set cache limits @@ -73,8 +70,8 @@ Napi::Value concurrency(const Napi::CallbackInfo& info) { */ Napi::Value counters(const Napi::CallbackInfo& info) { Napi::Object counters = Napi::Object::New(info.Env()); - counters.Set("queue", static_cast(sharp::counterQueue)); - counters.Set("process", static_cast(sharp::counterProcess)); + counters.Set("queue", sharp::counterQueue); + counters.Set("process", sharp::counterProcess); return counters; } @@ -94,23 +91,9 @@ Napi::Value simd(const Napi::CallbackInfo& info) { Get libvips version */ Napi::Value libvipsVersion(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - Napi::Object version = Napi::Object::New(env); - - char semver[9]; - std::snprintf(semver, sizeof(semver), "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2)); - version.Set("semver", Napi::String::New(env, semver)); -#ifdef SHARP_USE_GLOBAL_LIBVIPS - version.Set("isGlobal", Napi::Boolean::New(env, true)); -#else - version.Set("isGlobal", Napi::Boolean::New(env, false)); -#endif -#ifdef __EMSCRIPTEN__ - version.Set("isWasm", Napi::Boolean::New(env, true)); -#else - version.Set("isWasm", Napi::Boolean::New(env, false)); -#endif - return version; + char version[9]; + g_snprintf(version, sizeof(version), "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2)); + return Napi::String::New(info.Env(), version); } /* @@ -121,7 +104,7 @@ Napi::Value format(const Napi::CallbackInfo& info) { Napi::Object format = Napi::Object::New(env); for (std::string const f : { "jpeg", "png", "webp", "tiff", "magick", "openslide", "dz", - "ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k", "jxl", "rad", "dcraw" + "ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k", "jxl" }) { // Input const VipsObjectClass *oc = vips_class_find("VipsOperation", (f + "load").c_str()); @@ -235,10 +218,10 @@ Napi::Value _maxColourDistance(const Napi::CallbackInfo& info) { double maxColourDistance; try { // Premultiply and remove alpha - if (image1.has_alpha()) { + if (sharp::HasAlpha(image1)) { image1 = image1.premultiply().extract_band(1, VImage::option()->set("n", image1.bands() - 1)); } - if (image2.has_alpha()) { + if (sharp::HasAlpha(image2)) { image2 = image2.premultiply().extract_band(1, VImage::option()->set("n", image2.bands() - 1)); } // Calculate colour distance @@ -269,20 +252,3 @@ Napi::Value _isUsingJemalloc(const Napi::CallbackInfo& info) { return Napi::Boolean::New(env, false); } #endif - -#if defined(__GNUC__) && defined(__x86_64__) -// Are SSE 4.2 intrinsics available at runtime? -Napi::Value _isUsingX64V2(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - unsigned int eax, ebx, ecx, edx; - __asm__ __volatile__("cpuid" - : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) - : "a"(1)); - return Napi::Boolean::New(env, (ecx & 1U << 20) != 0); -} -#else -Napi::Value _isUsingX64V2(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - return Napi::Boolean::New(env, false); -} -#endif diff --git a/backend/node_modules/sharp/src/utilities.h b/backend/node_modules/sharp/src/utilities.h index a1719fa2..0f499ad3 100644 --- a/backend/node_modules/sharp/src/utilities.h +++ b/backend/node_modules/sharp/src/utilities.h @@ -1,7 +1,5 @@ -/*! - Copyright 2013 Lovell Fuller and others. - SPDX-License-Identifier: Apache-2.0 -*/ +// Copyright 2013 Lovell Fuller and others. +// SPDX-License-Identifier: Apache-2.0 #ifndef SRC_UTILITIES_H_ #define SRC_UTILITIES_H_ @@ -17,6 +15,5 @@ Napi::Value format(const Napi::CallbackInfo& info); void block(const Napi::CallbackInfo& info); Napi::Value _maxColourDistance(const Napi::CallbackInfo& info); Napi::Value _isUsingJemalloc(const Napi::CallbackInfo& info); -Napi::Value _isUsingX64V2(const Napi::CallbackInfo& info); #endif // SRC_UTILITIES_H_ diff --git a/backend/node_modules/@img/sharp-libvips-linux-x64/README.md b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/THIRD-PARTY-NOTICES.md similarity index 89% rename from backend/node_modules/@img/sharp-libvips-linux-x64/README.md rename to backend/node_modules/sharp/vendor/8.14.5/linux-x64/THIRD-PARTY-NOTICES.md index 9b2ce937..eed1033b 100644 --- a/backend/node_modules/@img/sharp-libvips-linux-x64/README.md +++ b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/THIRD-PARTY-NOTICES.md @@ -1,8 +1,4 @@ -# `@img/sharp-libvips-linux-x64` - -Prebuilt libvips and dependencies for use with sharp on Linux (glibc) x64. - -## Licensing +# Third-party notices This software contains third-party libraries used under the terms of the following licences: @@ -16,24 +12,25 @@ used under the terms of the following licences: | fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) | | freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) | | fribidi | LGPLv3 | +| gdk-pixbuf | LGPLv3 | | glib | LGPLv3 | | harfbuzz | MIT Licence | -| highway | Apache-2.0 License, BSD 3-Clause | | lcms | MIT Licence | | libarchive | BSD 2-Clause | | libexif | LGPLv3 | | libffi | MIT Licence | | libheif | LGPLv3 | | libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) | +| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) | | libnsgif | MIT Licence | -| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) | +| libpng | [libpng License](https://github.com/glennrp/libpng/blob/master/LICENSE) | | librsvg | LGPLv3 | | libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) | -| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) | +| libtiff | [libtiff License](https://libtiff.gitlab.io/libtiff/misc.html) (BSD-like) | | libvips | LGPLv3 | | libwebp | New BSD License | | libxml2 | MIT Licence | -| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) | +| orc | [orc License](https://gitlab.freedesktop.org/gstreamer/orc/blob/master/COPYING) (BSD-like) | | pango | LGPLv3 | | pixman | MIT Licence | | proxy-libintl | LGPLv3 | diff --git a/backend/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/lib/libvips-cpp.so.42 similarity index 65% rename from backend/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 rename to backend/node_modules/sharp/vendor/8.14.5/linux-x64/lib/libvips-cpp.so.42 index dc9f57f5..79a9ea76 100644 Binary files a/backend/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 and b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/lib/libvips-cpp.so.42 differ diff --git a/backend/node_modules/sharp/vendor/8.14.5/linux-x64/platform.json b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/platform.json new file mode 100644 index 00000000..f8bbb411 --- /dev/null +++ b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/platform.json @@ -0,0 +1 @@ +"linux-x64" \ No newline at end of file diff --git a/backend/node_modules/sharp/vendor/8.14.5/linux-x64/versions.json b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/versions.json new file mode 100644 index 00000000..15fd100c --- /dev/null +++ b/backend/node_modules/sharp/vendor/8.14.5/linux-x64/versions.json @@ -0,0 +1,31 @@ +{ + "aom": "3.6.1", + "archive": "3.7.2", + "cairo": "1.17.8", + "cgif": "0.3.2", + "exif": "0.6.24", + "expat": "2.5.0", + "ffi": "3.4.4", + "fontconfig": "2.14.2", + "freetype": "2.13.2", + "fribidi": "1.0.13", + "gdkpixbuf": "2.42.10", + "glib": "2.78.0", + "harfbuzz": "8.2.0", + "heif": "1.16.2", + "imagequant": "2.4.1", + "lcms": "2.15", + "mozjpeg": "4.1.4", + "orc": "0.4.34", + "pango": "1.51.0", + "pixman": "0.42.2", + "png": "1.6.40", + "proxy-libintl": "0.4", + "rsvg": "2.57.0", + "spng": "0.7.4", + "tiff": "4.6.0", + "vips": "8.14.5", + "webp": "1.3.2", + "xml": "2.11.5", + "zlib-ng": "2.1.3" +} \ No newline at end of file diff --git a/backend/node_modules/simple-concat/.travis.yml b/backend/node_modules/simple-concat/.travis.yml new file mode 100644 index 00000000..c159f6ac --- /dev/null +++ b/backend/node_modules/simple-concat/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - lts/* diff --git a/backend/node_modules/simple-concat/LICENSE b/backend/node_modules/simple-concat/LICENSE new file mode 100644 index 00000000..c7e68527 --- /dev/null +++ b/backend/node_modules/simple-concat/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/simple-concat/README.md b/backend/node_modules/simple-concat/README.md new file mode 100644 index 00000000..b7d39bde --- /dev/null +++ b/backend/node_modules/simple-concat/README.md @@ -0,0 +1,44 @@ +# simple-concat [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] + +[travis-image]: https://img.shields.io/travis/feross/simple-concat/master.svg +[travis-url]: https://travis-ci.org/feross/simple-concat +[npm-image]: https://img.shields.io/npm/v/simple-concat.svg +[npm-url]: https://npmjs.org/package/simple-concat +[downloads-image]: https://img.shields.io/npm/dm/simple-concat.svg +[downloads-url]: https://npmjs.org/package/simple-concat +[standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg +[standard-url]: https://standardjs.com + +### Super-minimalist version of [`concat-stream`](https://github.com/maxogden/concat-stream). Less than 15 lines! + +## install + +``` +npm install simple-concat +``` + +## usage + +This example is longer than the implementation. + +```js +var s = new stream.PassThrough() +concat(s, function (err, buf) { + if (err) throw err + console.error(buf) +}) +s.write('abc') +setTimeout(function () { + s.write('123') +}, 10) +setTimeout(function () { + s.write('456') +}, 20) +setTimeout(function () { + s.end('789') +}, 30) +``` + +## license + +MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). diff --git a/backend/node_modules/simple-concat/index.js b/backend/node_modules/simple-concat/index.js new file mode 100644 index 00000000..59237fc6 --- /dev/null +++ b/backend/node_modules/simple-concat/index.js @@ -0,0 +1,15 @@ +/*! simple-concat. MIT License. Feross Aboukhadijeh */ +module.exports = function (stream, cb) { + var chunks = [] + stream.on('data', function (chunk) { + chunks.push(chunk) + }) + stream.once('end', function () { + if (cb) cb(null, Buffer.concat(chunks)) + cb = null + }) + stream.once('error', function (err) { + if (cb) cb(err) + cb = null + }) +} diff --git a/backend/node_modules/simple-concat/package.json b/backend/node_modules/simple-concat/package.json new file mode 100644 index 00000000..2bb2c60a --- /dev/null +++ b/backend/node_modules/simple-concat/package.json @@ -0,0 +1,47 @@ +{ + "name": "simple-concat", + "description": "Super-minimalist version of `concat-stream`. Less than 15 lines!", + "version": "1.0.1", + "author": { + "name": "Feross Aboukhadijeh", + "email": "feross@feross.org", + "url": "https://feross.org" + }, + "bugs": { + "url": "https://github.com/feross/simple-concat/issues" + }, + "dependencies": {}, + "devDependencies": { + "standard": "*", + "tape": "^5.0.1" + }, + "homepage": "https://github.com/feross/simple-concat", + "keywords": [ + "concat", + "concat-stream", + "concat stream" + ], + "license": "MIT", + "main": "index.js", + "repository": { + "type": "git", + "url": "git://github.com/feross/simple-concat.git" + }, + "scripts": { + "test": "standard && tape test/*.js" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] +} diff --git a/backend/node_modules/simple-concat/test/basic.js b/backend/node_modules/simple-concat/test/basic.js new file mode 100644 index 00000000..4bf6f9c2 --- /dev/null +++ b/backend/node_modules/simple-concat/test/basic.js @@ -0,0 +1,41 @@ +var concat = require('../') +var stream = require('stream') +var test = require('tape') + +test('basic', function (t) { + t.plan(2) + var s = new stream.PassThrough() + concat(s, function (err, buf) { + t.error(err) + t.deepEqual(buf, Buffer.from('abc123456789')) + }) + s.write('abc') + setTimeout(function () { + s.write('123') + }, 10) + setTimeout(function () { + s.write('456') + }, 20) + setTimeout(function () { + s.end('789') + }, 30) +}) + +test('error', function (t) { + t.plan(2) + var s = new stream.PassThrough() + concat(s, function (err, buf) { + t.ok(err, 'got expected error') + t.ok(!buf) + }) + s.write('abc') + setTimeout(function () { + s.write('123') + }, 10) + setTimeout(function () { + s.write('456') + }, 20) + setTimeout(function () { + s.emit('error', new Error('error')) + }, 30) +}) diff --git a/backend/node_modules/simple-get/.github/dependabot.yml b/backend/node_modules/simple-get/.github/dependabot.yml new file mode 100644 index 00000000..0221fbcb --- /dev/null +++ b/backend/node_modules/simple-get/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: daily + labels: + - dependency + versioning-strategy: increase-if-necessary + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + labels: + - dependency diff --git a/backend/node_modules/simple-get/.github/workflows/ci.yml b/backend/node_modules/simple-get/.github/workflows/ci.yml new file mode 100644 index 00000000..822d21cc --- /dev/null +++ b/backend/node_modules/simple-get/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: ci +'on': + - push + - pull_request +jobs: + test: + name: Node ${{ matrix.node }} / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + node: + - '14' + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run build --if-present + - run: npm test diff --git a/backend/node_modules/simple-get/LICENSE b/backend/node_modules/simple-get/LICENSE new file mode 100644 index 00000000..c7e68527 --- /dev/null +++ b/backend/node_modules/simple-get/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/simple-get/README.md b/backend/node_modules/simple-get/README.md new file mode 100644 index 00000000..63c6a6ba --- /dev/null +++ b/backend/node_modules/simple-get/README.md @@ -0,0 +1,333 @@ +# simple-get [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] + +[ci-image]: https://img.shields.io/github/workflow/status/feross/simple-get/ci/master +[ci-url]: https://github.com/feross/simple-get/actions +[npm-image]: https://img.shields.io/npm/v/simple-get.svg +[npm-url]: https://npmjs.org/package/simple-get +[downloads-image]: https://img.shields.io/npm/dm/simple-get.svg +[downloads-url]: https://npmjs.org/package/simple-get +[standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg +[standard-url]: https://standardjs.com + +### Simplest way to make http get requests + +## features + +This module is the lightest possible wrapper on top of node.js `http`, but supporting these essential features: + +- follows redirects +- automatically handles gzip/deflate responses +- supports HTTPS +- supports specifying a timeout +- supports convenience `url` key so there's no need to use `url.parse` on the url when specifying options +- composes well with npm packages for features like cookies, proxies, form data, & OAuth + +All this in < 100 lines of code. + +## install + +``` +npm install simple-get +``` + +## usage + +Note, all these examples also work in the browser with [browserify](http://browserify.org/). + +### simple GET request + +Doesn't get easier than this: + +```js +const get = require('simple-get') + +get('http://example.com', function (err, res) { + if (err) throw err + console.log(res.statusCode) // 200 + res.pipe(process.stdout) // `res` is a stream +}) +``` + +### even simpler GET request + +If you just want the data, and don't want to deal with streams: + +```js +const get = require('simple-get') + +get.concat('http://example.com', function (err, res, data) { + if (err) throw err + console.log(res.statusCode) // 200 + console.log(data) // Buffer('this is the server response') +}) +``` + +### POST, PUT, PATCH, HEAD, DELETE support + +For `POST`, call `get.post` or use option `{ method: 'POST' }`. + +```js +const get = require('simple-get') + +const opts = { + url: 'http://example.com', + body: 'this is the POST body' +} +get.post(opts, function (err, res) { + if (err) throw err + res.pipe(process.stdout) // `res` is a stream +}) +``` + +#### A more complex example: + +```js +const get = require('simple-get') + +get({ + url: 'http://example.com', + method: 'POST', + body: 'this is the POST body', + + // simple-get accepts all options that node.js `http` accepts + // See: http://nodejs.org/api/http.html#http_http_request_options_callback + headers: { + 'user-agent': 'my cool app' + } +}, function (err, res) { + if (err) throw err + + // All properties/methods from http.IncomingResponse are available, + // even if a gunzip/inflate transform stream was returned. + // See: http://nodejs.org/api/http.html#http_http_incomingmessage + res.setTimeout(10000) + console.log(res.headers) + + res.on('data', function (chunk) { + // `chunk` is the decoded response, after it's been gunzipped or inflated + // (if applicable) + console.log('got a chunk of the response: ' + chunk) + })) + +}) +``` + +### JSON + +You can serialize/deserialize request and response with JSON: + +```js +const get = require('simple-get') + +const opts = { + method: 'POST', + url: 'http://example.com', + body: { + key: 'value' + }, + json: true +} +get.concat(opts, function (err, res, data) { + if (err) throw err + console.log(data.key) // `data` is an object +}) +``` + +### Timeout + +You can set a timeout (in milliseconds) on the request with the `timeout` option. +If the request takes longer than `timeout` to complete, then the entire request +will fail with an `Error`. + +```js +const get = require('simple-get') + +const opts = { + url: 'http://example.com', + timeout: 2000 // 2 second timeout +} + +get(opts, function (err, res) {}) +``` + +### One Quick Tip + +It's a good idea to set the `'user-agent'` header so the provider can more easily +see how their resource is used. + +```js +const get = require('simple-get') +const pkg = require('./package.json') + +get('http://example.com', { + headers: { + 'user-agent': `my-module/${pkg.version} (https://github.com/username/my-module)` + } +}) +``` + +### Proxies + +You can use the [`tunnel`](https://github.com/koichik/node-tunnel) module with the +`agent` option to work with proxies: + +```js +const get = require('simple-get') +const tunnel = require('tunnel') + +const opts = { + url: 'http://example.com', + agent: tunnel.httpOverHttp({ + proxy: { + host: 'localhost' + } + }) +} + +get(opts, function (err, res) {}) +``` + +### Cookies + +You can use the [`cookie`](https://github.com/jshttp/cookie) module to include +cookies in a request: + +```js +const get = require('simple-get') +const cookie = require('cookie') + +const opts = { + url: 'http://example.com', + headers: { + cookie: cookie.serialize('foo', 'bar') + } +} + +get(opts, function (err, res) {}) +``` + +### Form data + +You can use the [`form-data`](https://github.com/form-data/form-data) module to +create POST request with form data: + +```js +const fs = require('fs') +const get = require('simple-get') +const FormData = require('form-data') +const form = new FormData() + +form.append('my_file', fs.createReadStream('/foo/bar.jpg')) + +const opts = { + url: 'http://example.com', + body: form +} + +get.post(opts, function (err, res) {}) +``` + +#### Or, include `application/x-www-form-urlencoded` form data manually: + +```js +const get = require('simple-get') + +const opts = { + url: 'http://example.com', + form: { + key: 'value' + } +} +get.post(opts, function (err, res) {}) +``` + +### Specifically disallowing redirects + +```js +const get = require('simple-get') + +const opts = { + url: 'http://example.com/will-redirect-elsewhere', + followRedirects: false +} +// res.statusCode will be 301, no error thrown +get(opts, function (err, res) {}) +``` + +### Basic Auth + +```js +const user = 'someuser' +const pass = 'pa$$word' +const encodedAuth = Buffer.from(`${user}:${pass}`).toString('base64') + +get('http://example.com', { + headers: { + authorization: `Basic ${encodedAuth}` + } +}) +``` + +### OAuth + +You can use the [`oauth-1.0a`](https://github.com/ddo/oauth-1.0a) module to create +a signed OAuth request: + +```js +const get = require('simple-get') +const crypto = require('crypto') +const OAuth = require('oauth-1.0a') + +const oauth = OAuth({ + consumer: { + key: process.env.CONSUMER_KEY, + secret: process.env.CONSUMER_SECRET + }, + signature_method: 'HMAC-SHA1', + hash_function: (baseString, key) => crypto.createHmac('sha1', key).update(baseString).digest('base64') +}) + +const token = { + key: process.env.ACCESS_TOKEN, + secret: process.env.ACCESS_TOKEN_SECRET +} + +const url = 'https://api.twitter.com/1.1/statuses/home_timeline.json' + +const opts = { + url: url, + headers: oauth.toHeader(oauth.authorize({url, method: 'GET'}, token)), + json: true +} + +get(opts, function (err, res) {}) +``` + +### Throttle requests + +You can use [limiter](https://github.com/jhurliman/node-rate-limiter) to throttle requests. This is useful when calling an API that is rate limited. + +```js +const simpleGet = require('simple-get') +const RateLimiter = require('limiter').RateLimiter +const limiter = new RateLimiter(1, 'second') + +const get = (opts, cb) => limiter.removeTokens(1, () => simpleGet(opts, cb)) +get.concat = (opts, cb) => limiter.removeTokens(1, () => simpleGet.concat(opts, cb)) + +var opts = { + url: 'http://example.com' +} + +get.concat(opts, processResult) +get.concat(opts, processResult) + +function processResult (err, res, data) { + if (err) throw err + console.log(data.toString()) +} +``` + +## license + +MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). diff --git a/backend/node_modules/simple-get/index.js b/backend/node_modules/simple-get/index.js new file mode 100644 index 00000000..80e52e8c --- /dev/null +++ b/backend/node_modules/simple-get/index.js @@ -0,0 +1,108 @@ +/*! simple-get. MIT License. Feross Aboukhadijeh */ +module.exports = simpleGet + +const concat = require('simple-concat') +const decompressResponse = require('decompress-response') // excluded from browser build +const http = require('http') +const https = require('https') +const once = require('once') +const querystring = require('querystring') +const url = require('url') + +const isStream = o => o !== null && typeof o === 'object' && typeof o.pipe === 'function' + +function simpleGet (opts, cb) { + opts = Object.assign({ maxRedirects: 10 }, typeof opts === 'string' ? { url: opts } : opts) + cb = once(cb) + + if (opts.url) { + const { hostname, port, protocol, auth, path } = url.parse(opts.url) // eslint-disable-line node/no-deprecated-api + delete opts.url + if (!hostname && !port && !protocol && !auth) opts.path = path // Relative redirect + else Object.assign(opts, { hostname, port, protocol, auth, path }) // Absolute redirect + } + + const headers = { 'accept-encoding': 'gzip, deflate' } + if (opts.headers) Object.keys(opts.headers).forEach(k => (headers[k.toLowerCase()] = opts.headers[k])) + opts.headers = headers + + let body + if (opts.body) { + body = opts.json && !isStream(opts.body) ? JSON.stringify(opts.body) : opts.body + } else if (opts.form) { + body = typeof opts.form === 'string' ? opts.form : querystring.stringify(opts.form) + opts.headers['content-type'] = 'application/x-www-form-urlencoded' + } + + if (body) { + if (!opts.method) opts.method = 'POST' + if (!isStream(body)) opts.headers['content-length'] = Buffer.byteLength(body) + if (opts.json && !opts.form) opts.headers['content-type'] = 'application/json' + } + delete opts.body; delete opts.form + + if (opts.json) opts.headers.accept = 'application/json' + if (opts.method) opts.method = opts.method.toUpperCase() + + const originalHost = opts.hostname // hostname before potential redirect + const protocol = opts.protocol === 'https:' ? https : http // Support http/https urls + const req = protocol.request(opts, res => { + if (opts.followRedirects !== false && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + opts.url = res.headers.location // Follow 3xx redirects + delete opts.headers.host // Discard `host` header on redirect (see #32) + res.resume() // Discard response + + const redirectHost = url.parse(opts.url).hostname // eslint-disable-line node/no-deprecated-api + // If redirected host is different than original host, drop headers to prevent cookie leak (#73) + if (redirectHost !== null && redirectHost !== originalHost) { + delete opts.headers.cookie + delete opts.headers.authorization + } + + if (opts.method === 'POST' && [301, 302].includes(res.statusCode)) { + opts.method = 'GET' // On 301/302 redirect, change POST to GET (see #35) + delete opts.headers['content-length']; delete opts.headers['content-type'] + } + + if (opts.maxRedirects-- === 0) return cb(new Error('too many redirects')) + else return simpleGet(opts, cb) + } + + const tryUnzip = typeof decompressResponse === 'function' && opts.method !== 'HEAD' + cb(null, tryUnzip ? decompressResponse(res) : res) + }) + req.on('timeout', () => { + req.abort() + cb(new Error('Request timed out')) + }) + req.on('error', cb) + + if (isStream(body)) body.on('error', cb).pipe(req) + else req.end(body) + + return req +} + +simpleGet.concat = (opts, cb) => { + return simpleGet(opts, (err, res) => { + if (err) return cb(err) + concat(res, (err, data) => { + if (err) return cb(err) + if (opts.json) { + try { + data = JSON.parse(data.toString()) + } catch (err) { + return cb(err, res, data) + } + } + cb(null, res, data) + }) + }) +} + +;['get', 'post', 'put', 'patch', 'head', 'delete'].forEach(method => { + simpleGet[method] = (opts, cb) => { + if (typeof opts === 'string') opts = { url: opts } + return simpleGet(Object.assign({ method: method.toUpperCase() }, opts), cb) + } +}) diff --git a/backend/node_modules/simple-get/package.json b/backend/node_modules/simple-get/package.json new file mode 100644 index 00000000..e80fc5eb --- /dev/null +++ b/backend/node_modules/simple-get/package.json @@ -0,0 +1,67 @@ +{ + "name": "simple-get", + "description": "Simplest way to make http get requests. Supports HTTPS, redirects, gzip/deflate, streams in < 100 lines.", + "version": "4.0.1", + "author": { + "name": "Feross Aboukhadijeh", + "email": "feross@feross.org", + "url": "https://feross.org" + }, + "browser": { + "decompress-response": false + }, + "bugs": { + "url": "https://github.com/feross/simple-get/issues" + }, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + }, + "devDependencies": { + "self-signed-https": "^1.0.5", + "standard": "*", + "string-to-stream": "^3.0.0", + "tape": "^5.0.0" + }, + "homepage": "https://github.com/feross/simple-get", + "keywords": [ + "request", + "http", + "GET", + "get request", + "http.get", + "redirects", + "follow redirects", + "gzip", + "deflate", + "https", + "http-https", + "stream", + "simple request", + "simple get" + ], + "license": "MIT", + "main": "index.js", + "repository": { + "type": "git", + "url": "git://github.com/feross/simple-get.git" + }, + "scripts": { + "test": "standard && tape test/*.js" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] +} diff --git a/backend/node_modules/simple-swizzle/LICENSE b/backend/node_modules/simple-swizzle/LICENSE new file mode 100644 index 00000000..1b77e5be --- /dev/null +++ b/backend/node_modules/simple-swizzle/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/backend/node_modules/simple-swizzle/README.md b/backend/node_modules/simple-swizzle/README.md new file mode 100644 index 00000000..299fedf0 --- /dev/null +++ b/backend/node_modules/simple-swizzle/README.md @@ -0,0 +1,43 @@ +> **NOTE:** ⚠️ **Don't use this package in new projects.** It is a **huge anti-pattern** and will only confuse and annoy people who use whatever code you write with it. I wrote this in a time when Javascript and Node.js were still pretty experimental and clever things like this weren't frowned upon. I've also learned a LOT about proper API design since I wrote this package. DO. NOT. USE. THIS. PACKAGE. If you're reaching for it, please *really* reconsider your API's design. + +--- + +# simple-swizzle [![Travis-CI.org Build Status](https://img.shields.io/travis/Qix-/node-simple-swizzle.svg?style=flat-square)](https://travis-ci.org/Qix-/node-simple-swizzle) [![Coveralls.io Coverage Rating](https://img.shields.io/coveralls/Qix-/node-simple-swizzle.svg?style=flat-square)](https://coveralls.io/r/Qix-/node-simple-swizzle) + +> [Swizzle](https://en.wikipedia.org/wiki/Swizzling_(computer_graphics)) your function arguments; pass in mixed arrays/values and get a clean array + +## Usage + +```js +var swizzle = require('simple-swizzle'); + +function myFunc() { + var args = swizzle(arguments); + // ... + return args; +} + +myFunc(1, [2, 3], 4); // [1, 2, 3, 4] +myFunc(1, 2, 3, 4); // [1, 2, 3, 4] +myFunc([1, 2, 3, 4]); // [1, 2, 3, 4] +``` + +Functions can also be wrapped to automatically swizzle arguments and be passed +the resulting array. + +```js +var swizzle = require('simple-swizzle'); + +var swizzledFn = swizzle.wrap(function (args) { + // ... + return args; +}); + +swizzledFn(1, [2, 3], 4); // [1, 2, 3, 4] +swizzledFn(1, 2, 3, 4); // [1, 2, 3, 4] +swizzledFn([1, 2, 3, 4]); // [1, 2, 3, 4] +``` + +## License +Licensed under the [MIT License](http://opensource.org/licenses/MIT). +You can find a copy of it in [LICENSE](LICENSE). diff --git a/backend/node_modules/simple-swizzle/index.js b/backend/node_modules/simple-swizzle/index.js new file mode 100644 index 00000000..4d6b8ff7 --- /dev/null +++ b/backend/node_modules/simple-swizzle/index.js @@ -0,0 +1,29 @@ +'use strict'; + +var isArrayish = require('is-arrayish'); + +var concat = Array.prototype.concat; +var slice = Array.prototype.slice; + +var swizzle = module.exports = function swizzle(args) { + var results = []; + + for (var i = 0, len = args.length; i < len; i++) { + var arg = args[i]; + + if (isArrayish(arg)) { + // http://jsperf.com/javascript-array-concat-vs-push/98 + results = concat.call(results, slice.call(arg)); + } else { + results.push(arg); + } + } + + return results; +}; + +swizzle.wrap = function (fn) { + return function () { + return fn(swizzle(arguments)); + }; +}; diff --git a/backend/node_modules/simple-swizzle/node_modules/is-arrayish/LICENSE b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/LICENSE new file mode 100644 index 00000000..0a5f461a --- /dev/null +++ b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 JD Ballard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/backend/node_modules/simple-swizzle/node_modules/is-arrayish/README.md b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/README.md new file mode 100644 index 00000000..7d360724 --- /dev/null +++ b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/README.md @@ -0,0 +1,16 @@ +# node-is-arrayish [![Travis-CI.org Build Status](https://img.shields.io/travis/Qix-/node-is-arrayish.svg?style=flat-square)](https://travis-ci.org/Qix-/node-is-arrayish) [![Coveralls.io Coverage Rating](https://img.shields.io/coveralls/Qix-/node-is-arrayish.svg?style=flat-square)](https://coveralls.io/r/Qix-/node-is-arrayish) +> Determines if an object can be used like an Array + +## Example +```javascript +var isArrayish = require('is-arrayish'); + +isArrayish([]); // true +isArrayish({__proto__: []}); // true +isArrayish({}); // false +isArrayish({length:10}); // false +``` + +## License +Licensed under the [MIT License](http://opensource.org/licenses/MIT). +You can find a copy of it in [LICENSE](LICENSE). diff --git a/backend/node_modules/simple-swizzle/node_modules/is-arrayish/index.js b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/index.js new file mode 100644 index 00000000..729ca40c --- /dev/null +++ b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/index.js @@ -0,0 +1,9 @@ +module.exports = function isArrayish(obj) { + if (!obj || typeof obj === 'string') { + return false; + } + + return obj instanceof Array || Array.isArray(obj) || + (obj.length >= 0 && (obj.splice instanceof Function || + (Object.getOwnPropertyDescriptor(obj, (obj.length - 1)) && obj.constructor.name !== 'String'))); +}; diff --git a/backend/node_modules/simple-swizzle/node_modules/is-arrayish/package.json b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/package.json new file mode 100644 index 00000000..e851c5be --- /dev/null +++ b/backend/node_modules/simple-swizzle/node_modules/is-arrayish/package.json @@ -0,0 +1,45 @@ +{ + "name": "is-arrayish", + "description": "Determines if an object can be used as an array", + "version": "0.3.4", + "author": "Qix (http://github.com/qix-)", + "keywords": [ + "is", + "array", + "duck", + "type", + "arrayish", + "similar", + "proto", + "prototype", + "type" + ], + "license": "MIT", + "scripts": { + "test": "mocha --require coffeescript/register ./test/**/*.coffee", + "lint": "zeit-eslint --ext .jsx,.js .", + "lint-staged": "git diff --diff-filter=ACMRT --cached --name-only '*.js' '*.jsx' | xargs zeit-eslint" + }, + "repository": { + "type": "git", + "url": "https://github.com/qix-/node-is-arrayish.git" + }, + "devDependencies": { + "@zeit/eslint-config-node": "^0.3.0", + "@zeit/git-hooks": "^0.1.4", + "coffeescript": "^2.3.1", + "coveralls": "^3.0.1", + "eslint": "^4.19.1", + "istanbul": "^0.4.5", + "mocha": "^5.2.0", + "should": "^13.2.1" + }, + "eslintConfig": { + "extends": [ + "@zeit/eslint-config-node" + ] + }, + "git": { + "pre-commit": "lint-staged" + } +} diff --git a/backend/node_modules/simple-swizzle/package.json b/backend/node_modules/simple-swizzle/package.json new file mode 100644 index 00000000..82c8218c --- /dev/null +++ b/backend/node_modules/simple-swizzle/package.json @@ -0,0 +1,36 @@ +{ + "name": "simple-swizzle", + "description": "Simply swizzle your arguments", + "version": "0.2.4", + "author": "Qix (http://github.com/qix-)", + "keywords": [ + "argument", + "arguments", + "swizzle", + "swizzling", + "parameter", + "parameters", + "mixed", + "array" + ], + "license": "MIT", + "scripts": { + "pretest": "xo", + "test": "mocha --compilers coffee:coffee-script/register" + }, + "files": [ + "index.js" + ], + "repository": "qix-/node-simple-swizzle", + "devDependencies": { + "coffee-script": "^1.9.3", + "coveralls": "^2.11.2", + "istanbul": "^0.3.17", + "mocha": "^2.2.5", + "should": "^7.0.1", + "xo": "^0.7.1" + }, + "dependencies": { + "is-arrayish": "^0.3.1" + } +} diff --git a/backend/node_modules/strip-json-comments/index.js b/backend/node_modules/strip-json-comments/index.js new file mode 100644 index 00000000..4e6576e6 --- /dev/null +++ b/backend/node_modules/strip-json-comments/index.js @@ -0,0 +1,70 @@ +'use strict'; +var singleComment = 1; +var multiComment = 2; + +function stripWithoutWhitespace() { + return ''; +} + +function stripWithWhitespace(str, start, end) { + return str.slice(start, end).replace(/\S/g, ' '); +} + +module.exports = function (str, opts) { + opts = opts || {}; + + var currentChar; + var nextChar; + var insideString = false; + var insideComment = false; + var offset = 0; + var ret = ''; + var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace; + + for (var i = 0; i < str.length; i++) { + currentChar = str[i]; + nextChar = str[i + 1]; + + if (!insideComment && currentChar === '"') { + var escaped = str[i - 1] === '\\' && str[i - 2] !== '\\'; + if (!escaped) { + insideString = !insideString; + } + } + + if (insideString) { + continue; + } + + if (!insideComment && currentChar + nextChar === '//') { + ret += str.slice(offset, i); + offset = i; + insideComment = singleComment; + i++; + } else if (insideComment === singleComment && currentChar + nextChar === '\r\n') { + i++; + insideComment = false; + ret += strip(str, offset, i); + offset = i; + continue; + } else if (insideComment === singleComment && currentChar === '\n') { + insideComment = false; + ret += strip(str, offset, i); + offset = i; + } else if (!insideComment && currentChar + nextChar === '/*') { + ret += str.slice(offset, i); + offset = i; + insideComment = multiComment; + i++; + continue; + } else if (insideComment === multiComment && currentChar + nextChar === '*/') { + i++; + insideComment = false; + ret += strip(str, offset, i + 1); + offset = i + 1; + continue; + } + } + + return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset)); +}; diff --git a/backend/node_modules/strip-json-comments/license b/backend/node_modules/strip-json-comments/license new file mode 100644 index 00000000..654d0bfe --- /dev/null +++ b/backend/node_modules/strip-json-comments/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/backend/node_modules/strip-json-comments/package.json b/backend/node_modules/strip-json-comments/package.json new file mode 100644 index 00000000..288ecc77 --- /dev/null +++ b/backend/node_modules/strip-json-comments/package.json @@ -0,0 +1,42 @@ +{ + "name": "strip-json-comments", + "version": "2.0.1", + "description": "Strip comments from JSON. Lets you use comments in your JSON files!", + "license": "MIT", + "repository": "sindresorhus/strip-json-comments", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "test": "xo && ava" + }, + "files": [ + "index.js" + ], + "keywords": [ + "json", + "strip", + "remove", + "delete", + "trim", + "comments", + "multiline", + "parse", + "config", + "configuration", + "conf", + "settings", + "util", + "env", + "environment" + ], + "devDependencies": { + "ava": "*", + "xo": "*" + } +} diff --git a/backend/node_modules/strip-json-comments/readme.md b/backend/node_modules/strip-json-comments/readme.md new file mode 100644 index 00000000..0ee58dfe --- /dev/null +++ b/backend/node_modules/strip-json-comments/readme.md @@ -0,0 +1,64 @@ +# strip-json-comments [![Build Status](https://travis-ci.org/sindresorhus/strip-json-comments.svg?branch=master)](https://travis-ci.org/sindresorhus/strip-json-comments) + +> Strip comments from JSON. Lets you use comments in your JSON files! + +This is now possible: + +```js +{ + // rainbows + "unicorn": /* ❤ */ "cake" +} +``` + +It will replace single-line comments `//` and multi-line comments `/**/` with whitespace. This allows JSON error positions to remain as close as possible to the original source. + +Also available as a [gulp](https://github.com/sindresorhus/gulp-strip-json-comments)/[grunt](https://github.com/sindresorhus/grunt-strip-json-comments)/[broccoli](https://github.com/sindresorhus/broccoli-strip-json-comments) plugin. + + +## Install + +``` +$ npm install --save strip-json-comments +``` + + +## Usage + +```js +const json = '{/*rainbows*/"unicorn":"cake"}'; + +JSON.parse(stripJsonComments(json)); +//=> {unicorn: 'cake'} +``` + + +## API + +### stripJsonComments(input, [options]) + +#### input + +Type: `string` + +Accepts a string with JSON and returns a string without comments. + +#### options + +##### whitespace + +Type: `boolean` +Default: `true` + +Replace comments with whitespace instead of stripping them entirely. + + +## Related + +- [strip-json-comments-cli](https://github.com/sindresorhus/strip-json-comments-cli) - CLI for this module +- [strip-css-comments](https://github.com/sindresorhus/strip-css-comments) - Strip comments from CSS + + +## License + +MIT © [Sindre Sorhus](http://sindresorhus.com) diff --git a/backend/node_modules/tunnel-agent/LICENSE b/backend/node_modules/tunnel-agent/LICENSE new file mode 100644 index 00000000..a4a9aee0 --- /dev/null +++ b/backend/node_modules/tunnel-agent/LICENSE @@ -0,0 +1,55 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/backend/node_modules/tunnel-agent/README.md b/backend/node_modules/tunnel-agent/README.md new file mode 100644 index 00000000..bb533d56 --- /dev/null +++ b/backend/node_modules/tunnel-agent/README.md @@ -0,0 +1,4 @@ +tunnel-agent +============ + +HTTP proxy tunneling agent. Formerly part of mikeal/request, now a standalone module. diff --git a/backend/node_modules/tunnel-agent/index.js b/backend/node_modules/tunnel-agent/index.js new file mode 100644 index 00000000..3ee9abc5 --- /dev/null +++ b/backend/node_modules/tunnel-agent/index.js @@ -0,0 +1,244 @@ +'use strict' + +var net = require('net') + , tls = require('tls') + , http = require('http') + , https = require('https') + , events = require('events') + , assert = require('assert') + , util = require('util') + , Buffer = require('safe-buffer').Buffer + ; + +exports.httpOverHttp = httpOverHttp +exports.httpsOverHttp = httpsOverHttp +exports.httpOverHttps = httpOverHttps +exports.httpsOverHttps = httpsOverHttps + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options) + agent.request = http.request + return agent +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options) + agent.request = http.request + agent.createSocket = createSecureSocket + agent.defaultPort = 443 + return agent +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options) + agent.request = https.request + return agent +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options) + agent.request = https.request + agent.createSocket = createSecureSocket + agent.defaultPort = 443 + return agent +} + + +function TunnelingAgent(options) { + var self = this + self.options = options || {} + self.proxyOptions = self.options.proxy || {} + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets + self.requests = [] + self.sockets = [] + + self.on('free', function onFree(socket, host, port) { + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i] + if (pending.host === host && pending.port === port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1) + pending.request.onSocket(socket) + return + } + } + socket.destroy() + self.removeSocket(socket) + }) +} +util.inherits(TunnelingAgent, events.EventEmitter) + +TunnelingAgent.prototype.addRequest = function addRequest(req, options) { + var self = this + + // Legacy API: addRequest(req, host, port, path) + if (typeof options === 'string') { + options = { + host: options, + port: arguments[2], + path: arguments[3] + }; + } + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push({host: options.host, port: options.port, request: req}) + return + } + + // If we are under maxSockets create a new one. + self.createConnection({host: options.host, port: options.port, request: req}) +} + +TunnelingAgent.prototype.createConnection = function createConnection(pending) { + var self = this + + self.createSocket(pending, function(socket) { + socket.on('free', onFree) + socket.on('close', onCloseOrRemove) + socket.on('agentRemove', onCloseOrRemove) + pending.request.onSocket(socket) + + function onFree() { + self.emit('free', socket, pending.host, pending.port) + } + + function onCloseOrRemove(err) { + self.removeSocket(socket) + socket.removeListener('free', onFree) + socket.removeListener('close', onCloseOrRemove) + socket.removeListener('agentRemove', onCloseOrRemove) + } + }) +} + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this + var placeholder = {} + self.sockets.push(placeholder) + + var connectOptions = mergeOptions({}, self.proxyOptions, + { method: 'CONNECT' + , path: options.host + ':' + options.port + , agent: false + } + ) + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {} + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + Buffer.from(connectOptions.proxyAuth).toString('base64') + } + + debug('making CONNECT request') + var connectReq = self.request(connectOptions) + connectReq.useChunkedEncodingByDefault = false // for v0.6 + connectReq.once('response', onResponse) // for v0.6 + connectReq.once('upgrade', onUpgrade) // for v0.6 + connectReq.once('connect', onConnect) // for v0.7 or later + connectReq.once('error', onError) + connectReq.end() + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head) + }) + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners() + socket.removeAllListeners() + + if (res.statusCode === 200) { + assert.equal(head.length, 0) + debug('tunneling connection has established') + self.sockets[self.sockets.indexOf(placeholder)] = socket + cb(socket) + } else { + debug('tunneling socket could not be established, statusCode=%d', res.statusCode) + var error = new Error('tunneling socket could not be established, ' + 'statusCode=' + res.statusCode) + error.code = 'ECONNRESET' + options.request.emit('error', error) + self.removeSocket(placeholder) + } + } + + function onError(cause) { + connectReq.removeAllListeners() + + debug('tunneling socket could not be established, cause=%s\n', cause.message, cause.stack) + var error = new Error('tunneling socket could not be established, ' + 'cause=' + cause.message) + error.code = 'ECONNRESET' + options.request.emit('error', error) + self.removeSocket(placeholder) + } +} + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) return + + this.sockets.splice(pos, 1) + + var pending = this.requests.shift() + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createConnection(pending) + } +} + +function createSecureSocket(options, cb) { + var self = this + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, mergeOptions({}, self.options, + { servername: options.host + , socket: socket + } + )) + self.sockets[self.sockets.indexOf(socket)] = secureSocket + cb(secureSocket) + }) +} + + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i] + if (typeof overrides === 'object') { + var keys = Object.keys(overrides) + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j] + if (overrides[k] !== undefined) { + target[k] = overrides[k] + } + } + } + } + return target +} + + +var debug +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments) + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0] + } else { + args.unshift('TUNNEL:') + } + console.error.apply(console, args) + } +} else { + debug = function() {} +} +exports.debug = debug // for test diff --git a/backend/node_modules/tunnel-agent/package.json b/backend/node_modules/tunnel-agent/package.json new file mode 100644 index 00000000..a271fda9 --- /dev/null +++ b/backend/node_modules/tunnel-agent/package.json @@ -0,0 +1,22 @@ +{ + "author": "Mikeal Rogers (http://www.futurealoof.com)", + "name": "tunnel-agent", + "license": "Apache-2.0", + "description": "HTTP proxy tunneling agent. Formerly part of mikeal/request, now a standalone module.", + "version": "0.6.0", + "repository": { + "url": "https://github.com/mikeal/tunnel-agent" + }, + "main": "index.js", + "files": [ + "index.js" + ], + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "devDependencies": {}, + "optionalDependencies": {}, + "engines": { + "node": "*" + } +} diff --git a/backend/public/app/assets/index-DKdvsMWP.css b/backend/public/app/assets/index-DKdvsMWP.css new file mode 100644 index 00000000..191c0ce7 --- /dev/null +++ b/backend/public/app/assets/index-DKdvsMWP.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root,[data-theme]{background-color:var(--fallback-b1,oklch(var(--b1)/1));color:var(--fallback-bc,oklch(var(--bc)/1))}@supports not (color: oklch(0% 0 0)){:root{color-scheme:light;--fallback-p: #491eff;--fallback-pc: #d4dbff;--fallback-s: #ff41c7;--fallback-sc: #fff9fc;--fallback-a: #00cfbd;--fallback-ac: #00100d;--fallback-n: #2b3440;--fallback-nc: #d7dde4;--fallback-b1: #ffffff;--fallback-b2: #e5e6e6;--fallback-b3: #e5e6e6;--fallback-bc: #1f2937;--fallback-in: #00b3f0;--fallback-inc: #000000;--fallback-su: #00ca92;--fallback-suc: #000000;--fallback-wa: #ffc22d;--fallback-wac: #000000;--fallback-er: #ff6f70;--fallback-erc: #000000}@media (prefers-color-scheme: dark){:root{color-scheme:dark;--fallback-p: #7582ff;--fallback-pc: #050617;--fallback-s: #ff71cf;--fallback-sc: #190211;--fallback-a: #00c7b5;--fallback-ac: #000e0c;--fallback-n: #2a323c;--fallback-nc: #a6adbb;--fallback-b1: #1d232a;--fallback-b2: #191e24;--fallback-b3: #15191e;--fallback-bc: #a6adbb;--fallback-in: #00b3f0;--fallback-inc: #000000;--fallback-su: #00ca92;--fallback-suc: #000000;--fallback-wa: #ffc22d;--fallback-wac: #000000;--fallback-er: #ff6f70;--fallback-erc: #000000}}}html{-webkit-tap-highlight-color:transparent}*{scrollbar-color:color-mix(in oklch,currentColor 35%,transparent) transparent}*:hover{scrollbar-color:color-mix(in oklch,currentColor 60%,transparent) transparent}:root{color-scheme:light;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 89.824% .06192 275.75;--ac: 15.352% .0368 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 49.12% .3096 275.75;--s: 69.71% .329 342.55;--sc: 98.71% .0106 342.55;--a: 76.76% .184 183.61;--n: 32.1785% .02476 255.701624;--nc: 89.4994% .011585 252.096176;--b1: 100% 0 0;--b2: 96.1151% 0 0;--b3: 92.4169% .00108 197.137559;--bc: 27.8078% .029596 256.847952}@media (prefers-color-scheme: dark){:root{color-scheme:dark;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 13.138% .0392 275.75;--sc: 14.96% .052 342.55;--ac: 14.902% .0334 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 65.69% .196 275.75;--s: 74.8% .26 342.55;--a: 74.51% .167 183.61;--n: 31.3815% .021108 254.139175;--nc: 74.6477% .0216 264.435964;--b1: 25.3267% .015896 252.417568;--b2: 23.2607% .013807 253.100675;--b3: 21.1484% .01165 254.087939;--bc: 74.6477% .0216 264.435964}}[data-theme=light]{color-scheme:light;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 89.824% .06192 275.75;--ac: 15.352% .0368 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 49.12% .3096 275.75;--s: 69.71% .329 342.55;--sc: 98.71% .0106 342.55;--a: 76.76% .184 183.61;--n: 32.1785% .02476 255.701624;--nc: 89.4994% .011585 252.096176;--b1: 100% 0 0;--b2: 96.1151% 0 0;--b3: 92.4169% .00108 197.137559;--bc: 27.8078% .029596 256.847952}[data-theme=dark]{color-scheme:dark;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 13.138% .0392 275.75;--sc: 14.96% .052 342.55;--ac: 14.902% .0334 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 65.69% .196 275.75;--s: 74.8% .26 342.55;--a: 74.51% .167 183.61;--n: 31.3815% .021108 254.139175;--nc: 74.6477% .0216 264.435964;--b1: 25.3267% .015896 252.417568;--b2: 23.2607% .013807 253.100675;--b3: 21.1484% .01165 254.087939;--bc: 74.6477% .0216 264.435964}[data-theme=cupcake]{color-scheme:light;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 15.2344% .017892 200.026556;--sc: 15.787% .020249 356.29965;--ac: 15.8762% .029206 78.618794;--nc: 84.7148% .013247 313.189598;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--p: 76.172% .089459 200.026556;--s: 78.9351% .101246 356.29965;--a: 79.3811% .146032 78.618794;--n: 23.5742% .066235 313.189598;--b1: 97.7882% .00418 56.375637;--b2: 93.9822% .007638 61.449292;--b3: 91.5861% .006811 53.440502;--bc: 23.5742% .066235 313.189598;--rounded-btn: 1.9rem;--tab-border: 2px;--tab-radius: .7rem}.alert{display:grid;width:100%;grid-auto-flow:row;align-content:flex-start;align-items:center;justify-items:center;gap:1rem;text-align:center;border-radius:var(--rounded-box, 1rem);border-width:1px;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));padding:1rem;--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--alert-bg: var(--fallback-b2,oklch(var(--b2)/1));--alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));background-color:var(--alert-bg)}@media (min-width: 640px){.alert{grid-auto-flow:column;grid-template-columns:auto minmax(auto,1fr);justify-items:start;text-align:start}}.avatar.placeholder>div{display:flex;align-items:center;justify-content:center}.badge{display:inline-flex;align-items:center;justify-content:center;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);transition-duration:.2s;height:1.25rem;font-size:.875rem;line-height:1.25rem;width:-moz-fit-content;width:fit-content;padding-left:.563rem;padding-right:.563rem;border-radius:var(--rounded-badge, 1.9rem);border-width:1px;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}@media (hover:hover){.label a:hover{--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.menu li>*:not(ul,.menu-title,details,.btn):active,.menu li>*:not(ul,.menu-title,details,.btn).active,.menu li>details>summary:active{--tw-bg-opacity: 1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.table tr.hover:hover,.table tr.hover:nth-child(2n):hover{--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.hover:hover,.table-zebra tr.hover:nth-child(2n):hover{--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}}.btn{display:inline-flex;height:3rem;min-height:3rem;flex-shrink:0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-wrap:wrap;align-items:center;justify-content:center;border-radius:var(--rounded-btn, .5rem);border-color:transparent;border-color:oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity));padding-left:1rem;padding-right:1rem;text-align:center;font-size:.875rem;line-height:1em;gap:.5rem;font-weight:600;text-decoration-line:none;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);border-width:var(--border-btn, 1px);transition-property:color,background-color,border-color,opacity,box-shadow,transform;--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);outline-color:var(--fallback-bc,oklch(var(--bc)/1));background-color:oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity));--tw-bg-opacity: 1;--tw-border-opacity: 1}.btn-disabled,.btn[disabled],.btn:disabled{pointer-events:none}:where(.btn:is(input[type=checkbox])),:where(.btn:is(input[type=radio])){width:auto;-webkit-appearance:none;-moz-appearance:none;appearance:none}.btn:is(input[type=checkbox]):after,.btn:is(input[type=radio]):after{--tw-content: attr(aria-label);content:var(--tw-content)}.card{position:relative;display:flex;flex-direction:column;border-radius:var(--rounded-box, 1rem)}.card:focus{outline:2px solid transparent;outline-offset:2px}.card-body{display:flex;flex:1 1 auto;flex-direction:column;padding:var(--padding-card, 2rem);gap:.5rem}.card-body :where(p){flex-grow:1}.card-actions{display:flex;flex-wrap:wrap;align-items:flex-start;gap:.5rem}.card figure{display:flex;align-items:center;justify-content:center}.card.image-full{display:grid}.card.image-full:before{position:relative;content:"";z-index:10;border-radius:var(--rounded-box, 1rem);--tw-bg-opacity: 1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));opacity:.75}.card.image-full:before,.card.image-full>*{grid-column-start:1;grid-row-start:1}.card.image-full>figure img{height:100%;-o-object-fit:cover;object-fit:cover}.card.image-full>.card-body{position:relative;z-index:20;--tw-text-opacity: 1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.carousel{display:inline-flex;overflow-x:scroll;scroll-snap-type:x mandatory;scroll-behavior:smooth;-ms-overflow-style:none;scrollbar-width:none}.checkbox{flex-shrink:0;--chkbg: var(--fallback-bc,oklch(var(--bc)/1));--chkfg: var(--fallback-b1,oklch(var(--b1)/1));height:1.5rem;width:1.5rem;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-border-opacity: .2}.collapse:not(td):not(tr):not(colgroup){visibility:visible}.collapse{position:relative;display:grid;overflow:hidden;grid-template-rows:max-content 0fr;transition:grid-template-rows .2s;width:100%;border-radius:var(--rounded-box, 1rem)}.collapse-title,.collapse>input[type=checkbox],.collapse>input[type=radio],.collapse-content{grid-column-start:1;grid-row-start:1}.collapse>input[type=checkbox],.collapse>input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;opacity:0}:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){height:100%;width:100%;z-index:1}.collapse[open],.collapse-open,.collapse:focus:not(.collapse-close){grid-template-rows:max-content 1fr}.collapse:not(.collapse-close):has(>input[type=checkbox]:checked),.collapse:not(.collapse-close):has(>input[type=radio]:checked){grid-template-rows:max-content 1fr}.collapse[open]>.collapse-content,.collapse-open>.collapse-content,.collapse:focus:not(.collapse-close)>.collapse-content,.collapse:not(.collapse-close)>input[type=checkbox]:checked~.collapse-content,.collapse:not(.collapse-close)>input[type=radio]:checked~.collapse-content{visibility:visible;min-height:-moz-fit-content;min-height:fit-content}.dropdown{position:relative;display:inline-block}.dropdown>*:not(summary):focus{outline:2px solid transparent;outline-offset:2px}.dropdown .dropdown-content{position:absolute}.dropdown:is(:not(details)) .dropdown-content{visibility:hidden;opacity:0;transform-origin:top;--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);transition-duration:.2s}.dropdown.dropdown-open .dropdown-content,.dropdown:not(.dropdown-hover):focus .dropdown-content,.dropdown:focus-within .dropdown-content{visibility:visible;opacity:1}@media (hover: hover){.dropdown.dropdown-hover:hover .dropdown-content{visibility:visible;opacity:1}.btm-nav>*.disabled:hover,.btm-nav>*[disabled]:hover{pointer-events:none;--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}.btn:hover{--tw-border-opacity: 1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn:hover{background-color:color-mix(in oklab,oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%,black);border-color:color-mix(in oklab,oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%,black)}}@supports not (color: oklch(0% 0 0)){.btn:hover{background-color:var(--btn-color, var(--fallback-b2));border-color:var(--btn-color, var(--fallback-b2))}}.btn.glass:hover{--glass-opacity: 25%;--glass-border-opacity: 15%}.btn-ghost:hover{border-color:transparent}@supports (color: oklch(0% 0 0)){.btn-ghost:hover{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}}.btn-outline:hover{--tw-border-opacity: 1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary:hover{--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-primary:hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black)}}.btn-outline.btn-secondary:hover{--tw-text-opacity: 1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-secondary:hover{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black)}}.btn-outline.btn-accent:hover{--tw-text-opacity: 1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-accent:hover{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black)}}.btn-outline.btn-success:hover{--tw-text-opacity: 1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-success:hover{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black)}}.btn-outline.btn-info:hover{--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-info:hover{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black)}}.btn-outline.btn-warning:hover{--tw-text-opacity: 1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-warning:hover{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black)}}.btn-outline.btn-error:hover{--tw-text-opacity: 1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-error:hover{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black)}}.btn-disabled:hover,.btn[disabled]:hover,.btn:disabled:hover{--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}@supports (color: color-mix(in oklab,black,black)){.btn:is(input[type=checkbox]:checked):hover,.btn:is(input[type=radio]:checked):hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black)}}.dropdown.dropdown-hover:hover .dropdown-content{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{cursor:pointer;outline:2px solid transparent;outline-offset:2px}@supports (color: oklch(0% 0 0)){:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{background-color:var(--fallback-bc,oklch(var(--bc)/.1))}}}.dropdown:is(details) summary::-webkit-details-marker{display:none}.label{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;align-items:center;justify-content:space-between;padding:.5rem .25rem}.input{flex-shrink:1;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:3rem;padding-left:1rem;padding-right:1rem;font-size:1rem;line-height:2;line-height:1.5rem;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.input[type=number]::-webkit-inner-spin-button,.input-md[type=number]::-webkit-inner-spin-button{margin-top:-1rem;margin-bottom:-1rem;margin-inline-end:-1rem}.input-sm[type=number]::-webkit-inner-spin-button{margin-top:0;margin-bottom:0;margin-inline-end:-0px}.join{display:inline-flex;align-items:stretch;border-radius:var(--rounded-btn, .5rem)}.join :where(.join-item){border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:0;border-start-start-radius:0}.join .join-item:not(:first-child):not(:last-child),.join *:not(:first-child):not(:last-child) .join-item{border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:0;border-start-start-radius:0}.join .join-item:first-child:not(:last-child),.join *:first-child:not(:last-child) .join-item{border-start-end-radius:0;border-end-end-radius:0}.join .dropdown .join-item:first-child:not(:last-child),.join *:first-child:not(:last-child) .dropdown .join-item{border-start-end-radius:inherit;border-end-end-radius:inherit}.join :where(.join-item:first-child:not(:last-child)),.join :where(*:first-child:not(:last-child) .join-item){border-end-start-radius:inherit;border-start-start-radius:inherit}.join .join-item:last-child:not(:first-child),.join *:last-child:not(:first-child) .join-item{border-end-start-radius:0;border-start-start-radius:0}.join :where(.join-item:last-child:not(:first-child)),.join :where(*:last-child:not(:first-child) .join-item){border-start-end-radius:inherit;border-end-end-radius:inherit}@supports not selector(:has(*)){:where(.join *){border-radius:inherit}}@supports selector(:has(*)){:where(.join *:has(.join-item)){border-radius:inherit}}.link{cursor:pointer;text-decoration-line:underline}.menu{display:flex;flex-direction:column;flex-wrap:wrap;font-size:.875rem;line-height:1.25rem;padding:.5rem}.menu :where(li ul){position:relative;white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem}.menu :where(li:not(.menu-title)>*:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){display:grid;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;grid-auto-columns:minmax(auto,max-content) auto max-content;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu li.disabled{cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;user-select:none;color:var(--fallback-bc,oklch(var(--bc)/.3))}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}:where(.menu li){position:relative;display:flex;flex-shrink:0;flex-direction:column;flex-wrap:wrap;align-items:stretch}:where(.menu li) .badge{justify-self:end}.progress{position:relative;width:100%;-webkit-appearance:none;-moz-appearance:none;appearance:none;overflow:hidden;height:.5rem;border-radius:var(--rounded-box, 1rem);background-color:var(--fallback-bc,oklch(var(--bc)/.2))}.range{height:1.5rem;width:100%;cursor:pointer;-moz-appearance:none;appearance:none;-webkit-appearance:none;--range-shdw: var(--fallback-bc,oklch(var(--bc)/1));overflow:hidden;border-radius:var(--rounded-box, 1rem);background-color:transparent}.range:focus{outline:none}.select{display:inline-flex;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:3rem;min-height:3rem;padding-inline-start:1rem;padding-inline-end:2.5rem;font-size:.875rem;line-height:1.25rem;line-height:2;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-size:4px 4px,4px 4px;background-repeat:no-repeat}.select[multiple]{height:auto}.stats{display:inline-grid;border-radius:var(--rounded-box, 1rem);--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}:where(.stats){grid-auto-flow:column;overflow-x:auto}.steps .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-columns:auto;grid-template-rows:repeat(2,minmax(0,1fr));grid-template-rows:40px 1fr;place-items:center;text-align:center;min-width:4rem}.table{position:relative;width:100%;border-radius:var(--rounded-box, 1rem);text-align:left;font-size:.875rem;line-height:1.25rem}.table :where(.table-pin-rows thead tr){position:sticky;top:0;z-index:1;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table :where(.table-pin-rows tfoot tr){position:sticky;bottom:0;z-index:1;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table :where(.table-pin-cols tr th){position:sticky;left:0;right:0;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table-zebra tbody tr:nth-child(2n) :where(.table-pin-cols tr th){--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.textarea{min-height:3rem;flex-shrink:1;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;line-height:2;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.toggle{flex-shrink:0;--tglbg: var(--fallback-b1,oklch(var(--b1)/1));--handleoffset: 1.5rem;--handleoffsetcalculator: calc(var(--handleoffset) * -1);--togglehandleborder: 0 0;height:1.5rem;width:3rem;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--rounded-badge, 1.9rem);border-width:1px;border-color:currentColor;background-color:currentColor;color:var(--fallback-bc,oklch(var(--bc)/.5));transition:background,box-shadow var(--animation-input, .2s) ease-out;box-shadow:var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset,var(--togglehandleborder)}.alert-info{border-color:var(--fallback-in,oklch(var(--in)/.2));--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));--alert-bg: var(--fallback-in,oklch(var(--in)/1));--alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1))}.badge-info{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.badge-success{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.badge-warning{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.badge-error{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.badge-ghost{--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.badge-outline.badge-info{--tw-text-opacity: 1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.badge-outline.badge-success{--tw-text-opacity: 1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.badge-outline.badge-warning{--tw-text-opacity: 1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.badge-outline.badge-error{--tw-text-opacity: 1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btm-nav>*:where(.active){border-top-width:2px;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.btm-nav>*.disabled,.btm-nav>*[disabled]{pointer-events:none;--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}.btm-nav>* .label{font-size:1rem;line-height:1.5rem}@media (prefers-reduced-motion: no-preference){.btn{animation:button-pop var(--animation-btn, .25s) ease-out}}.btn:active:hover,.btn:active:focus{animation:button-pop 0s ease-out;transform:scale(var(--btn-focus-scale, .97))}@supports not (color: oklch(0% 0 0)){.btn{background-color:var(--btn-color, var(--fallback-b2));border-color:var(--btn-color, var(--fallback-b2))}.btn-primary{--btn-color: var(--fallback-p)}.btn-secondary{--btn-color: var(--fallback-s)}}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-primary.btn-active{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black)}.btn-outline.btn-secondary.btn-active{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black)}.btn-outline.btn-accent.btn-active{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black)}.btn-outline.btn-success.btn-active{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black)}.btn-outline.btn-info.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black)}.btn-outline.btn-warning.btn-active{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black)}.btn-outline.btn-error.btn-active{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black)}}.btn:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px}.btn-primary{--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@supports (color: oklch(0% 0 0)){.btn-primary{--btn-color: var(--p)}.btn-secondary{--btn-color: var(--s)}}.btn-secondary{--tw-text-opacity: 1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));outline-color:var(--fallback-s,oklch(var(--s)/1))}.btn.glass{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn.glass.btn-active{--glass-opacity: 25%;--glass-border-opacity: 15%}.btn-ghost{border-width:1px;border-color:transparent;background-color:transparent;color:currentColor;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn-ghost.btn-active{border-color:transparent;background-color:var(--fallback-bc,oklch(var(--bc)/.2))}.btn-outline{border-color:currentColor;background-color:transparent;--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.btn-outline.btn-active{--tw-border-opacity: 1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary{--tw-text-opacity: 1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.btn-outline.btn-primary.btn-active{--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn-outline.btn-secondary{--tw-text-opacity: 1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.btn-outline.btn-secondary.btn-active{--tw-text-opacity: 1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.btn-outline.btn-accent{--tw-text-opacity: 1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.btn-outline.btn-accent.btn-active{--tw-text-opacity: 1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.btn-outline.btn-success{--tw-text-opacity: 1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.btn-outline.btn-success.btn-active{--tw-text-opacity: 1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.btn-outline.btn-info{--tw-text-opacity: 1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.btn-outline.btn-info.btn-active{--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.btn-outline.btn-warning{--tw-text-opacity: 1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.btn-outline.btn-warning.btn-active{--tw-text-opacity: 1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.btn-outline.btn-error{--tw-text-opacity: 1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btn-outline.btn-error.btn-active{--tw-text-opacity: 1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.btn.btn-disabled,.btn[disabled],.btn:disabled{--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity: 1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked):focus-visible,.btn:is(input[type=radio]:checked):focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}@keyframes button-pop{0%{transform:scale(var(--btn-focus-scale, .98))}40%{transform:scale(1.02)}to{transform:scale(1)}}.card :where(figure:first-child){overflow:hidden;border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-start-radius:unset;border-end-end-radius:unset}.card :where(figure:last-child){overflow:hidden;border-start-start-radius:unset;border-start-end-radius:unset;border-end-start-radius:inherit;border-end-end-radius:inherit}.card:focus-visible{outline:2px solid currentColor;outline-offset:2px}.card.bordered{border-width:1px;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.card.compact .card-body{padding:1rem;font-size:.875rem;line-height:1.25rem}.card-title{display:flex;align-items:center;gap:.5rem;font-size:1.25rem;line-height:1.75rem;font-weight:600}.card.image-full :where(figure){overflow:hidden;border-radius:inherit}.carousel::-webkit-scrollbar{display:none}.checkbox:focus{box-shadow:none}.checkbox:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/1))}.checkbox:disabled{border-width:0px;cursor:not-allowed;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.2}.checkbox:checked,.checkbox[aria-checked=true]{background-repeat:no-repeat;animation:checkmark var(--animation-input, .2s) ease-out;background-color:var(--chkbg);background-image:linear-gradient(-45deg,transparent 65%,var(--chkbg) 65.99%),linear-gradient(45deg,transparent 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,transparent 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,transparent 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%)}.checkbox:indeterminate{--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-repeat:no-repeat;animation:checkmark var(--animation-input, .2s) ease-out;background-image:linear-gradient(90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(-90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%)}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}details.collapse{width:100%}details.collapse summary{position:relative;display:block;outline:2px solid transparent;outline-offset:2px}details.collapse summary::-webkit-details-marker{display:none}.collapse:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/1))}.collapse:has(.collapse-title:focus-visible),.collapse:has(>input[type=checkbox]:focus-visible),.collapse:has(>input[type=radio]:focus-visible){outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/1))}.collapse:not(.collapse-open):not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-open):not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-open):not(.collapse-close)>.collapse-title{cursor:pointer}.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open])>.collapse-title{cursor:unset}.collapse-title,:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){padding:1rem;padding-inline-end:3rem;min-height:3.75rem;transition:background-color .2s ease-out}.collapse[open]>:where(.collapse-content),.collapse-open>:where(.collapse-content),.collapse:focus:not(.collapse-close)>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input[type=checkbox]:checked~.collapse-content),.collapse:not(.collapse-close)>:where(input[type=radio]:checked~.collapse-content){padding-bottom:1rem;transition:padding .2s ease-out,background-color .2s ease-out}.collapse[open].collapse-arrow>.collapse-title:after,.collapse-open.collapse-arrow>.collapse-title:after,.collapse-arrow:focus:not(.collapse-close)>.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after{--tw-translate-y: -50%;--tw-rotate: 225deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.collapse[open].collapse-plus>.collapse-title:after,.collapse-open.collapse-plus>.collapse-title:after,.collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after{content:"−"}.dropdown.dropdown-open .dropdown-content,.dropdown:focus .dropdown-content,.dropdown:focus-within .dropdown-content{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.input input{--tw-bg-opacity: 1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:transparent}.input input:focus{outline:2px solid transparent;outline-offset:2px}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{box-shadow:none;border-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:has(>input[disabled]),.input-disabled,.input:disabled,.input[disabled]{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.input:has(>input[disabled])::-moz-placeholder,.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.input:has(>input[disabled])::placeholder,.input-disabled::placeholder,.input:disabled::placeholder,.input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.join>:where(*:not(:first-child)){margin-top:0;margin-bottom:0;margin-inline-start:-1px}.join>:where(*:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn) * -1)}.link-primary{--tw-text-opacity: 1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){@media (hover:hover){.link-primary:hover{color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,black)}}}.link:focus{outline:2px solid transparent;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}.loading{pointer-events:none;display:inline-block;aspect-ratio:1 / 1;width:1.5rem;background-color:currentColor;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")}.loading-spinner{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")}.loading-lg{width:2.5rem}:where(.menu li:empty){--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.1;margin:.5rem 1rem;height:1px}.menu :where(li ul):before{position:absolute;bottom:.75rem;inset-inline-start:0px;top:.75rem;width:1px;--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.1;content:""}.menu :where(li:not(.menu-title)>*:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn, .5rem);padding:.5rem 1rem;text-align:start;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);transition-duration:.2s;text-wrap:balance}:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):is(summary):not(.active,.btn):focus-visible{cursor:pointer;background-color:var(--fallback-bc,oklch(var(--bc)/.1));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));outline:2px solid transparent;outline-offset:2px}.menu li>*:not(ul,.menu-title,details,.btn):active,.menu li>*:not(ul,.menu-title,details,.btn).active,.menu li>details>summary:active{--tw-bg-opacity: 1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.menu :where(li>details>summary)::-webkit-details-marker{display:none}.menu :where(li>details>summary):after,.menu :where(li>.menu-dropdown-toggle):after{justify-self:end;display:block;margin-top:-.5rem;height:.5rem;width:.5rem;transform:rotate(45deg);transition-property:transform,margin-top;transition-duration:.3s;transition-timing-function:cubic-bezier(.4,0,.2,1);content:"";transform-origin:75% 75%;box-shadow:2px 2px;pointer-events:none}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{transform:rotate(225deg);margin-top:0}.mockup-phone .display{overflow:hidden;border-radius:40px;margin-top:-25px}.mockup-browser .mockup-browser-toolbar .input{position:relative;margin-left:auto;margin-right:auto;display:block;height:1.75rem;width:24rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));padding-left:2rem;direction:ltr}.mockup-browser .mockup-browser-toolbar .input:before{content:"";position:absolute;left:.5rem;top:50%;aspect-ratio:1 / 1;height:.75rem;--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:9999px;border-width:2px;border-color:currentColor;opacity:.6}.mockup-browser .mockup-browser-toolbar .input:after{content:"";position:absolute;left:1.25rem;top:50%;height:.5rem;--tw-translate-y: 25%;--tw-rotate: -45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:9999px;border-width:1px;border-color:currentColor;opacity:.6}@keyframes modal-pop{0%{opacity:0}}.progress::-moz-progress-bar{border-radius:var(--rounded-box, 1rem);background-color:currentColor}.progress:indeterminate{--progress-color: var(--fallback-bc,oklch(var(--bc)/1));background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-size:200%;background-position-x:15%;animation:progress-loading 5s ease-in-out infinite}.progress::-webkit-progress-bar{border-radius:var(--rounded-box, 1rem);background-color:transparent}.progress::-webkit-progress-value{border-radius:var(--rounded-box, 1rem);background-color:currentColor}.progress:indeterminate::-moz-progress-bar{background-color:transparent;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-size:200%;background-position-x:15%;animation:progress-loading 5s ease-in-out infinite}@keyframes progress-loading{50%{background-position-x:-115%}}@keyframes radiomark{0%{box-shadow:0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset}50%{box-shadow:0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset}to{box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}}.range:focus-visible::-webkit-slider-thumb{--focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset}.range:focus-visible::-moz-range-thumb{--focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset}.range::-webkit-slider-runnable-track{height:.5rem;width:100%;border-radius:var(--rounded-box, 1rem);background-color:var(--fallback-bc,oklch(var(--bc)/.1))}.range::-moz-range-track{height:.5rem;width:100%;border-radius:var(--rounded-box, 1rem);background-color:var(--fallback-bc,oklch(var(--bc)/.1))}.range::-webkit-slider-thumb{position:relative;height:1.5rem;width:1.5rem;border-radius:var(--rounded-box, 1rem);border-style:none;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));-moz-appearance:none;appearance:none;-webkit-appearance:none;top:50%;color:var(--range-shdw);transform:translateY(-50%);--filler-size: 100rem;--filler-offset: .6rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow, 0 0),calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size)}.range::-moz-range-thumb{position:relative;height:1.5rem;width:1.5rem;border-radius:var(--rounded-box, 1rem);border-style:none;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));top:50%;color:var(--range-shdw);--filler-size: 100rem;--filler-offset: .5rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow, 0 0),calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size)}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.select-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.select:focus{box-shadow:none;border-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.select-disabled,.select:disabled,.select[disabled]{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.select-disabled::-moz-placeholder,.select:disabled::-moz-placeholder,.select[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.select-disabled::placeholder,.select:disabled::placeholder,.select[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.select-multiple,.select[multiple],.select[size].select:not([size="1"]){background-image:none;padding-right:1rem}[dir=rtl] .select{background-position:calc(0% + 12px) calc(1px + 50%),calc(0% + 16px) calc(1px + 50%)}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}:where(.stats)>:not([hidden])~:not([hidden]){--tw-divide-x-reverse: 0;border-right-width:calc(1px * var(--tw-divide-x-reverse));border-left-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)));--tw-divide-y-reverse: 0;border-top-width:calc(0px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(0px * var(--tw-divide-y-reverse))}[dir=rtl] .stats>*:not([hidden])~*:not([hidden]){--tw-divide-x-reverse: 1}.steps .step:before{top:0;grid-column-start:1;grid-row-start:1;height:.5rem;width:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:"";margin-inline-start:-100%}.steps .step:after{content:counter(step);counter-increment:step;z-index:1;position:relative;grid-column-start:1;grid-row-start:1;display:grid;height:2rem;width:2rem;place-items:center;place-self:center;border-radius:9999px;--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.steps .step:first-child:before{content:none}.steps .step[data-content]:after{content:attr(data-content)}.table:where([dir=rtl],[dir=rtl] *){text-align:right}.table :where(th,td){padding:.75rem 1rem;vertical-align:middle}.table tr.active,.table tr.active:nth-child(2n),.table-zebra tbody tr:nth-child(2n){--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.active,.table-zebra tr.active:nth-child(2n),.table-zebra-zebra tbody tr:nth-child(2n){--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.table :where(thead tr,tbody tr:not(:last-child),tbody tr:first-child:last-child){border-bottom-width:1px;--tw-border-opacity: 1;border-bottom-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.table :where(thead,tfoot){white-space:nowrap;font-size:.75rem;line-height:1rem;font-weight:700;color:var(--fallback-bc,oklch(var(--bc)/.6))}.table :where(tfoot){border-top-width:1px;--tw-border-opacity: 1;border-top-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.textarea:focus{box-shadow:none;border-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.textarea-disabled,.textarea:disabled,.textarea[disabled]{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.textarea-disabled::-moz-placeholder,.textarea:disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.textarea-disabled::placeholder,.textarea:disabled::placeholder,.textarea[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}@keyframes toast-pop{0%{transform:scale(.9);opacity:0}to{transform:scale(1);opacity:1}}[dir=rtl] .toggle{--handleoffsetcalculator: calc(var(--handleoffset) * 1)}.toggle:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.toggle:hover{background-color:currentColor}.toggle:checked,.toggle[aria-checked=true]{background-image:none;--handleoffsetcalculator: var(--handleoffset);--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:checked,[dir=rtl] .toggle[aria-checked=true]{--handleoffsetcalculator: calc(var(--handleoffset) * -1)}.toggle:indeterminate{--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));box-shadow:calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset}[dir=rtl] .toggle:indeterminate{box-shadow:calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset}.toggle:disabled{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));background-color:transparent;opacity:.3;--togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset, var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset}.artboard.phone{width:320px}.badge-sm{height:1rem;font-size:.75rem;line-height:1rem;padding-left:.438rem;padding-right:.438rem}.badge-lg{height:1.5rem;font-size:1rem;line-height:1.5rem;padding-left:.688rem;padding-right:.688rem}.btm-nav-xs>*:where(.active){border-top-width:1px}.btm-nav-sm>*:where(.active){border-top-width:2px}.btm-nav-md>*:where(.active){border-top-width:2px}.btm-nav-lg>*:where(.active){border-top-width:4px}.btn-xs{height:1.5rem;min-height:1.5rem;padding-left:.5rem;padding-right:.5rem;font-size:.75rem}.btn-sm{height:2rem;min-height:2rem;padding-left:.75rem;padding-right:.75rem;font-size:.875rem}.btn-square:where(.btn-xs){height:1.5rem;width:1.5rem;padding:0}.btn-square:where(.btn-sm){height:2rem;width:2rem;padding:0}.btn-circle:where(.btn-xs){height:1.5rem;width:1.5rem;border-radius:9999px;padding:0}.btn-circle:where(.btn-sm){height:2rem;width:2rem;border-radius:9999px;padding:0}.input-sm{height:2rem;padding-left:.75rem;padding-right:.75rem;font-size:.875rem;line-height:2rem}.join.join-vertical{flex-direction:column}.join.join-vertical .join-item:first-child:not(:last-child),.join.join-vertical *:first-child:not(:last-child) .join-item{border-end-start-radius:0;border-end-end-radius:0;border-start-start-radius:inherit;border-start-end-radius:inherit}.join.join-vertical .join-item:last-child:not(:first-child),.join.join-vertical *:last-child:not(:first-child) .join-item{border-start-start-radius:0;border-start-end-radius:0;border-end-start-radius:inherit;border-end-end-radius:inherit}.join.join-horizontal{flex-direction:row}.join.join-horizontal .join-item:first-child:not(:last-child),.join.join-horizontal *:first-child:not(:last-child) .join-item{border-end-end-radius:0;border-start-end-radius:0;border-end-start-radius:inherit;border-start-start-radius:inherit}.join.join-horizontal .join-item:last-child:not(:first-child),.join.join-horizontal *:last-child:not(:first-child) .join-item{border-end-start-radius:0;border-start-start-radius:0;border-end-end-radius:inherit;border-start-end-radius:inherit}.select-sm{height:2rem;min-height:2rem;padding-left:.75rem;padding-right:2rem;font-size:.875rem;line-height:2rem}[dir=rtl] .select-sm{padding-left:2rem;padding-right:.75rem}.steps-horizontal .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-rows:repeat(2,minmax(0,1fr));place-items:center;text-align:center}.steps-vertical .step{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(1,minmax(0,1fr))}.card-compact .card-body{padding:1rem;font-size:.875rem;line-height:1.25rem}.card-compact .card-title{margin-bottom:.25rem}.card-normal .card-body{padding:var(--padding-card, 2rem);font-size:1rem;line-height:1.5rem}.card-normal .card-title{margin-bottom:.75rem}.join.join-vertical>:where(*:not(:first-child)){margin-left:0;margin-right:0;margin-top:-1px}.join.join-vertical>:where(*:not(:first-child)):is(.btn){margin-top:calc(var(--border-btn) * -1)}.join.join-horizontal>:where(*:not(:first-child)){margin-top:0;margin-bottom:0;margin-inline-start:-1px}.join.join-horizontal>:where(*:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn) * -1);margin-top:0}.steps-horizontal .step{grid-template-rows:40px 1fr;grid-template-columns:auto;min-width:4rem}.steps-horizontal .step:before{height:.5rem;width:100%;--tw-translate-x: 0px;--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));content:"";margin-inline-start:-100%}.steps-horizontal .step:where([dir=rtl],[dir=rtl] *):before{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.steps-vertical .step{gap:.5rem;grid-template-columns:40px 1fr;grid-template-rows:auto;min-height:4rem;justify-items:start}.steps-vertical .step:before{height:100%;width:.5rem;--tw-translate-x: -50%;--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));margin-inline-start:50%}.steps-vertical .step:where([dir=rtl],[dir=rtl] *):before{--tw-translate-x: 50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.table-xs :not(thead):not(tfoot) tr{font-size:.75rem;line-height:1rem}.table-xs :where(th,td){padding:.25rem .5rem}.table-sm :not(thead):not(tfoot) tr{font-size:.875rem;line-height:1.25rem}.table-sm :where(th,td){padding:.5rem .75rem}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.left-3{left:.75rem}.right-0{right:0}.right-2{right:.5rem}.top-1\/2{top:50%}.top-2{top:.5rem}.z-10{z-index:10}.z-50{z-index:50}.col-span-2{grid-column:span 2 / span 2}.-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-\[120px\]{width:120px}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[200px\]{min-width:200px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[100px\]{max-width:100px}.max-w-\[120px\]{max-width:120px}.max-w-\[150px\]{max-width:150px}.max-w-\[200px\]{max-width:200px}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(243 244 246 / var(--tw-divide-opacity, 1))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-amber-500{--tw-border-opacity: 1;border-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}.border-blue-700{--tw-border-opacity: 1;border-color:rgb(29 78 216 / var(--tw-border-opacity, 1))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.border-yellow-400{--tw-border-opacity: 1;border-color:rgb(250 204 21 / var(--tw-border-opacity, 1))}.border-yellow-700{--tw-border-opacity: 1;border-color:rgb(161 98 7 / var(--tw-border-opacity, 1))}.border-t-blue-600{--tw-border-opacity: 1;border-top-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}.border-t-transparent{border-top-color:transparent}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity, 1))}.bg-base-100{--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity, 1)))}.bg-base-200{--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity, 1)))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-cyan-50{--tw-bg-opacity: 1;background-color:rgb(236 254 255 / var(--tw-bg-opacity, 1))}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-emerald-50{--tw-bg-opacity: 1;background-color:rgb(236 253 245 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity, 1))}.bg-indigo-50{--tw-bg-opacity: 1;background-color:rgb(238 242 255 / var(--tw-bg-opacity, 1))}.bg-orange-100{--tw-bg-opacity: 1;background-color:rgb(255 237 213 / var(--tw-bg-opacity, 1))}.bg-orange-50{--tw-bg-opacity: 1;background-color:rgb(255 247 237 / var(--tw-bg-opacity, 1))}.bg-pink-100{--tw-bg-opacity: 1;background-color:rgb(252 231 243 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-600{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.bg-opacity-50{--tw-bg-opacity: .5}.stroke-current{stroke:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-10{padding-left:2.5rem}.pl-12{padding-left:3rem}.pl-16{padding-left:4rem}.pr-3{padding-right:.75rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-cyan-600{--tw-text-opacity: 1;color:rgb(8 145 178 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-error{--tw-text-opacity: 1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity, 1)))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity, 1))}.text-indigo-700{--tw-text-opacity: 1;color:rgb(67 56 202 / var(--tw-text-opacity, 1))}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-orange-700{--tw-text-opacity: 1;color:rgb(194 65 12 / var(--tw-text-opacity, 1))}.text-orange-800{--tw-text-opacity: 1;color:rgb(154 52 18 / var(--tw-text-opacity, 1))}.text-pink-700{--tw-text-opacity: 1;color:rgb(190 24 93 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity, 1)))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-purple-700{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.text-success{--tw-text-opacity: 1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity, 1)))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.opacity-60{opacity:.6}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.hover\:border-blue-300:hover{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.hover\:border-blue-500:hover{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.hover\:border-orange-300:hover{--tw-border-opacity: 1;border-color:rgb(253 186 116 / var(--tw-border-opacity, 1))}.hover\:border-purple-300:hover{--tw-border-opacity: 1;border-color:rgb(216 180 254 / var(--tw-border-opacity, 1))}.hover\:bg-amber-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.hover\:bg-amber-700:hover{--tw-bg-opacity: 1;background-color:rgb(180 83 9 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-100:hover{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-yellow-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.hover\:bg-yellow-700:hover{--tw-bg-opacity: 1;background-color:rgb(161 98 7 / var(--tw-bg-opacity, 1))}.hover\:text-blue-700:hover{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-indigo-700:hover{--tw-text-opacity: 1;color:rgb(67 56 202 / var(--tw-text-opacity, 1))}.hover\:text-red-800:hover{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.hover\:text-yellow-800:hover{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}} diff --git a/backend/public/app/assets/index-TpSq-kLx.js b/backend/public/app/assets/index-TpSq-kLx.js new file mode 100644 index 00000000..ad5095c9 --- /dev/null +++ b/backend/public/app/assets/index-TpSq-kLx.js @@ -0,0 +1,416 @@ +var Pk=Object.defineProperty;var _k=(e,t,r)=>t in e?Pk(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var ho=(e,t,r)=>_k(e,typeof t!="symbol"?t+"":t,r);function Ck(e,t){for(var r=0;rn[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function r(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=r(i);fetch(i.href,s)}})();function Tr(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Sv={exports:{}},kc={},Nv={exports:{}},ie={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ks=Symbol.for("react.element"),Ak=Symbol.for("react.portal"),Ok=Symbol.for("react.fragment"),Ek=Symbol.for("react.strict_mode"),Dk=Symbol.for("react.profiler"),Tk=Symbol.for("react.provider"),Mk=Symbol.for("react.context"),Ik=Symbol.for("react.forward_ref"),$k=Symbol.for("react.suspense"),Lk=Symbol.for("react.memo"),zk=Symbol.for("react.lazy"),dg=Symbol.iterator;function Rk(e){return e===null||typeof e!="object"?null:(e=dg&&e[dg]||e["@@iterator"],typeof e=="function"?e:null)}var kv={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Pv=Object.assign,_v={};function va(e,t,r){this.props=e,this.context=t,this.refs=_v,this.updater=r||kv}va.prototype.isReactComponent={};va.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};va.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Cv(){}Cv.prototype=va.prototype;function Pp(e,t,r){this.props=e,this.context=t,this.refs=_v,this.updater=r||kv}var _p=Pp.prototype=new Cv;_p.constructor=Pp;Pv(_p,va.prototype);_p.isPureReactComponent=!0;var fg=Array.isArray,Av=Object.prototype.hasOwnProperty,Cp={current:null},Ov={key:!0,ref:!0,__self:!0,__source:!0};function Ev(e,t,r){var n,i={},s=null,o=null;if(t!=null)for(n in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(s=""+t.key),t)Av.call(t,n)&&!Ov.hasOwnProperty(n)&&(i[n]=t[n]);var l=arguments.length-2;if(l===1)i.children=r;else if(1>>1,H=O[U];if(0>>1;Ui(we,L))Ai(J,we)?(O[U]=J,O[A]=L,U=A):(O[U]=we,O[re]=L,U=re);else if(Ai(J,L))O[U]=J,O[A]=L,U=A;else break e}}return k}function i(O,k){var L=O.sortIndex-k.sortIndex;return L!==0?L:O.id-k.id}if(typeof performance=="object"&&typeof performance.now=="function"){var s=performance;e.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();e.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,f=null,p=3,m=!1,x=!1,g=!1,b=typeof setTimeout=="function"?setTimeout:null,v=typeof clearTimeout=="function"?clearTimeout:null,j=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(O){for(var k=r(d);k!==null;){if(k.callback===null)n(d);else if(k.startTime<=O)n(d),k.sortIndex=k.expirationTime,t(c,k);else break;k=r(d)}}function w(O){if(g=!1,y(O),!x)if(r(c)!==null)x=!0,E(S);else{var k=r(d);k!==null&&D(w,k.startTime-O)}}function S(O,k){x=!1,g&&(g=!1,v(_),_=-1),m=!0;var L=p;try{for(y(k),f=r(c);f!==null&&(!(f.expirationTime>k)||O&&!M());){var U=f.callback;if(typeof U=="function"){f.callback=null,p=f.priorityLevel;var H=U(f.expirationTime<=k);k=e.unstable_now(),typeof H=="function"?f.callback=H:f===r(c)&&n(c),y(k)}else n(c);f=r(c)}if(f!==null)var te=!0;else{var re=r(d);re!==null&&D(w,re.startTime-k),te=!1}return te}finally{f=null,p=L,m=!1}}var N=!1,P=null,_=-1,T=5,$=-1;function M(){return!(e.unstable_now()-$O||125U?(O.sortIndex=L,t(d,O),r(c)===null&&O===r(d)&&(g?(v(_),_=-1):g=!0,D(w,L-U))):(O.sortIndex=H,t(c,O),x||m||(x=!0,E(S))),O},e.unstable_shouldYield=M,e.unstable_wrapCallback=function(O){var k=p;return function(){var L=p;p=k;try{return O.apply(this,arguments)}finally{p=L}}}})(Lv);$v.exports=Lv;var Zk=$v.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Xk=h,Ft=Zk;function F(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ed=Object.prototype.hasOwnProperty,Jk=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,hg={},mg={};function Qk(e){return Ed.call(mg,e)?!0:Ed.call(hg,e)?!1:Jk.test(e)?mg[e]=!0:(hg[e]=!0,!1)}function eP(e,t,r,n){if(r!==null&&r.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return n?!1:r!==null?!r.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function tP(e,t,r,n){if(t===null||typeof t>"u"||eP(e,t,r,n))return!0;if(n)return!1;if(r!==null)switch(r.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function yt(e,t,r,n,i,s,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=n,this.attributeNamespace=i,this.mustUseProperty=r,this.propertyName=e,this.type=t,this.sanitizeURL=s,this.removeEmptyString=o}var tt={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){tt[e]=new yt(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];tt[t]=new yt(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){tt[e]=new yt(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){tt[e]=new yt(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){tt[e]=new yt(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){tt[e]=new yt(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){tt[e]=new yt(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){tt[e]=new yt(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){tt[e]=new yt(e,5,!1,e.toLowerCase(),null,!1,!1)});var Op=/[\-:]([a-z])/g;function Ep(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Op,Ep);tt[t]=new yt(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Op,Ep);tt[t]=new yt(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Op,Ep);tt[t]=new yt(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){tt[e]=new yt(e,1,!1,e.toLowerCase(),null,!1,!1)});tt.xlinkHref=new yt("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){tt[e]=new yt(e,1,!1,e.toLowerCase(),null,!0,!0)});function Dp(e,t,r,n){var i=tt.hasOwnProperty(t)?tt[t]:null;(i!==null?i.type!==0:n||!(2l||i[o]!==s[l]){var c=` +`+i[o].replace(" at new "," at ");return e.displayName&&c.includes("")&&(c=c.replace("",e.displayName)),c}while(1<=o&&0<=l);break}}}finally{Lu=!1,Error.prepareStackTrace=r}return(e=e?e.displayName||e.name:"")?Ga(e):""}function rP(e){switch(e.tag){case 5:return Ga(e.type);case 16:return Ga("Lazy");case 13:return Ga("Suspense");case 19:return Ga("SuspenseList");case 0:case 2:case 15:return e=zu(e.type,!1),e;case 11:return e=zu(e.type.render,!1),e;case 1:return e=zu(e.type,!0),e;default:return""}}function Id(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Li:return"Fragment";case $i:return"Portal";case Dd:return"Profiler";case Tp:return"StrictMode";case Td:return"Suspense";case Md:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Bv:return(e.displayName||"Context")+".Consumer";case Rv:return(e._context.displayName||"Context")+".Provider";case Mp:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Ip:return t=e.displayName||null,t!==null?t:Id(e.type)||"Memo";case mn:t=e._payload,e=e._init;try{return Id(e(t))}catch{}}return null}function nP(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Id(t);case 8:return t===Tp?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function In(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Wv(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function iP(e){var t=Wv(e)?"checked":"value",r=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),n=""+e[t];if(!e.hasOwnProperty(t)&&typeof r<"u"&&typeof r.get=="function"&&typeof r.set=="function"){var i=r.get,s=r.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(o){n=""+o,s.call(this,o)}}),Object.defineProperty(e,t,{enumerable:r.enumerable}),{getValue:function(){return n},setValue:function(o){n=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function xo(e){e._valueTracker||(e._valueTracker=iP(e))}function Uv(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var r=t.getValue(),n="";return e&&(n=Wv(e)?e.checked?"true":"false":e.value),e=n,e!==r?(t.setValue(e),!0):!1}function ul(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function $d(e,t){var r=t.checked;return ke({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:r??e._wrapperState.initialChecked})}function xg(e,t){var r=t.defaultValue==null?"":t.defaultValue,n=t.checked!=null?t.checked:t.defaultChecked;r=In(t.value!=null?t.value:r),e._wrapperState={initialChecked:n,initialValue:r,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function qv(e,t){t=t.checked,t!=null&&Dp(e,"checked",t,!1)}function Ld(e,t){qv(e,t);var r=In(t.value),n=t.type;if(r!=null)n==="number"?(r===0&&e.value===""||e.value!=r)&&(e.value=""+r):e.value!==""+r&&(e.value=""+r);else if(n==="submit"||n==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?zd(e,t.type,r):t.hasOwnProperty("defaultValue")&&zd(e,t.type,In(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function yg(e,t,r){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var n=t.type;if(!(n!=="submit"&&n!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,r||t===e.value||(e.value=t),e.defaultValue=t}r=e.name,r!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,r!==""&&(e.name=r)}function zd(e,t,r){(t!=="number"||ul(e.ownerDocument)!==e)&&(r==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+r&&(e.defaultValue=""+r))}var Za=Array.isArray;function Xi(e,t,r,n){if(e=e.options,t){t={};for(var i=0;i"+t.valueOf().toString()+"",t=yo.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function hs(e,t){if(t){var r=e.firstChild;if(r&&r===e.lastChild&&r.nodeType===3){r.nodeValue=t;return}}e.textContent=t}var ts={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},aP=["Webkit","ms","Moz","O"];Object.keys(ts).forEach(function(e){aP.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),ts[t]=ts[e]})});function Yv(e,t,r){return t==null||typeof t=="boolean"||t===""?"":r||typeof t!="number"||t===0||ts.hasOwnProperty(e)&&ts[e]?(""+t).trim():t+"px"}function Gv(e,t){e=e.style;for(var r in t)if(t.hasOwnProperty(r)){var n=r.indexOf("--")===0,i=Yv(r,t[r],n);r==="float"&&(r="cssFloat"),n?e.setProperty(r,i):e[r]=i}}var sP=ke({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Fd(e,t){if(t){if(sP[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(F(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(F(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(F(61))}if(t.style!=null&&typeof t.style!="object")throw Error(F(62))}}function Wd(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ud=null;function $p(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var qd=null,Ji=null,Qi=null;function jg(e){if(e=Gs(e)){if(typeof qd!="function")throw Error(F(280));var t=e.stateNode;t&&(t=Oc(t),qd(e.stateNode,e.type,t))}}function Zv(e){Ji?Qi?Qi.push(e):Qi=[e]:Ji=e}function Xv(){if(Ji){var e=Ji,t=Qi;if(Qi=Ji=null,jg(e),t)for(e=0;e>>=0,e===0?32:31-(xP(e)/yP|0)|0}var vo=64,bo=4194304;function Xa(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function hl(e,t){var r=e.pendingLanes;if(r===0)return 0;var n=0,i=e.suspendedLanes,s=e.pingedLanes,o=r&268435455;if(o!==0){var l=o&~i;l!==0?n=Xa(l):(s&=o,s!==0&&(n=Xa(s)))}else o=r&~i,o!==0?n=Xa(o):s!==0&&(n=Xa(s));if(n===0)return 0;if(t!==0&&t!==n&&!(t&i)&&(i=n&-n,s=t&-t,i>=s||i===16&&(s&4194240)!==0))return t;if(n&4&&(n|=r&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=n;0r;r++)t.push(e);return t}function Vs(e,t,r){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-hr(t),e[t]=r}function wP(e,t){var r=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var n=e.eventTimes;for(e=e.expirationTimes;0=ns),Og=" ",Eg=!1;function xb(e,t){switch(e){case"keyup":return ZP.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function yb(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var zi=!1;function JP(e,t){switch(e){case"compositionend":return yb(t);case"keypress":return t.which!==32?null:(Eg=!0,Og);case"textInput":return e=t.data,e===Og&&Eg?null:e;default:return null}}function QP(e,t){if(zi)return e==="compositionend"||!qp&&xb(e,t)?(e=mb(),Xo=Fp=wn=null,zi=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=Ig(r)}}function wb(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?wb(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Sb(){for(var e=window,t=ul();t instanceof e.HTMLIFrameElement;){try{var r=typeof t.contentWindow.location.href=="string"}catch{r=!1}if(r)e=t.contentWindow;else break;t=ul(e.document)}return t}function Hp(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function l_(e){var t=Sb(),r=e.focusedElem,n=e.selectionRange;if(t!==r&&r&&r.ownerDocument&&wb(r.ownerDocument.documentElement,r)){if(n!==null&&Hp(r)){if(t=n.start,e=n.end,e===void 0&&(e=t),"selectionStart"in r)r.selectionStart=t,r.selectionEnd=Math.min(e,r.value.length);else if(e=(t=r.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=r.textContent.length,s=Math.min(n.start,i);n=n.end===void 0?s:Math.min(n.end,i),!e.extend&&s>n&&(i=n,n=s,s=i),i=$g(r,s);var o=$g(r,n);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),s>n?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=r;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof r.focus=="function"&&r.focus(),r=0;r=document.documentMode,Ri=null,Zd=null,as=null,Xd=!1;function Lg(e,t,r){var n=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;Xd||Ri==null||Ri!==ul(n)||(n=Ri,"selectionStart"in n&&Hp(n)?n={start:n.selectionStart,end:n.selectionEnd}:(n=(n.ownerDocument&&n.ownerDocument.defaultView||window).getSelection(),n={anchorNode:n.anchorNode,anchorOffset:n.anchorOffset,focusNode:n.focusNode,focusOffset:n.focusOffset}),as&&bs(as,n)||(as=n,n=xl(Zd,"onSelect"),0Wi||(e.current=nf[Wi],nf[Wi]=null,Wi--)}function me(e,t){Wi++,nf[Wi]=e.current,e.current=t}var $n={},ct=Fn($n),kt=Fn(!1),pi=$n;function sa(e,t){var r=e.type.contextTypes;if(!r)return $n;var n=e.stateNode;if(n&&n.__reactInternalMemoizedUnmaskedChildContext===t)return n.__reactInternalMemoizedMaskedChildContext;var i={},s;for(s in r)i[s]=t[s];return n&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Pt(e){return e=e.childContextTypes,e!=null}function vl(){ye(kt),ye(ct)}function qg(e,t,r){if(ct.current!==$n)throw Error(F(168));me(ct,t),me(kt,r)}function Db(e,t,r){var n=e.stateNode;if(t=t.childContextTypes,typeof n.getChildContext!="function")return r;n=n.getChildContext();for(var i in n)if(!(i in t))throw Error(F(108,nP(e)||"Unknown",i));return ke({},r,n)}function bl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||$n,pi=ct.current,me(ct,e),me(kt,kt.current),!0}function Hg(e,t,r){var n=e.stateNode;if(!n)throw Error(F(169));r?(e=Db(e,t,pi),n.__reactInternalMemoizedMergedChildContext=e,ye(kt),ye(ct),me(ct,e)):ye(kt),me(kt,r)}var Lr=null,Ec=!1,Ju=!1;function Tb(e){Lr===null?Lr=[e]:Lr.push(e)}function b_(e){Ec=!0,Tb(e)}function Wn(){if(!Ju&&Lr!==null){Ju=!0;var e=0,t=le;try{var r=Lr;for(le=1;e>=o,i-=o,Br=1<<32-hr(t)+i|r<_?(T=P,P=null):T=P.sibling;var $=p(v,P,y[_],w);if($===null){P===null&&(P=T);break}e&&P&&$.alternate===null&&t(v,P),j=s($,j,_),N===null?S=$:N.sibling=$,N=$,P=T}if(_===y.length)return r(v,P),be&&Gn(v,_),S;if(P===null){for(;__?(T=P,P=null):T=P.sibling;var M=p(v,P,$.value,w);if(M===null){P===null&&(P=T);break}e&&P&&M.alternate===null&&t(v,P),j=s(M,j,_),N===null?S=M:N.sibling=M,N=M,P=T}if($.done)return r(v,P),be&&Gn(v,_),S;if(P===null){for(;!$.done;_++,$=y.next())$=f(v,$.value,w),$!==null&&(j=s($,j,_),N===null?S=$:N.sibling=$,N=$);return be&&Gn(v,_),S}for(P=n(v,P);!$.done;_++,$=y.next())$=m(P,v,_,$.value,w),$!==null&&(e&&$.alternate!==null&&P.delete($.key===null?_:$.key),j=s($,j,_),N===null?S=$:N.sibling=$,N=$);return e&&P.forEach(function(C){return t(v,C)}),be&&Gn(v,_),S}function b(v,j,y,w){if(typeof y=="object"&&y!==null&&y.type===Li&&y.key===null&&(y=y.props.children),typeof y=="object"&&y!==null){switch(y.$$typeof){case go:e:{for(var S=y.key,N=j;N!==null;){if(N.key===S){if(S=y.type,S===Li){if(N.tag===7){r(v,N.sibling),j=i(N,y.props.children),j.return=v,v=j;break e}}else if(N.elementType===S||typeof S=="object"&&S!==null&&S.$$typeof===mn&&Yg(S)===N.type){r(v,N.sibling),j=i(N,y.props),j.ref=Ra(v,N,y),j.return=v,v=j;break e}r(v,N);break}else t(v,N);N=N.sibling}y.type===Li?(j=oi(y.props.children,v.mode,w,y.key),j.return=v,v=j):(w=al(y.type,y.key,y.props,null,v.mode,w),w.ref=Ra(v,j,y),w.return=v,v=w)}return o(v);case $i:e:{for(N=y.key;j!==null;){if(j.key===N)if(j.tag===4&&j.stateNode.containerInfo===y.containerInfo&&j.stateNode.implementation===y.implementation){r(v,j.sibling),j=i(j,y.children||[]),j.return=v,v=j;break e}else{r(v,j);break}else t(v,j);j=j.sibling}j=sd(y,v.mode,w),j.return=v,v=j}return o(v);case mn:return N=y._init,b(v,j,N(y._payload),w)}if(Za(y))return x(v,j,y,w);if(Ma(y))return g(v,j,y,w);_o(v,y)}return typeof y=="string"&&y!==""||typeof y=="number"?(y=""+y,j!==null&&j.tag===6?(r(v,j.sibling),j=i(j,y),j.return=v,v=j):(r(v,j),j=ad(y,v.mode,w),j.return=v,v=j),o(v)):r(v,j)}return b}var la=Lb(!0),zb=Lb(!1),Sl=Fn(null),Nl=null,Hi=null,Gp=null;function Zp(){Gp=Hi=Nl=null}function Xp(e){var t=Sl.current;ye(Sl),e._currentValue=t}function of(e,t,r){for(;e!==null;){var n=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,n!==null&&(n.childLanes|=t)):n!==null&&(n.childLanes&t)!==t&&(n.childLanes|=t),e===r)break;e=e.return}}function ta(e,t){Nl=e,Gp=Hi=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(St=!0),e.firstContext=null)}function tr(e){var t=e._currentValue;if(Gp!==e)if(e={context:e,memoizedValue:t,next:null},Hi===null){if(Nl===null)throw Error(F(308));Hi=e,Nl.dependencies={lanes:0,firstContext:e}}else Hi=Hi.next=e;return t}var ti=null;function Jp(e){ti===null?ti=[e]:ti.push(e)}function Rb(e,t,r,n){var i=t.interleaved;return i===null?(r.next=r,Jp(t)):(r.next=i.next,i.next=r),t.interleaved=r,Zr(e,n)}function Zr(e,t){e.lanes|=t;var r=e.alternate;for(r!==null&&(r.lanes|=t),r=e,e=e.return;e!==null;)e.childLanes|=t,r=e.alternate,r!==null&&(r.childLanes|=t),r=e,e=e.return;return r.tag===3?r.stateNode:null}var gn=!1;function Qp(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Bb(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function qr(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function An(e,t,r){var n=e.updateQueue;if(n===null)return null;if(n=n.shared,ae&2){var i=n.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),n.pending=t,Zr(e,r)}return i=n.interleaved,i===null?(t.next=t,Jp(n)):(t.next=i.next,i.next=t),n.interleaved=t,Zr(e,r)}function Qo(e,t,r){if(t=t.updateQueue,t!==null&&(t=t.shared,(r&4194240)!==0)){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,zp(e,r)}}function Gg(e,t){var r=e.updateQueue,n=e.alternate;if(n!==null&&(n=n.updateQueue,r===n)){var i=null,s=null;if(r=r.firstBaseUpdate,r!==null){do{var o={eventTime:r.eventTime,lane:r.lane,tag:r.tag,payload:r.payload,callback:r.callback,next:null};s===null?i=s=o:s=s.next=o,r=r.next}while(r!==null);s===null?i=s=t:s=s.next=t}else i=s=t;r={baseState:n.baseState,firstBaseUpdate:i,lastBaseUpdate:s,shared:n.shared,effects:n.effects},e.updateQueue=r;return}e=r.lastBaseUpdate,e===null?r.firstBaseUpdate=t:e.next=t,r.lastBaseUpdate=t}function kl(e,t,r,n){var i=e.updateQueue;gn=!1;var s=i.firstBaseUpdate,o=i.lastBaseUpdate,l=i.shared.pending;if(l!==null){i.shared.pending=null;var c=l,d=c.next;c.next=null,o===null?s=d:o.next=d,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,l=u.lastBaseUpdate,l!==o&&(l===null?u.firstBaseUpdate=d:l.next=d,u.lastBaseUpdate=c))}if(s!==null){var f=i.baseState;o=0,u=d=c=null,l=s;do{var p=l.lane,m=l.eventTime;if((n&p)===p){u!==null&&(u=u.next={eventTime:m,lane:0,tag:l.tag,payload:l.payload,callback:l.callback,next:null});e:{var x=e,g=l;switch(p=t,m=r,g.tag){case 1:if(x=g.payload,typeof x=="function"){f=x.call(m,f,p);break e}f=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=g.payload,p=typeof x=="function"?x.call(m,f,p):x,p==null)break e;f=ke({},f,p);break e;case 2:gn=!0}}l.callback!==null&&l.lane!==0&&(e.flags|=64,p=i.effects,p===null?i.effects=[l]:p.push(l))}else m={eventTime:m,lane:p,tag:l.tag,payload:l.payload,callback:l.callback,next:null},u===null?(d=u=m,c=f):u=u.next=m,o|=p;if(l=l.next,l===null){if(l=i.shared.pending,l===null)break;p=l,l=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(!0);if(u===null&&(c=f),i.baseState=c,i.firstBaseUpdate=d,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else s===null&&(i.shared.lanes=0);gi|=o,e.lanes=o,e.memoizedState=f}}function Zg(e,t,r){if(e=t.effects,t.effects=null,e!==null)for(t=0;tr?r:4,e(!0);var n=ed.transition;ed.transition={};try{e(!1),t()}finally{le=r,ed.transition=n}}function n1(){return rr().memoizedState}function N_(e,t,r){var n=En(e);if(r={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null},i1(e))a1(t,r);else if(r=Rb(e,t,r,n),r!==null){var i=gt();mr(r,e,n,i),s1(r,t,n)}}function k_(e,t,r){var n=En(e),i={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null};if(i1(e))a1(t,i);else{var s=e.alternate;if(e.lanes===0&&(s===null||s.lanes===0)&&(s=t.lastRenderedReducer,s!==null))try{var o=t.lastRenderedState,l=s(o,r);if(i.hasEagerState=!0,i.eagerState=l,gr(l,o)){var c=t.interleaved;c===null?(i.next=i,Jp(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}finally{}r=Rb(e,t,i,n),r!==null&&(i=gt(),mr(r,e,n,i),s1(r,t,n))}}function i1(e){var t=e.alternate;return e===Ne||t!==null&&t===Ne}function a1(e,t){ss=_l=!0;var r=e.pending;r===null?t.next=t:(t.next=r.next,r.next=t),e.pending=t}function s1(e,t,r){if(r&4194240){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,zp(e,r)}}var Cl={readContext:tr,useCallback:nt,useContext:nt,useEffect:nt,useImperativeHandle:nt,useInsertionEffect:nt,useLayoutEffect:nt,useMemo:nt,useReducer:nt,useRef:nt,useState:nt,useDebugValue:nt,useDeferredValue:nt,useTransition:nt,useMutableSource:nt,useSyncExternalStore:nt,useId:nt,unstable_isNewReconciler:!1},P_={readContext:tr,useCallback:function(e,t){return jr().memoizedState=[e,t===void 0?null:t],e},useContext:tr,useEffect:Jg,useImperativeHandle:function(e,t,r){return r=r!=null?r.concat([e]):null,tl(4194308,4,Jb.bind(null,t,e),r)},useLayoutEffect:function(e,t){return tl(4194308,4,e,t)},useInsertionEffect:function(e,t){return tl(4,2,e,t)},useMemo:function(e,t){var r=jr();return t=t===void 0?null:t,e=e(),r.memoizedState=[e,t],e},useReducer:function(e,t,r){var n=jr();return t=r!==void 0?r(t):t,n.memoizedState=n.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},n.queue=e,e=e.dispatch=N_.bind(null,Ne,e),[n.memoizedState,e]},useRef:function(e){var t=jr();return e={current:e},t.memoizedState=e},useState:Xg,useDebugValue:oh,useDeferredValue:function(e){return jr().memoizedState=e},useTransition:function(){var e=Xg(!1),t=e[0];return e=S_.bind(null,e[1]),jr().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,r){var n=Ne,i=jr();if(be){if(r===void 0)throw Error(F(407));r=r()}else{if(r=t(),Ke===null)throw Error(F(349));mi&30||qb(n,t,r)}i.memoizedState=r;var s={value:r,getSnapshot:t};return i.queue=s,Jg(Kb.bind(null,n,s,e),[e]),n.flags|=2048,Cs(9,Hb.bind(null,n,s,r,t),void 0,null),r},useId:function(){var e=jr(),t=Ke.identifierPrefix;if(be){var r=Fr,n=Br;r=(n&~(1<<32-hr(n)-1)).toString(32)+r,t=":"+t+"R"+r,r=Ps++,0<\/script>",e=e.removeChild(e.firstChild)):typeof n.is=="string"?e=o.createElement(r,{is:n.is}):(e=o.createElement(r),r==="select"&&(o=e,n.multiple?o.multiple=!0:n.size&&(o.size=n.size))):e=o.createElementNS(e,r),e[Sr]=t,e[Ss]=n,g1(e,t,!1,!1),t.stateNode=e;e:{switch(o=Wd(r,n),r){case"dialog":ge("cancel",e),ge("close",e),i=n;break;case"iframe":case"object":case"embed":ge("load",e),i=n;break;case"video":case"audio":for(i=0;ida&&(t.flags|=128,n=!0,Ba(s,!1),t.lanes=4194304)}else{if(!n)if(e=Pl(o),e!==null){if(t.flags|=128,n=!0,r=e.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),Ba(s,!0),s.tail===null&&s.tailMode==="hidden"&&!o.alternate&&!be)return it(t),null}else 2*Ae()-s.renderingStartTime>da&&r!==1073741824&&(t.flags|=128,n=!0,Ba(s,!1),t.lanes=4194304);s.isBackwards?(o.sibling=t.child,t.child=o):(r=s.last,r!==null?r.sibling=o:t.child=o,s.last=o)}return s.tail!==null?(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=Ae(),t.sibling=null,r=Se.current,me(Se,n?r&1|2:r&1),t):(it(t),null);case 22:case 23:return ph(),n=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==n&&(t.flags|=8192),n&&t.mode&1?It&1073741824&&(it(t),t.subtreeFlags&6&&(t.flags|=8192)):it(t),null;case 24:return null;case 25:return null}throw Error(F(156,t.tag))}function M_(e,t){switch(Vp(t),t.tag){case 1:return Pt(t.type)&&vl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ca(),ye(kt),ye(ct),rh(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return th(t),null;case 13:if(ye(Se),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(F(340));oa()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ye(Se),null;case 4:return ca(),null;case 10:return Xp(t.type._context),null;case 22:case 23:return ph(),null;case 24:return null;default:return null}}var Ao=!1,st=!1,I_=typeof WeakSet=="function"?WeakSet:Set,K=null;function Ki(e,t){var r=e.ref;if(r!==null)if(typeof r=="function")try{r(null)}catch(n){Pe(e,t,n)}else r.current=null}function gf(e,t,r){try{r()}catch(n){Pe(e,t,n)}}var cx=!1;function $_(e,t){if(Jd=ml,e=Sb(),Hp(e)){if("selectionStart"in e)var r={start:e.selectionStart,end:e.selectionEnd};else e:{r=(r=e.ownerDocument)&&r.defaultView||window;var n=r.getSelection&&r.getSelection();if(n&&n.rangeCount!==0){r=n.anchorNode;var i=n.anchorOffset,s=n.focusNode;n=n.focusOffset;try{r.nodeType,s.nodeType}catch{r=null;break e}var o=0,l=-1,c=-1,d=0,u=0,f=e,p=null;t:for(;;){for(var m;f!==r||i!==0&&f.nodeType!==3||(l=o+i),f!==s||n!==0&&f.nodeType!==3||(c=o+n),f.nodeType===3&&(o+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break t;if(p===r&&++d===i&&(l=o),p===s&&++u===n&&(c=o),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}r=l===-1||c===-1?null:{start:l,end:c}}else r=null}r=r||{start:0,end:0}}else r=null;for(Qd={focusedElem:e,selectionRange:r},ml=!1,K=t;K!==null;)if(t=K,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,K=e;else for(;K!==null;){t=K;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var g=x.memoizedProps,b=x.memoizedState,v=t.stateNode,j=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:cr(t.type,g),b);v.__reactInternalSnapshotBeforeUpdate=j}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent="":y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(F(163))}}catch(w){Pe(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,K=e;break}K=t.return}return x=cx,cx=!1,x}function os(e,t,r){var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var i=n=n.next;do{if((i.tag&e)===e){var s=i.destroy;i.destroy=void 0,s!==void 0&&gf(t,r,s)}i=i.next}while(i!==n)}}function Mc(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var r=t=t.next;do{if((r.tag&e)===e){var n=r.create;r.destroy=n()}r=r.next}while(r!==t)}}function xf(e){var t=e.ref;if(t!==null){var r=e.stateNode;switch(e.tag){case 5:e=r;break;default:e=r}typeof t=="function"?t(e):t.current=e}}function v1(e){var t=e.alternate;t!==null&&(e.alternate=null,v1(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Sr],delete t[Ss],delete t[rf],delete t[y_],delete t[v_])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function b1(e){return e.tag===5||e.tag===3||e.tag===4}function ux(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||b1(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function yf(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.nodeType===8?r.parentNode.insertBefore(e,t):r.insertBefore(e,t):(r.nodeType===8?(t=r.parentNode,t.insertBefore(e,r)):(t=r,t.appendChild(e)),r=r._reactRootContainer,r!=null||t.onclick!==null||(t.onclick=yl));else if(n!==4&&(e=e.child,e!==null))for(yf(e,t,r),e=e.sibling;e!==null;)yf(e,t,r),e=e.sibling}function vf(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.insertBefore(e,t):r.appendChild(e);else if(n!==4&&(e=e.child,e!==null))for(vf(e,t,r),e=e.sibling;e!==null;)vf(e,t,r),e=e.sibling}var Xe=null,ur=!1;function hn(e,t,r){for(r=r.child;r!==null;)j1(e,t,r),r=r.sibling}function j1(e,t,r){if(kr&&typeof kr.onCommitFiberUnmount=="function")try{kr.onCommitFiberUnmount(Pc,r)}catch{}switch(r.tag){case 5:st||Ki(r,t);case 6:var n=Xe,i=ur;Xe=null,hn(e,t,r),Xe=n,ur=i,Xe!==null&&(ur?(e=Xe,r=r.stateNode,e.nodeType===8?e.parentNode.removeChild(r):e.removeChild(r)):Xe.removeChild(r.stateNode));break;case 18:Xe!==null&&(ur?(e=Xe,r=r.stateNode,e.nodeType===8?Xu(e.parentNode,r):e.nodeType===1&&Xu(e,r),ys(e)):Xu(Xe,r.stateNode));break;case 4:n=Xe,i=ur,Xe=r.stateNode.containerInfo,ur=!0,hn(e,t,r),Xe=n,ur=i;break;case 0:case 11:case 14:case 15:if(!st&&(n=r.updateQueue,n!==null&&(n=n.lastEffect,n!==null))){i=n=n.next;do{var s=i,o=s.destroy;s=s.tag,o!==void 0&&(s&2||s&4)&&gf(r,t,o),i=i.next}while(i!==n)}hn(e,t,r);break;case 1:if(!st&&(Ki(r,t),n=r.stateNode,typeof n.componentWillUnmount=="function"))try{n.props=r.memoizedProps,n.state=r.memoizedState,n.componentWillUnmount()}catch(l){Pe(r,t,l)}hn(e,t,r);break;case 21:hn(e,t,r);break;case 22:r.mode&1?(st=(n=st)||r.memoizedState!==null,hn(e,t,r),st=n):hn(e,t,r);break;default:hn(e,t,r)}}function dx(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var r=e.stateNode;r===null&&(r=e.stateNode=new I_),t.forEach(function(n){var i=H_.bind(null,e,n);r.has(n)||(r.add(n),n.then(i,i))})}}function lr(e,t){var r=t.deletions;if(r!==null)for(var n=0;ni&&(i=o),n&=~s}if(n=i,n=Ae()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*z_(n/1960))-n,10e?16:e,Sn===null)var n=!1;else{if(e=Sn,Sn=null,El=0,ae&6)throw Error(F(331));var i=ae;for(ae|=4,K=e.current;K!==null;){var s=K,o=s.child;if(K.flags&16){var l=s.deletions;if(l!==null){for(var c=0;cAe()-dh?si(e,0):uh|=r),_t(e,t)}function A1(e,t){t===0&&(e.mode&1?(t=bo,bo<<=1,!(bo&130023424)&&(bo=4194304)):t=1);var r=gt();e=Zr(e,t),e!==null&&(Vs(e,t,r),_t(e,r))}function q_(e){var t=e.memoizedState,r=0;t!==null&&(r=t.retryLane),A1(e,r)}function H_(e,t){var r=0;switch(e.tag){case 13:var n=e.stateNode,i=e.memoizedState;i!==null&&(r=i.retryLane);break;case 19:n=e.stateNode;break;default:throw Error(F(314))}n!==null&&n.delete(t),A1(e,r)}var O1;O1=function(e,t,r){if(e!==null)if(e.memoizedProps!==t.pendingProps||kt.current)St=!0;else{if(!(e.lanes&r)&&!(t.flags&128))return St=!1,D_(e,t,r);St=!!(e.flags&131072)}else St=!1,be&&t.flags&1048576&&Mb(t,wl,t.index);switch(t.lanes=0,t.tag){case 2:var n=t.type;rl(e,t),e=t.pendingProps;var i=sa(t,ct.current);ta(t,r),i=ih(null,t,n,e,i,r);var s=ah();return t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Pt(n)?(s=!0,bl(t)):s=!1,t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,Qp(t),i.updater=Tc,t.stateNode=i,i._reactInternals=t,cf(t,n,e,r),t=ff(null,t,n,!0,s,r)):(t.tag=0,be&&s&&Kp(t),ht(null,t,i,r),t=t.child),t;case 16:n=t.elementType;e:{switch(rl(e,t),e=t.pendingProps,i=n._init,n=i(n._payload),t.type=n,i=t.tag=V_(n),e=cr(n,e),i){case 0:t=df(null,t,n,e,r);break e;case 1:t=sx(null,t,n,e,r);break e;case 11:t=ix(null,t,n,e,r);break e;case 14:t=ax(null,t,n,cr(n.type,e),r);break e}throw Error(F(306,n,""))}return t;case 0:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),df(e,t,n,i,r);case 1:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),sx(e,t,n,i,r);case 3:e:{if(p1(t),e===null)throw Error(F(387));n=t.pendingProps,s=t.memoizedState,i=s.element,Bb(e,t),kl(t,n,null,r);var o=t.memoizedState;if(n=o.element,s.isDehydrated)if(s={element:n,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=s,t.memoizedState=s,t.flags&256){i=ua(Error(F(423)),t),t=ox(e,t,n,r,i);break e}else if(n!==i){i=ua(Error(F(424)),t),t=ox(e,t,n,r,i);break e}else for(zt=Cn(t.stateNode.containerInfo.firstChild),Rt=t,be=!0,dr=null,r=zb(t,null,n,r),t.child=r;r;)r.flags=r.flags&-3|4096,r=r.sibling;else{if(oa(),n===i){t=Xr(e,t,r);break e}ht(e,t,n,r)}t=t.child}return t;case 5:return Fb(t),e===null&&sf(t),n=t.type,i=t.pendingProps,s=e!==null?e.memoizedProps:null,o=i.children,ef(n,i)?o=null:s!==null&&ef(n,s)&&(t.flags|=32),f1(e,t),ht(e,t,o,r),t.child;case 6:return e===null&&sf(t),null;case 13:return h1(e,t,r);case 4:return eh(t,t.stateNode.containerInfo),n=t.pendingProps,e===null?t.child=la(t,null,n,r):ht(e,t,n,r),t.child;case 11:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),ix(e,t,n,i,r);case 7:return ht(e,t,t.pendingProps,r),t.child;case 8:return ht(e,t,t.pendingProps.children,r),t.child;case 12:return ht(e,t,t.pendingProps.children,r),t.child;case 10:e:{if(n=t.type._context,i=t.pendingProps,s=t.memoizedProps,o=i.value,me(Sl,n._currentValue),n._currentValue=o,s!==null)if(gr(s.value,o)){if(s.children===i.children&&!kt.current){t=Xr(e,t,r);break e}}else for(s=t.child,s!==null&&(s.return=t);s!==null;){var l=s.dependencies;if(l!==null){o=s.child;for(var c=l.firstContext;c!==null;){if(c.context===n){if(s.tag===1){c=qr(-1,r&-r),c.tag=2;var d=s.updateQueue;if(d!==null){d=d.shared;var u=d.pending;u===null?c.next=c:(c.next=u.next,u.next=c),d.pending=c}}s.lanes|=r,c=s.alternate,c!==null&&(c.lanes|=r),of(s.return,r,t),l.lanes|=r;break}c=c.next}}else if(s.tag===10)o=s.type===t.type?null:s.child;else if(s.tag===18){if(o=s.return,o===null)throw Error(F(341));o.lanes|=r,l=o.alternate,l!==null&&(l.lanes|=r),of(o,r,t),o=s.sibling}else o=s.child;if(o!==null)o.return=s;else for(o=s;o!==null;){if(o===t){o=null;break}if(s=o.sibling,s!==null){s.return=o.return,o=s;break}o=o.return}s=o}ht(e,t,i.children,r),t=t.child}return t;case 9:return i=t.type,n=t.pendingProps.children,ta(t,r),i=tr(i),n=n(i),t.flags|=1,ht(e,t,n,r),t.child;case 14:return n=t.type,i=cr(n,t.pendingProps),i=cr(n.type,i),ax(e,t,n,i,r);case 15:return u1(e,t,t.type,t.pendingProps,r);case 17:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),rl(e,t),t.tag=1,Pt(n)?(e=!0,bl(t)):e=!1,ta(t,r),o1(t,n,i),cf(t,n,i,r),ff(null,t,n,!0,e,r);case 19:return m1(e,t,r);case 22:return d1(e,t,r)}throw Error(F(156,t.tag))};function E1(e,t){return ib(e,t)}function K_(e,t,r,n){this.tag=e,this.key=r,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=n,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Zt(e,t,r,n){return new K_(e,t,r,n)}function mh(e){return e=e.prototype,!(!e||!e.isReactComponent)}function V_(e){if(typeof e=="function")return mh(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Mp)return 11;if(e===Ip)return 14}return 2}function Dn(e,t){var r=e.alternate;return r===null?(r=Zt(e.tag,t,e.key,e.mode),r.elementType=e.elementType,r.type=e.type,r.stateNode=e.stateNode,r.alternate=e,e.alternate=r):(r.pendingProps=t,r.type=e.type,r.flags=0,r.subtreeFlags=0,r.deletions=null),r.flags=e.flags&14680064,r.childLanes=e.childLanes,r.lanes=e.lanes,r.child=e.child,r.memoizedProps=e.memoizedProps,r.memoizedState=e.memoizedState,r.updateQueue=e.updateQueue,t=e.dependencies,r.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},r.sibling=e.sibling,r.index=e.index,r.ref=e.ref,r}function al(e,t,r,n,i,s){var o=2;if(n=e,typeof e=="function")mh(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case Li:return oi(r.children,i,s,t);case Tp:o=8,i|=8;break;case Dd:return e=Zt(12,r,t,i|2),e.elementType=Dd,e.lanes=s,e;case Td:return e=Zt(13,r,t,i),e.elementType=Td,e.lanes=s,e;case Md:return e=Zt(19,r,t,i),e.elementType=Md,e.lanes=s,e;case Fv:return $c(r,i,s,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Rv:o=10;break e;case Bv:o=9;break e;case Mp:o=11;break e;case Ip:o=14;break e;case mn:o=16,n=null;break e}throw Error(F(130,e==null?e:typeof e,""))}return t=Zt(o,r,t,i),t.elementType=e,t.type=n,t.lanes=s,t}function oi(e,t,r,n){return e=Zt(7,e,n,t),e.lanes=r,e}function $c(e,t,r,n){return e=Zt(22,e,n,t),e.elementType=Fv,e.lanes=r,e.stateNode={isHidden:!1},e}function ad(e,t,r){return e=Zt(6,e,null,t),e.lanes=r,e}function sd(e,t,r){return t=Zt(4,e.children!==null?e.children:[],e.key,t),t.lanes=r,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Y_(e,t,r,n,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Bu(0),this.expirationTimes=Bu(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Bu(0),this.identifierPrefix=n,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function gh(e,t,r,n,i,s,o,l,c){return e=new Y_(e,t,r,l,c),t===1?(t=1,s===!0&&(t|=8)):t=0,s=Zt(3,null,null,t),e.current=s,s.stateNode=e,s.memoizedState={element:n,isDehydrated:r,cache:null,transitions:null,pendingSuspenseBoundaries:null},Qp(s),e}function G_(e,t,r){var n=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(I1)}catch(e){console.error(e)}}I1(),Iv.exports=Ut;var bh=Iv.exports,vx=bh;Od.createRoot=vx.createRoot,Od.hydrateRoot=vx.hydrateRoot;/** + * @remix-run/router v1.23.1 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Os(){return Os=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function jh(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function tC(){return Math.random().toString(36).substr(2,8)}function jx(e,t){return{usr:e.state,key:e.key,idx:t}}function Nf(e,t,r,n){return r===void 0&&(r=null),Os({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?wa(t):t,{state:r,key:t&&t.key||n||tC()})}function Ml(e){let{pathname:t="/",search:r="",hash:n=""}=e;return r&&r!=="?"&&(t+=r.charAt(0)==="?"?r:"?"+r),n&&n!=="#"&&(t+=n.charAt(0)==="#"?n:"#"+n),t}function wa(e){let t={};if(e){let r=e.indexOf("#");r>=0&&(t.hash=e.substr(r),e=e.substr(0,r));let n=e.indexOf("?");n>=0&&(t.search=e.substr(n),e=e.substr(0,n)),e&&(t.pathname=e)}return t}function rC(e,t,r,n){n===void 0&&(n={});let{window:i=document.defaultView,v5Compat:s=!1}=n,o=i.history,l=Nn.Pop,c=null,d=u();d==null&&(d=0,o.replaceState(Os({},o.state,{idx:d}),""));function u(){return(o.state||{idx:null}).idx}function f(){l=Nn.Pop;let b=u(),v=b==null?null:b-d;d=b,c&&c({action:l,location:g.location,delta:v})}function p(b,v){l=Nn.Push;let j=Nf(g.location,b,v);d=u()+1;let y=jx(j,d),w=g.createHref(j);try{o.pushState(y,"",w)}catch(S){if(S instanceof DOMException&&S.name==="DataCloneError")throw S;i.location.assign(w)}s&&c&&c({action:l,location:g.location,delta:1})}function m(b,v){l=Nn.Replace;let j=Nf(g.location,b,v);d=u();let y=jx(j,d),w=g.createHref(j);o.replaceState(y,"",w),s&&c&&c({action:l,location:g.location,delta:0})}function x(b){let v=i.location.origin!=="null"?i.location.origin:i.location.href,j=typeof b=="string"?b:Ml(b);return j=j.replace(/ $/,"%20"),Oe(v,"No window.location.(origin|href) available to create URL for href: "+j),new URL(j,v)}let g={get action(){return l},get location(){return e(i,o)},listen(b){if(c)throw new Error("A history only accepts one active listener");return i.addEventListener(bx,f),c=b,()=>{i.removeEventListener(bx,f),c=null}},createHref(b){return t(i,b)},createURL:x,encodeLocation(b){let v=x(b);return{pathname:v.pathname,search:v.search,hash:v.hash}},push:p,replace:m,go(b){return o.go(b)}};return g}var wx;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(wx||(wx={}));function nC(e,t,r){return r===void 0&&(r="/"),iC(e,t,r)}function iC(e,t,r,n){let i=typeof t=="string"?wa(t):t,s=wh(i.pathname||"/",r);if(s==null)return null;let o=$1(e);aC(o);let l=null;for(let c=0;l==null&&c{let c={relativePath:l===void 0?s.path||"":l,caseSensitive:s.caseSensitive===!0,childrenIndex:o,route:s};c.relativePath.startsWith("/")&&(Oe(c.relativePath.startsWith(n),'Absolute route path "'+c.relativePath+'" nested under path '+('"'+n+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),c.relativePath=c.relativePath.slice(n.length));let d=Tn([n,c.relativePath]),u=r.concat(c);s.children&&s.children.length>0&&(Oe(s.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+d+'".')),$1(s.children,t,u,d)),!(s.path==null&&!s.index)&&t.push({path:d,score:fC(d,s.index),routesMeta:u})};return e.forEach((s,o)=>{var l;if(s.path===""||!((l=s.path)!=null&&l.includes("?")))i(s,o);else for(let c of L1(s.path))i(s,o,c)}),t}function L1(e){let t=e.split("/");if(t.length===0)return[];let[r,...n]=t,i=r.endsWith("?"),s=r.replace(/\?$/,"");if(n.length===0)return i?[s,""]:[s];let o=L1(n.join("/")),l=[];return l.push(...o.map(c=>c===""?s:[s,c].join("/"))),i&&l.push(...o),l.map(c=>e.startsWith("/")&&c===""?"/":c)}function aC(e){e.sort((t,r)=>t.score!==r.score?r.score-t.score:pC(t.routesMeta.map(n=>n.childrenIndex),r.routesMeta.map(n=>n.childrenIndex)))}const sC=/^:[\w-]+$/,oC=3,lC=2,cC=1,uC=10,dC=-2,Sx=e=>e==="*";function fC(e,t){let r=e.split("/"),n=r.length;return r.some(Sx)&&(n+=dC),t&&(n+=lC),r.filter(i=>!Sx(i)).reduce((i,s)=>i+(sC.test(s)?oC:s===""?cC:uC),n)}function pC(e,t){return e.length===t.length&&e.slice(0,-1).every((n,i)=>n===t[i])?e[e.length-1]-t[t.length-1]:0}function hC(e,t,r){let{routesMeta:n}=e,i={},s="/",o=[];for(let l=0;l{let{paramName:p,isOptional:m}=u;if(p==="*"){let g=l[f]||"";o=s.slice(0,s.length-g.length).replace(/(.)\/+$/,"$1")}const x=l[f];return m&&!x?d[p]=void 0:d[p]=(x||"").replace(/%2F/g,"/"),d},{}),pathname:s,pathnameBase:o,pattern:e}}function gC(e,t,r){t===void 0&&(t=!1),r===void 0&&(r=!0),jh(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let n=[],i="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(o,l,c)=>(n.push({paramName:l,isOptional:c!=null}),c?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(n.push({paramName:"*"}),i+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?i+="\\/*$":e!==""&&e!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,t?void 0:"i"),n]}function xC(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return jh(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function wh(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let r=t.endsWith("/")?t.length-1:t.length,n=e.charAt(r);return n&&n!=="/"?null:e.slice(r)||"/"}const yC=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,vC=e=>yC.test(e);function bC(e,t){t===void 0&&(t="/");let{pathname:r,search:n="",hash:i=""}=typeof e=="string"?wa(e):e,s;if(r)if(vC(r))s=r;else{if(r.includes("//")){let o=r;r=r.replace(/\/\/+/g,"/"),jh(!1,"Pathnames cannot have embedded double slashes - normalizing "+(o+" -> "+r))}r.startsWith("/")?s=Nx(r.substring(1),"/"):s=Nx(r,t)}else s=t;return{pathname:s,search:SC(n),hash:NC(i)}}function Nx(e,t){let r=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(i=>{i===".."?r.length>1&&r.pop():i!=="."&&r.push(i)}),r.length>1?r.join("/"):"/"}function od(e,t,r,n){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(n)+"]. Please separate it out to the ")+("`to."+r+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function jC(e){return e.filter((t,r)=>r===0||t.route.path&&t.route.path.length>0)}function Sh(e,t){let r=jC(e);return t?r.map((n,i)=>i===r.length-1?n.pathname:n.pathnameBase):r.map(n=>n.pathnameBase)}function Nh(e,t,r,n){n===void 0&&(n=!1);let i;typeof e=="string"?i=wa(e):(i=Os({},e),Oe(!i.pathname||!i.pathname.includes("?"),od("?","pathname","search",i)),Oe(!i.pathname||!i.pathname.includes("#"),od("#","pathname","hash",i)),Oe(!i.search||!i.search.includes("#"),od("#","search","hash",i)));let s=e===""||i.pathname==="",o=s?"/":i.pathname,l;if(o==null)l=r;else{let f=t.length-1;if(!n&&o.startsWith("..")){let p=o.split("/");for(;p[0]==="..";)p.shift(),f-=1;i.pathname=p.join("/")}l=f>=0?t[f]:"/"}let c=bC(i,l),d=o&&o!=="/"&&o.endsWith("/"),u=(s||o===".")&&r.endsWith("/");return!c.pathname.endsWith("/")&&(d||u)&&(c.pathname+="/"),c}const Tn=e=>e.join("/").replace(/\/\/+/g,"/"),wC=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),SC=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,NC=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function kC(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const z1=["post","put","patch","delete"];new Set(z1);const PC=["get",...z1];new Set(PC);/** + * React Router v6.30.2 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Es(){return Es=Object.assign?Object.assign.bind():function(e){for(var t=1;t{l.current=!0}),h.useCallback(function(d,u){if(u===void 0&&(u={}),!l.current)return;if(typeof d=="number"){n.go(d);return}let f=Nh(d,JSON.parse(o),s,u.relative==="path");e==null&&t!=="/"&&(f.pathname=f.pathname==="/"?t:Tn([t,f.pathname])),(u.replace?n.replace:n.push)(f,u.state,u)},[t,n,o,s,e])}function Na(){let{matches:e}=h.useContext(on),t=e[e.length-1];return t?t.params:{}}function F1(e,t){let{relative:r}=t===void 0?{}:t,{future:n}=h.useContext(Un),{matches:i}=h.useContext(on),{pathname:s}=_i(),o=JSON.stringify(Sh(i,n.v7_relativeSplatPath));return h.useMemo(()=>Nh(e,JSON.parse(o),s,r==="path"),[e,o,s,r])}function OC(e,t){return EC(e,t)}function EC(e,t,r,n){Sa()||Oe(!1);let{navigator:i}=h.useContext(Un),{matches:s}=h.useContext(on),o=s[s.length-1],l=o?o.params:{};o&&o.pathname;let c=o?o.pathnameBase:"/";o&&o.route;let d=_i(),u;if(t){var f;let b=typeof t=="string"?wa(t):t;c==="/"||(f=b.pathname)!=null&&f.startsWith(c)||Oe(!1),u=b}else u=d;let p=u.pathname||"/",m=p;if(c!=="/"){let b=c.replace(/^\//,"").split("/");m="/"+p.replace(/^\//,"").split("/").slice(b.length).join("/")}let x=nC(e,{pathname:m}),g=$C(x&&x.map(b=>Object.assign({},b,{params:Object.assign({},l,b.params),pathname:Tn([c,i.encodeLocation?i.encodeLocation(b.pathname).pathname:b.pathname]),pathnameBase:b.pathnameBase==="/"?c:Tn([c,i.encodeLocation?i.encodeLocation(b.pathnameBase).pathname:b.pathnameBase])})),s,r,n);return t&&g?h.createElement(Fc.Provider,{value:{location:Es({pathname:"/",search:"",hash:"",state:null,key:"default"},u),navigationType:Nn.Pop}},g):g}function DC(){let e=BC(),t=kC(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,i={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return h.createElement(h.Fragment,null,h.createElement("h2",null,"Unexpected Application Error!"),h.createElement("h3",{style:{fontStyle:"italic"}},t),r?h.createElement("pre",{style:i},r):null,null)}const TC=h.createElement(DC,null);class MC extends h.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,r){return r.location!==t.location||r.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:r.error,location:r.location,revalidation:t.revalidation||r.revalidation}}componentDidCatch(t,r){console.error("React Router caught the following error during render",t,r)}render(){return this.state.error!==void 0?h.createElement(on.Provider,{value:this.props.routeContext},h.createElement(R1.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function IC(e){let{routeContext:t,match:r,children:n}=e,i=h.useContext(kh);return i&&i.static&&i.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(i.staticContext._deepestRenderedBoundaryId=r.route.id),h.createElement(on.Provider,{value:t},n)}function $C(e,t,r,n){var i;if(t===void 0&&(t=[]),r===void 0&&(r=null),n===void 0&&(n=null),e==null){var s;if(!r)return null;if(r.errors)e=r.matches;else if((s=n)!=null&&s.v7_partialHydration&&t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let o=e,l=(i=r)==null?void 0:i.errors;if(l!=null){let u=o.findIndex(f=>f.route.id&&(l==null?void 0:l[f.route.id])!==void 0);u>=0||Oe(!1),o=o.slice(0,Math.min(o.length,u+1))}let c=!1,d=-1;if(r&&n&&n.v7_partialHydration)for(let u=0;u=0?o=o.slice(0,d+1):o=[o[0]];break}}}return o.reduceRight((u,f,p)=>{let m,x=!1,g=null,b=null;r&&(m=l&&f.route.id?l[f.route.id]:void 0,g=f.route.errorElement||TC,c&&(d<0&&p===0?(WC("route-fallback"),x=!0,b=null):d===p&&(x=!0,b=f.route.hydrateFallbackElement||null)));let v=t.concat(o.slice(0,p+1)),j=()=>{let y;return m?y=g:x?y=b:f.route.Component?y=h.createElement(f.route.Component,null):f.route.element?y=f.route.element:y=u,h.createElement(IC,{match:f,routeContext:{outlet:u,matches:v,isDataRoute:r!=null},children:y})};return r&&(f.route.ErrorBoundary||f.route.errorElement||p===0)?h.createElement(MC,{location:r.location,revalidation:r.revalidation,component:g,error:m,children:j(),routeContext:{outlet:null,matches:v,isDataRoute:!0}}):j()},null)}var W1=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(W1||{}),U1=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(U1||{});function LC(e){let t=h.useContext(kh);return t||Oe(!1),t}function zC(e){let t=h.useContext(_C);return t||Oe(!1),t}function RC(e){let t=h.useContext(on);return t||Oe(!1),t}function q1(e){let t=RC(),r=t.matches[t.matches.length-1];return r.route.id||Oe(!1),r.route.id}function BC(){var e;let t=h.useContext(R1),r=zC(),n=q1();return t!==void 0?t:(e=r.errors)==null?void 0:e[n]}function FC(){let{router:e}=LC(W1.UseNavigateStable),t=q1(U1.UseNavigateStable),r=h.useRef(!1);return B1(()=>{r.current=!0}),h.useCallback(function(i,s){s===void 0&&(s={}),r.current&&(typeof i=="number"?e.navigate(i):e.navigate(i,Es({fromRouteId:t},s)))},[e,t])}const kx={};function WC(e,t,r){kx[e]||(kx[e]=!0)}function UC(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function H1(e){let{to:t,replace:r,state:n,relative:i}=e;Sa()||Oe(!1);let{future:s,static:o}=h.useContext(Un),{matches:l}=h.useContext(on),{pathname:c}=_i(),d=dt(),u=Nh(t,Sh(l,s.v7_relativeSplatPath),c,i==="path"),f=JSON.stringify(u);return h.useEffect(()=>d(JSON.parse(f),{replace:r,state:n,relative:i}),[d,f,i,r,n]),null}function de(e){Oe(!1)}function qC(e){let{basename:t="/",children:r=null,location:n,navigationType:i=Nn.Pop,navigator:s,static:o=!1,future:l}=e;Sa()&&Oe(!1);let c=t.replace(/^\/*/,"/"),d=h.useMemo(()=>({basename:c,navigator:s,static:o,future:Es({v7_relativeSplatPath:!1},l)}),[c,l,s,o]);typeof n=="string"&&(n=wa(n));let{pathname:u="/",search:f="",hash:p="",state:m=null,key:x="default"}=n,g=h.useMemo(()=>{let b=wh(u,c);return b==null?null:{location:{pathname:b,search:f,hash:p,state:m,key:x},navigationType:i}},[c,u,f,p,m,x,i]);return g==null?null:h.createElement(Un.Provider,{value:d},h.createElement(Fc.Provider,{children:r,value:g}))}function HC(e){let{children:t,location:r}=e;return OC(kf(t),r)}new Promise(()=>{});function kf(e,t){t===void 0&&(t=[]);let r=[];return h.Children.forEach(e,(n,i)=>{if(!h.isValidElement(n))return;let s=[...t,i];if(n.type===h.Fragment){r.push.apply(r,kf(n.props.children,s));return}n.type!==de&&Oe(!1),!n.props.index||!n.props.children||Oe(!1);let o={id:n.props.id||s.join("-"),caseSensitive:n.props.caseSensitive,element:n.props.element,Component:n.props.Component,index:n.props.index,path:n.props.path,loader:n.props.loader,action:n.props.action,errorElement:n.props.errorElement,ErrorBoundary:n.props.ErrorBoundary,hasErrorBoundary:n.props.ErrorBoundary!=null||n.props.errorElement!=null,shouldRevalidate:n.props.shouldRevalidate,handle:n.props.handle,lazy:n.props.lazy};n.props.children&&(o.children=kf(n.props.children,s)),r.push(o)}),r}/** + * React Router DOM v6.30.2 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Pf(){return Pf=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(r[i]=e[i]);return r}function VC(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function YC(e,t){return e.button===0&&(!t||t==="_self")&&!VC(e)}function _f(e){return e===void 0&&(e=""),new URLSearchParams(typeof e=="string"||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,r)=>{let n=e[r];return t.concat(Array.isArray(n)?n.map(i=>[r,i]):[[r,n]])},[]))}function GC(e,t){let r=_f(e);return t&&t.forEach((n,i)=>{r.has(i)||t.getAll(i).forEach(s=>{r.append(i,s)})}),r}const ZC=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],XC="6";try{window.__reactRouterVersion=XC}catch{}const JC="startTransition",Px=Tv[JC];function QC(e){let{basename:t,children:r,future:n,window:i}=e,s=h.useRef();s.current==null&&(s.current=eC({window:i,v5Compat:!0}));let o=s.current,[l,c]=h.useState({action:o.action,location:o.location}),{v7_startTransition:d}=n||{},u=h.useCallback(f=>{d&&Px?Px(()=>c(f)):c(f)},[c,d]);return h.useLayoutEffect(()=>o.listen(u),[o,u]),h.useEffect(()=>UC(n),[n]),h.createElement(qC,{basename:t,children:r,location:l.location,navigationType:l.action,navigator:o,future:n})}const eA=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",tA=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,K1=h.forwardRef(function(t,r){let{onClick:n,relative:i,reloadDocument:s,replace:o,state:l,target:c,to:d,preventScrollReset:u,viewTransition:f}=t,p=KC(t,ZC),{basename:m}=h.useContext(Un),x,g=!1;if(typeof d=="string"&&tA.test(d)&&(x=d,eA))try{let y=new URL(window.location.href),w=d.startsWith("//")?new URL(y.protocol+d):new URL(d),S=wh(w.pathname,m);w.origin===y.origin&&S!=null?d=S+w.search+w.hash:g=!0}catch{}let b=CC(d,{relative:i}),v=rA(d,{replace:o,state:l,target:c,preventScrollReset:u,relative:i,viewTransition:f});function j(y){n&&n(y),y.defaultPrevented||v(y)}return h.createElement("a",Pf({},p,{href:x||b,onClick:g||s?n:j,ref:r,target:c}))});var _x;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(_x||(_x={}));var Cx;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(Cx||(Cx={}));function rA(e,t){let{target:r,replace:n,state:i,preventScrollReset:s,relative:o,viewTransition:l}=t===void 0?{}:t,c=dt(),d=_i(),u=F1(e,{relative:o});return h.useCallback(f=>{if(YC(f,r)){f.preventDefault();let p=n!==void 0?n:Ml(d)===Ml(u);c(e,{replace:p,state:i,preventScrollReset:s,relative:o,viewTransition:l})}},[d,c,u,n,i,r,e,s,o,l])}function nA(e){let t=h.useRef(_f(e)),r=h.useRef(!1),n=_i(),i=h.useMemo(()=>GC(n.search,r.current?null:t.current),[n.search]),s=dt(),o=h.useCallback((l,c)=>{const d=_f(typeof l=="function"?l(i):l);r.current=!0,s("?"+d,c)},[s,i]);return[i,o]}const iA={},Ax=e=>{let t;const r=new Set,n=(u,f)=>{const p=typeof u=="function"?u(t):u;if(!Object.is(p,t)){const m=t;t=f??(typeof p!="object"||p===null)?p:Object.assign({},t,p),r.forEach(x=>x(t,m))}},i=()=>t,c={setState:n,getState:i,getInitialState:()=>d,subscribe:u=>(r.add(u),()=>r.delete(u)),destroy:()=>{(iA?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),r.clear()}},d=t=e(n,i,c);return c},aA=e=>e?Ax(e):Ax;var V1={exports:{}},Y1={},G1={exports:{}},Z1={};/** + * @license React + * use-sync-external-store-shim.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var fa=h;function sA(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var oA=typeof Object.is=="function"?Object.is:sA,lA=fa.useState,cA=fa.useEffect,uA=fa.useLayoutEffect,dA=fa.useDebugValue;function fA(e,t){var r=t(),n=lA({inst:{value:r,getSnapshot:t}}),i=n[0].inst,s=n[1];return uA(function(){i.value=r,i.getSnapshot=t,ld(i)&&s({inst:i})},[e,r,t]),cA(function(){return ld(i)&&s({inst:i}),e(function(){ld(i)&&s({inst:i})})},[e]),dA(r),r}function ld(e){var t=e.getSnapshot;e=e.value;try{var r=t();return!oA(e,r)}catch{return!0}}function pA(e,t){return t()}var hA=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?pA:fA;Z1.useSyncExternalStore=fa.useSyncExternalStore!==void 0?fa.useSyncExternalStore:hA;G1.exports=Z1;var mA=G1.exports;/** + * @license React + * use-sync-external-store-shim/with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Wc=h,gA=mA;function xA(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var yA=typeof Object.is=="function"?Object.is:xA,vA=gA.useSyncExternalStore,bA=Wc.useRef,jA=Wc.useEffect,wA=Wc.useMemo,SA=Wc.useDebugValue;Y1.useSyncExternalStoreWithSelector=function(e,t,r,n,i){var s=bA(null);if(s.current===null){var o={hasValue:!1,value:null};s.current=o}else o=s.current;s=wA(function(){function c(m){if(!d){if(d=!0,u=m,m=n(m),i!==void 0&&o.hasValue){var x=o.value;if(i(x,m))return f=x}return f=m}if(x=f,yA(u,m))return x;var g=n(m);return i!==void 0&&i(x,g)?(u=m,x):(u=m,f=g)}var d=!1,u,f,p=r===void 0?null:r;return[function(){return c(t())},p===null?void 0:function(){return c(p())}]},[t,r,n,i]);var l=vA(e,s[0],s[1]);return jA(function(){o.hasValue=!0,o.value=l},[l]),SA(l),l};V1.exports=Y1;var X1=V1.exports;const NA=Tr(X1),J1={},{useDebugValue:kA}=fs,{useSyncExternalStoreWithSelector:PA}=NA;let Ox=!1;const _A=e=>e;function CA(e,t=_A,r){(J1?"production":void 0)!=="production"&&r&&!Ox&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),Ox=!0);const n=PA(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,r);return kA(n),n}const Ex=e=>{(J1?"production":void 0)!=="production"&&typeof e!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const t=typeof e=="function"?aA(e):e,r=(n,i)=>CA(t,n,i);return Object.assign(r,t),r},AA=e=>e?Ex(e):Ex,OA="https://dispos.crawlsy.com";class EA{constructor(t){ho(this,"baseUrl");this.baseUrl=t}getHeaders(){const t=localStorage.getItem("token");return{"Content-Type":"application/json",...t?{Authorization:`Bearer ${t}`}:{}}}async request(t,r){const n=`${this.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...this.getHeaders(),...r==null?void 0:r.headers}});if(!i.ok){const s=await i.json().catch(()=>({error:"Request failed"}));throw new Error(s.error||`HTTP ${i.status}`)}return i.json()}async login(t,r){return this.request("/api/auth/login",{method:"POST",body:JSON.stringify({email:t,password:r})})}async getMe(){return this.request("/api/auth/me")}async getDashboardStats(){return this.request("/api/dashboard/stats")}async getDashboardActivity(){return this.request("/api/dashboard/activity")}async getStores(){return this.request("/api/stores")}async getStore(t){return this.request(`/api/stores/${t}`)}async createStore(t){return this.request("/api/stores",{method:"POST",body:JSON.stringify(t)})}async updateStore(t,r){return this.request(`/api/stores/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getDispensaries(){return this.request("/api/dispensaries")}async getDispensary(t){return this.request(`/api/dispensaries/${t}`)}async updateDispensary(t,r){return this.request(`/api/dispensaries/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteStore(t){return this.request(`/api/stores/${t}`,{method:"DELETE"})}async scrapeStore(t,r,n){return this.request(`/api/stores/${t}/scrape`,{method:"POST",body:JSON.stringify({parallel:r,userAgent:n})})}async downloadStoreImages(t){return this.request(`/api/stores/${t}/download-images`,{method:"POST"})}async discoverStoreCategories(t){return this.request(`/api/stores/${t}/discover-categories`,{method:"POST"})}async debugScrapeStore(t){return this.request(`/api/stores/${t}/debug-scrape`,{method:"POST"})}async getStoreBrands(t){return this.request(`/api/stores/${t}/brands`)}async getStoreSpecials(t,r){const n=r?`?date=${r}`:"";return this.request(`/api/stores/${t}/specials${n}`)}async getCategories(t){const r=t?`?store_id=${t}`:"";return this.request(`/api/categories${r}`)}async getCategoryTree(t){return this.request(`/api/categories/tree?store_id=${t}`)}async getProducts(t){const r=new URLSearchParams(t).toString();return this.request(`/api/products${r?`?${r}`:""}`)}async getProduct(t){return this.request(`/api/products/${t}`)}async getCampaigns(){return this.request("/api/campaigns")}async getCampaign(t){return this.request(`/api/campaigns/${t}`)}async createCampaign(t){return this.request("/api/campaigns",{method:"POST",body:JSON.stringify(t)})}async updateCampaign(t,r){return this.request(`/api/campaigns/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteCampaign(t){return this.request(`/api/campaigns/${t}`,{method:"DELETE"})}async addProductToCampaign(t,r,n){return this.request(`/api/campaigns/${t}/products`,{method:"POST",body:JSON.stringify({product_id:r,display_order:n})})}async removeProductFromCampaign(t,r){return this.request(`/api/campaigns/${t}/products/${r}`,{method:"DELETE"})}async getAnalyticsOverview(t){return this.request(`/api/analytics/overview${t?`?days=${t}`:""}`)}async getProductAnalytics(t,r){return this.request(`/api/analytics/products/${t}${r?`?days=${r}`:""}`)}async getCampaignAnalytics(t,r){return this.request(`/api/analytics/campaigns/${t}${r?`?days=${r}`:""}`)}async getSettings(){return this.request("/api/settings")}async updateSetting(t,r){return this.request(`/api/settings/${t}`,{method:"PUT",body:JSON.stringify({value:r})})}async updateSettings(t){return this.request("/api/settings",{method:"PUT",body:JSON.stringify({settings:t})})}async getProxies(){return this.request("/api/proxies")}async getProxy(t){return this.request(`/api/proxies/${t}`)}async addProxy(t){return this.request("/api/proxies",{method:"POST",body:JSON.stringify(t)})}async addProxiesBulk(t){return this.request("/api/proxies/bulk",{method:"POST",body:JSON.stringify({proxies:t})})}async testProxy(t){return this.request(`/api/proxies/${t}/test`,{method:"POST"})}async testAllProxies(){return this.request("/api/proxies/test-all",{method:"POST"})}async getProxyTestJob(t){return this.request(`/api/proxies/test-job/${t}`)}async getActiveProxyTestJob(){return this.request("/api/proxies/test-job")}async cancelProxyTestJob(t){return this.request(`/api/proxies/test-job/${t}/cancel`,{method:"POST"})}async updateProxy(t,r){return this.request(`/api/proxies/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteProxy(t){return this.request(`/api/proxies/${t}`,{method:"DELETE"})}async updateProxyLocations(){return this.request("/api/proxies/update-locations",{method:"POST"})}async getLogs(t,r,n){const i=new URLSearchParams;return t&&i.append("limit",t.toString()),r&&i.append("level",r),n&&i.append("category",n),this.request(`/api/logs?${i.toString()}`)}async clearLogs(){return this.request("/api/logs",{method:"DELETE"})}async getActiveScrapers(){return this.request("/api/scraper-monitor/active")}async getScraperHistory(t){const r=t?`?store_id=${t}`:"";return this.request(`/api/scraper-monitor/history${r}`)}async getJobStats(t){const r=t?`?dispensary_id=${t}`:"";return this.request(`/api/scraper-monitor/jobs/stats${r}`)}async getActiveJobs(t){const r=t?`?dispensary_id=${t}`:"";return this.request(`/api/scraper-monitor/jobs/active${r}`)}async getRecentJobs(t){const r=new URLSearchParams;t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.dispensaryId&&r.append("dispensary_id",t.dispensaryId.toString()),t!=null&&t.status&&r.append("status",t.status);const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/scraper-monitor/jobs/recent${n}`)}async getWorkerStats(t){const r=t?`?dispensary_id=${t}`:"";return this.request(`/api/scraper-monitor/jobs/workers${r}`)}async getAZMonitorActiveJobs(){return this.request("/api/az/monitor/active-jobs")}async getAZMonitorRecentJobs(t){const r=t?`?limit=${t}`:"";return this.request(`/api/az/monitor/recent-jobs${r}`)}async getAZMonitorErrors(t){const r=new URLSearchParams;t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.hours&&r.append("hours",t.hours.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/monitor/errors${n}`)}async getAZMonitorSummary(){return this.request("/api/az/monitor/summary")}async getChanges(t){const r=t?`?status=${t}`:"";return this.request(`/api/changes${r}`)}async getChangeStats(){return this.request("/api/changes/stats")}async approveChange(t){return this.request(`/api/changes/${t}/approve`,{method:"POST"})}async rejectChange(t,r){return this.request(`/api/changes/${t}/reject`,{method:"POST",body:JSON.stringify({reason:r})})}async getDispensaryProducts(t,r){const n=r?`?category=${r}`:"";return this.request(`/api/dispensaries/${t}/products${n}`)}async getDispensaryBrands(t){return this.request(`/api/dispensaries/${t}/brands`)}async getDispensarySpecials(t){return this.request(`/api/dispensaries/${t}/specials`)}async getApiPermissions(){return this.request("/api/api-permissions")}async getApiPermissionDispensaries(){return this.request("/api/api-permissions/dispensaries")}async createApiPermission(t){return this.request("/api/api-permissions",{method:"POST",body:JSON.stringify(t)})}async updateApiPermission(t,r){return this.request(`/api/api-permissions/${t}`,{method:"PUT",body:JSON.stringify(r)})}async toggleApiPermission(t){return this.request(`/api/api-permissions/${t}/toggle`,{method:"PATCH"})}async deleteApiPermission(t){return this.request(`/api/api-permissions/${t}`,{method:"DELETE"})}async getGlobalSchedule(){return this.request("/api/schedule/global")}async updateGlobalSchedule(t,r){return this.request(`/api/schedule/global/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getStoreSchedules(){return this.request("/api/schedule/stores")}async getStoreSchedule(t){return this.request(`/api/schedule/stores/${t}`)}async updateStoreSchedule(t,r){return this.request(`/api/schedule/stores/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getDispensarySchedules(t){const r=new URLSearchParams;t!=null&&t.state&&r.append("state",t.state),t!=null&&t.search&&r.append("search",t.search);const n=r.toString();return this.request(`/api/schedule/dispensaries${n?`?${n}`:""}`)}async getDispensarySchedule(t){return this.request(`/api/schedule/dispensaries/${t}`)}async updateDispensarySchedule(t,r){return this.request(`/api/schedule/dispensaries/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getDispensaryCrawlJobs(t){const r=t?`?limit=${t}`:"";return this.request(`/api/schedule/dispensary-jobs${r}`)}async triggerDispensaryCrawl(t){return this.request(`/api/schedule/trigger/dispensary/${t}`,{method:"POST"})}async resolvePlatformId(t){return this.request(`/api/schedule/dispensaries/${t}/resolve-platform-id`,{method:"POST"})}async detectMenuType(t){return this.request(`/api/schedule/dispensaries/${t}/detect-menu-type`,{method:"POST"})}async refreshDetection(t){return this.request(`/api/schedule/dispensaries/${t}/refresh-detection`,{method:"POST"})}async toggleDispensarySchedule(t,r){return this.request(`/api/schedule/dispensaries/${t}/toggle-active`,{method:"PUT",body:JSON.stringify({is_active:r})})}async deleteDispensarySchedule(t){return this.request(`/api/schedule/dispensaries/${t}/schedule`,{method:"DELETE"})}async getCrawlJobs(t){const r=t?`?limit=${t}`:"";return this.request(`/api/schedule/jobs${r}`)}async getStoreCrawlJobs(t,r){const n=r?`?limit=${r}`:"";return this.request(`/api/schedule/jobs/store/${t}${n}`)}async cancelCrawlJob(t){return this.request(`/api/schedule/jobs/${t}/cancel`,{method:"POST"})}async triggerStoreCrawl(t){return this.request(`/api/schedule/trigger/store/${t}`,{method:"POST"})}async triggerAllCrawls(){return this.request("/api/schedule/trigger/all",{method:"POST"})}async restartScheduler(){return this.request("/api/schedule/restart",{method:"POST"})}async getVersion(){return this.request("/api/version")}async getDutchieAZDashboard(){return this.request("/api/az/dashboard")}async getDutchieAZSchedules(){return this.request("/api/az/admin/schedules")}async getDutchieAZSchedule(t){return this.request(`/api/az/admin/schedules/${t}`)}async createDutchieAZSchedule(t){return this.request("/api/az/admin/schedules",{method:"POST",body:JSON.stringify(t)})}async updateDutchieAZSchedule(t,r){return this.request(`/api/az/admin/schedules/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteDutchieAZSchedule(t){return this.request(`/api/az/admin/schedules/${t}`,{method:"DELETE"})}async triggerDutchieAZSchedule(t){return this.request(`/api/az/admin/schedules/${t}/trigger`,{method:"POST"})}async initDutchieAZSchedules(){return this.request("/api/az/admin/schedules/init",{method:"POST"})}async getDutchieAZScheduleLogs(t,r,n){const i=new URLSearchParams;r&&i.append("limit",r.toString()),n&&i.append("offset",n.toString());const s=i.toString()?`?${i.toString()}`:"";return this.request(`/api/az/admin/schedules/${t}/logs${s}`)}async getDutchieAZRunLogs(t){const r=new URLSearchParams;t!=null&&t.scheduleId&&r.append("scheduleId",t.scheduleId.toString()),t!=null&&t.jobName&&r.append("jobName",t.jobName),t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.offset&&r.append("offset",t.offset.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/admin/run-logs${n}`)}async getDutchieAZSchedulerStatus(){return this.request("/api/az/admin/scheduler/status")}async startDutchieAZScheduler(){return this.request("/api/az/admin/scheduler/start",{method:"POST"})}async stopDutchieAZScheduler(){return this.request("/api/az/admin/scheduler/stop",{method:"POST"})}async triggerDutchieAZImmediateCrawl(){return this.request("/api/az/admin/scheduler/trigger",{method:"POST"})}async getDutchieAZStores(t){const r=new URLSearchParams;t!=null&&t.city&&r.append("city",t.city),(t==null?void 0:t.hasPlatformId)!==void 0&&r.append("hasPlatformId",String(t.hasPlatformId)),t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.offset&&r.append("offset",t.offset.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/stores${n}`)}async getDutchieAZStore(t){return this.request(`/api/az/stores/${t}`)}async getDutchieAZStoreSummary(t){return this.request(`/api/az/stores/${t}/summary`)}async getDutchieAZStoreProducts(t,r){const n=new URLSearchParams;r!=null&&r.stockStatus&&n.append("stockStatus",r.stockStatus),r!=null&&r.type&&n.append("type",r.type),r!=null&&r.subcategory&&n.append("subcategory",r.subcategory),r!=null&&r.brandName&&n.append("brandName",r.brandName),r!=null&&r.search&&n.append("search",r.search),r!=null&&r.limit&&n.append("limit",r.limit.toString()),r!=null&&r.offset&&n.append("offset",r.offset.toString());const i=n.toString()?`?${n.toString()}`:"";return this.request(`/api/az/stores/${t}/products${i}`)}async getDutchieAZStoreBrands(t){return this.request(`/api/az/stores/${t}/brands`)}async getDutchieAZStoreCategories(t){return this.request(`/api/az/stores/${t}/categories`)}async getDutchieAZBrands(t){const r=new URLSearchParams;t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.offset&&r.append("offset",t.offset.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/brands${n}`)}async getDutchieAZCategories(){return this.request("/api/az/categories")}async getDutchieAZDebugSummary(){return this.request("/api/az/debug/summary")}async getDutchieAZDebugStore(t){return this.request(`/api/az/debug/store/${t}`)}async triggerDutchieAZCrawl(t,r){return this.request(`/api/az/admin/crawl/${t}`,{method:"POST",body:JSON.stringify(r||{})})}async getDetectionStats(){return this.request("/api/az/admin/detection/stats")}async getDispensariesNeedingDetection(t){const r=new URLSearchParams;t!=null&&t.state&&r.append("state",t.state),t!=null&&t.limit&&r.append("limit",t.limit.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/admin/detection/pending${n}`)}async detectDispensary(t){return this.request(`/api/az/admin/detection/detect/${t}`,{method:"POST"})}async detectAllDispensaries(t){return this.request("/api/az/admin/detection/detect-all",{method:"POST",body:JSON.stringify(t||{})})}async triggerMenuDetectionJob(){return this.request("/api/az/admin/detection/trigger",{method:"POST"})}}const B=new EA(OA),Ph=AA(e=>({user:null,token:localStorage.getItem("token"),isAuthenticated:!!localStorage.getItem("token"),login:async(t,r)=>{const n=await B.login(t,r);localStorage.setItem("token",n.token),e({user:n.user,token:n.token,isAuthenticated:!0})},logout:()=>{localStorage.removeItem("token"),e({user:null,token:null,isAuthenticated:!1})},checkAuth:async()=>{try{const t=await B.getMe();e({user:t.user,isAuthenticated:!0})}catch{localStorage.removeItem("token"),e({user:null,token:null,isAuthenticated:!1})}}}));function DA(){const[e,t]=h.useState("admin@example.com"),[r,n]=h.useState("password"),[i,s]=h.useState(""),[o,l]=h.useState(!1),c=dt(),d=Ph(f=>f.login),u=async f=>{f.preventDefault(),s(""),l(!0);try{await d(e,r),c("/")}catch(p){s(p.message||"Login failed")}finally{l(!1)}};return a.jsx("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",minHeight:"100vh",background:"linear-gradient(135deg, #667eea 0%, #764ba2 100%)"},children:a.jsxs("div",{style:{background:"white",padding:"40px",borderRadius:"12px",boxShadow:"0 10px 40px rgba(0,0,0,0.2)",width:"100%",maxWidth:"400px"},children:[a.jsx("h1",{style:{marginBottom:"10px",fontSize:"28px"},children:"Dutchie Menus"}),a.jsx("p",{style:{color:"#666",marginBottom:"30px"},children:"Admin Dashboard"}),i&&a.jsx("div",{style:{background:"#fee",color:"#c33",padding:"12px",borderRadius:"6px",marginBottom:"20px",fontSize:"14px"},children:i}),a.jsxs("form",{onSubmit:u,children:[a.jsxs("div",{style:{marginBottom:"20px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"500"},children:"Email"}),a.jsx("input",{type:"email",value:e,onChange:f=>t(f.target.value),required:!0,style:{width:"100%",padding:"12px",border:"1px solid #ddd",borderRadius:"6px",fontSize:"14px"}})]}),a.jsxs("div",{style:{marginBottom:"25px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"500"},children:"Password"}),a.jsx("input",{type:"password",value:r,onChange:f=>n(f.target.value),required:!0,style:{width:"100%",padding:"12px",border:"1px solid #ddd",borderRadius:"6px",fontSize:"14px"}})]}),a.jsx("button",{type:"submit",disabled:o,style:{width:"100%",padding:"14px",background:o?"#999":"#667eea",color:"white",border:"none",borderRadius:"6px",cursor:o?"not-allowed":"pointer",fontSize:"16px",fontWeight:"500",transition:"background 0.2s"},children:o?"Logging in...":"Login"})]})]})})}/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const TA=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),MA=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),Dx=e=>{const t=MA(e);return t.charAt(0).toUpperCase()+t.slice(1)},Q1=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),IA=e=>{for(const t in e)if(t.startsWith("aria-")||t==="role"||t==="title")return!0};/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var $A={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const LA=h.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,className:i="",children:s,iconNode:o,...l},c)=>h.createElement("svg",{ref:c,...$A,width:t,height:t,stroke:e,strokeWidth:n?Number(r)*24/Number(t):r,className:Q1("lucide",i),...!s&&!IA(l)&&{"aria-hidden":"true"},...l},[...o.map(([d,u])=>h.createElement(d,u)),...Array.isArray(s)?s:[s]]));/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Q=(e,t)=>{const r=h.forwardRef(({className:n,...i},s)=>h.createElement(LA,{ref:s,iconNode:t,className:Q1(`lucide-${TA(Dx(e))}`,`lucide-${e}`,n),...i}));return r.displayName=Dx(e),r};/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zA=[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]],Ds=Q("activity",zA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const RA=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],_h=Q("arrow-left",RA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const BA=[["path",{d:"M10 12h4",key:"a56b0p"}],["path",{d:"M10 8h4",key:"1sr2af"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3",key:"1rgiei"}],["path",{d:"M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2",key:"secmi2"}],["path",{d:"M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16",key:"16ra0t"}]],Ln=Q("building-2",BA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const FA=[["path",{d:"M12 10h.01",key:"1nrarc"}],["path",{d:"M12 14h.01",key:"1etili"}],["path",{d:"M12 6h.01",key:"1vi96p"}],["path",{d:"M16 10h.01",key:"1m94wz"}],["path",{d:"M16 14h.01",key:"1gbofw"}],["path",{d:"M16 6h.01",key:"1x0f13"}],["path",{d:"M8 10h.01",key:"19clt8"}],["path",{d:"M8 14h.01",key:"6423bh"}],["path",{d:"M8 6h.01",key:"1dz90k"}],["path",{d:"M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3",key:"cabbwy"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2",key:"1uxh74"}]],WA=Q("building",FA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const UA=[["path",{d:"M8 2v4",key:"1cmpym"}],["path",{d:"M16 2v4",key:"4m81vk"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2",key:"1hopcy"}],["path",{d:"M3 10h18",key:"8toen8"}]],Ch=Q("calendar",UA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const qA=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16",key:"c24i48"}],["path",{d:"M18 17V9",key:"2bz60n"}],["path",{d:"M13 17V5",key:"1frdt8"}],["path",{d:"M8 17v-3",key:"17ska0"}]],HA=Q("chart-column",qA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const KA=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],ej=Q("chevron-down",KA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const VA=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],Cf=Q("chevron-right",VA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const YA=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],Uc=Q("circle-alert",YA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const GA=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]],_r=Q("circle-check-big",GA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ZA=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]],Hr=Q("circle-x",ZA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const XA=[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],xr=Q("clock",XA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const JA=[["line",{x1:"12",x2:"12",y1:"2",y2:"22",key:"7eqyqh"}],["path",{d:"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6",key:"1b0p4s"}]],QA=Q("dollar-sign",JA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const eO=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],Jr=Q("external-link",eO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const tO=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],rO=Q("eye",tO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const nO=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]],iO=Q("file-text",nO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const aO=[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]],sO=Q("folder-open",aO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const oO=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2",key:"1m3agn"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}]],lO=Q("image",oO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const cO=[["path",{d:"m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4",key:"g0fldk"}],["path",{d:"m21 2-9.6 9.6",key:"1j0ho8"}],["circle",{cx:"7.5",cy:"15.5",r:"5.5",key:"yqb3hr"}]],uO=Q("key",cO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const dO=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",key:"zw3jo"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",key:"1wduqc"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",key:"kqbvx6"}]],Tx=Q("layers",dO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fO=[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]],pO=Q("layout-dashboard",fO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const hO=[["path",{d:"m16 17 5-5-5-5",key:"1bji2h"}],["path",{d:"M21 12H9",key:"dn1m92"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}]],mO=Q("log-out",hO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const gO=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7",key:"132q7q"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2",key:"izxlao"}]],tj=Q("mail",gO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xO=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0",key:"1r0f0z"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}]],yi=Q("map-pin",xO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const yO=[["path",{d:"M12.586 12.586 19 19",key:"ea5xo7"}],["path",{d:"M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z",key:"277e5u"}]],vO=Q("mouse-pointer",yO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const bO=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z",key:"1a0edw"}],["path",{d:"M12 22V12",key:"d0xqtd"}],["polyline",{points:"3.29 7 12 12 20.71 7",key:"ousv84"}],["path",{d:"m7.5 4.27 9 5.15",key:"1c824w"}]],Ct=Q("package",bO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const jO=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]],wO=Q("pencil",jO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const SO=[["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384",key:"9njp5v"}]],Ah=Q("phone",SO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const NO=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],Af=Q("plus",NO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const kO=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],Xt=Q("refresh-cw",kO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const PO=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]],_O=Q("save",PO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const CO=[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]],AO=Q("search",CO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const OO=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915",key:"1i5ecw"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],EO=Q("settings",OO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const DO=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]],sl=Q("shield",DO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const TO=[["path",{d:"M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5",key:"slp6dd"}],["path",{d:"M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244",key:"o0xfot"}],["path",{d:"M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05",key:"wn3emo"}]],Il=Q("store",TO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const MO=[["path",{d:"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z",key:"vktsd0"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor",key:"kqv944"}]],Cr=Q("tag",MO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const IO=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"6",key:"1vlfrh"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}]],rj=Q("target",IO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const $O=[["circle",{cx:"9",cy:"12",r:"3",key:"u3jwor"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7",key:"g7kal2"}]],Mx=Q("toggle-left",$O);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const LO=[["circle",{cx:"15",cy:"12",r:"3",key:"1afu0r"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7",key:"g7kal2"}]],Ix=Q("toggle-right",LO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zO=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],RO=Q("trash-2",zO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const BO=[["path",{d:"M16 17h6v-6",key:"t6n2it"}],["path",{d:"m22 17-8.5-8.5-5 5L2 7",key:"x473p"}]],FO=Q("trending-down",BO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const WO=[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]],zn=Q("trending-up",WO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const UO=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],nj=Q("triangle-alert",UO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const qO=[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]],HO=Q("upload",qO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const KO=[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",key:"1ngwbx"}]],VO=Q("wrench",KO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const YO=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],ij=Q("x",YO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const GO=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],$x=Q("zap",GO);function Ge({to:e,icon:t,label:r,isActive:n}){return a.jsxs("a",{href:e,className:`flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${n?"bg-blue-50 text-blue-600":"text-gray-700 hover:bg-gray-50"}`,children:[a.jsx("span",{className:"flex-shrink-0",children:t}),a.jsx("span",{children:r})]})}function Do({title:e,children:t}){return a.jsxs("div",{className:"space-y-1",children:[a.jsx("div",{className:"px-3 mb-2",children:a.jsx("h3",{className:"text-xs font-semibold text-gray-400 uppercase tracking-wider",children:e})}),t]})}function X({children:e}){const t=dt(),r=_i(),{user:n,logout:i}=Ph(),[s,o]=h.useState(null);h.useEffect(()=>{(async()=>{try{const u=await B.getVersion();o(u)}catch(u){console.error("Failed to fetch version info:",u)}})()},[]);const l=()=>{i(),t("/login")},c=(d,u=!0)=>u?r.pathname===d:r.pathname.startsWith(d);return a.jsxs("div",{className:"flex min-h-screen bg-gray-50",children:[a.jsxs("div",{className:"w-64 bg-white border-r border-gray-200 flex flex-col",style:{position:"sticky",top:0,height:"100vh",overflowY:"auto"},children:[a.jsxs("div",{className:"px-6 py-5 border-b border-gray-200",children:[a.jsx("h1",{className:"text-lg font-semibold text-gray-900",children:"Dutchie Analytics"}),a.jsx("p",{className:"text-xs text-gray-500 mt-0.5",children:n==null?void 0:n.email})]}),a.jsxs("nav",{className:"flex-1 px-3 py-4 space-y-6",children:[a.jsxs(Do,{title:"Main",children:[a.jsx(Ge,{to:"/",icon:a.jsx(pO,{className:"w-4 h-4"}),label:"Dashboard",isActive:c("/",!0)}),a.jsx(Ge,{to:"/dispensaries",icon:a.jsx(Ln,{className:"w-4 h-4"}),label:"Dispensaries",isActive:c("/dispensaries")}),a.jsx(Ge,{to:"/categories",icon:a.jsx(sO,{className:"w-4 h-4"}),label:"Categories",isActive:c("/categories")}),a.jsx(Ge,{to:"/products",icon:a.jsx(Ct,{className:"w-4 h-4"}),label:"Products",isActive:c("/products")}),a.jsx(Ge,{to:"/campaigns",icon:a.jsx(rj,{className:"w-4 h-4"}),label:"Campaigns",isActive:c("/campaigns")}),a.jsx(Ge,{to:"/analytics",icon:a.jsx(zn,{className:"w-4 h-4"}),label:"Analytics",isActive:c("/analytics")})]}),a.jsxs(Do,{title:"AZ Data",children:[a.jsx(Ge,{to:"/wholesale-analytics",icon:a.jsx(zn,{className:"w-4 h-4"}),label:"Wholesale Analytics",isActive:c("/wholesale-analytics")}),a.jsx(Ge,{to:"/az",icon:a.jsx(Il,{className:"w-4 h-4"}),label:"AZ Stores",isActive:c("/az",!1)}),a.jsx(Ge,{to:"/az-schedule",icon:a.jsx(Ch,{className:"w-4 h-4"}),label:"AZ Schedule",isActive:c("/az-schedule")})]}),a.jsxs(Do,{title:"Scraper",children:[a.jsx(Ge,{to:"/scraper-tools",icon:a.jsx(VO,{className:"w-4 h-4"}),label:"Tools",isActive:c("/scraper-tools")}),a.jsx(Ge,{to:"/scraper-schedule",icon:a.jsx(xr,{className:"w-4 h-4"}),label:"Schedule",isActive:c("/scraper-schedule")}),a.jsx(Ge,{to:"/scraper-monitor",icon:a.jsx(Ds,{className:"w-4 h-4"}),label:"Monitor",isActive:c("/scraper-monitor")})]}),a.jsxs(Do,{title:"System",children:[a.jsx(Ge,{to:"/changes",icon:a.jsx(_r,{className:"w-4 h-4"}),label:"Change Approval",isActive:c("/changes")}),a.jsx(Ge,{to:"/api-permissions",icon:a.jsx(uO,{className:"w-4 h-4"}),label:"API Permissions",isActive:c("/api-permissions")}),a.jsx(Ge,{to:"/proxies",icon:a.jsx(sl,{className:"w-4 h-4"}),label:"Proxies",isActive:c("/proxies")}),a.jsx(Ge,{to:"/logs",icon:a.jsx(iO,{className:"w-4 h-4"}),label:"Logs",isActive:c("/logs")}),a.jsx(Ge,{to:"/settings",icon:a.jsx(EO,{className:"w-4 h-4"}),label:"Settings",isActive:c("/settings")})]})]}),a.jsx("div",{className:"px-3 py-4 border-t border-gray-200",children:a.jsxs("button",{onClick:l,className:"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-red-600 hover:bg-red-50 transition-colors",children:[a.jsx(mO,{className:"w-4 h-4"}),a.jsx("span",{children:"Logout"})]})}),s&&a.jsxs("div",{className:"px-3 py-2 border-t border-gray-200 bg-gray-50",children:[a.jsxs("p",{className:"text-xs text-gray-500 text-center",children:[s.build_version," (",s.git_sha.slice(0,7),")"]}),a.jsx("p",{className:"text-xs text-gray-400 text-center mt-0.5",children:s.image_tag})]})]}),a.jsx("div",{className:"flex-1 overflow-y-auto",children:a.jsx("div",{className:"max-w-7xl mx-auto px-8 py-8",children:e})})]})}function aj(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var i=e.length;for(t=0;t{var[n]=r;return sj(n)||oj(n)});return Object.fromEntries(t)}function qc(e){if(e==null)return null;if(h.isValidElement(e)&&typeof e.props=="object"&&e.props!==null){var t=e.props;return nr(t)}return typeof e=="object"&&!Array.isArray(e)?nr(e):null}function ut(e){var t=Object.entries(e).filter(r=>{var[n]=r;return sj(n)||oj(n)||Oh(n)});return Object.fromEntries(t)}function JO(e){return e==null?null:h.isValidElement(e)?ut(e.props):typeof e=="object"&&!Array.isArray(e)?ut(e):null}var QO=["children","width","height","viewBox","className","style","title","desc"];function Of(){return Of=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:r,width:n,height:i,viewBox:s,className:o,style:l,title:c,desc:d}=e,u=e6(e,QO),f=s||{width:n,height:i,x:0,y:0},p=ce("recharts-surface",o);return h.createElement("svg",Of({},ut(u),{className:p,width:n,height:i,style:l,viewBox:"".concat(f.x," ").concat(f.y," ").concat(f.width," ").concat(f.height),ref:t}),h.createElement("title",null,c),h.createElement("desc",null,d),r)}),r6=["children","className"];function Ef(){return Ef=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:r,className:n}=e,i=n6(e,r6),s=ce("recharts-layer",n);return h.createElement("g",Ef({className:s},ut(i),{ref:t}),r)}),a6=h.createContext(null);function he(e){return function(){return e}}const cj=Math.cos,$l=Math.sin,vr=Math.sqrt,Ll=Math.PI,Hc=2*Ll,Df=Math.PI,Tf=2*Df,Xn=1e-6,s6=Tf-Xn;function uj(e){this._+=e[0];for(let t=1,r=e.length;t=0))throw new Error(`invalid digits: ${e}`);if(t>15)return uj;const r=10**t;return function(n){this._+=n[0];for(let i=1,s=n.length;iXn)if(!(Math.abs(f*c-d*u)>Xn)||!s)this._append`L${this._x1=t},${this._y1=r}`;else{let m=n-o,x=i-l,g=c*c+d*d,b=m*m+x*x,v=Math.sqrt(g),j=Math.sqrt(p),y=s*Math.tan((Df-Math.acos((g+p-b)/(2*v*j)))/2),w=y/j,S=y/v;Math.abs(w-1)>Xn&&this._append`L${t+w*u},${r+w*f}`,this._append`A${s},${s},0,0,${+(f*m>u*x)},${this._x1=t+S*c},${this._y1=r+S*d}`}}arc(t,r,n,i,s,o){if(t=+t,r=+r,n=+n,o=!!o,n<0)throw new Error(`negative radius: ${n}`);let l=n*Math.cos(i),c=n*Math.sin(i),d=t+l,u=r+c,f=1^o,p=o?i-s:s-i;this._x1===null?this._append`M${d},${u}`:(Math.abs(this._x1-d)>Xn||Math.abs(this._y1-u)>Xn)&&this._append`L${d},${u}`,n&&(p<0&&(p=p%Tf+Tf),p>s6?this._append`A${n},${n},0,1,${f},${t-l},${r-c}A${n},${n},0,1,${f},${this._x1=d},${this._y1=u}`:p>Xn&&this._append`A${n},${n},0,${+(p>=Df)},${f},${this._x1=t+n*Math.cos(s)},${this._y1=r+n*Math.sin(s)}`)}rect(t,r,n,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+r}h${n=+n}v${+i}h${-n}Z`}toString(){return this._}}function Eh(e){let t=3;return e.digits=function(r){if(!arguments.length)return t;if(r==null)t=null;else{const n=Math.floor(r);if(!(n>=0))throw new RangeError(`invalid digits: ${r}`);t=n}return e},()=>new l6(t)}function Dh(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}function dj(e){this._context=e}dj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:this._context.lineTo(e,t);break}}};function Kc(e){return new dj(e)}function fj(e){return e[0]}function pj(e){return e[1]}function hj(e,t){var r=he(!0),n=null,i=Kc,s=null,o=Eh(l);e=typeof e=="function"?e:e===void 0?fj:he(e),t=typeof t=="function"?t:t===void 0?pj:he(t);function l(c){var d,u=(c=Dh(c)).length,f,p=!1,m;for(n==null&&(s=i(m=o())),d=0;d<=u;++d)!(d=m;--x)l.point(y[x],w[x]);l.lineEnd(),l.areaEnd()}v&&(y[p]=+e(b,p,f),w[p]=+t(b,p,f),l.point(n?+n(b,p,f):y[p],r?+r(b,p,f):w[p]))}if(j)return l=null,j+""||null}function u(){return hj().defined(i).curve(o).context(s)}return d.x=function(f){return arguments.length?(e=typeof f=="function"?f:he(+f),n=null,d):e},d.x0=function(f){return arguments.length?(e=typeof f=="function"?f:he(+f),d):e},d.x1=function(f){return arguments.length?(n=f==null?null:typeof f=="function"?f:he(+f),d):n},d.y=function(f){return arguments.length?(t=typeof f=="function"?f:he(+f),r=null,d):t},d.y0=function(f){return arguments.length?(t=typeof f=="function"?f:he(+f),d):t},d.y1=function(f){return arguments.length?(r=f==null?null:typeof f=="function"?f:he(+f),d):r},d.lineX0=d.lineY0=function(){return u().x(e).y(t)},d.lineY1=function(){return u().x(e).y(r)},d.lineX1=function(){return u().x(n).y(t)},d.defined=function(f){return arguments.length?(i=typeof f=="function"?f:he(!!f),d):i},d.curve=function(f){return arguments.length?(o=f,s!=null&&(l=o(s)),d):o},d.context=function(f){return arguments.length?(f==null?s=l=null:l=o(s=f),d):s},d}class mj{constructor(t,r){this._context=t,this._x=r}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line}point(t,r){switch(t=+t,r=+r,this._point){case 0:{this._point=1,this._line?this._context.lineTo(t,r):this._context.moveTo(t,r);break}case 1:this._point=2;default:{this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,r,t,r):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+r)/2,t,this._y0,t,r);break}}this._x0=t,this._y0=r}}function c6(e){return new mj(e,!0)}function u6(e){return new mj(e,!1)}const Th={draw(e,t){const r=vr(t/Ll);e.moveTo(r,0),e.arc(0,0,r,0,Hc)}},d6={draw(e,t){const r=vr(t/5)/2;e.moveTo(-3*r,-r),e.lineTo(-r,-r),e.lineTo(-r,-3*r),e.lineTo(r,-3*r),e.lineTo(r,-r),e.lineTo(3*r,-r),e.lineTo(3*r,r),e.lineTo(r,r),e.lineTo(r,3*r),e.lineTo(-r,3*r),e.lineTo(-r,r),e.lineTo(-3*r,r),e.closePath()}},gj=vr(1/3),f6=gj*2,p6={draw(e,t){const r=vr(t/f6),n=r*gj;e.moveTo(0,-r),e.lineTo(n,0),e.lineTo(0,r),e.lineTo(-n,0),e.closePath()}},h6={draw(e,t){const r=vr(t),n=-r/2;e.rect(n,n,r,r)}},m6=.8908130915292852,xj=$l(Ll/10)/$l(7*Ll/10),g6=$l(Hc/10)*xj,x6=-cj(Hc/10)*xj,y6={draw(e,t){const r=vr(t*m6),n=g6*r,i=x6*r;e.moveTo(0,-r),e.lineTo(n,i);for(let s=1;s<5;++s){const o=Hc*s/5,l=cj(o),c=$l(o);e.lineTo(c*r,-l*r),e.lineTo(l*n-c*i,c*n+l*i)}e.closePath()}},cd=vr(3),v6={draw(e,t){const r=-vr(t/(cd*3));e.moveTo(0,r*2),e.lineTo(-cd*r,-r),e.lineTo(cd*r,-r),e.closePath()}},Ht=-.5,Kt=vr(3)/2,Mf=1/vr(12),b6=(Mf/2+1)*3,j6={draw(e,t){const r=vr(t/b6),n=r/2,i=r*Mf,s=n,o=r*Mf+r,l=-s,c=o;e.moveTo(n,i),e.lineTo(s,o),e.lineTo(l,c),e.lineTo(Ht*n-Kt*i,Kt*n+Ht*i),e.lineTo(Ht*s-Kt*o,Kt*s+Ht*o),e.lineTo(Ht*l-Kt*c,Kt*l+Ht*c),e.lineTo(Ht*n+Kt*i,Ht*i-Kt*n),e.lineTo(Ht*s+Kt*o,Ht*o-Kt*s),e.lineTo(Ht*l+Kt*c,Ht*c-Kt*l),e.closePath()}};function w6(e,t){let r=null,n=Eh(i);e=typeof e=="function"?e:he(e||Th),t=typeof t=="function"?t:he(t===void 0?64:+t);function i(){let s;if(r||(r=s=n()),e.apply(this,arguments).draw(r,+t.apply(this,arguments)),s)return r=null,s+""||null}return i.type=function(s){return arguments.length?(e=typeof s=="function"?s:he(s),i):e},i.size=function(s){return arguments.length?(t=typeof s=="function"?s:he(+s),i):t},i.context=function(s){return arguments.length?(r=s??null,i):r},i}function zl(){}function Rl(e,t,r){e._context.bezierCurveTo((2*e._x0+e._x1)/3,(2*e._y0+e._y1)/3,(e._x0+2*e._x1)/3,(e._y0+2*e._y1)/3,(e._x0+4*e._x1+t)/6,(e._y0+4*e._y1+r)/6)}function yj(e){this._context=e}yj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Rl(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Rl(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function S6(e){return new yj(e)}function vj(e){this._context=e}vj.prototype={areaStart:zl,areaEnd:zl,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:{this._context.moveTo(this._x2,this._y2),this._context.closePath();break}case 2:{this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break}case 3:{this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4);break}}},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x2=e,this._y2=t;break;case 1:this._point=2,this._x3=e,this._y3=t;break;case 2:this._point=3,this._x4=e,this._y4=t,this._context.moveTo((this._x0+4*this._x1+e)/6,(this._y0+4*this._y1+t)/6);break;default:Rl(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function N6(e){return new vj(e)}function bj(e){this._context=e}bj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var r=(this._x0+4*this._x1+e)/6,n=(this._y0+4*this._y1+t)/6;this._line?this._context.lineTo(r,n):this._context.moveTo(r,n);break;case 3:this._point=4;default:Rl(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function k6(e){return new bj(e)}function jj(e){this._context=e}jj.prototype={areaStart:zl,areaEnd:zl,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(e,t){e=+e,t=+t,this._point?this._context.lineTo(e,t):(this._point=1,this._context.moveTo(e,t))}};function P6(e){return new jj(e)}function Lx(e){return e<0?-1:1}function zx(e,t,r){var n=e._x1-e._x0,i=t-e._x1,s=(e._y1-e._y0)/(n||i<0&&-0),o=(r-e._y1)/(i||n<0&&-0),l=(s*i+o*n)/(n+i);return(Lx(s)+Lx(o))*Math.min(Math.abs(s),Math.abs(o),.5*Math.abs(l))||0}function Rx(e,t){var r=e._x1-e._x0;return r?(3*(e._y1-e._y0)/r-t)/2:t}function ud(e,t,r){var n=e._x0,i=e._y0,s=e._x1,o=e._y1,l=(s-n)/3;e._context.bezierCurveTo(n+l,i+l*t,s-l,o-l*r,s,o)}function Bl(e){this._context=e}Bl.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:ud(this,this._t0,Rx(this,this._t0));break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){var r=NaN;if(e=+e,t=+t,!(e===this._x1&&t===this._y1)){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,ud(this,Rx(this,r=zx(this,e,t)),r);break;default:ud(this,this._t0,r=zx(this,e,t));break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t,this._t0=r}}};function wj(e){this._context=new Sj(e)}(wj.prototype=Object.create(Bl.prototype)).point=function(e,t){Bl.prototype.point.call(this,t,e)};function Sj(e){this._context=e}Sj.prototype={moveTo:function(e,t){this._context.moveTo(t,e)},closePath:function(){this._context.closePath()},lineTo:function(e,t){this._context.lineTo(t,e)},bezierCurveTo:function(e,t,r,n,i,s){this._context.bezierCurveTo(t,e,n,r,s,i)}};function _6(e){return new Bl(e)}function C6(e){return new wj(e)}function Nj(e){this._context=e}Nj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var e=this._x,t=this._y,r=e.length;if(r)if(this._line?this._context.lineTo(e[0],t[0]):this._context.moveTo(e[0],t[0]),r===2)this._context.lineTo(e[1],t[1]);else for(var n=Bx(e),i=Bx(t),s=0,o=1;o=0;--t)i[t]=(o[t]-i[t+1])/s[t];for(s[r-1]=(e[r]+i[r-1])/2,t=0;t=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:{if(this._t<=0)this._context.lineTo(this._x,t),this._context.lineTo(e,t);else{var r=this._x*(1-this._t)+e*this._t;this._context.lineTo(r,this._y),this._context.lineTo(r,t)}break}}this._x=e,this._y=t}};function O6(e){return new Vc(e,.5)}function E6(e){return new Vc(e,0)}function D6(e){return new Vc(e,1)}function pa(e,t){if((o=e.length)>1)for(var r=1,n,i,s=e[t[0]],o,l=s.length;r=0;)r[t]=t;return r}function T6(e,t){return e[t]}function M6(e){const t=[];return t.key=e,t}function I6(){var e=he([]),t=If,r=pa,n=T6;function i(s){var o=Array.from(e.apply(this,arguments),M6),l,c=o.length,d=-1,u;for(const f of s)for(l=0,++d;l0){for(var r,n,i=0,s=e[0].length,o;i0){for(var r=0,n=e[t[0]],i,s=n.length;r0)||!((s=(i=e[t[0]]).length)>0))){for(var r=0,n=1,i,s,o;ne===0?0:e>0?1:-1,yr=e=>typeof e=="number"&&e!=+e,Qr=e=>typeof e=="string"&&e.indexOf("%")===e.length-1,Y=e=>(typeof e=="number"||e instanceof Number)&&!yr(e),Or=e=>Y(e)||typeof e=="string",B6=0,Ts=e=>{var t=++B6;return"".concat(e||"").concat(t)},Rn=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(!Y(t)&&typeof t!="string")return n;var s;if(Qr(t)){if(r==null)return n;var o=t.indexOf("%");s=r*parseFloat(t.slice(0,o))/100}else s=+t;return yr(s)&&(s=n),i&&r!=null&&s>r&&(s=r),s},_j=e=>{if(!Array.isArray(e))return!1;for(var t=e.length,r={},n=0;nn&&(typeof t=="function"?t(n):Xc(n,t))===r)}var Re=e=>e===null||typeof e>"u",Xs=e=>Re(e)?e:"".concat(e.charAt(0).toUpperCase()).concat(e.slice(1));function F6(e){return e!=null}function ka(){}var W6=["type","size","sizeType"];function $f(){return $f=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var t="symbol".concat(Xs(e));return Aj[t]||Th},Z6=(e,t,r)=>{if(t==="area")return e;switch(r){case"cross":return 5*e*e/9;case"diamond":return .5*e*e/Math.sqrt(3);case"square":return e*e;case"star":{var n=18*Y6;return 1.25*e*e*(Math.tan(n)-Math.tan(n*2)*Math.tan(n)**2)}case"triangle":return Math.sqrt(3)*e*e/4;case"wye":return(21-10*Math.sqrt(3))*e*e/8;default:return Math.PI*e*e/4}},X6=(e,t)=>{Aj["symbol".concat(Xs(e))]=t},Oj=e=>{var{type:t="circle",size:r=64,sizeType:n="area"}=e,i=K6(e,W6),s=Wx(Wx({},i),{},{type:t,size:r,sizeType:n}),o="circle";typeof t=="string"&&(o=t);var l=()=>{var p=G6(o),m=w6().type(p).size(Z6(r,n,o)),x=m();if(x!==null)return x},{className:c,cx:d,cy:u}=s,f=ut(s);return Y(d)&&Y(u)&&Y(r)?h.createElement("path",$f({},f,{className:ce("recharts-symbols",c),transform:"translate(".concat(d,", ").concat(u,")"),d:l()})):null};Oj.registerSymbol=X6;var Ej=e=>"radius"in e&&"startAngle"in e&&"endAngle"in e,Ih=(e,t)=>{if(!e||typeof e=="function"||typeof e=="boolean")return null;var r=e;if(h.isValidElement(e)&&(r=e.props),typeof r!="object"&&typeof r!="function")return null;var n={};return Object.keys(r).forEach(i=>{Oh(i)&&(n[i]=s=>r[i](r,s))}),n},J6=(e,t,r)=>n=>(e(t,r,n),null),Q6=(e,t,r)=>{if(e===null||typeof e!="object"&&typeof e!="function")return null;var n=null;return Object.keys(e).forEach(i=>{var s=e[i];Oh(i)&&typeof s=="function"&&(n||(n={}),n[i]=J6(s,t,r))}),n};function Ux(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function e4(e){for(var t=1;t(o[l]===void 0&&n[l]!==void 0&&(o[l]=n[l]),o),r);return s}var Dj={},Tj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n){const i=new Map;for(let s=0;s=0}e.isLength=t})(Ij);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Ij;function r(n){return n!=null&&typeof n!="function"&&t.isLength(n.length)}e.isArrayLike=r})(Jc);var $j={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return typeof r=="object"&&r!==null}e.isObjectLike=t})($j);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Jc,r=$j;function n(i){return r.isObjectLike(i)&&t.isArrayLike(i)}e.isArrayLikeObject=n})(Mj);var Lj={},zj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Yc;function r(n){return function(i){return t.get(i,n)}}e.property=r})(zj);var Rj={},Lh={},Bj={},zh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r!==null&&(typeof r=="object"||typeof r=="function")}e.isObject=t})(zh);var Rh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r==null||typeof r!="object"&&typeof r!="function"}e.isPrimitive=t})(Rh);var Bh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n){return r===n||Number.isNaN(r)&&Number.isNaN(n)}e.eq=t})(Bh);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=zh,r=Rh,n=Bh;function i(u,f,p){return typeof p!="function"?i(u,f,()=>{}):s(u,f,function m(x,g,b,v,j,y){const w=p(x,g,b,v,j,y);return w!==void 0?!!w:s(x,g,m,y)},new Map)}function s(u,f,p,m){if(f===u)return!0;switch(typeof f){case"object":return o(u,f,p,m);case"function":return Object.keys(f).length>0?s(u,{...f},p,m):n.eq(u,f);default:return t.isObject(u)?typeof f=="string"?f==="":!0:n.eq(u,f)}}function o(u,f,p,m){if(f==null)return!0;if(Array.isArray(f))return c(u,f,p,m);if(f instanceof Map)return l(u,f,p,m);if(f instanceof Set)return d(u,f,p,m);const x=Object.keys(f);if(u==null)return x.length===0;if(x.length===0)return!0;if(m!=null&&m.has(f))return m.get(f)===u;m==null||m.set(f,u);try{for(let g=0;g{})}e.isMatch=r})(Lh);var Fj={},Fh={},Wj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return Object.getOwnPropertySymbols(r).filter(n=>Object.prototype.propertyIsEnumerable.call(r,n))}e.getSymbols=t})(Wj);var Wh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r==null?r===void 0?"[object Undefined]":"[object Null]":Object.prototype.toString.call(r)}e.getTag=t})(Wh);var Uh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t="[object RegExp]",r="[object String]",n="[object Number]",i="[object Boolean]",s="[object Arguments]",o="[object Symbol]",l="[object Date]",c="[object Map]",d="[object Set]",u="[object Array]",f="[object Function]",p="[object ArrayBuffer]",m="[object Object]",x="[object Error]",g="[object DataView]",b="[object Uint8Array]",v="[object Uint8ClampedArray]",j="[object Uint16Array]",y="[object Uint32Array]",w="[object BigUint64Array]",S="[object Int8Array]",N="[object Int16Array]",P="[object Int32Array]",_="[object BigInt64Array]",T="[object Float32Array]",$="[object Float64Array]";e.argumentsTag=s,e.arrayBufferTag=p,e.arrayTag=u,e.bigInt64ArrayTag=_,e.bigUint64ArrayTag=w,e.booleanTag=i,e.dataViewTag=g,e.dateTag=l,e.errorTag=x,e.float32ArrayTag=T,e.float64ArrayTag=$,e.functionTag=f,e.int16ArrayTag=N,e.int32ArrayTag=P,e.int8ArrayTag=S,e.mapTag=c,e.numberTag=n,e.objectTag=m,e.regexpTag=t,e.setTag=d,e.stringTag=r,e.symbolTag=o,e.uint16ArrayTag=j,e.uint32ArrayTag=y,e.uint8ArrayTag=b,e.uint8ClampedArrayTag=v})(Uh);var Uj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return ArrayBuffer.isView(r)&&!(r instanceof DataView)}e.isTypedArray=t})(Uj);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Wj,r=Wh,n=Uh,i=Rh,s=Uj;function o(u,f){return l(u,void 0,u,new Map,f)}function l(u,f,p,m=new Map,x=void 0){const g=x==null?void 0:x(u,f,p,m);if(g!==void 0)return g;if(i.isPrimitive(u))return u;if(m.has(u))return m.get(u);if(Array.isArray(u)){const b=new Array(u.length);m.set(u,b);for(let v=0;vt.isMatch(s,i)}e.matches=n})(Rj);var qj={},Hj={},Kj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Fh,r=Uh;function n(i,s){return t.cloneDeepWith(i,(o,l,c,d)=>{const u=s==null?void 0:s(o,l,c,d);if(u!==void 0)return u;if(typeof i=="object")switch(Object.prototype.toString.call(i)){case r.numberTag:case r.stringTag:case r.booleanTag:{const f=new i.constructor(i==null?void 0:i.valueOf());return t.copyProperties(f,i),f}case r.argumentsTag:{const f={};return t.copyProperties(f,i),f.length=i.length,f[Symbol.iterator]=i[Symbol.iterator],f}default:return}})}e.cloneDeepWith=n})(Kj);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Kj;function r(n){return t.cloneDeepWith(n)}e.cloneDeep=r})(Hj);var Vj={},qh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=/^(?:0|[1-9]\d*)$/;function r(n,i=Number.MAX_SAFE_INTEGER){switch(typeof n){case"number":return Number.isInteger(n)&&n>=0&&ne,Ve=()=>{var e=h.useContext(Hh);return e?e.store.dispatch:s4},ol=()=>{},o4=()=>ol,l4=(e,t)=>e===t;function G(e){var t=h.useContext(Hh);return X1.useSyncExternalStoreWithSelector(t?t.subscription.addNestedSub:o4,t?t.store.getState:ol,t?t.store.getState:ol,t?e:ol,l4)}function c4(e,t=`expected a function, instead received ${typeof e}`){if(typeof e!="function")throw new TypeError(t)}function u4(e,t=`expected an object, instead received ${typeof e}`){if(typeof e!="object")throw new TypeError(t)}function d4(e,t="expected all items to be functions, instead received the following types: "){if(!e.every(r=>typeof r=="function")){const r=e.map(n=>typeof n=="function"?`function ${n.name||"unnamed"}()`:typeof n).join(", ");throw new TypeError(`${t}[${r}]`)}}var Hx=e=>Array.isArray(e)?e:[e];function f4(e){const t=Array.isArray(e[0])?e[0]:e;return d4(t,"createSelector expects all input-selectors to be functions, but received the following types: "),t}function p4(e,t){const r=[],{length:n}=e;for(let i=0;i{r=Mo(),o.resetResultsCount()},o.resultsCount=()=>s,o.resetResultsCount=()=>{s=0},o}function x4(e,...t){const r=typeof e=="function"?{memoize:e,memoizeOptions:t}:e,n=(...i)=>{let s=0,o=0,l,c={},d=i.pop();typeof d=="object"&&(c=d,d=i.pop()),c4(d,`createSelector expects an output function after the inputs, but received: [${typeof d}]`);const u={...r,...c},{memoize:f,memoizeOptions:p=[],argsMemoize:m=Gj,argsMemoizeOptions:x=[]}=u,g=Hx(p),b=Hx(x),v=f4(i),j=f(function(){return s++,d.apply(null,arguments)},...g),y=m(function(){o++;const S=p4(v,arguments);return l=j.apply(null,S),l},...b);return Object.assign(y,{resultFunc:d,memoizedResultFunc:j,dependencies:v,dependencyRecomputations:()=>o,resetDependencyRecomputations:()=>{o=0},lastResult:()=>l,recomputations:()=>s,resetRecomputations:()=>{s=0},memoize:f,argsMemoize:m})};return Object.assign(n,{withTypes:()=>n}),n}var I=x4(Gj),y4=Object.assign((e,t=I)=>{u4(e,`createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ${typeof e}`);const r=Object.keys(e),n=r.map(s=>e[s]);return t(n,(...s)=>s.reduce((o,l,c)=>(o[r[c]]=l,o),{}))},{withTypes:()=>y4}),Zj={},Xj={},Jj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return typeof n=="symbol"?1:n===null?2:n===void 0?3:n!==n?4:0}const r=(n,i,s)=>{if(n!==i){const o=t(n),l=t(i);if(o===l&&o===0){if(ni)return s==="desc"?-1:1}return s==="desc"?l-o:o-l}return 0};e.compareValues=r})(Jj);var Qj={},Kh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return typeof r=="symbol"||r instanceof Symbol}e.isSymbol=t})(Kh);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Kh,r=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,n=/^\w*$/;function i(s,o){return Array.isArray(s)?!1:typeof s=="number"||typeof s=="boolean"||s==null||t.isSymbol(s)?!0:typeof s=="string"&&(n.test(s)||!r.test(s))||o!=null&&Object.hasOwn(o,s)}e.isKey=i})(Qj);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Jj,r=Qj,n=Zc;function i(s,o,l,c){if(s==null)return[];l=c?void 0:l,Array.isArray(s)||(s=Object.values(s)),Array.isArray(o)||(o=o==null?[null]:[o]),o.length===0&&(o=[null]),Array.isArray(l)||(l=l==null?[]:[l]),l=l.map(m=>String(m));const d=(m,x)=>{let g=m;for(let b=0;bx==null||m==null?x:typeof m=="object"&&"key"in m?Object.hasOwn(x,m.key)?x[m.key]:d(x,m.path):typeof m=="function"?m(x):Array.isArray(m)?d(x,m):typeof x=="object"?x[m]:x,f=o.map(m=>(Array.isArray(m)&&m.length===1&&(m=m[0]),m==null||typeof m=="function"||Array.isArray(m)||r.isKey(m)?m:{key:m,path:n.toPath(m)}));return s.map(m=>({original:m,criteria:f.map(x=>u(x,m))})).slice().sort((m,x)=>{for(let g=0;gm.original)}e.orderBy=i})(Xj);var ew={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n=1){const i=[],s=Math.floor(n),o=(l,c)=>{for(let d=0;d1&&n.isIterateeCall(s,o[0],o[1])?o=[]:l>2&&n.isIterateeCall(o[0],o[1],o[2])&&(o=[o[0]]),t.orderBy(s,r.flatten(o),["asc"])}e.sortBy=i})(Zj);var v4=Zj.sortBy;const Qc=Tr(v4);var tw=e=>e.legend.settings,b4=e=>e.legend.size,j4=e=>e.legend.payload;I([j4,tw],(e,t)=>{var{itemSorter:r}=t,n=e.flat(1);return r?Qc(n,r):n});var Io=1;function w4(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:[],[t,r]=h.useState({height:0,left:0,top:0,width:0}),n=h.useCallback(i=>{if(i!=null){var s=i.getBoundingClientRect(),o={height:s.height,left:s.left,top:s.top,width:s.width};(Math.abs(o.height-t.height)>Io||Math.abs(o.left-t.left)>Io||Math.abs(o.top-t.top)>Io||Math.abs(o.width-t.width)>Io)&&r({height:o.height,left:o.left,top:o.top,width:o.width})}},[t.width,t.height,t.top,t.left,...e]);return[t,n]}function Ze(e){return`Minified Redux error #${e}; visit https://redux.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}var S4=typeof Symbol=="function"&&Symbol.observable||"@@observable",Vx=S4,dd=()=>Math.random().toString(36).substring(7).split("").join("."),N4={INIT:`@@redux/INIT${dd()}`,REPLACE:`@@redux/REPLACE${dd()}`,PROBE_UNKNOWN_ACTION:()=>`@@redux/PROBE_UNKNOWN_ACTION${dd()}`},Fl=N4;function Yh(e){if(typeof e!="object"||e===null)return!1;let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t||Object.getPrototypeOf(e)===null}function rw(e,t,r){if(typeof e!="function")throw new Error(Ze(2));if(typeof t=="function"&&typeof r=="function"||typeof r=="function"&&typeof arguments[3]=="function")throw new Error(Ze(0));if(typeof t=="function"&&typeof r>"u"&&(r=t,t=void 0),typeof r<"u"){if(typeof r!="function")throw new Error(Ze(1));return r(rw)(e,t)}let n=e,i=t,s=new Map,o=s,l=0,c=!1;function d(){o===s&&(o=new Map,s.forEach((b,v)=>{o.set(v,b)}))}function u(){if(c)throw new Error(Ze(3));return i}function f(b){if(typeof b!="function")throw new Error(Ze(4));if(c)throw new Error(Ze(5));let v=!0;d();const j=l++;return o.set(j,b),function(){if(v){if(c)throw new Error(Ze(6));v=!1,d(),o.delete(j),s=null}}}function p(b){if(!Yh(b))throw new Error(Ze(7));if(typeof b.type>"u")throw new Error(Ze(8));if(typeof b.type!="string")throw new Error(Ze(17));if(c)throw new Error(Ze(9));try{c=!0,i=n(i,b)}finally{c=!1}return(s=o).forEach(j=>{j()}),b}function m(b){if(typeof b!="function")throw new Error(Ze(10));n=b,p({type:Fl.REPLACE})}function x(){const b=f;return{subscribe(v){if(typeof v!="object"||v===null)throw new Error(Ze(11));function j(){const w=v;w.next&&w.next(u())}return j(),{unsubscribe:b(j)}},[Vx](){return this}}}return p({type:Fl.INIT}),{dispatch:p,subscribe:f,getState:u,replaceReducer:m,[Vx]:x}}function k4(e){Object.keys(e).forEach(t=>{const r=e[t];if(typeof r(void 0,{type:Fl.INIT})>"u")throw new Error(Ze(12));if(typeof r(void 0,{type:Fl.PROBE_UNKNOWN_ACTION()})>"u")throw new Error(Ze(13))})}function nw(e){const t=Object.keys(e),r={};for(let s=0;s"u")throw l&&l.type,new Error(Ze(14));d[f]=x,c=c||x!==m}return c=c||n.length!==Object.keys(o).length,c?d:o}}function Wl(...e){return e.length===0?t=>t:e.length===1?e[0]:e.reduce((t,r)=>(...n)=>t(r(...n)))}function P4(...e){return t=>(r,n)=>{const i=t(r,n);let s=()=>{throw new Error(Ze(15))};const o={getState:i.getState,dispatch:(c,...d)=>s(c,...d)},l=e.map(c=>c(o));return s=Wl(...l)(i.dispatch),{...i,dispatch:s}}}function iw(e){return Yh(e)&&"type"in e&&typeof e.type=="string"}var aw=Symbol.for("immer-nothing"),Yx=Symbol.for("immer-draftable"),Wt=Symbol.for("immer-state");function fr(e,...t){throw new Error(`[Immer] minified error nr: ${e}. Full error at: https://bit.ly/3cXEKWf`)}var Ms=Object.getPrototypeOf;function vi(e){return!!e&&!!e[Wt]}function en(e){var t;return e?sw(e)||Array.isArray(e)||!!e[Yx]||!!((t=e.constructor)!=null&&t[Yx])||Js(e)||tu(e):!1}var _4=Object.prototype.constructor.toString(),Gx=new WeakMap;function sw(e){if(!e||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);if(t===null||t===Object.prototype)return!0;const r=Object.hasOwnProperty.call(t,"constructor")&&t.constructor;if(r===Object)return!0;if(typeof r!="function")return!1;let n=Gx.get(r);return n===void 0&&(n=Function.toString.call(r),Gx.set(r,n)),n===_4}function Ul(e,t,r=!0){eu(e)===0?(r?Reflect.ownKeys(e):Object.keys(e)).forEach(i=>{t(i,e[i],e)}):e.forEach((n,i)=>t(i,n,e))}function eu(e){const t=e[Wt];return t?t.type_:Array.isArray(e)?1:Js(e)?2:tu(e)?3:0}function Lf(e,t){return eu(e)===2?e.has(t):Object.prototype.hasOwnProperty.call(e,t)}function ow(e,t,r){const n=eu(e);n===2?e.set(t,r):n===3?e.add(r):e[t]=r}function C4(e,t){return e===t?e!==0||1/e===1/t:e!==e&&t!==t}function Js(e){return e instanceof Map}function tu(e){return e instanceof Set}function Jn(e){return e.copy_||e.base_}function zf(e,t){if(Js(e))return new Map(e);if(tu(e))return new Set(e);if(Array.isArray(e))return Array.prototype.slice.call(e);const r=sw(e);if(t===!0||t==="class_only"&&!r){const n=Object.getOwnPropertyDescriptors(e);delete n[Wt];let i=Reflect.ownKeys(n);for(let s=0;s1&&Object.defineProperties(e,{set:$o,add:$o,clear:$o,delete:$o}),Object.freeze(e),t&&Object.values(e).forEach(r=>Gh(r,!0))),e}function A4(){fr(2)}var $o={value:A4};function ru(e){return e===null||typeof e!="object"?!0:Object.isFrozen(e)}var O4={};function bi(e){const t=O4[e];return t||fr(0,e),t}var Is;function lw(){return Is}function E4(e,t){return{drafts_:[],parent_:e,immer_:t,canAutoFreeze_:!0,unfinalizedDrafts_:0}}function Zx(e,t){t&&(bi("Patches"),e.patches_=[],e.inversePatches_=[],e.patchListener_=t)}function Rf(e){Bf(e),e.drafts_.forEach(D4),e.drafts_=null}function Bf(e){e===Is&&(Is=e.parent_)}function Xx(e){return Is=E4(Is,e)}function D4(e){const t=e[Wt];t.type_===0||t.type_===1?t.revoke_():t.revoked_=!0}function Jx(e,t){t.unfinalizedDrafts_=t.drafts_.length;const r=t.drafts_[0];return e!==void 0&&e!==r?(r[Wt].modified_&&(Rf(t),fr(4)),en(e)&&(e=ql(t,e),t.parent_||Hl(t,e)),t.patches_&&bi("Patches").generateReplacementPatches_(r[Wt].base_,e,t.patches_,t.inversePatches_)):e=ql(t,r,[]),Rf(t),t.patches_&&t.patchListener_(t.patches_,t.inversePatches_),e!==aw?e:void 0}function ql(e,t,r){if(ru(t))return t;const n=e.immer_.shouldUseStrictIteration(),i=t[Wt];if(!i)return Ul(t,(s,o)=>Qx(e,i,t,s,o,r),n),t;if(i.scope_!==e)return t;if(!i.modified_)return Hl(e,i.base_,!0),i.base_;if(!i.finalized_){i.finalized_=!0,i.scope_.unfinalizedDrafts_--;const s=i.copy_;let o=s,l=!1;i.type_===3&&(o=new Set(s),s.clear(),l=!0),Ul(o,(c,d)=>Qx(e,i,s,c,d,r,l),n),Hl(e,s,!1),r&&e.patches_&&bi("Patches").generatePatches_(i,r,e.patches_,e.inversePatches_)}return i.copy_}function Qx(e,t,r,n,i,s,o){if(i==null||typeof i!="object"&&!o)return;const l=ru(i);if(!(l&&!o)){if(vi(i)){const c=s&&t&&t.type_!==3&&!Lf(t.assigned_,n)?s.concat(n):void 0,d=ql(e,i,c);if(ow(r,n,d),vi(d))e.canAutoFreeze_=!1;else return}else o&&r.add(i);if(en(i)&&!l){if(!e.immer_.autoFreeze_&&e.unfinalizedDrafts_<1||t&&t.base_&&t.base_[n]===i&&l)return;ql(e,i),(!t||!t.scope_.parent_)&&typeof n!="symbol"&&(Js(r)?r.has(n):Object.prototype.propertyIsEnumerable.call(r,n))&&Hl(e,i)}}}function Hl(e,t,r=!1){!e.parent_&&e.immer_.autoFreeze_&&e.canAutoFreeze_&&Gh(t,r)}function T4(e,t){const r=Array.isArray(e),n={type_:r?1:0,scope_:t?t.scope_:lw(),modified_:!1,finalized_:!1,assigned_:{},parent_:t,base_:e,draft_:null,copy_:null,revoke_:null,isManual_:!1};let i=n,s=Zh;r&&(i=[n],s=$s);const{revoke:o,proxy:l}=Proxy.revocable(i,s);return n.draft_=l,n.revoke_=o,l}var Zh={get(e,t){if(t===Wt)return e;const r=Jn(e);if(!Lf(r,t))return M4(e,r,t);const n=r[t];return e.finalized_||!en(n)?n:n===fd(e.base_,t)?(pd(e),e.copy_[t]=Wf(n,e)):n},has(e,t){return t in Jn(e)},ownKeys(e){return Reflect.ownKeys(Jn(e))},set(e,t,r){const n=cw(Jn(e),t);if(n!=null&&n.set)return n.set.call(e.draft_,r),!0;if(!e.modified_){const i=fd(Jn(e),t),s=i==null?void 0:i[Wt];if(s&&s.base_===r)return e.copy_[t]=r,e.assigned_[t]=!1,!0;if(C4(r,i)&&(r!==void 0||Lf(e.base_,t)))return!0;pd(e),Ff(e)}return e.copy_[t]===r&&(r!==void 0||t in e.copy_)||Number.isNaN(r)&&Number.isNaN(e.copy_[t])||(e.copy_[t]=r,e.assigned_[t]=!0),!0},deleteProperty(e,t){return fd(e.base_,t)!==void 0||t in e.base_?(e.assigned_[t]=!1,pd(e),Ff(e)):delete e.assigned_[t],e.copy_&&delete e.copy_[t],!0},getOwnPropertyDescriptor(e,t){const r=Jn(e),n=Reflect.getOwnPropertyDescriptor(r,t);return n&&{writable:!0,configurable:e.type_!==1||t!=="length",enumerable:n.enumerable,value:r[t]}},defineProperty(){fr(11)},getPrototypeOf(e){return Ms(e.base_)},setPrototypeOf(){fr(12)}},$s={};Ul(Zh,(e,t)=>{$s[e]=function(){return arguments[0]=arguments[0][0],t.apply(this,arguments)}});$s.deleteProperty=function(e,t){return $s.set.call(this,e,t,void 0)};$s.set=function(e,t,r){return Zh.set.call(this,e[0],t,r,e[0])};function fd(e,t){const r=e[Wt];return(r?Jn(r):e)[t]}function M4(e,t,r){var i;const n=cw(t,r);return n?"value"in n?n.value:(i=n.get)==null?void 0:i.call(e.draft_):void 0}function cw(e,t){if(!(t in e))return;let r=Ms(e);for(;r;){const n=Object.getOwnPropertyDescriptor(r,t);if(n)return n;r=Ms(r)}}function Ff(e){e.modified_||(e.modified_=!0,e.parent_&&Ff(e.parent_))}function pd(e){e.copy_||(e.copy_=zf(e.base_,e.scope_.immer_.useStrictShallowCopy_))}var I4=class{constructor(e){this.autoFreeze_=!0,this.useStrictShallowCopy_=!1,this.useStrictIteration_=!0,this.produce=(t,r,n)=>{if(typeof t=="function"&&typeof r!="function"){const s=r;r=t;const o=this;return function(c=s,...d){return o.produce(c,u=>r.call(this,u,...d))}}typeof r!="function"&&fr(6),n!==void 0&&typeof n!="function"&&fr(7);let i;if(en(t)){const s=Xx(this),o=Wf(t,void 0);let l=!0;try{i=r(o),l=!1}finally{l?Rf(s):Bf(s)}return Zx(s,n),Jx(i,s)}else if(!t||typeof t!="object"){if(i=r(t),i===void 0&&(i=t),i===aw&&(i=void 0),this.autoFreeze_&&Gh(i,!0),n){const s=[],o=[];bi("Patches").generateReplacementPatches_(t,i,s,o),n(s,o)}return i}else fr(1,t)},this.produceWithPatches=(t,r)=>{if(typeof t=="function")return(o,...l)=>this.produceWithPatches(o,c=>t(c,...l));let n,i;return[this.produce(t,r,(o,l)=>{n=o,i=l}),n,i]},typeof(e==null?void 0:e.autoFreeze)=="boolean"&&this.setAutoFreeze(e.autoFreeze),typeof(e==null?void 0:e.useStrictShallowCopy)=="boolean"&&this.setUseStrictShallowCopy(e.useStrictShallowCopy),typeof(e==null?void 0:e.useStrictIteration)=="boolean"&&this.setUseStrictIteration(e.useStrictIteration)}createDraft(e){en(e)||fr(8),vi(e)&&(e=Kr(e));const t=Xx(this),r=Wf(e,void 0);return r[Wt].isManual_=!0,Bf(t),r}finishDraft(e,t){const r=e&&e[Wt];(!r||!r.isManual_)&&fr(9);const{scope_:n}=r;return Zx(n,t),Jx(void 0,n)}setAutoFreeze(e){this.autoFreeze_=e}setUseStrictShallowCopy(e){this.useStrictShallowCopy_=e}setUseStrictIteration(e){this.useStrictIteration_=e}shouldUseStrictIteration(){return this.useStrictIteration_}applyPatches(e,t){let r;for(r=t.length-1;r>=0;r--){const i=t[r];if(i.path.length===0&&i.op==="replace"){e=i.value;break}}r>-1&&(t=t.slice(r+1));const n=bi("Patches").applyPatches_;return vi(e)?n(e,t):this.produce(e,i=>n(i,t))}};function Wf(e,t){const r=Js(e)?bi("MapSet").proxyMap_(e,t):tu(e)?bi("MapSet").proxySet_(e,t):T4(e,t);return(t?t.scope_:lw()).drafts_.push(r),r}function Kr(e){return vi(e)||fr(10,e),uw(e)}function uw(e){if(!en(e)||ru(e))return e;const t=e[Wt];let r,n=!0;if(t){if(!t.modified_)return t.base_;t.finalized_=!0,r=zf(e,t.scope_.immer_.useStrictShallowCopy_),n=t.scope_.immer_.shouldUseStrictIteration()}else r=zf(e,!0);return Ul(r,(i,s)=>{ow(r,i,uw(s))},n),t&&(t.finalized_=!1),r}var Uf=new I4,dw=Uf.produce,$4=Uf.setUseStrictIteration.bind(Uf);function fw(e){return({dispatch:r,getState:n})=>i=>s=>typeof s=="function"?s(r,n,e):i(s)}var L4=fw(),z4=fw,R4=typeof window<"u"&&window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__:function(){if(arguments.length!==0)return typeof arguments[0]=="object"?Wl:Wl.apply(null,arguments)};function ar(e,t){function r(...n){if(t){let i=t(...n);if(!i)throw new Error(Bt(0));return{type:e,payload:i.payload,..."meta"in i&&{meta:i.meta},..."error"in i&&{error:i.error}}}return{type:e,payload:n[0]}}return r.toString=()=>`${e}`,r.type=e,r.match=n=>iw(n)&&n.type===e,r}var pw=class Qa extends Array{constructor(...t){super(...t),Object.setPrototypeOf(this,Qa.prototype)}static get[Symbol.species](){return Qa}concat(...t){return super.concat.apply(this,t)}prepend(...t){return t.length===1&&Array.isArray(t[0])?new Qa(...t[0].concat(this)):new Qa(...t.concat(this))}};function e0(e){return en(e)?dw(e,()=>{}):e}function Lo(e,t,r){return e.has(t)?e.get(t):e.set(t,r(t)).get(t)}function B4(e){return typeof e=="boolean"}var F4=()=>function(t){const{thunk:r=!0,immutableCheck:n=!0,serializableCheck:i=!0,actionCreatorCheck:s=!0}=t??{};let o=new pw;return r&&(B4(r)?o.push(L4):o.push(z4(r.extraArgument))),o},hw="RTK_autoBatch",Le=()=>e=>({payload:e,meta:{[hw]:!0}}),t0=e=>t=>{setTimeout(t,e)},mw=(e={type:"raf"})=>t=>(...r)=>{const n=t(...r);let i=!0,s=!1,o=!1;const l=new Set,c=e.type==="tick"?queueMicrotask:e.type==="raf"?typeof window<"u"&&window.requestAnimationFrame?window.requestAnimationFrame:t0(10):e.type==="callback"?e.queueNotification:t0(e.timeout),d=()=>{o=!1,s&&(s=!1,l.forEach(u=>u()))};return Object.assign({},n,{subscribe(u){const f=()=>i&&u(),p=n.subscribe(f);return l.add(u),()=>{p(),l.delete(u)}},dispatch(u){var f;try{return i=!((f=u==null?void 0:u.meta)!=null&&f[hw]),s=!i,s&&(o||(o=!0,c(d))),n.dispatch(u)}finally{i=!0}}})},W4=e=>function(r){const{autoBatch:n=!0}=r??{};let i=new pw(e);return n&&i.push(mw(typeof n=="object"?n:void 0)),i};function U4(e){const t=F4(),{reducer:r=void 0,middleware:n,devTools:i=!0,preloadedState:s=void 0,enhancers:o=void 0}=e||{};let l;if(typeof r=="function")l=r;else if(Yh(r))l=nw(r);else throw new Error(Bt(1));let c;typeof n=="function"?c=n(t):c=t();let d=Wl;i&&(d=R4({trace:!1,...typeof i=="object"&&i}));const u=P4(...c),f=W4(u);let p=typeof o=="function"?o(f):f();const m=d(...p);return rw(l,s,m)}function gw(e){const t={},r=[];let n;const i={addCase(s,o){const l=typeof s=="string"?s:s.type;if(!l)throw new Error(Bt(28));if(l in t)throw new Error(Bt(29));return t[l]=o,i},addAsyncThunk(s,o){return o.pending&&(t[s.pending.type]=o.pending),o.rejected&&(t[s.rejected.type]=o.rejected),o.fulfilled&&(t[s.fulfilled.type]=o.fulfilled),o.settled&&r.push({matcher:s.settled,reducer:o.settled}),i},addMatcher(s,o){return r.push({matcher:s,reducer:o}),i},addDefaultCase(s){return n=s,i}};return e(i),[t,r,n]}$4(!1);function q4(e){return typeof e=="function"}function H4(e,t){let[r,n,i]=gw(t),s;if(q4(e))s=()=>e0(e());else{const l=e0(e);s=()=>l}function o(l=s(),c){let d=[r[c.type],...n.filter(({matcher:u})=>u(c)).map(({reducer:u})=>u)];return d.filter(u=>!!u).length===0&&(d=[i]),d.reduce((u,f)=>{if(f)if(vi(u)){const m=f(u,c);return m===void 0?u:m}else{if(en(u))return dw(u,p=>f(p,c));{const p=f(u,c);if(p===void 0){if(u===null)return u;throw Error("A case reducer on a non-draftable value must not return undefined")}return p}}return u},l)}return o.getInitialState=s,o}var K4="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW",V4=(e=21)=>{let t="",r=e;for(;r--;)t+=K4[Math.random()*64|0];return t},Y4=Symbol.for("rtk-slice-createasyncthunk");function G4(e,t){return`${e}/${t}`}function Z4({creators:e}={}){var r;const t=(r=e==null?void 0:e.asyncThunk)==null?void 0:r[Y4];return function(i){const{name:s,reducerPath:o=s}=i;if(!s)throw new Error(Bt(11));const l=(typeof i.reducers=="function"?i.reducers(J4()):i.reducers)||{},c=Object.keys(l),d={sliceCaseReducersByName:{},sliceCaseReducersByType:{},actionCreators:{},sliceMatchers:[]},u={addCase(w,S){const N=typeof w=="string"?w:w.type;if(!N)throw new Error(Bt(12));if(N in d.sliceCaseReducersByType)throw new Error(Bt(13));return d.sliceCaseReducersByType[N]=S,u},addMatcher(w,S){return d.sliceMatchers.push({matcher:w,reducer:S}),u},exposeAction(w,S){return d.actionCreators[w]=S,u},exposeCaseReducer(w,S){return d.sliceCaseReducersByName[w]=S,u}};c.forEach(w=>{const S=l[w],N={reducerName:w,type:G4(s,w),createNotation:typeof i.reducers=="function"};eE(S)?rE(N,S,u,t):Q4(N,S,u)});function f(){const[w={},S=[],N=void 0]=typeof i.extraReducers=="function"?gw(i.extraReducers):[i.extraReducers],P={...w,...d.sliceCaseReducersByType};return H4(i.initialState,_=>{for(let T in P)_.addCase(T,P[T]);for(let T of d.sliceMatchers)_.addMatcher(T.matcher,T.reducer);for(let T of S)_.addMatcher(T.matcher,T.reducer);N&&_.addDefaultCase(N)})}const p=w=>w,m=new Map,x=new WeakMap;let g;function b(w,S){return g||(g=f()),g(w,S)}function v(){return g||(g=f()),g.getInitialState()}function j(w,S=!1){function N(_){let T=_[w];return typeof T>"u"&&S&&(T=Lo(x,N,v)),T}function P(_=p){const T=Lo(m,S,()=>new WeakMap);return Lo(T,_,()=>{const $={};for(const[M,C]of Object.entries(i.selectors??{}))$[M]=X4(C,_,()=>Lo(x,_,v),S);return $})}return{reducerPath:w,getSelectors:P,get selectors(){return P(N)},selectSlice:N}}const y={name:s,reducer:b,actions:d.actionCreators,caseReducers:d.sliceCaseReducersByName,getInitialState:v,...j(o),injectInto(w,{reducerPath:S,...N}={}){const P=S??o;return w.inject({reducerPath:P,reducer:b},N),{...y,...j(P,!0)}}};return y}}function X4(e,t,r,n){function i(s,...o){let l=t(s);return typeof l>"u"&&n&&(l=r()),e(l,...o)}return i.unwrapped=e,i}var At=Z4();function J4(){function e(t,r){return{_reducerDefinitionType:"asyncThunk",payloadCreator:t,...r}}return e.withTypes=()=>e,{reducer(t){return Object.assign({[t.name](...r){return t(...r)}}[t.name],{_reducerDefinitionType:"reducer"})},preparedReducer(t,r){return{_reducerDefinitionType:"reducerWithPrepare",prepare:t,reducer:r}},asyncThunk:e}}function Q4({type:e,reducerName:t,createNotation:r},n,i){let s,o;if("reducer"in n){if(r&&!tE(n))throw new Error(Bt(17));s=n.reducer,o=n.prepare}else s=n;i.addCase(e,s).exposeCaseReducer(t,s).exposeAction(t,o?ar(e,o):ar(e))}function eE(e){return e._reducerDefinitionType==="asyncThunk"}function tE(e){return e._reducerDefinitionType==="reducerWithPrepare"}function rE({type:e,reducerName:t},r,n,i){if(!i)throw new Error(Bt(18));const{payloadCreator:s,fulfilled:o,pending:l,rejected:c,settled:d,options:u}=r,f=i(e,s,u);n.exposeAction(t,f),o&&n.addCase(f.fulfilled,o),l&&n.addCase(f.pending,l),c&&n.addCase(f.rejected,c),d&&n.addMatcher(f.settled,d),n.exposeCaseReducer(t,{fulfilled:o||zo,pending:l||zo,rejected:c||zo,settled:d||zo})}function zo(){}var nE="task",xw="listener",yw="completed",Xh="cancelled",iE=`task-${Xh}`,aE=`task-${yw}`,qf=`${xw}-${Xh}`,sE=`${xw}-${yw}`,nu=class{constructor(e){ho(this,"name","TaskAbortError");ho(this,"message");this.code=e,this.message=`${nE} ${Xh} (reason: ${e})`}},Jh=(e,t)=>{if(typeof e!="function")throw new TypeError(Bt(32))},Kl=()=>{},vw=(e,t=Kl)=>(e.catch(t),e),bw=(e,t)=>(e.addEventListener("abort",t,{once:!0}),()=>e.removeEventListener("abort",t)),li=(e,t)=>{const r=e.signal;r.aborted||("reason"in r||Object.defineProperty(r,"reason",{enumerable:!0,value:t,configurable:!0,writable:!0}),e.abort(t))},ci=e=>{if(e.aborted){const{reason:t}=e;throw new nu(t)}};function jw(e,t){let r=Kl;return new Promise((n,i)=>{const s=()=>i(new nu(e.reason));if(e.aborted){s();return}r=bw(e,s),t.finally(()=>r()).then(n,i)}).finally(()=>{r=Kl})}var oE=async(e,t)=>{try{return await Promise.resolve(),{status:"ok",value:await e()}}catch(r){return{status:r instanceof nu?"cancelled":"rejected",error:r}}finally{t==null||t()}},Vl=e=>t=>vw(jw(e,t).then(r=>(ci(e),r))),ww=e=>{const t=Vl(e);return r=>t(new Promise(n=>setTimeout(n,r)))},{assign:na}=Object,r0={},iu="listenerMiddleware",lE=(e,t)=>{const r=n=>bw(e,()=>li(n,e.reason));return(n,i)=>{Jh(n);const s=new AbortController;r(s);const o=oE(async()=>{ci(e),ci(s.signal);const l=await n({pause:Vl(s.signal),delay:ww(s.signal),signal:s.signal});return ci(s.signal),l},()=>li(s,aE));return i!=null&&i.autoJoin&&t.push(o.catch(Kl)),{result:Vl(e)(o),cancel(){li(s,iE)}}}},cE=(e,t)=>{const r=async(n,i)=>{ci(t);let s=()=>{};const l=[new Promise((c,d)=>{let u=e({predicate:n,effect:(f,p)=>{p.unsubscribe(),c([f,p.getState(),p.getOriginalState()])}});s=()=>{u(),d()}})];i!=null&&l.push(new Promise(c=>setTimeout(c,i,null)));try{const c=await jw(t,Promise.race(l));return ci(t),c}finally{s()}};return(n,i)=>vw(r(n,i))},Sw=e=>{let{type:t,actionCreator:r,matcher:n,predicate:i,effect:s}=e;if(t)i=ar(t).match;else if(r)t=r.type,i=r.match;else if(n)i=n;else if(!i)throw new Error(Bt(21));return Jh(s),{predicate:i,type:t,effect:s}},Nw=na(e=>{const{type:t,predicate:r,effect:n}=Sw(e);return{id:V4(),effect:n,type:t,predicate:r,pending:new Set,unsubscribe:()=>{throw new Error(Bt(22))}}},{withTypes:()=>Nw}),n0=(e,t)=>{const{type:r,effect:n,predicate:i}=Sw(t);return Array.from(e.values()).find(s=>(typeof r=="string"?s.type===r:s.predicate===i)&&s.effect===n)},Hf=e=>{e.pending.forEach(t=>{li(t,qf)})},uE=(e,t)=>()=>{for(const r of t.keys())Hf(r);e.clear()},i0=(e,t,r)=>{try{e(t,r)}catch(n){setTimeout(()=>{throw n},0)}},kw=na(ar(`${iu}/add`),{withTypes:()=>kw}),dE=ar(`${iu}/removeAll`),Pw=na(ar(`${iu}/remove`),{withTypes:()=>Pw}),fE=(...e)=>{console.error(`${iu}/error`,...e)},Qs=(e={})=>{const t=new Map,r=new Map,n=m=>{const x=r.get(m)??0;r.set(m,x+1)},i=m=>{const x=r.get(m)??1;x===1?r.delete(m):r.set(m,x-1)},{extra:s,onError:o=fE}=e;Jh(o);const l=m=>(m.unsubscribe=()=>t.delete(m.id),t.set(m.id,m),x=>{m.unsubscribe(),x!=null&&x.cancelActive&&Hf(m)}),c=m=>{const x=n0(t,m)??Nw(m);return l(x)};na(c,{withTypes:()=>c});const d=m=>{const x=n0(t,m);return x&&(x.unsubscribe(),m.cancelActive&&Hf(x)),!!x};na(d,{withTypes:()=>d});const u=async(m,x,g,b)=>{const v=new AbortController,j=cE(c,v.signal),y=[];try{m.pending.add(v),n(m),await Promise.resolve(m.effect(x,na({},g,{getOriginalState:b,condition:(w,S)=>j(w,S).then(Boolean),take:j,delay:ww(v.signal),pause:Vl(v.signal),extra:s,signal:v.signal,fork:lE(v.signal,y),unsubscribe:m.unsubscribe,subscribe:()=>{t.set(m.id,m)},cancelActiveListeners:()=>{m.pending.forEach((w,S,N)=>{w!==v&&(li(w,qf),N.delete(w))})},cancel:()=>{li(v,qf),m.pending.delete(v)},throwIfCancelled:()=>{ci(v.signal)}})))}catch(w){w instanceof nu||i0(o,w,{raisedBy:"effect"})}finally{await Promise.all(y),li(v,sE),i(m),m.pending.delete(v)}},f=uE(t,r);return{middleware:m=>x=>g=>{if(!iw(g))return x(g);if(kw.match(g))return c(g.payload);if(dE.match(g)){f();return}if(Pw.match(g))return d(g.payload);let b=m.getState();const v=()=>{if(b===r0)throw new Error(Bt(23));return b};let j;try{if(j=x(g),t.size>0){const y=m.getState(),w=Array.from(t.values());for(const S of w){let N=!1;try{N=S.predicate(g,y,b)}catch(P){N=!1,i0(o,P,{raisedBy:"predicate"})}N&&u(S,g,m,v)}}}finally{b=r0}return j},startListening:c,stopListening:d,clearListeners:f}};function Bt(e){return`Minified Redux Toolkit error #${e}; visit https://redux-toolkit.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}var pE={layoutType:"horizontal",width:0,height:0,margin:{top:5,right:5,bottom:5,left:5},scale:1},_w=At({name:"chartLayout",initialState:pE,reducers:{setLayout(e,t){e.layoutType=t.payload},setChartSize(e,t){e.width=t.payload.width,e.height=t.payload.height},setMargin(e,t){var r,n,i,s;e.margin.top=(r=t.payload.top)!==null&&r!==void 0?r:0,e.margin.right=(n=t.payload.right)!==null&&n!==void 0?n:0,e.margin.bottom=(i=t.payload.bottom)!==null&&i!==void 0?i:0,e.margin.left=(s=t.payload.left)!==null&&s!==void 0?s:0},setScale(e,t){e.scale=t.payload}}}),{setMargin:hE,setLayout:mE,setChartSize:gE,setScale:xE}=_w.actions,yE=_w.reducer;function Cw(e,t,r){return Array.isArray(e)&&e&&t+r!==0?e.slice(t,r+1):e}function a0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Yi(e){for(var t=1;t{if(t&&r){var{width:n,height:i}=r,{align:s,verticalAlign:o,layout:l}=t;if((l==="vertical"||l==="horizontal"&&o==="middle")&&s!=="center"&&Y(e[s]))return Yi(Yi({},e),{},{[s]:e[s]+(n||0)});if((l==="horizontal"||l==="vertical"&&s==="center")&&o!=="middle"&&Y(e[o]))return Yi(Yi({},e),{},{[o]:e[o]+(i||0)})}return e},Mr=(e,t)=>e==="horizontal"&&t==="xAxis"||e==="vertical"&&t==="yAxis"||e==="centric"&&t==="angleAxis"||e==="radial"&&t==="radiusAxis",Aw=(e,t,r,n)=>{if(n)return e.map(l=>l.coordinate);var i,s,o=e.map(l=>(l.coordinate===t&&(i=!0),l.coordinate===r&&(s=!0),l.coordinate));return i||o.push(t),s||o.push(r),o},Ow=(e,t,r)=>{if(!e)return null;var{duplicateDomain:n,type:i,range:s,scale:o,realScaleType:l,isCategorical:c,categoricalDomain:d,tickCount:u,ticks:f,niceTicks:p,axisType:m}=e;if(!o)return null;var x=l==="scaleBand"&&o.bandwidth?o.bandwidth()/2:2,g=i==="category"&&o.bandwidth?o.bandwidth()/x:0;if(g=m==="angleAxis"&&s&&s.length>=2?Jt(s[0]-s[1])*2*g:g,f||p){var b=(f||p||[]).map((v,j)=>{var y=n?n.indexOf(v):v;return{coordinate:o(y)+g,value:v,offset:g,index:j}});return b.filter(v=>!yr(v.coordinate))}return c&&d?d.map((v,j)=>({coordinate:o(v)+g,value:v,index:j,offset:g})):o.ticks&&u!=null?o.ticks(u).map((v,j)=>({coordinate:o(v)+g,value:v,offset:g,index:j})):o.domain().map((v,j)=>({coordinate:o(v)+g,value:n?n[v]:v,index:j,offset:g}))},s0=1e-4,SE=e=>{var t=e.domain();if(!(!t||t.length<=2)){var r=t.length,n=e.range(),i=Math.min(n[0],n[1])-s0,s=Math.max(n[0],n[1])+s0,o=e(t[0]),l=e(t[r-1]);(os||ls)&&e.domain([t[0],t[r-1]])}},NE=e=>{var t=e.length;if(!(t<=0))for(var r=0,n=e[0].length;r=0?(e[o][r][0]=i,e[o][r][1]=i+l,i=e[o][r][1]):(e[o][r][0]=s,e[o][r][1]=s+l,s=e[o][r][1])}},kE=e=>{var t=e.length;if(!(t<=0))for(var r=0,n=e[0].length;r=0?(e[s][r][0]=i,e[s][r][1]=i+o,i=e[s][r][1]):(e[s][r][0]=0,e[s][r][1]=0)}},PE={sign:NE,expand:$6,none:pa,silhouette:L6,wiggle:z6,positive:kE},_E=(e,t,r)=>{var n=PE[r],i=I6().keys(t).value((s,o)=>Number(et(s,o,0))).order(If).offset(n);return i(e)};function CE(e){return e==null?void 0:String(e)}function Yl(e){var{axis:t,ticks:r,bandSize:n,entry:i,index:s,dataKey:o}=e;if(t.type==="category"){if(!t.allowDuplicatedCategory&&t.dataKey&&!Re(i[t.dataKey])){var l=Cj(r,"value",i[t.dataKey]);if(l)return l.coordinate+n/2}return r[s]?r[s].coordinate+n/2:null}var c=et(i,Re(o)?t.dataKey:o);return Re(c)?null:t.scale(c)}var AE=e=>{var t=e.flat(2).filter(Y);return[Math.min(...t),Math.max(...t)]},OE=e=>[e[0]===1/0?0:e[0],e[1]===-1/0?0:e[1]],EE=(e,t,r)=>{if(e!=null)return OE(Object.keys(e).reduce((n,i)=>{var s=e[i],{stackedData:o}=s,l=o.reduce((c,d)=>{var u=Cw(d,t,r),f=AE(u);return[Math.min(c[0],f[0]),Math.max(c[1],f[1])]},[1/0,-1/0]);return[Math.min(l[0],n[0]),Math.max(l[1],n[1])]},[1/0,-1/0]))},o0=/^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,l0=/^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,ha=(e,t,r)=>{if(e&&e.scale&&e.scale.bandwidth){var n=e.scale.bandwidth();if(!r||n>0)return n}if(e&&t&&t.length>=2){for(var i=Qc(t,u=>u.coordinate),s=1/0,o=1,l=i.length;o{if(t==="horizontal")return e.chartX;if(t==="vertical")return e.chartY},TE=(e,t)=>t==="centric"?e.angle:e.radius,ln=e=>e.layout.width,cn=e=>e.layout.height,ME=e=>e.layout.scale,Ew=e=>e.layout.margin,su=I(e=>e.cartesianAxis.xAxis,e=>Object.values(e)),ou=I(e=>e.cartesianAxis.yAxis,e=>Object.values(e)),IE="data-recharts-item-index",$E="data-recharts-item-data-key",eo=60;function u0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ro(e){for(var t=1;te.brush.height;function FE(e){var t=ou(e);return t.reduce((r,n)=>{if(n.orientation==="left"&&!n.mirror&&!n.hide){var i=typeof n.width=="number"?n.width:eo;return r+i}return r},0)}function WE(e){var t=ou(e);return t.reduce((r,n)=>{if(n.orientation==="right"&&!n.mirror&&!n.hide){var i=typeof n.width=="number"?n.width:eo;return r+i}return r},0)}function UE(e){var t=su(e);return t.reduce((r,n)=>n.orientation==="top"&&!n.mirror&&!n.hide?r+n.height:r,0)}function qE(e){var t=su(e);return t.reduce((r,n)=>n.orientation==="bottom"&&!n.mirror&&!n.hide?r+n.height:r,0)}var rt=I([ln,cn,Ew,BE,FE,WE,UE,qE,tw,b4],(e,t,r,n,i,s,o,l,c,d)=>{var u={left:(r.left||0)+i,right:(r.right||0)+s},f={top:(r.top||0)+o,bottom:(r.bottom||0)+l},p=Ro(Ro({},f),u),m=p.bottom;p.bottom+=n,p=wE(p,c,d);var x=e-p.left-p.right,g=t-p.top-p.bottom;return Ro(Ro({brushBottom:m},p),{},{width:Math.max(x,0),height:Math.max(g,0)})}),HE=I(rt,e=>({x:e.left,y:e.top,width:e.width,height:e.height})),Dw=I(ln,cn,(e,t)=>({x:0,y:0,width:e,height:t})),KE=h.createContext(null),pt=()=>h.useContext(KE)!=null,lu=e=>e.brush,cu=I([lu,rt,Ew],(e,t,r)=>({height:e.height,x:Y(e.x)?e.x:t.left,y:Y(e.y)?e.y:t.top+t.height+t.brushBottom-((r==null?void 0:r.bottom)||0),width:Y(e.width)?e.width:t.width})),Tw={},Mw={},Iw={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n,{signal:i,edges:s}={}){let o,l=null;const c=s!=null&&s.includes("leading"),d=s==null||s.includes("trailing"),u=()=>{l!==null&&(r.apply(o,l),o=void 0,l=null)},f=()=>{d&&u(),g()};let p=null;const m=()=>{p!=null&&clearTimeout(p),p=setTimeout(()=>{p=null,f()},n)},x=()=>{p!==null&&(clearTimeout(p),p=null)},g=()=>{x(),o=void 0,l=null},b=()=>{u()},v=function(...j){if(i!=null&&i.aborted)return;o=this,l=j;const y=p==null;m(),c&&y&&u()};return v.schedule=m,v.cancel=g,v.flush=b,i==null||i.addEventListener("abort",g,{once:!0}),v}e.debounce=t})(Iw);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Iw;function r(n,i=0,s={}){typeof s!="object"&&(s={});const{leading:o=!1,trailing:l=!0,maxWait:c}=s,d=Array(2);o&&(d[0]="leading"),l&&(d[1]="trailing");let u,f=null;const p=t.debounce(function(...g){u=n.apply(this,g),f=null},i,{edges:d}),m=function(...g){return c!=null&&(f===null&&(f=Date.now()),Date.now()-f>=c)?(u=n.apply(this,g),f=Date.now(),p.cancel(),p.schedule(),u):(p.apply(this,g),u)},x=()=>(p.flush(),u);return m.cancel=p.cancel,m.flush=x,m}e.debounce=r})(Mw);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Mw;function r(n,i=0,s={}){const{leading:o=!0,trailing:l=!0}=s;return t.debounce(n,i,{leading:o,maxWait:i,trailing:l})}e.throttle=r})(Tw);var VE=Tw.throttle;const YE=Tr(VE);var Gl=function(t,r){for(var n=arguments.length,i=new Array(n>2?n-2:0),s=2;s{var{width:n="100%",height:i="100%",aspect:s,maxHeight:o}=r,l=Qr(n)?e:Number(n),c=Qr(i)?t:Number(i);return s&&s>0&&(l?c=l/s:c&&(l=c*s),o&&c!=null&&c>o&&(c=o)),{calculatedWidth:l,calculatedHeight:c}},GE={width:0,height:0,overflow:"visible"},ZE={width:0,overflowX:"visible"},XE={height:0,overflowY:"visible"},JE={},QE=e=>{var{width:t,height:r}=e,n=Qr(t),i=Qr(r);return n&&i?GE:n?ZE:i?XE:JE};function e3(e){var{width:t,height:r,aspect:n}=e,i=t,s=r;return i===void 0&&s===void 0?(i="100%",s="100%"):i===void 0?i=n&&n>0?void 0:"100%":s===void 0&&(s=n&&n>0?void 0:"100%"),{width:i,height:s}}function _e(e){return Number.isFinite(e)}function Er(e){return typeof e=="number"&&e>0&&Number.isFinite(e)}function Kf(){return Kf=Object.assign?Object.assign.bind():function(e){for(var t=1;t({width:r,height:n}),[r,n]);return i3(i)?h.createElement(Lw.Provider,{value:i},t):null}var Qh=()=>h.useContext(Lw),a3=h.forwardRef((e,t)=>{var{aspect:r,initialDimension:n={width:-1,height:-1},width:i,height:s,minWidth:o=0,minHeight:l,maxHeight:c,children:d,debounce:u=0,id:f,className:p,onResize:m,style:x={}}=e,g=h.useRef(null),b=h.useRef();b.current=m,h.useImperativeHandle(t,()=>g.current);var[v,j]=h.useState({containerWidth:n.width,containerHeight:n.height}),y=h.useCallback((_,T)=>{j($=>{var M=Math.round(_),C=Math.round(T);return $.containerWidth===M&&$.containerHeight===C?$:{containerWidth:M,containerHeight:C}})},[]);h.useEffect(()=>{if(g.current==null||typeof ResizeObserver>"u")return ka;var _=C=>{var R,{width:q,height:Z}=C[0].contentRect;y(q,Z),(R=b.current)===null||R===void 0||R.call(b,q,Z)};u>0&&(_=YE(_,u,{trailing:!0,leading:!1}));var T=new ResizeObserver(_),{width:$,height:M}=g.current.getBoundingClientRect();return y($,M),T.observe(g.current),()=>{T.disconnect()}},[y,u]);var{containerWidth:w,containerHeight:S}=v;Gl(!r||r>0,"The aspect(%s) must be greater than zero.",r);var{calculatedWidth:N,calculatedHeight:P}=$w(w,S,{width:i,height:s,aspect:r,maxHeight:c});return Gl(N!=null&&N>0||P!=null&&P>0,`The width(%s) and height(%s) of chart should be greater than 0, + please check the style of container, or the props width(%s) and height(%s), + or add a minWidth(%s) or minHeight(%s) or use aspect(%s) to control the + height and width.`,N,P,i,s,o,l,r),h.createElement("div",{id:f?"".concat(f):void 0,className:ce("recharts-responsive-container",p),style:f0(f0({},x),{},{width:i,height:s,minWidth:o,minHeight:l,maxHeight:c}),ref:g},h.createElement("div",{style:QE({width:i,height:s})},h.createElement(zw,{width:N,height:P},d)))}),p0=h.forwardRef((e,t)=>{var r=Qh();if(Er(r.width)&&Er(r.height))return e.children;var{width:n,height:i}=e3({width:e.width,height:e.height,aspect:e.aspect}),{calculatedWidth:s,calculatedHeight:o}=$w(void 0,void 0,{width:n,height:i,aspect:e.aspect,maxHeight:e.maxHeight});return Y(s)&&Y(o)?h.createElement(zw,{width:s,height:o},e.children):h.createElement(a3,Kf({},e,{width:n,height:i,ref:t}))});function Rw(e){if(e)return{x:e.x,y:e.y,upperWidth:"upperWidth"in e?e.upperWidth:e.width,lowerWidth:"lowerWidth"in e?e.lowerWidth:e.width,width:e.width,height:e.height}}var uu=()=>{var e,t=pt(),r=G(HE),n=G(cu),i=(e=G(lu))===null||e===void 0?void 0:e.padding;return!t||!n||!i?r:{width:n.width-i.left-i.right,height:n.height-i.top-i.bottom,x:i.left,y:i.top}},s3={top:0,bottom:0,left:0,right:0,width:0,height:0,brushBottom:0},Bw=()=>{var e;return(e=G(rt))!==null&&e!==void 0?e:s3},Fw=()=>G(ln),Ww=()=>G(cn),ue=e=>e.layout.layoutType,to=()=>G(ue),o3=()=>{var e=to();return e!==void 0},du=e=>{var t=Ve(),r=pt(),{width:n,height:i}=e,s=Qh(),o=n,l=i;return s&&(o=s.width>0?s.width:n,l=s.height>0?s.height:i),h.useEffect(()=>{!r&&Er(o)&&Er(l)&&t(gE({width:o,height:l}))},[t,r,o,l]),null},l3={settings:{layout:"horizontal",align:"center",verticalAlign:"middle",itemSorter:"value"},size:{width:0,height:0},payload:[]},Uw=At({name:"legend",initialState:l3,reducers:{setLegendSize(e,t){e.size.width=t.payload.width,e.size.height=t.payload.height},setLegendSettings(e,t){e.settings.align=t.payload.align,e.settings.layout=t.payload.layout,e.settings.verticalAlign=t.payload.verticalAlign,e.settings.itemSorter=t.payload.itemSorter},addLegendPayload:{reducer(e,t){e.payload.push(t.payload)},prepare:Le()},removeLegendPayload:{reducer(e,t){var r=Kr(e).payload.indexOf(t.payload);r>-1&&e.payload.splice(r,1)},prepare:Le()}}}),{setLegendSize:_7,setLegendSettings:C7,addLegendPayload:c3,removeLegendPayload:u3}=Uw.actions,d3=Uw.reducer;function Vf(){return Vf=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{separator:t=" : ",contentStyle:r={},itemStyle:n={},labelStyle:i={},payload:s,formatter:o,itemSorter:l,wrapperClassName:c,labelClassName:d,label:u,labelFormatter:f,accessibilityLayer:p=!1}=e,m=()=>{if(s&&s.length){var S={padding:0,margin:0},N=(l?Qc(s,l):s).map((P,_)=>{if(P.type==="none")return null;var T=P.formatter||o||m3,{value:$,name:M}=P,C=$,R=M;if(T){var q=T($,M,P,_,s);if(Array.isArray(q))[C,R]=q;else if(q!=null)C=q;else return null}var Z=hd({display:"block",paddingTop:4,paddingBottom:4,color:P.color||"#000"},n);return h.createElement("li",{className:"recharts-tooltip-item",key:"tooltip-item-".concat(_),style:Z},Or(R)?h.createElement("span",{className:"recharts-tooltip-item-name"},R):null,Or(R)?h.createElement("span",{className:"recharts-tooltip-item-separator"},t):null,h.createElement("span",{className:"recharts-tooltip-item-value"},C),h.createElement("span",{className:"recharts-tooltip-item-unit"},P.unit||""))});return h.createElement("ul",{className:"recharts-tooltip-item-list",style:S},N)}return null},x=hd({margin:0,padding:10,backgroundColor:"#fff",border:"1px solid #ccc",whiteSpace:"nowrap"},r),g=hd({margin:0},i),b=!Re(u),v=b?u:"",j=ce("recharts-default-tooltip",c),y=ce("recharts-tooltip-label",d);b&&f&&s!==void 0&&s!==null&&(v=f(u,s));var w=p?{role:"status","aria-live":"assertive"}:{};return h.createElement("div",Vf({className:j,style:x},w),h.createElement("p",{className:y,style:g},h.isValidElement(v)?v:"".concat(v)),m())},Wa="recharts-tooltip-wrapper",x3={visibility:"hidden"};function y3(e){var{coordinate:t,translateX:r,translateY:n}=e;return ce(Wa,{["".concat(Wa,"-right")]:Y(r)&&t&&Y(t.x)&&r>=t.x,["".concat(Wa,"-left")]:Y(r)&&t&&Y(t.x)&&r=t.y,["".concat(Wa,"-top")]:Y(n)&&t&&Y(t.y)&&n0?i:0),f=r[n]+i;if(t[n])return o[n]?u:f;var p=c[n];if(p==null)return 0;if(o[n]){var m=u,x=p;return mb?Math.max(u,p):Math.max(f,p)}function v3(e){var{translateX:t,translateY:r,useTranslate3d:n}=e;return{transform:n?"translate3d(".concat(t,"px, ").concat(r,"px, 0)"):"translate(".concat(t,"px, ").concat(r,"px)")}}function b3(e){var{allowEscapeViewBox:t,coordinate:r,offsetTopLeft:n,position:i,reverseDirection:s,tooltipBox:o,useTranslate3d:l,viewBox:c}=e,d,u,f;return o.height>0&&o.width>0&&r?(u=m0({allowEscapeViewBox:t,coordinate:r,key:"x",offsetTopLeft:n,position:i,reverseDirection:s,tooltipDimension:o.width,viewBox:c,viewBoxDimension:c.width}),f=m0({allowEscapeViewBox:t,coordinate:r,key:"y",offsetTopLeft:n,position:i,reverseDirection:s,tooltipDimension:o.height,viewBox:c,viewBoxDimension:c.height}),d=v3({translateX:u,translateY:f,useTranslate3d:l})):d=x3,{cssProperties:d,cssClasses:y3({translateX:u,translateY:f,coordinate:r})}}function g0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Bo(e){for(var t=1;t{if(t.key==="Escape"){var r,n,i,s;this.setState({dismissed:!0,dismissedAtCoordinate:{x:(r=(n=this.props.coordinate)===null||n===void 0?void 0:n.x)!==null&&r!==void 0?r:0,y:(i=(s=this.props.coordinate)===null||s===void 0?void 0:s.y)!==null&&i!==void 0?i:0}})}})}componentDidMount(){document.addEventListener("keydown",this.handleKeyDown)}componentWillUnmount(){document.removeEventListener("keydown",this.handleKeyDown)}componentDidUpdate(){var t,r;this.state.dismissed&&(((t=this.props.coordinate)===null||t===void 0?void 0:t.x)!==this.state.dismissedAtCoordinate.x||((r=this.props.coordinate)===null||r===void 0?void 0:r.y)!==this.state.dismissedAtCoordinate.y)&&(this.state.dismissed=!1)}render(){var{active:t,allowEscapeViewBox:r,animationDuration:n,animationEasing:i,children:s,coordinate:o,hasPayload:l,isAnimationActive:c,offset:d,position:u,reverseDirection:f,useTranslate3d:p,viewBox:m,wrapperStyle:x,lastBoundingBox:g,innerRef:b,hasPortalFromProps:v}=this.props,{cssClasses:j,cssProperties:y}=b3({allowEscapeViewBox:r,coordinate:o,offsetTopLeft:d,position:u,reverseDirection:f,tooltipBox:{height:g.height,width:g.width},useTranslate3d:p,viewBox:m}),w=v?{}:Bo(Bo({transition:c&&t?"transform ".concat(n,"ms ").concat(i):void 0},y),{},{pointerEvents:"none",visibility:!this.state.dismissed&&t&&l?"visible":"hidden",position:"absolute",top:0,left:0}),S=Bo(Bo({},w),{},{visibility:!this.state.dismissed&&t&&l?"visible":"hidden"},x);return h.createElement("div",{xmlns:"http://www.w3.org/1999/xhtml",tabIndex:-1,className:j,style:S,ref:b},s)}}var N3=()=>!(typeof window<"u"&&window.document&&window.document.createElement&&window.setTimeout),Ci={devToolsEnabled:!1,isSsr:N3()},qw=()=>{var e;return(e=G(t=>t.rootProps.accessibilityLayer))!==null&&e!==void 0?e:!0};function Gf(){return Gf=Object.assign?Object.assign.bind():function(e){for(var t=1;t_e(e.x)&&_e(e.y),b0=e=>e.base!=null&&Zl(e.base)&&Zl(e),Ua=e=>e.x,qa=e=>e.y,C3=(e,t)=>{if(typeof e=="function")return e;var r="curve".concat(Xs(e));return(r==="curveMonotone"||r==="curveBump")&&t?v0["".concat(r).concat(t==="vertical"?"Y":"X")]:v0[r]||Kc},A3=e=>{var{type:t="linear",points:r=[],baseLine:n,layout:i,connectNulls:s=!1}=e,o=C3(t,i),l=s?r.filter(Zl):r,c;if(Array.isArray(n)){var d=r.map((m,x)=>y0(y0({},m),{},{base:n[x]}));i==="vertical"?c=To().y(qa).x1(Ua).x0(m=>m.base.x):c=To().x(Ua).y1(qa).y0(m=>m.base.y);var u=c.defined(b0).curve(o),f=s?d.filter(b0):d;return u(f)}i==="vertical"&&Y(n)?c=To().y(qa).x1(Ua).x0(n):Y(n)?c=To().x(Ua).y1(qa).y0(n):c=hj().x(Ua).y(qa);var p=c.defined(Zl).curve(o);return p(l)},us=e=>{var{className:t,points:r,path:n,pathRef:i}=e;if((!r||!r.length)&&!n)return null;var s=r&&r.length?A3(e):n;return h.createElement("path",Gf({},nr(e),Ih(e),{className:ce("recharts-curve",t),d:s===null?void 0:s,ref:i}))},O3=["x","y","top","left","width","height","className"];function Zf(){return Zf=Object.assign?Object.assign.bind():function(e){for(var t=1;t"M".concat(e,",").concat(i,"v").concat(n,"M").concat(s,",").concat(t,"h").concat(r),z3=e=>{var{x:t=0,y:r=0,top:n=0,left:i=0,width:s=0,height:o=0,className:l}=e,c=I3(e,O3),d=E3({x:t,y:r,top:n,left:i,width:s,height:o},c);return!Y(t)||!Y(r)||!Y(s)||!Y(o)||!Y(n)||!Y(i)?null:h.createElement("path",Zf({},ut(d),{className:ce("recharts-cross",l),d:L3(t,r,s,o,n,i)}))};function R3(e,t,r,n){var i=n/2;return{stroke:"none",fill:"#ccc",x:e==="horizontal"?t.x-i:r.left+.5,y:e==="horizontal"?r.top+.5:t.y-i,width:e==="horizontal"?n:r.width-1,height:e==="horizontal"?r.height-1:n}}function w0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function S0(e){for(var t=1;te.replace(/([A-Z])/g,t=>"-".concat(t.toLowerCase())),Hw=(e,t,r)=>e.map(n=>"".concat(U3(n)," ").concat(t,"ms ").concat(r)).join(","),q3=(e,t)=>[Object.keys(e),Object.keys(t)].reduce((r,n)=>r.filter(i=>n.includes(i))),Ls=(e,t)=>Object.keys(t).reduce((r,n)=>S0(S0({},r),{},{[n]:e(n,t[n])}),{});function N0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function $e(e){for(var t=1;te+(t-e)*r,Xf=e=>{var{from:t,to:r}=e;return t!==r},Kw=(e,t,r)=>{var n=Ls((i,s)=>{if(Xf(s)){var[o,l]=e(s.from,s.to,s.velocity);return $e($e({},s),{},{from:o,velocity:l})}return s},t);return r<1?Ls((i,s)=>Xf(s)?$e($e({},s),{},{velocity:Xl(s.velocity,n[i].velocity,r),from:Xl(s.from,n[i].from,r)}):s,t):Kw(e,n,r-1)};function Y3(e,t,r,n,i,s){var o,l=n.reduce((p,m)=>$e($e({},p),{},{[m]:{from:e[m],velocity:0,to:t[m]}}),{}),c=()=>Ls((p,m)=>m.from,l),d=()=>!Object.values(l).filter(Xf).length,u=null,f=p=>{o||(o=p);var m=p-o,x=m/r.dt;l=Kw(r,l,x),i($e($e($e({},e),t),c())),o=p,d()||(u=s.setTimeout(f))};return()=>(u=s.setTimeout(f),()=>{var p;(p=u)===null||p===void 0||p()})}function G3(e,t,r,n,i,s,o){var l=null,c=i.reduce((f,p)=>$e($e({},f),{},{[p]:[e[p],t[p]]}),{}),d,u=f=>{d||(d=f);var p=(f-d)/n,m=Ls((g,b)=>Xl(...b,r(p)),c);if(s($e($e($e({},e),t),m)),p<1)l=o.setTimeout(u);else{var x=Ls((g,b)=>Xl(...b,r(1)),c);s($e($e($e({},e),t),x))}};return()=>(l=o.setTimeout(u),()=>{var f;(f=l)===null||f===void 0||f()})}const Z3=(e,t,r,n,i,s)=>{var o=q3(e,t);return r==null?()=>(i($e($e({},e),t)),()=>{}):r.isStepper===!0?Y3(e,t,r,o,i,s):G3(e,t,r,n,o,i,s)};var Jl=1e-4,Vw=(e,t)=>[0,3*e,3*t-6*e,3*e-3*t+1],Yw=(e,t)=>e.map((r,n)=>r*t**n).reduce((r,n)=>r+n),k0=(e,t)=>r=>{var n=Vw(e,t);return Yw(n,r)},X3=(e,t)=>r=>{var n=Vw(e,t),i=[...n.map((s,o)=>s*o).slice(1),0];return Yw(i,r)},J3=function(){for(var t=arguments.length,r=new Array(t),n=0;nparseFloat(l));return[o[0],o[1],o[2],o[3]]}}}return r.length===4?r:[0,0,1,1]},Q3=(e,t,r,n)=>{var i=k0(e,r),s=k0(t,n),o=X3(e,r),l=d=>d>1?1:d<0?0:d,c=d=>{for(var u=d>1?1:d,f=u,p=0;p<8;++p){var m=i(f)-u,x=o(f);if(Math.abs(m-u)0&&arguments[0]!==void 0?arguments[0]:{},{stiff:r=100,damping:n=8,dt:i=17}=t,s=(o,l,c)=>{var d=-(o-l)*r,u=c*n,f=c+(d-u)*i/1e3,p=c*i/1e3+o;return Math.abs(p-l){if(typeof e=="string")switch(e){case"ease":case"ease-in-out":case"ease-out":case"ease-in":case"linear":return P0(e);case"spring":return eD();default:if(e.split("(")[0]==="cubic-bezier")return P0(e)}return typeof e=="function"?e:null};function rD(e){var t,r=()=>null,n=!1,i=null,s=o=>{if(!n){if(Array.isArray(o)){if(!o.length)return;var l=o,[c,...d]=l;if(typeof c=="number"){i=e.setTimeout(s.bind(null,d),c);return}s(c),i=e.setTimeout(s.bind(null,d));return}typeof o=="string"&&(t=o,r(t)),typeof o=="object"&&(t=o,r(t)),typeof o=="function"&&o()}};return{stop:()=>{n=!0},start:o=>{n=!1,i&&(i(),i=null),s(o)},subscribe:o=>(r=o,()=>{r=()=>null}),getTimeoutController:()=>e}}class nD{setTimeout(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,n=performance.now(),i=null,s=o=>{o-n>=r?t(o):typeof requestAnimationFrame=="function"&&(i=requestAnimationFrame(s))};return i=requestAnimationFrame(s),()=>{i!=null&&cancelAnimationFrame(i)}}}function iD(){return rD(new nD)}var aD=h.createContext(iD);function sD(e,t){var r=h.useContext(aD);return h.useMemo(()=>t??r(e),[e,t,r])}var oD={begin:0,duration:1e3,easing:"ease",isActive:!0,canBegin:!0,onAnimationEnd:()=>{},onAnimationStart:()=>{}},_0={t:0},md={t:1};function fu(e){var t=ft(e,oD),{isActive:r,canBegin:n,duration:i,easing:s,begin:o,onAnimationEnd:l,onAnimationStart:c,children:d}=t,u=sD(t.animationId,t.animationManager),[f,p]=h.useState(r?_0:md),m=h.useRef(null);return h.useEffect(()=>{r||p(md)},[r]),h.useEffect(()=>{if(!r||!n)return ka;var x=Z3(_0,md,tD(s),i,p,u.getTimeoutController()),g=()=>{m.current=x()};return u.start([c,o,g,i,l]),()=>{u.stop(),m.current&&m.current(),l()}},[r,n,i,s,o,c,l,u]),d(f.t)}function pu(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"animation-",r=h.useRef(Ts(t)),n=h.useRef(e);return n.current!==e&&(r.current=Ts(t),n.current=e),r.current}var lD=["radius"],cD=["radius"];function C0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function A0(e){for(var t=1;t{var s=Math.min(Math.abs(r)/2,Math.abs(n)/2),o=n>=0?1:-1,l=r>=0?1:-1,c=n>=0&&r>=0||n<0&&r<0?1:0,d;if(s>0&&i instanceof Array){for(var u=[0,0,0,0],f=0,p=4;fs?s:i[f];d="M".concat(e,",").concat(t+o*u[0]),u[0]>0&&(d+="A ".concat(u[0],",").concat(u[0],",0,0,").concat(c,",").concat(e+l*u[0],",").concat(t)),d+="L ".concat(e+r-l*u[1],",").concat(t),u[1]>0&&(d+="A ".concat(u[1],",").concat(u[1],",0,0,").concat(c,`, + `).concat(e+r,",").concat(t+o*u[1])),d+="L ".concat(e+r,",").concat(t+n-o*u[2]),u[2]>0&&(d+="A ".concat(u[2],",").concat(u[2],",0,0,").concat(c,`, + `).concat(e+r-l*u[2],",").concat(t+n)),d+="L ".concat(e+l*u[3],",").concat(t+n),u[3]>0&&(d+="A ".concat(u[3],",").concat(u[3],",0,0,").concat(c,`, + `).concat(e,",").concat(t+n-o*u[3])),d+="Z"}else if(s>0&&i===+i&&i>0){var m=Math.min(s,i);d="M ".concat(e,",").concat(t+o*m,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e+l*m,",").concat(t,` + L `).concat(e+r-l*m,",").concat(t,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e+r,",").concat(t+o*m,` + L `).concat(e+r,",").concat(t+n-o*m,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e+r-l*m,",").concat(t+n,` + L `).concat(e+l*m,",").concat(t+n,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e,",").concat(t+n-o*m," Z")}else d="M ".concat(e,",").concat(t," h ").concat(r," v ").concat(n," h ").concat(-r," Z");return d},D0={x:0,y:0,width:0,height:0,radius:0,isAnimationActive:!1,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},Gw=e=>{var t=ft(e,D0),r=h.useRef(null),[n,i]=h.useState(-1);h.useEffect(()=>{if(r.current&&r.current.getTotalLength)try{var D=r.current.getTotalLength();D&&i(D)}catch{}},[]);var{x:s,y:o,width:l,height:c,radius:d,className:u}=t,{animationEasing:f,animationDuration:p,animationBegin:m,isAnimationActive:x,isUpdateAnimationActive:g}=t,b=h.useRef(l),v=h.useRef(c),j=h.useRef(s),y=h.useRef(o),w=h.useMemo(()=>({x:s,y:o,width:l,height:c,radius:d}),[s,o,l,c,d]),S=pu(w,"rectangle-");if(s!==+s||o!==+o||l!==+l||c!==+c||l===0||c===0)return null;var N=ce("recharts-rectangle",u);if(!g){var P=ut(t),{radius:_}=P,T=O0(P,lD);return h.createElement("path",Ql({},T,{radius:typeof d=="number"?d:void 0,className:N,d:E0(s,o,l,c,d)}))}var $=b.current,M=v.current,C=j.current,R=y.current,q="0px ".concat(n===-1?1:n,"px"),Z="".concat(n,"px 0px"),E=Hw(["strokeDasharray"],p,typeof f=="string"?f:D0.animationEasing);return h.createElement(fu,{animationId:S,key:S,canBegin:n>0,duration:p,easing:f,isActive:g,begin:m},D=>{var O=De($,l,D),k=De(M,c,D),L=De(C,s,D),U=De(R,o,D);r.current&&(b.current=O,v.current=k,j.current=L,y.current=U);var H;x?D>0?H={transition:E,strokeDasharray:Z}:H={strokeDasharray:q}:H={strokeDasharray:Z};var te=ut(t),{radius:re}=te,we=O0(te,cD);return h.createElement("path",Ql({},we,{radius:typeof d=="number"?d:void 0,className:N,d:E0(L,U,O,k,d),ref:r,style:A0(A0({},H),t.style)}))})};function T0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function M0(e){for(var t=1;te*180/Math.PI,Je=(e,t,r,n)=>({x:e+Math.cos(-ec*n)*r,y:t+Math.sin(-ec*n)*r}),yD=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{top:0,right:0,bottom:0,left:0};return Math.min(Math.abs(t-(n.left||0)-(n.right||0)),Math.abs(r-(n.top||0)-(n.bottom||0)))/2},vD=(e,t)=>{var{x:r,y:n}=e,{x:i,y:s}=t;return Math.sqrt((r-i)**2+(n-s)**2)},bD=(e,t)=>{var{x:r,y:n}=e,{cx:i,cy:s}=t,o=vD({x:r,y:n},{x:i,y:s});if(o<=0)return{radius:o,angle:0};var l=(r-i)/o,c=Math.acos(l);return n>s&&(c=2*Math.PI-c),{radius:o,angle:xD(c),angleInRadian:c}},jD=e=>{var{startAngle:t,endAngle:r}=e,n=Math.floor(t/360),i=Math.floor(r/360),s=Math.min(n,i);return{startAngle:t-s*360,endAngle:r-s*360}},wD=(e,t)=>{var{startAngle:r,endAngle:n}=t,i=Math.floor(r/360),s=Math.floor(n/360),o=Math.min(i,s);return e+o*360},SD=(e,t)=>{var{chartX:r,chartY:n}=e,{radius:i,angle:s}=bD({x:r,y:n},t),{innerRadius:o,outerRadius:l}=t;if(il||i===0)return null;var{startAngle:c,endAngle:d}=jD(t),u=s,f;if(c<=d){for(;u>d;)u-=360;for(;u=c&&u<=d}else{for(;u>c;)u-=360;for(;u=d&&u<=c}return f?M0(M0({},t),{},{radius:i,angle:wD(u,t)}):null};function Zw(e){var{cx:t,cy:r,radius:n,startAngle:i,endAngle:s}=e,o=Je(t,r,n,i),l=Je(t,r,n,s);return{points:[o,l],cx:t,cy:r,radius:n,startAngle:i,endAngle:s}}function Jf(){return Jf=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var r=Jt(t-e),n=Math.min(Math.abs(t-e),359.999);return r*n},Fo=e=>{var{cx:t,cy:r,radius:n,angle:i,sign:s,isExternal:o,cornerRadius:l,cornerIsExternal:c}=e,d=l*(o?1:-1)+n,u=Math.asin(l/d)/ec,f=c?i:i+s*u,p=Je(t,r,d,f),m=Je(t,r,n,f),x=c?i-s*u:i,g=Je(t,r,d*Math.cos(u*ec),x);return{center:p,circleTangency:m,lineTangency:g,theta:u}},Xw=e=>{var{cx:t,cy:r,innerRadius:n,outerRadius:i,startAngle:s,endAngle:o}=e,l=ND(s,o),c=s+l,d=Je(t,r,i,s),u=Je(t,r,i,c),f="M ".concat(d.x,",").concat(d.y,` + A `).concat(i,",").concat(i,`,0, + `).concat(+(Math.abs(l)>180),",").concat(+(s>c),`, + `).concat(u.x,",").concat(u.y,` + `);if(n>0){var p=Je(t,r,n,s),m=Je(t,r,n,c);f+="L ".concat(m.x,",").concat(m.y,` + A `).concat(n,",").concat(n,`,0, + `).concat(+(Math.abs(l)>180),",").concat(+(s<=c),`, + `).concat(p.x,",").concat(p.y," Z")}else f+="L ".concat(t,",").concat(r," Z");return f},kD=e=>{var{cx:t,cy:r,innerRadius:n,outerRadius:i,cornerRadius:s,forceCornerRadius:o,cornerIsExternal:l,startAngle:c,endAngle:d}=e,u=Jt(d-c),{circleTangency:f,lineTangency:p,theta:m}=Fo({cx:t,cy:r,radius:i,angle:c,sign:u,cornerRadius:s,cornerIsExternal:l}),{circleTangency:x,lineTangency:g,theta:b}=Fo({cx:t,cy:r,radius:i,angle:d,sign:-u,cornerRadius:s,cornerIsExternal:l}),v=l?Math.abs(c-d):Math.abs(c-d)-m-b;if(v<0)return o?"M ".concat(p.x,",").concat(p.y,` + a`).concat(s,",").concat(s,",0,0,1,").concat(s*2,`,0 + a`).concat(s,",").concat(s,",0,0,1,").concat(-s*2,`,0 + `):Xw({cx:t,cy:r,innerRadius:n,outerRadius:i,startAngle:c,endAngle:d});var j="M ".concat(p.x,",").concat(p.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(f.x,",").concat(f.y,` + A`).concat(i,",").concat(i,",0,").concat(+(v>180),",").concat(+(u<0),",").concat(x.x,",").concat(x.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(g.x,",").concat(g.y,` + `);if(n>0){var{circleTangency:y,lineTangency:w,theta:S}=Fo({cx:t,cy:r,radius:n,angle:c,sign:u,isExternal:!0,cornerRadius:s,cornerIsExternal:l}),{circleTangency:N,lineTangency:P,theta:_}=Fo({cx:t,cy:r,radius:n,angle:d,sign:-u,isExternal:!0,cornerRadius:s,cornerIsExternal:l}),T=l?Math.abs(c-d):Math.abs(c-d)-S-_;if(T<0&&s===0)return"".concat(j,"L").concat(t,",").concat(r,"Z");j+="L".concat(P.x,",").concat(P.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(N.x,",").concat(N.y,` + A`).concat(n,",").concat(n,",0,").concat(+(T>180),",").concat(+(u>0),",").concat(y.x,",").concat(y.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(w.x,",").concat(w.y,"Z")}else j+="L".concat(t,",").concat(r,"Z");return j},PD={cx:0,cy:0,innerRadius:0,outerRadius:0,startAngle:0,endAngle:0,cornerRadius:0,forceCornerRadius:!1,cornerIsExternal:!1},Jw=e=>{var t=ft(e,PD),{cx:r,cy:n,innerRadius:i,outerRadius:s,cornerRadius:o,forceCornerRadius:l,cornerIsExternal:c,startAngle:d,endAngle:u,className:f}=t;if(s0&&Math.abs(d-u)<360?g=kD({cx:r,cy:n,innerRadius:i,outerRadius:s,cornerRadius:Math.min(x,m/2),forceCornerRadius:l,cornerIsExternal:c,startAngle:d,endAngle:u}):g=Xw({cx:r,cy:n,innerRadius:i,outerRadius:s,startAngle:d,endAngle:u}),h.createElement("path",Jf({},ut(t),{className:p,d:g}))};function _D(e,t,r){if(e==="horizontal")return[{x:t.x,y:r.top},{x:t.x,y:r.top+r.height}];if(e==="vertical")return[{x:r.left,y:t.y},{x:r.left+r.width,y:t.y}];if(Ej(t)){if(e==="centric"){var{cx:n,cy:i,innerRadius:s,outerRadius:o,angle:l}=t,c=Je(n,i,s,l),d=Je(n,i,o,l);return[{x:c.x,y:c.y},{x:d.x,y:d.y}]}return Zw(t)}}var Qw={},eS={},tS={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Kh;function r(n){return t.isSymbol(n)?NaN:Number(n)}e.toNumber=r})(tS);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=tS;function r(n){return n?(n=t.toNumber(n),n===1/0||n===-1/0?(n<0?-1:1)*Number.MAX_VALUE:n===n?n:0):n===0?n:0}e.toFinite=r})(eS);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Vh,r=eS;function n(i,s,o){o&&typeof o!="number"&&t.isIterateeCall(i,s,o)&&(s=o=void 0),i=r.toFinite(i),s===void 0?(s=i,i=0):s=r.toFinite(s),o=o===void 0?it?1:e>=t?0:NaN}function AD(e,t){return e==null||t==null?NaN:te?1:t>=e?0:NaN}function em(e){let t,r,n;e.length!==2?(t=Mn,r=(l,c)=>Mn(e(l),c),n=(l,c)=>e(l)-c):(t=e===Mn||e===AD?e:OD,r=e,n=e);function i(l,c,d=0,u=l.length){if(d>>1;r(l[f],c)<0?d=f+1:u=f}while(d>>1;r(l[f],c)<=0?d=f+1:u=f}while(dd&&n(l[f-1],c)>-n(l[f],c)?f-1:f}return{left:i,center:o,right:s}}function OD(){return 0}function nS(e){return e===null?NaN:+e}function*ED(e,t){for(let r of e)r!=null&&(r=+r)>=r&&(yield r)}const DD=em(Mn),ro=DD.right;em(nS).center;class I0 extends Map{constructor(t,r=ID){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:r}}),t!=null)for(const[n,i]of t)this.set(n,i)}get(t){return super.get($0(this,t))}has(t){return super.has($0(this,t))}set(t,r){return super.set(TD(this,t),r)}delete(t){return super.delete(MD(this,t))}}function $0({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):r}function TD({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):(e.set(n,r),r)}function MD({_intern:e,_key:t},r){const n=t(r);return e.has(n)&&(r=e.get(n),e.delete(n)),r}function ID(e){return e!==null&&typeof e=="object"?e.valueOf():e}function $D(e=Mn){if(e===Mn)return iS;if(typeof e!="function")throw new TypeError("compare is not a function");return(t,r)=>{const n=e(t,r);return n||n===0?n:(e(r,r)===0)-(e(t,t)===0)}}function iS(e,t){return(e==null||!(e>=e))-(t==null||!(t>=t))||(et?1:0)}const LD=Math.sqrt(50),zD=Math.sqrt(10),RD=Math.sqrt(2);function tc(e,t,r){const n=(t-e)/Math.max(0,r),i=Math.floor(Math.log10(n)),s=n/Math.pow(10,i),o=s>=LD?10:s>=zD?5:s>=RD?2:1;let l,c,d;return i<0?(d=Math.pow(10,-i)/o,l=Math.round(e*d),c=Math.round(t*d),l/dt&&--c,d=-d):(d=Math.pow(10,i)*o,l=Math.round(e/d),c=Math.round(t/d),l*dt&&--c),c0))return[];if(e===t)return[e];const n=t=i))return[];const l=s-i+1,c=new Array(l);if(n)if(o<0)for(let d=0;d=n)&&(r=n);return r}function z0(e,t){let r;for(const n of e)n!=null&&(r>n||r===void 0&&n>=n)&&(r=n);return r}function aS(e,t,r=0,n=1/0,i){if(t=Math.floor(t),r=Math.floor(Math.max(0,r)),n=Math.floor(Math.min(e.length-1,n)),!(r<=t&&t<=n))return e;for(i=i===void 0?iS:$D(i);n>r;){if(n-r>600){const c=n-r+1,d=t-r+1,u=Math.log(c),f=.5*Math.exp(2*u/3),p=.5*Math.sqrt(u*f*(c-f)/c)*(d-c/2<0?-1:1),m=Math.max(r,Math.floor(t-d*f/c+p)),x=Math.min(n,Math.floor(t+(c-d)*f/c+p));aS(e,t,m,x,i)}const s=e[t];let o=r,l=n;for(Ha(e,r,t),i(e[n],s)>0&&Ha(e,r,n);o0;)--l}i(e[r],s)===0?Ha(e,r,l):(++l,Ha(e,l,n)),l<=t&&(r=l+1),t<=l&&(n=l-1)}return e}function Ha(e,t,r){const n=e[t];e[t]=e[r],e[r]=n}function BD(e,t,r){if(e=Float64Array.from(ED(e)),!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return z0(e);if(t>=1)return L0(e);var n,i=(n-1)*t,s=Math.floor(i),o=L0(aS(e,s).subarray(0,s+1)),l=z0(e.subarray(s+1));return o+(l-o)*(i-s)}}function FD(e,t,r=nS){if(!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return+r(e[0],0,e);if(t>=1)return+r(e[n-1],n-1,e);var n,i=(n-1)*t,s=Math.floor(i),o=+r(e[s],s,e),l=+r(e[s+1],s+1,e);return o+(l-o)*(i-s)}}function WD(e,t,r){e=+e,t=+t,r=(i=arguments.length)<2?(t=e,e=0,1):i<3?1:+r;for(var n=-1,i=Math.max(0,Math.ceil((t-e)/r))|0,s=new Array(i);++n>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):r===8?Wo(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):r===4?Wo(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=HD.exec(e))?new Nt(t[1],t[2],t[3],1):(t=KD.exec(e))?new Nt(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=VD.exec(e))?Wo(t[1],t[2],t[3],t[4]):(t=YD.exec(e))?Wo(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=GD.exec(e))?H0(t[1],t[2]/100,t[3]/100,1):(t=ZD.exec(e))?H0(t[1],t[2]/100,t[3]/100,t[4]):R0.hasOwnProperty(e)?W0(R0[e]):e==="transparent"?new Nt(NaN,NaN,NaN,0):null}function W0(e){return new Nt(e>>16&255,e>>8&255,e&255,1)}function Wo(e,t,r,n){return n<=0&&(e=t=r=NaN),new Nt(e,t,r,n)}function QD(e){return e instanceof no||(e=Bs(e)),e?(e=e.rgb(),new Nt(e.r,e.g,e.b,e.opacity)):new Nt}function np(e,t,r,n){return arguments.length===1?QD(e):new Nt(e,t,r,n??1)}function Nt(e,t,r,n){this.r=+e,this.g=+t,this.b=+r,this.opacity=+n}nm(Nt,np,oS(no,{brighter(e){return e=e==null?rc:Math.pow(rc,e),new Nt(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?zs:Math.pow(zs,e),new Nt(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new Nt(ui(this.r),ui(this.g),ui(this.b),nc(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:U0,formatHex:U0,formatHex8:e5,formatRgb:q0,toString:q0}));function U0(){return`#${ni(this.r)}${ni(this.g)}${ni(this.b)}`}function e5(){return`#${ni(this.r)}${ni(this.g)}${ni(this.b)}${ni((isNaN(this.opacity)?1:this.opacity)*255)}`}function q0(){const e=nc(this.opacity);return`${e===1?"rgb(":"rgba("}${ui(this.r)}, ${ui(this.g)}, ${ui(this.b)}${e===1?")":`, ${e})`}`}function nc(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function ui(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function ni(e){return e=ui(e),(e<16?"0":"")+e.toString(16)}function H0(e,t,r,n){return n<=0?e=t=r=NaN:r<=0||r>=1?e=t=NaN:t<=0&&(e=NaN),new pr(e,t,r,n)}function lS(e){if(e instanceof pr)return new pr(e.h,e.s,e.l,e.opacity);if(e instanceof no||(e=Bs(e)),!e)return new pr;if(e instanceof pr)return e;e=e.rgb();var t=e.r/255,r=e.g/255,n=e.b/255,i=Math.min(t,r,n),s=Math.max(t,r,n),o=NaN,l=s-i,c=(s+i)/2;return l?(t===s?o=(r-n)/l+(r0&&c<1?0:o,new pr(o,l,c,e.opacity)}function t5(e,t,r,n){return arguments.length===1?lS(e):new pr(e,t,r,n??1)}function pr(e,t,r,n){this.h=+e,this.s=+t,this.l=+r,this.opacity=+n}nm(pr,t5,oS(no,{brighter(e){return e=e==null?rc:Math.pow(rc,e),new pr(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?zs:Math.pow(zs,e),new pr(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,r=this.l,n=r+(r<.5?r:1-r)*t,i=2*r-n;return new Nt(gd(e>=240?e-240:e+120,i,n),gd(e,i,n),gd(e<120?e+240:e-120,i,n),this.opacity)},clamp(){return new pr(K0(this.h),Uo(this.s),Uo(this.l),nc(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const e=nc(this.opacity);return`${e===1?"hsl(":"hsla("}${K0(this.h)}, ${Uo(this.s)*100}%, ${Uo(this.l)*100}%${e===1?")":`, ${e})`}`}}));function K0(e){return e=(e||0)%360,e<0?e+360:e}function Uo(e){return Math.max(0,Math.min(1,e||0))}function gd(e,t,r){return(e<60?t+(r-t)*e/60:e<180?r:e<240?t+(r-t)*(240-e)/60:t)*255}const im=e=>()=>e;function r5(e,t){return function(r){return e+r*t}}function n5(e,t,r){return e=Math.pow(e,r),t=Math.pow(t,r)-e,r=1/r,function(n){return Math.pow(e+n*t,r)}}function i5(e){return(e=+e)==1?cS:function(t,r){return r-t?n5(t,r,e):im(isNaN(t)?r:t)}}function cS(e,t){var r=t-e;return r?r5(e,r):im(isNaN(e)?t:e)}const V0=function e(t){var r=i5(t);function n(i,s){var o=r((i=np(i)).r,(s=np(s)).r),l=r(i.g,s.g),c=r(i.b,s.b),d=cS(i.opacity,s.opacity);return function(u){return i.r=o(u),i.g=l(u),i.b=c(u),i.opacity=d(u),i+""}}return n.gamma=e,n}(1);function a5(e,t){t||(t=[]);var r=e?Math.min(t.length,e.length):0,n=t.slice(),i;return function(s){for(i=0;ir&&(s=t.slice(r,s),l[o]?l[o]+=s:l[++o]=s),(n=n[0])===(i=i[0])?l[o]?l[o]+=i:l[++o]=i:(l[++o]=null,c.push({i:o,x:ic(n,i)})),r=xd.lastIndex;return rt&&(r=e,e=t,t=r),function(n){return Math.max(e,Math.min(t,n))}}function g5(e,t,r){var n=e[0],i=e[1],s=t[0],o=t[1];return i2?x5:g5,c=d=null,f}function f(p){return p==null||isNaN(p=+p)?s:(c||(c=l(e.map(n),t,r)))(n(o(p)))}return f.invert=function(p){return o(i((d||(d=l(t,e.map(n),ic)))(p)))},f.domain=function(p){return arguments.length?(e=Array.from(p,ac),u()):e.slice()},f.range=function(p){return arguments.length?(t=Array.from(p),u()):t.slice()},f.rangeRound=function(p){return t=Array.from(p),r=am,u()},f.clamp=function(p){return arguments.length?(o=p?!0:mt,u()):o!==mt},f.interpolate=function(p){return arguments.length?(r=p,u()):r},f.unknown=function(p){return arguments.length?(s=p,f):s},function(p,m){return n=p,i=m,u()}}function sm(){return hu()(mt,mt)}function y5(e){return Math.abs(e=Math.round(e))>=1e21?e.toLocaleString("en").replace(/,/g,""):e.toString(10)}function sc(e,t){if((r=(e=t?e.toExponential(t-1):e.toExponential()).indexOf("e"))<0)return null;var r,n=e.slice(0,r);return[n.length>1?n[0]+n.slice(2):n,+e.slice(r+1)]}function ma(e){return e=sc(Math.abs(e)),e?e[1]:NaN}function v5(e,t){return function(r,n){for(var i=r.length,s=[],o=0,l=e[0],c=0;i>0&&l>0&&(c+l+1>n&&(l=Math.max(1,n-c)),s.push(r.substring(i-=l,i+l)),!((c+=l+1)>n));)l=e[o=(o+1)%e.length];return s.reverse().join(t)}}function b5(e){return function(t){return t.replace(/[0-9]/g,function(r){return e[+r]})}}var j5=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Fs(e){if(!(t=j5.exec(e)))throw new Error("invalid format: "+e);var t;return new om({fill:t[1],align:t[2],sign:t[3],symbol:t[4],zero:t[5],width:t[6],comma:t[7],precision:t[8]&&t[8].slice(1),trim:t[9],type:t[10]})}Fs.prototype=om.prototype;function om(e){this.fill=e.fill===void 0?" ":e.fill+"",this.align=e.align===void 0?">":e.align+"",this.sign=e.sign===void 0?"-":e.sign+"",this.symbol=e.symbol===void 0?"":e.symbol+"",this.zero=!!e.zero,this.width=e.width===void 0?void 0:+e.width,this.comma=!!e.comma,this.precision=e.precision===void 0?void 0:+e.precision,this.trim=!!e.trim,this.type=e.type===void 0?"":e.type+""}om.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(this.width===void 0?"":Math.max(1,this.width|0))+(this.comma?",":"")+(this.precision===void 0?"":"."+Math.max(0,this.precision|0))+(this.trim?"~":"")+this.type};function w5(e){e:for(var t=e.length,r=1,n=-1,i;r0&&(n=0);break}return n>0?e.slice(0,n)+e.slice(i+1):e}var uS;function S5(e,t){var r=sc(e,t);if(!r)return e+"";var n=r[0],i=r[1],s=i-(uS=Math.max(-8,Math.min(8,Math.floor(i/3)))*3)+1,o=n.length;return s===o?n:s>o?n+new Array(s-o+1).join("0"):s>0?n.slice(0,s)+"."+n.slice(s):"0."+new Array(1-s).join("0")+sc(e,Math.max(0,t+s-1))[0]}function G0(e,t){var r=sc(e,t);if(!r)return e+"";var n=r[0],i=r[1];return i<0?"0."+new Array(-i).join("0")+n:n.length>i+1?n.slice(0,i+1)+"."+n.slice(i+1):n+new Array(i-n.length+2).join("0")}const Z0={"%":(e,t)=>(e*100).toFixed(t),b:e=>Math.round(e).toString(2),c:e=>e+"",d:y5,e:(e,t)=>e.toExponential(t),f:(e,t)=>e.toFixed(t),g:(e,t)=>e.toPrecision(t),o:e=>Math.round(e).toString(8),p:(e,t)=>G0(e*100,t),r:G0,s:S5,X:e=>Math.round(e).toString(16).toUpperCase(),x:e=>Math.round(e).toString(16)};function X0(e){return e}var J0=Array.prototype.map,Q0=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function N5(e){var t=e.grouping===void 0||e.thousands===void 0?X0:v5(J0.call(e.grouping,Number),e.thousands+""),r=e.currency===void 0?"":e.currency[0]+"",n=e.currency===void 0?"":e.currency[1]+"",i=e.decimal===void 0?".":e.decimal+"",s=e.numerals===void 0?X0:b5(J0.call(e.numerals,String)),o=e.percent===void 0?"%":e.percent+"",l=e.minus===void 0?"−":e.minus+"",c=e.nan===void 0?"NaN":e.nan+"";function d(f){f=Fs(f);var p=f.fill,m=f.align,x=f.sign,g=f.symbol,b=f.zero,v=f.width,j=f.comma,y=f.precision,w=f.trim,S=f.type;S==="n"?(j=!0,S="g"):Z0[S]||(y===void 0&&(y=12),w=!0,S="g"),(b||p==="0"&&m==="=")&&(b=!0,p="0",m="=");var N=g==="$"?r:g==="#"&&/[boxX]/.test(S)?"0"+S.toLowerCase():"",P=g==="$"?n:/[%p]/.test(S)?o:"",_=Z0[S],T=/[defgprs%]/.test(S);y=y===void 0?6:/[gprs]/.test(S)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y));function $(M){var C=N,R=P,q,Z,E;if(S==="c")R=_(M)+R,M="";else{M=+M;var D=M<0||1/M<0;if(M=isNaN(M)?c:_(Math.abs(M),y),w&&(M=w5(M)),D&&+M==0&&x!=="+"&&(D=!1),C=(D?x==="("?x:l:x==="-"||x==="("?"":x)+C,R=(S==="s"?Q0[8+uS/3]:"")+R+(D&&x==="("?")":""),T){for(q=-1,Z=M.length;++qE||E>57){R=(E===46?i+M.slice(q+1):M.slice(q))+R,M=M.slice(0,q);break}}}j&&!b&&(M=t(M,1/0));var O=C.length+M.length+R.length,k=O>1)+C+M+R+k.slice(O);break;default:M=k+C+M+R;break}return s(M)}return $.toString=function(){return f+""},$}function u(f,p){var m=d((f=Fs(f),f.type="f",f)),x=Math.max(-8,Math.min(8,Math.floor(ma(p)/3)))*3,g=Math.pow(10,-x),b=Q0[8+x/3];return function(v){return m(g*v)+b}}return{format:d,formatPrefix:u}}var qo,lm,dS;k5({thousands:",",grouping:[3],currency:["$",""]});function k5(e){return qo=N5(e),lm=qo.format,dS=qo.formatPrefix,qo}function P5(e){return Math.max(0,-ma(Math.abs(e)))}function _5(e,t){return Math.max(0,Math.max(-8,Math.min(8,Math.floor(ma(t)/3)))*3-ma(Math.abs(e)))}function C5(e,t){return e=Math.abs(e),t=Math.abs(t)-e,Math.max(0,ma(t)-ma(e))+1}function fS(e,t,r,n){var i=tp(e,t,r),s;switch(n=Fs(n??",f"),n.type){case"s":{var o=Math.max(Math.abs(e),Math.abs(t));return n.precision==null&&!isNaN(s=_5(i,o))&&(n.precision=s),dS(n,o)}case"":case"e":case"g":case"p":case"r":{n.precision==null&&!isNaN(s=C5(i,Math.max(Math.abs(e),Math.abs(t))))&&(n.precision=s-(n.type==="e"));break}case"f":case"%":{n.precision==null&&!isNaN(s=P5(i))&&(n.precision=s-(n.type==="%")*2);break}}return lm(n)}function qn(e){var t=e.domain;return e.ticks=function(r){var n=t();return Qf(n[0],n[n.length-1],r??10)},e.tickFormat=function(r,n){var i=t();return fS(i[0],i[i.length-1],r??10,n)},e.nice=function(r){r==null&&(r=10);var n=t(),i=0,s=n.length-1,o=n[i],l=n[s],c,d,u=10;for(l0;){if(d=ep(o,l,r),d===c)return n[i]=o,n[s]=l,t(n);if(d>0)o=Math.floor(o/d)*d,l=Math.ceil(l/d)*d;else if(d<0)o=Math.ceil(o*d)/d,l=Math.floor(l*d)/d;else break;c=d}return e},e}function pS(){var e=sm();return e.copy=function(){return io(e,pS())},or.apply(e,arguments),qn(e)}function hS(e){var t;function r(n){return n==null||isNaN(n=+n)?t:n}return r.invert=r,r.domain=r.range=function(n){return arguments.length?(e=Array.from(n,ac),r):e.slice()},r.unknown=function(n){return arguments.length?(t=n,r):t},r.copy=function(){return hS(e).unknown(t)},e=arguments.length?Array.from(e,ac):[0,1],qn(r)}function mS(e,t){e=e.slice();var r=0,n=e.length-1,i=e[r],s=e[n],o;return sMath.pow(e,t)}function T5(e){return e===Math.E?Math.log:e===10&&Math.log10||e===2&&Math.log2||(e=Math.log(e),t=>Math.log(t)/e)}function ry(e){return(t,r)=>-e(-t,r)}function cm(e){const t=e(ey,ty),r=t.domain;let n=10,i,s;function o(){return i=T5(n),s=D5(n),r()[0]<0?(i=ry(i),s=ry(s),e(A5,O5)):e(ey,ty),t}return t.base=function(l){return arguments.length?(n=+l,o()):n},t.domain=function(l){return arguments.length?(r(l),o()):r()},t.ticks=l=>{const c=r();let d=c[0],u=c[c.length-1];const f=u0){for(;p<=m;++p)for(x=1;xu)break;v.push(g)}}else for(;p<=m;++p)for(x=n-1;x>=1;--x)if(g=p>0?x/s(-p):x*s(p),!(gu)break;v.push(g)}v.length*2{if(l==null&&(l=10),c==null&&(c=n===10?"s":","),typeof c!="function"&&(!(n%1)&&(c=Fs(c)).precision==null&&(c.trim=!0),c=lm(c)),l===1/0)return c;const d=Math.max(1,n*l/t.ticks().length);return u=>{let f=u/s(Math.round(i(u)));return f*nr(mS(r(),{floor:l=>s(Math.floor(i(l))),ceil:l=>s(Math.ceil(i(l)))})),t}function gS(){const e=cm(hu()).domain([1,10]);return e.copy=()=>io(e,gS()).base(e.base()),or.apply(e,arguments),e}function ny(e){return function(t){return Math.sign(t)*Math.log1p(Math.abs(t/e))}}function iy(e){return function(t){return Math.sign(t)*Math.expm1(Math.abs(t))*e}}function um(e){var t=1,r=e(ny(t),iy(t));return r.constant=function(n){return arguments.length?e(ny(t=+n),iy(t)):t},qn(r)}function xS(){var e=um(hu());return e.copy=function(){return io(e,xS()).constant(e.constant())},or.apply(e,arguments)}function ay(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function M5(e){return e<0?-Math.sqrt(-e):Math.sqrt(e)}function I5(e){return e<0?-e*e:e*e}function dm(e){var t=e(mt,mt),r=1;function n(){return r===1?e(mt,mt):r===.5?e(M5,I5):e(ay(r),ay(1/r))}return t.exponent=function(i){return arguments.length?(r=+i,n()):r},qn(t)}function fm(){var e=dm(hu());return e.copy=function(){return io(e,fm()).exponent(e.exponent())},or.apply(e,arguments),e}function $5(){return fm.apply(null,arguments).exponent(.5)}function sy(e){return Math.sign(e)*e*e}function L5(e){return Math.sign(e)*Math.sqrt(Math.abs(e))}function yS(){var e=sm(),t=[0,1],r=!1,n;function i(s){var o=L5(e(s));return isNaN(o)?n:r?Math.round(o):o}return i.invert=function(s){return e.invert(sy(s))},i.domain=function(s){return arguments.length?(e.domain(s),i):e.domain()},i.range=function(s){return arguments.length?(e.range((t=Array.from(s,ac)).map(sy)),i):t.slice()},i.rangeRound=function(s){return i.range(s).round(!0)},i.round=function(s){return arguments.length?(r=!!s,i):r},i.clamp=function(s){return arguments.length?(e.clamp(s),i):e.clamp()},i.unknown=function(s){return arguments.length?(n=s,i):n},i.copy=function(){return yS(e.domain(),t).round(r).clamp(e.clamp()).unknown(n)},or.apply(i,arguments),qn(i)}function vS(){var e=[],t=[],r=[],n;function i(){var o=0,l=Math.max(1,t.length);for(r=new Array(l-1);++o0?r[l-1]:e[0],l=r?[n[r-1],t]:[n[d-1],n[d]]},o.unknown=function(c){return arguments.length&&(s=c),o},o.thresholds=function(){return n.slice()},o.copy=function(){return bS().domain([e,t]).range(i).unknown(s)},or.apply(qn(o),arguments)}function jS(){var e=[.5],t=[0,1],r,n=1;function i(s){return s!=null&&s<=s?t[ro(e,s,0,n)]:r}return i.domain=function(s){return arguments.length?(e=Array.from(s),n=Math.min(e.length,t.length-1),i):e.slice()},i.range=function(s){return arguments.length?(t=Array.from(s),n=Math.min(e.length,t.length-1),i):t.slice()},i.invertExtent=function(s){var o=t.indexOf(s);return[e[o-1],e[o]]},i.unknown=function(s){return arguments.length?(r=s,i):r},i.copy=function(){return jS().domain(e).range(t).unknown(r)},or.apply(i,arguments)}const yd=new Date,vd=new Date;function Be(e,t,r,n){function i(s){return e(s=arguments.length===0?new Date:new Date(+s)),s}return i.floor=s=>(e(s=new Date(+s)),s),i.ceil=s=>(e(s=new Date(s-1)),t(s,1),e(s),s),i.round=s=>{const o=i(s),l=i.ceil(s);return s-o(t(s=new Date(+s),o==null?1:Math.floor(o)),s),i.range=(s,o,l)=>{const c=[];if(s=i.ceil(s),l=l==null?1:Math.floor(l),!(s0))return c;let d;do c.push(d=new Date(+s)),t(s,l),e(s);while(dBe(o=>{if(o>=o)for(;e(o),!s(o);)o.setTime(o-1)},(o,l)=>{if(o>=o)if(l<0)for(;++l<=0;)for(;t(o,-1),!s(o););else for(;--l>=0;)for(;t(o,1),!s(o););}),r&&(i.count=(s,o)=>(yd.setTime(+s),vd.setTime(+o),e(yd),e(vd),Math.floor(r(yd,vd))),i.every=s=>(s=Math.floor(s),!isFinite(s)||!(s>0)?null:s>1?i.filter(n?o=>n(o)%s===0:o=>i.count(0,o)%s===0):i)),i}const oc=Be(()=>{},(e,t)=>{e.setTime(+e+t)},(e,t)=>t-e);oc.every=e=>(e=Math.floor(e),!isFinite(e)||!(e>0)?null:e>1?Be(t=>{t.setTime(Math.floor(t/e)*e)},(t,r)=>{t.setTime(+t+r*e)},(t,r)=>(r-t)/e):oc);oc.range;const Wr=1e3,Qt=Wr*60,Ur=Qt*60,tn=Ur*24,pm=tn*7,oy=tn*30,bd=tn*365,ii=Be(e=>{e.setTime(e-e.getMilliseconds())},(e,t)=>{e.setTime(+e+t*Wr)},(e,t)=>(t-e)/Wr,e=>e.getUTCSeconds());ii.range;const hm=Be(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*Wr)},(e,t)=>{e.setTime(+e+t*Qt)},(e,t)=>(t-e)/Qt,e=>e.getMinutes());hm.range;const mm=Be(e=>{e.setUTCSeconds(0,0)},(e,t)=>{e.setTime(+e+t*Qt)},(e,t)=>(t-e)/Qt,e=>e.getUTCMinutes());mm.range;const gm=Be(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*Wr-e.getMinutes()*Qt)},(e,t)=>{e.setTime(+e+t*Ur)},(e,t)=>(t-e)/Ur,e=>e.getHours());gm.range;const xm=Be(e=>{e.setUTCMinutes(0,0,0)},(e,t)=>{e.setTime(+e+t*Ur)},(e,t)=>(t-e)/Ur,e=>e.getUTCHours());xm.range;const ao=Be(e=>e.setHours(0,0,0,0),(e,t)=>e.setDate(e.getDate()+t),(e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*Qt)/tn,e=>e.getDate()-1);ao.range;const mu=Be(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/tn,e=>e.getUTCDate()-1);mu.range;const wS=Be(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/tn,e=>Math.floor(e/tn));wS.range;function Ai(e){return Be(t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7),t.setHours(0,0,0,0)},(t,r)=>{t.setDate(t.getDate()+r*7)},(t,r)=>(r-t-(r.getTimezoneOffset()-t.getTimezoneOffset())*Qt)/pm)}const gu=Ai(0),lc=Ai(1),z5=Ai(2),R5=Ai(3),ga=Ai(4),B5=Ai(5),F5=Ai(6);gu.range;lc.range;z5.range;R5.range;ga.range;B5.range;F5.range;function Oi(e){return Be(t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCDate(t.getUTCDate()+r*7)},(t,r)=>(r-t)/pm)}const xu=Oi(0),cc=Oi(1),W5=Oi(2),U5=Oi(3),xa=Oi(4),q5=Oi(5),H5=Oi(6);xu.range;cc.range;W5.range;U5.range;xa.range;q5.range;H5.range;const ym=Be(e=>{e.setDate(1),e.setHours(0,0,0,0)},(e,t)=>{e.setMonth(e.getMonth()+t)},(e,t)=>t.getMonth()-e.getMonth()+(t.getFullYear()-e.getFullYear())*12,e=>e.getMonth());ym.range;const vm=Be(e=>{e.setUTCDate(1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)},(e,t)=>t.getUTCMonth()-e.getUTCMonth()+(t.getUTCFullYear()-e.getUTCFullYear())*12,e=>e.getUTCMonth());vm.range;const rn=Be(e=>{e.setMonth(0,1),e.setHours(0,0,0,0)},(e,t)=>{e.setFullYear(e.getFullYear()+t)},(e,t)=>t.getFullYear()-e.getFullYear(),e=>e.getFullYear());rn.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Be(t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e),t.setMonth(0,1),t.setHours(0,0,0,0)},(t,r)=>{t.setFullYear(t.getFullYear()+r*e)});rn.range;const nn=Be(e=>{e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)},(e,t)=>t.getUTCFullYear()-e.getUTCFullYear(),e=>e.getUTCFullYear());nn.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Be(t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e),t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCFullYear(t.getUTCFullYear()+r*e)});nn.range;function SS(e,t,r,n,i,s){const o=[[ii,1,Wr],[ii,5,5*Wr],[ii,15,15*Wr],[ii,30,30*Wr],[s,1,Qt],[s,5,5*Qt],[s,15,15*Qt],[s,30,30*Qt],[i,1,Ur],[i,3,3*Ur],[i,6,6*Ur],[i,12,12*Ur],[n,1,tn],[n,2,2*tn],[r,1,pm],[t,1,oy],[t,3,3*oy],[e,1,bd]];function l(d,u,f){const p=ub).right(o,p);if(m===o.length)return e.every(tp(d/bd,u/bd,f));if(m===0)return oc.every(Math.max(tp(d,u,f),1));const[x,g]=o[p/o[m-1][2]53)return null;"w"in W||(W.w=1),"Z"in W?(pe=wd(Ka(W.y,0,1)),Et=pe.getUTCDay(),pe=Et>4||Et===0?cc.ceil(pe):cc(pe),pe=mu.offset(pe,(W.V-1)*7),W.y=pe.getUTCFullYear(),W.m=pe.getUTCMonth(),W.d=pe.getUTCDate()+(W.w+6)%7):(pe=jd(Ka(W.y,0,1)),Et=pe.getDay(),pe=Et>4||Et===0?lc.ceil(pe):lc(pe),pe=ao.offset(pe,(W.V-1)*7),W.y=pe.getFullYear(),W.m=pe.getMonth(),W.d=pe.getDate()+(W.w+6)%7)}else("W"in W||"U"in W)&&("w"in W||(W.w="u"in W?W.u%7:"W"in W?1:0),Et="Z"in W?wd(Ka(W.y,0,1)).getUTCDay():jd(Ka(W.y,0,1)).getDay(),W.m=0,W.d="W"in W?(W.w+6)%7+W.W*7-(Et+5)%7:W.w+W.U*7-(Et+6)%7);return"Z"in W?(W.H+=W.Z/100|0,W.M+=W.Z%100,wd(W)):jd(W)}}function _(z,ee,ne,W){for(var bt=0,pe=ee.length,Et=ne.length,Dt,Yn;bt=Et)return-1;if(Dt=ee.charCodeAt(bt++),Dt===37){if(Dt=ee.charAt(bt++),Yn=S[Dt in ly?ee.charAt(bt++):Dt],!Yn||(W=Yn(z,ne,W))<0)return-1}else if(Dt!=ne.charCodeAt(W++))return-1}return W}function T(z,ee,ne){var W=d.exec(ee.slice(ne));return W?(z.p=u.get(W[0].toLowerCase()),ne+W[0].length):-1}function $(z,ee,ne){var W=m.exec(ee.slice(ne));return W?(z.w=x.get(W[0].toLowerCase()),ne+W[0].length):-1}function M(z,ee,ne){var W=f.exec(ee.slice(ne));return W?(z.w=p.get(W[0].toLowerCase()),ne+W[0].length):-1}function C(z,ee,ne){var W=v.exec(ee.slice(ne));return W?(z.m=j.get(W[0].toLowerCase()),ne+W[0].length):-1}function R(z,ee,ne){var W=g.exec(ee.slice(ne));return W?(z.m=b.get(W[0].toLowerCase()),ne+W[0].length):-1}function q(z,ee,ne){return _(z,t,ee,ne)}function Z(z,ee,ne){return _(z,r,ee,ne)}function E(z,ee,ne){return _(z,n,ee,ne)}function D(z){return o[z.getDay()]}function O(z){return s[z.getDay()]}function k(z){return c[z.getMonth()]}function L(z){return l[z.getMonth()]}function U(z){return i[+(z.getHours()>=12)]}function H(z){return 1+~~(z.getMonth()/3)}function te(z){return o[z.getUTCDay()]}function re(z){return s[z.getUTCDay()]}function we(z){return c[z.getUTCMonth()]}function A(z){return l[z.getUTCMonth()]}function J(z){return i[+(z.getUTCHours()>=12)]}function Ot(z){return 1+~~(z.getUTCMonth()/3)}return{format:function(z){var ee=N(z+="",y);return ee.toString=function(){return z},ee},parse:function(z){var ee=P(z+="",!1);return ee.toString=function(){return z},ee},utcFormat:function(z){var ee=N(z+="",w);return ee.toString=function(){return z},ee},utcParse:function(z){var ee=P(z+="",!0);return ee.toString=function(){return z},ee}}}var ly={"-":"",_:" ",0:"0"},Ye=/^\s*\d+/,X5=/^%/,J5=/[\\^$*+?|[\]().{}]/g;function se(e,t,r){var n=e<0?"-":"",i=(n?-e:e)+"",s=i.length;return n+(s[t.toLowerCase(),r]))}function eT(e,t,r){var n=Ye.exec(t.slice(r,r+1));return n?(e.w=+n[0],r+n[0].length):-1}function tT(e,t,r){var n=Ye.exec(t.slice(r,r+1));return n?(e.u=+n[0],r+n[0].length):-1}function rT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.U=+n[0],r+n[0].length):-1}function nT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.V=+n[0],r+n[0].length):-1}function iT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.W=+n[0],r+n[0].length):-1}function cy(e,t,r){var n=Ye.exec(t.slice(r,r+4));return n?(e.y=+n[0],r+n[0].length):-1}function uy(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.y=+n[0]+(+n[0]>68?1900:2e3),r+n[0].length):-1}function aT(e,t,r){var n=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(t.slice(r,r+6));return n?(e.Z=n[1]?0:-(n[2]+(n[3]||"00")),r+n[0].length):-1}function sT(e,t,r){var n=Ye.exec(t.slice(r,r+1));return n?(e.q=n[0]*3-3,r+n[0].length):-1}function oT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.m=n[0]-1,r+n[0].length):-1}function dy(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.d=+n[0],r+n[0].length):-1}function lT(e,t,r){var n=Ye.exec(t.slice(r,r+3));return n?(e.m=0,e.d=+n[0],r+n[0].length):-1}function fy(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.H=+n[0],r+n[0].length):-1}function cT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.M=+n[0],r+n[0].length):-1}function uT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.S=+n[0],r+n[0].length):-1}function dT(e,t,r){var n=Ye.exec(t.slice(r,r+3));return n?(e.L=+n[0],r+n[0].length):-1}function fT(e,t,r){var n=Ye.exec(t.slice(r,r+6));return n?(e.L=Math.floor(n[0]/1e3),r+n[0].length):-1}function pT(e,t,r){var n=X5.exec(t.slice(r,r+1));return n?r+n[0].length:-1}function hT(e,t,r){var n=Ye.exec(t.slice(r));return n?(e.Q=+n[0],r+n[0].length):-1}function mT(e,t,r){var n=Ye.exec(t.slice(r));return n?(e.s=+n[0],r+n[0].length):-1}function py(e,t){return se(e.getDate(),t,2)}function gT(e,t){return se(e.getHours(),t,2)}function xT(e,t){return se(e.getHours()%12||12,t,2)}function yT(e,t){return se(1+ao.count(rn(e),e),t,3)}function NS(e,t){return se(e.getMilliseconds(),t,3)}function vT(e,t){return NS(e,t)+"000"}function bT(e,t){return se(e.getMonth()+1,t,2)}function jT(e,t){return se(e.getMinutes(),t,2)}function wT(e,t){return se(e.getSeconds(),t,2)}function ST(e){var t=e.getDay();return t===0?7:t}function NT(e,t){return se(gu.count(rn(e)-1,e),t,2)}function kS(e){var t=e.getDay();return t>=4||t===0?ga(e):ga.ceil(e)}function kT(e,t){return e=kS(e),se(ga.count(rn(e),e)+(rn(e).getDay()===4),t,2)}function PT(e){return e.getDay()}function _T(e,t){return se(lc.count(rn(e)-1,e),t,2)}function CT(e,t){return se(e.getFullYear()%100,t,2)}function AT(e,t){return e=kS(e),se(e.getFullYear()%100,t,2)}function OT(e,t){return se(e.getFullYear()%1e4,t,4)}function ET(e,t){var r=e.getDay();return e=r>=4||r===0?ga(e):ga.ceil(e),se(e.getFullYear()%1e4,t,4)}function DT(e){var t=e.getTimezoneOffset();return(t>0?"-":(t*=-1,"+"))+se(t/60|0,"0",2)+se(t%60,"0",2)}function hy(e,t){return se(e.getUTCDate(),t,2)}function TT(e,t){return se(e.getUTCHours(),t,2)}function MT(e,t){return se(e.getUTCHours()%12||12,t,2)}function IT(e,t){return se(1+mu.count(nn(e),e),t,3)}function PS(e,t){return se(e.getUTCMilliseconds(),t,3)}function $T(e,t){return PS(e,t)+"000"}function LT(e,t){return se(e.getUTCMonth()+1,t,2)}function zT(e,t){return se(e.getUTCMinutes(),t,2)}function RT(e,t){return se(e.getUTCSeconds(),t,2)}function BT(e){var t=e.getUTCDay();return t===0?7:t}function FT(e,t){return se(xu.count(nn(e)-1,e),t,2)}function _S(e){var t=e.getUTCDay();return t>=4||t===0?xa(e):xa.ceil(e)}function WT(e,t){return e=_S(e),se(xa.count(nn(e),e)+(nn(e).getUTCDay()===4),t,2)}function UT(e){return e.getUTCDay()}function qT(e,t){return se(cc.count(nn(e)-1,e),t,2)}function HT(e,t){return se(e.getUTCFullYear()%100,t,2)}function KT(e,t){return e=_S(e),se(e.getUTCFullYear()%100,t,2)}function VT(e,t){return se(e.getUTCFullYear()%1e4,t,4)}function YT(e,t){var r=e.getUTCDay();return e=r>=4||r===0?xa(e):xa.ceil(e),se(e.getUTCFullYear()%1e4,t,4)}function GT(){return"+0000"}function my(){return"%"}function gy(e){return+e}function xy(e){return Math.floor(+e/1e3)}var Di,CS,AS;ZT({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});function ZT(e){return Di=Z5(e),CS=Di.format,Di.parse,AS=Di.utcFormat,Di.utcParse,Di}function XT(e){return new Date(e)}function JT(e){return e instanceof Date?+e:+new Date(+e)}function bm(e,t,r,n,i,s,o,l,c,d){var u=sm(),f=u.invert,p=u.domain,m=d(".%L"),x=d(":%S"),g=d("%I:%M"),b=d("%I %p"),v=d("%a %d"),j=d("%b %d"),y=d("%B"),w=d("%Y");function S(N){return(c(N)t(i/(e.length-1)))},r.quantiles=function(n){return Array.from({length:n+1},(i,s)=>BD(e,s/n))},r.copy=function(){return TS(t).domain(e)},un.apply(r,arguments)}function vu(){var e=0,t=.5,r=1,n=1,i,s,o,l,c,d=mt,u,f=!1,p;function m(g){return isNaN(g=+g)?p:(g=.5+((g=+u(g))-s)*(n*ge.chartData,nM=I([Kn],e=>{var t=e.chartData!=null?e.chartData.length-1:0;return{chartData:e.chartData,computedData:e.computedData,dataEndIndex:t,dataStartIndex:0}}),bu=(e,t,r,n)=>n?nM(e):Kn(e);function ji(e){if(Array.isArray(e)&&e.length===2){var[t,r]=e;if(_e(t)&&_e(r))return!0}return!1}function yy(e,t,r){return r?e:[Math.min(e[0],t[0]),Math.max(e[1],t[1])]}function LS(e,t){if(t&&typeof e!="function"&&Array.isArray(e)&&e.length===2){var[r,n]=e,i,s;if(_e(r))i=r;else if(typeof r=="function")return;if(_e(n))s=n;else if(typeof n=="function")return;var o=[i,s];if(ji(o))return o}}function iM(e,t,r){if(!(!r&&t==null)){if(typeof e=="function"&&t!=null)try{var n=e(t,r);if(ji(n))return yy(n,t,r)}catch{}if(Array.isArray(e)&&e.length===2){var[i,s]=e,o,l;if(i==="auto")t!=null&&(o=Math.min(...t));else if(Y(i))o=i;else if(typeof i=="function")try{t!=null&&(o=i(t==null?void 0:t[0]))}catch{}else if(typeof i=="string"&&o0.test(i)){var c=o0.exec(i);if(c==null||t==null)o=void 0;else{var d=+c[1];o=t[0]-d}}else o=t==null?void 0:t[0];if(s==="auto")t!=null&&(l=Math.max(...t));else if(Y(s))l=s;else if(typeof s=="function")try{t!=null&&(l=s(t==null?void 0:t[1]))}catch{}else if(typeof s=="string"&&l0.test(s)){var u=l0.exec(s);if(u==null||t==null)l=void 0;else{var f=+u[1];l=t[1]+f}}else l=t==null?void 0:t[1];var p=[o,l];if(ji(p))return t==null?p:yy(p,t,r)}}}var _a=1e9,aM={precision:20,rounding:4,toExpNeg:-7,toExpPos:21,LN10:"2.302585092994045684017991454684364207601101488628772976033327900967572609677352480235997205089598298341967784042286"},Nm,je=!0,sr="[DecimalError] ",di=sr+"Invalid argument: ",Sm=sr+"Exponent out of range: ",Ca=Math.floor,Qn=Math.pow,sM=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,Lt,qe=1e7,xe=7,zS=9007199254740991,uc=Ca(zS/xe),V={};V.absoluteValue=V.abs=function(){var e=new this.constructor(this);return e.s&&(e.s=1),e};V.comparedTo=V.cmp=function(e){var t,r,n,i,s=this;if(e=new s.constructor(e),s.s!==e.s)return s.s||-e.s;if(s.e!==e.e)return s.e>e.e^s.s<0?1:-1;for(n=s.d.length,i=e.d.length,t=0,r=ne.d[t]^s.s<0?1:-1;return n===i?0:n>i^s.s<0?1:-1};V.decimalPlaces=V.dp=function(){var e=this,t=e.d.length-1,r=(t-e.e)*xe;if(t=e.d[t],t)for(;t%10==0;t/=10)r--;return r<0?0:r};V.dividedBy=V.div=function(e){return Vr(this,new this.constructor(e))};V.dividedToIntegerBy=V.idiv=function(e){var t=this,r=t.constructor;return fe(Vr(t,new r(e),0,1),r.precision)};V.equals=V.eq=function(e){return!this.cmp(e)};V.exponent=function(){return Me(this)};V.greaterThan=V.gt=function(e){return this.cmp(e)>0};V.greaterThanOrEqualTo=V.gte=function(e){return this.cmp(e)>=0};V.isInteger=V.isint=function(){return this.e>this.d.length-2};V.isNegative=V.isneg=function(){return this.s<0};V.isPositive=V.ispos=function(){return this.s>0};V.isZero=function(){return this.s===0};V.lessThan=V.lt=function(e){return this.cmp(e)<0};V.lessThanOrEqualTo=V.lte=function(e){return this.cmp(e)<1};V.logarithm=V.log=function(e){var t,r=this,n=r.constructor,i=n.precision,s=i+5;if(e===void 0)e=new n(10);else if(e=new n(e),e.s<1||e.eq(Lt))throw Error(sr+"NaN");if(r.s<1)throw Error(sr+(r.s?"NaN":"-Infinity"));return r.eq(Lt)?new n(0):(je=!1,t=Vr(Ws(r,s),Ws(e,s),s),je=!0,fe(t,i))};V.minus=V.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?FS(t,e):RS(t,(e.s=-e.s,e))};V.modulo=V.mod=function(e){var t,r=this,n=r.constructor,i=n.precision;if(e=new n(e),!e.s)throw Error(sr+"NaN");return r.s?(je=!1,t=Vr(r,e,0,1).times(e),je=!0,r.minus(t)):fe(new n(r),i)};V.naturalExponential=V.exp=function(){return BS(this)};V.naturalLogarithm=V.ln=function(){return Ws(this)};V.negated=V.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e};V.plus=V.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?RS(t,e):FS(t,(e.s=-e.s,e))};V.precision=V.sd=function(e){var t,r,n,i=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(di+e);if(t=Me(i)+1,n=i.d.length-1,r=n*xe+1,n=i.d[n],n){for(;n%10==0;n/=10)r--;for(n=i.d[0];n>=10;n/=10)r++}return e&&t>r?t:r};V.squareRoot=V.sqrt=function(){var e,t,r,n,i,s,o,l=this,c=l.constructor;if(l.s<1){if(!l.s)return new c(0);throw Error(sr+"NaN")}for(e=Me(l),je=!1,i=Math.sqrt(+l),i==0||i==1/0?(t=Nr(l.d),(t.length+e)%2==0&&(t+="0"),i=Math.sqrt(t),e=Ca((e+1)/2)-(e<0||e%2),i==1/0?t="5e"+e:(t=i.toExponential(),t=t.slice(0,t.indexOf("e")+1)+e),n=new c(t)):n=new c(i.toString()),r=c.precision,i=o=r+3;;)if(s=n,n=s.plus(Vr(l,s,o+2)).times(.5),Nr(s.d).slice(0,o)===(t=Nr(n.d)).slice(0,o)){if(t=t.slice(o-3,o+1),i==o&&t=="4999"){if(fe(s,r+1,0),s.times(s).eq(l)){n=s;break}}else if(t!="9999")break;o+=4}return je=!0,fe(n,r)};V.times=V.mul=function(e){var t,r,n,i,s,o,l,c,d,u=this,f=u.constructor,p=u.d,m=(e=new f(e)).d;if(!u.s||!e.s)return new f(0);for(e.s*=u.s,r=u.e+e.e,c=p.length,d=m.length,c=0;){for(t=0,i=c+n;i>n;)l=s[i]+m[n]*p[i-n-1]+t,s[i--]=l%qe|0,t=l/qe|0;s[i]=(s[i]+t)%qe|0}for(;!s[--o];)s.pop();return t?++r:s.shift(),e.d=s,e.e=r,je?fe(e,f.precision):e};V.toDecimalPlaces=V.todp=function(e,t){var r=this,n=r.constructor;return r=new n(r),e===void 0?r:(Dr(e,0,_a),t===void 0?t=n.rounding:Dr(t,0,8),fe(r,e+Me(r)+1,t))};V.toExponential=function(e,t){var r,n=this,i=n.constructor;return e===void 0?r=wi(n,!0):(Dr(e,0,_a),t===void 0?t=i.rounding:Dr(t,0,8),n=fe(new i(n),e+1,t),r=wi(n,!0,e+1)),r};V.toFixed=function(e,t){var r,n,i=this,s=i.constructor;return e===void 0?wi(i):(Dr(e,0,_a),t===void 0?t=s.rounding:Dr(t,0,8),n=fe(new s(i),e+Me(i)+1,t),r=wi(n.abs(),!1,e+Me(n)+1),i.isneg()&&!i.isZero()?"-"+r:r)};V.toInteger=V.toint=function(){var e=this,t=e.constructor;return fe(new t(e),Me(e)+1,t.rounding)};V.toNumber=function(){return+this};V.toPower=V.pow=function(e){var t,r,n,i,s,o,l=this,c=l.constructor,d=12,u=+(e=new c(e));if(!e.s)return new c(Lt);if(l=new c(l),!l.s){if(e.s<1)throw Error(sr+"Infinity");return l}if(l.eq(Lt))return l;if(n=c.precision,e.eq(Lt))return fe(l,n);if(t=e.e,r=e.d.length-1,o=t>=r,s=l.s,o){if((r=u<0?-u:u)<=zS){for(i=new c(Lt),t=Math.ceil(n/xe+4),je=!1;r%2&&(i=i.times(l),by(i.d,t)),r=Ca(r/2),r!==0;)l=l.times(l),by(l.d,t);return je=!0,e.s<0?new c(Lt).div(i):fe(i,n)}}else if(s<0)throw Error(sr+"NaN");return s=s<0&&e.d[Math.max(t,r)]&1?-1:1,l.s=1,je=!1,i=e.times(Ws(l,n+d)),je=!0,i=BS(i),i.s=s,i};V.toPrecision=function(e,t){var r,n,i=this,s=i.constructor;return e===void 0?(r=Me(i),n=wi(i,r<=s.toExpNeg||r>=s.toExpPos)):(Dr(e,1,_a),t===void 0?t=s.rounding:Dr(t,0,8),i=fe(new s(i),e,t),r=Me(i),n=wi(i,e<=r||r<=s.toExpNeg,e)),n};V.toSignificantDigits=V.tosd=function(e,t){var r=this,n=r.constructor;return e===void 0?(e=n.precision,t=n.rounding):(Dr(e,1,_a),t===void 0?t=n.rounding:Dr(t,0,8)),fe(new n(r),e,t)};V.toString=V.valueOf=V.val=V.toJSON=V[Symbol.for("nodejs.util.inspect.custom")]=function(){var e=this,t=Me(e),r=e.constructor;return wi(e,t<=r.toExpNeg||t>=r.toExpPos)};function RS(e,t){var r,n,i,s,o,l,c,d,u=e.constructor,f=u.precision;if(!e.s||!t.s)return t.s||(t=new u(e)),je?fe(t,f):t;if(c=e.d,d=t.d,o=e.e,i=t.e,c=c.slice(),s=o-i,s){for(s<0?(n=c,s=-s,l=d.length):(n=d,i=o,l=c.length),o=Math.ceil(f/xe),l=o>l?o+1:l+1,s>l&&(s=l,n.length=1),n.reverse();s--;)n.push(0);n.reverse()}for(l=c.length,s=d.length,l-s<0&&(s=l,n=d,d=c,c=n),r=0;s;)r=(c[--s]=c[s]+d[s]+r)/qe|0,c[s]%=qe;for(r&&(c.unshift(r),++i),l=c.length;c[--l]==0;)c.pop();return t.d=c,t.e=i,je?fe(t,f):t}function Dr(e,t,r){if(e!==~~e||er)throw Error(di+e)}function Nr(e){var t,r,n,i=e.length-1,s="",o=e[0];if(i>0){for(s+=o,t=1;to?1:-1;else for(l=c=0;li[l]?1:-1;break}return c}function r(n,i,s){for(var o=0;s--;)n[s]-=o,o=n[s]1;)n.shift()}return function(n,i,s,o){var l,c,d,u,f,p,m,x,g,b,v,j,y,w,S,N,P,_,T=n.constructor,$=n.s==i.s?1:-1,M=n.d,C=i.d;if(!n.s)return new T(n);if(!i.s)throw Error(sr+"Division by zero");for(c=n.e-i.e,P=C.length,S=M.length,m=new T($),x=m.d=[],d=0;C[d]==(M[d]||0);)++d;if(C[d]>(M[d]||0)&&--c,s==null?j=s=T.precision:o?j=s+(Me(n)-Me(i))+1:j=s,j<0)return new T(0);if(j=j/xe+2|0,d=0,P==1)for(u=0,C=C[0],j++;(d1&&(C=e(C,u),M=e(M,u),P=C.length,S=M.length),w=P,g=M.slice(0,P),b=g.length;b=qe/2&&++N;do u=0,l=t(C,g,P,b),l<0?(v=g[0],P!=b&&(v=v*qe+(g[1]||0)),u=v/N|0,u>1?(u>=qe&&(u=qe-1),f=e(C,u),p=f.length,b=g.length,l=t(f,g,p,b),l==1&&(u--,r(f,P16)throw Error(Sm+Me(e));if(!e.s)return new u(Lt);for(je=!1,l=f,o=new u(.03125);e.abs().gte(.1);)e=e.times(o),d+=5;for(n=Math.log(Qn(2,d))/Math.LN10*2+5|0,l+=n,r=i=s=new u(Lt),u.precision=l;;){if(i=fe(i.times(e),l),r=r.times(++c),o=s.plus(Vr(i,r,l)),Nr(o.d).slice(0,l)===Nr(s.d).slice(0,l)){for(;d--;)s=fe(s.times(s),l);return u.precision=f,t==null?(je=!0,fe(s,f)):s}s=o}}function Me(e){for(var t=e.e*xe,r=e.d[0];r>=10;r/=10)t++;return t}function Sd(e,t,r){if(t>e.LN10.sd())throw je=!0,r&&(e.precision=r),Error(sr+"LN10 precision limit exceeded");return fe(new e(e.LN10),t)}function xn(e){for(var t="";e--;)t+="0";return t}function Ws(e,t){var r,n,i,s,o,l,c,d,u,f=1,p=10,m=e,x=m.d,g=m.constructor,b=g.precision;if(m.s<1)throw Error(sr+(m.s?"NaN":"-Infinity"));if(m.eq(Lt))return new g(0);if(t==null?(je=!1,d=b):d=t,m.eq(10))return t==null&&(je=!0),Sd(g,d);if(d+=p,g.precision=d,r=Nr(x),n=r.charAt(0),s=Me(m),Math.abs(s)<15e14){for(;n<7&&n!=1||n==1&&r.charAt(1)>3;)m=m.times(e),r=Nr(m.d),n=r.charAt(0),f++;s=Me(m),n>1?(m=new g("0."+r),s++):m=new g(n+"."+r.slice(1))}else return c=Sd(g,d+2,b).times(s+""),m=Ws(new g(n+"."+r.slice(1)),d-p).plus(c),g.precision=b,t==null?(je=!0,fe(m,b)):m;for(l=o=m=Vr(m.minus(Lt),m.plus(Lt),d),u=fe(m.times(m),d),i=3;;){if(o=fe(o.times(u),d),c=l.plus(Vr(o,new g(i),d)),Nr(c.d).slice(0,d)===Nr(l.d).slice(0,d))return l=l.times(2),s!==0&&(l=l.plus(Sd(g,d+2,b).times(s+""))),l=Vr(l,new g(f),d),g.precision=b,t==null?(je=!0,fe(l,b)):l;l=c,i+=2}}function vy(e,t){var r,n,i;for((r=t.indexOf("."))>-1&&(t=t.replace(".","")),(n=t.search(/e/i))>0?(r<0&&(r=n),r+=+t.slice(n+1),t=t.substring(0,n)):r<0&&(r=t.length),n=0;t.charCodeAt(n)===48;)++n;for(i=t.length;t.charCodeAt(i-1)===48;)--i;if(t=t.slice(n,i),t){if(i-=n,r=r-n-1,e.e=Ca(r/xe),e.d=[],n=(r+1)%xe,r<0&&(n+=xe),nuc||e.e<-uc))throw Error(Sm+r)}else e.s=0,e.e=0,e.d=[0];return e}function fe(e,t,r){var n,i,s,o,l,c,d,u,f=e.d;for(o=1,s=f[0];s>=10;s/=10)o++;if(n=t-o,n<0)n+=xe,i=t,d=f[u=0];else{if(u=Math.ceil((n+1)/xe),s=f.length,u>=s)return e;for(d=s=f[u],o=1;s>=10;s/=10)o++;n%=xe,i=n-xe+o}if(r!==void 0&&(s=Qn(10,o-i-1),l=d/s%10|0,c=t<0||f[u+1]!==void 0||d%s,c=r<4?(l||c)&&(r==0||r==(e.s<0?3:2)):l>5||l==5&&(r==4||c||r==6&&(n>0?i>0?d/Qn(10,o-i):0:f[u-1])%10&1||r==(e.s<0?8:7))),t<1||!f[0])return c?(s=Me(e),f.length=1,t=t-s-1,f[0]=Qn(10,(xe-t%xe)%xe),e.e=Ca(-t/xe)||0):(f.length=1,f[0]=e.e=e.s=0),e;if(n==0?(f.length=u,s=1,u--):(f.length=u+1,s=Qn(10,xe-n),f[u]=i>0?(d/Qn(10,o-i)%Qn(10,i)|0)*s:0),c)for(;;)if(u==0){(f[0]+=s)==qe&&(f[0]=1,++e.e);break}else{if(f[u]+=s,f[u]!=qe)break;f[u--]=0,s=1}for(n=f.length;f[--n]===0;)f.pop();if(je&&(e.e>uc||e.e<-uc))throw Error(Sm+Me(e));return e}function FS(e,t){var r,n,i,s,o,l,c,d,u,f,p=e.constructor,m=p.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new p(e),je?fe(t,m):t;if(c=e.d,f=t.d,n=t.e,d=e.e,c=c.slice(),o=d-n,o){for(u=o<0,u?(r=c,o=-o,l=f.length):(r=f,n=d,l=c.length),i=Math.max(Math.ceil(m/xe),l)+2,o>i&&(o=i,r.length=1),r.reverse(),i=o;i--;)r.push(0);r.reverse()}else{for(i=c.length,l=f.length,u=i0;--i)c[l++]=0;for(i=f.length;i>o;){if(c[--i]0?s=s.charAt(0)+"."+s.slice(1)+xn(n):o>1&&(s=s.charAt(0)+"."+s.slice(1)),s=s+(i<0?"e":"e+")+i):i<0?(s="0."+xn(-i-1)+s,r&&(n=r-o)>0&&(s+=xn(n))):i>=o?(s+=xn(i+1-o),r&&(n=r-i-1)>0&&(s=s+"."+xn(n))):((n=i+1)0&&(i+1===o&&(s+="."),s+=xn(n))),e.s<0?"-"+s:s}function by(e,t){if(e.length>t)return e.length=t,!0}function WS(e){var t,r,n;function i(s){var o=this;if(!(o instanceof i))return new i(s);if(o.constructor=i,s instanceof i){o.s=s.s,o.e=s.e,o.d=(s=s.d)?s.slice():s;return}if(typeof s=="number"){if(s*0!==0)throw Error(di+s);if(s>0)o.s=1;else if(s<0)s=-s,o.s=-1;else{o.s=0,o.e=0,o.d=[0];return}if(s===~~s&&s<1e7){o.e=0,o.d=[s];return}return vy(o,s.toString())}else if(typeof s!="string")throw Error(di+s);if(s.charCodeAt(0)===45?(s=s.slice(1),o.s=-1):o.s=1,sM.test(s))vy(o,s);else throw Error(di+s)}if(i.prototype=V,i.ROUND_UP=0,i.ROUND_DOWN=1,i.ROUND_CEIL=2,i.ROUND_FLOOR=3,i.ROUND_HALF_UP=4,i.ROUND_HALF_DOWN=5,i.ROUND_HALF_EVEN=6,i.ROUND_HALF_CEIL=7,i.ROUND_HALF_FLOOR=8,i.clone=WS,i.config=i.set=oM,e===void 0&&(e={}),e)for(n=["precision","rounding","toExpNeg","toExpPos","LN10"],t=0;t=i[t+1]&&n<=i[t+2])this[r]=n;else throw Error(di+r+": "+n);if((n=e[r="LN10"])!==void 0)if(n==Math.LN10)this[r]=new this(n);else throw Error(di+r+": "+n);return this}var Nm=WS(aM);Lt=new Nm(1);const oe=Nm;var lM=e=>e,US={},qS=e=>e===US,jy=e=>function t(){return arguments.length===0||arguments.length===1&&qS(arguments.length<=0?void 0:arguments[0])?t:e(...arguments)},HS=(e,t)=>e===1?t:jy(function(){for(var r=arguments.length,n=new Array(r),i=0;io!==US).length;return s>=e?t(...n):HS(e-s,jy(function(){for(var o=arguments.length,l=new Array(o),c=0;cqS(u)?l.shift():u);return t(...d,...l)}))}),ju=e=>HS(e.length,e),sp=(e,t)=>{for(var r=[],n=e;nArray.isArray(t)?t.map(e):Object.keys(t).map(r=>t[r]).map(e)),uM=function(){for(var t=arguments.length,r=new Array(t),n=0;nc(l),s(...arguments))}},op=e=>Array.isArray(e)?e.reverse():e.split("").reverse().join(""),KS=e=>{var t=null,r=null;return function(){for(var n=arguments.length,i=new Array(n),s=0;s{var c;return o===((c=t)===null||c===void 0?void 0:c[l])})||(t=i,r=e(...i)),r}};function VS(e){var t;return e===0?t=1:t=Math.floor(new oe(e).abs().log(10).toNumber())+1,t}function YS(e,t,r){for(var n=new oe(e),i=0,s=[];n.lt(t)&&i<1e5;)s.push(n.toNumber()),n=n.add(r),i++;return s}ju((e,t,r)=>{var n=+e,i=+t;return n+r*(i-n)});ju((e,t,r)=>{var n=t-+e;return n=n||1/0,(r-e)/n});ju((e,t,r)=>{var n=t-+e;return n=n||1/0,Math.max(0,Math.min(1,(r-e)/n))});var GS=e=>{var[t,r]=e,[n,i]=[t,r];return t>r&&([n,i]=[r,t]),[n,i]},ZS=(e,t,r)=>{if(e.lte(0))return new oe(0);var n=VS(e.toNumber()),i=new oe(10).pow(n),s=e.div(i),o=n!==1?.05:.1,l=new oe(Math.ceil(s.div(o).toNumber())).add(r).mul(o),c=l.mul(i);return t?new oe(c.toNumber()):new oe(Math.ceil(c.toNumber()))},dM=(e,t,r)=>{var n=new oe(1),i=new oe(e);if(!i.isint()&&r){var s=Math.abs(e);s<1?(n=new oe(10).pow(VS(e)-1),i=new oe(Math.floor(i.div(n).toNumber())).mul(n)):s>1&&(i=new oe(Math.floor(e)))}else e===0?i=new oe(Math.floor((t-1)/2)):r||(i=new oe(Math.floor(e)));var o=Math.floor((t-1)/2),l=uM(cM(c=>i.add(new oe(c-o).mul(n)).toNumber()),sp);return l(0,t)},XS=function(t,r,n,i){var s=arguments.length>4&&arguments[4]!==void 0?arguments[4]:0;if(!Number.isFinite((r-t)/(n-1)))return{step:new oe(0),tickMin:new oe(0),tickMax:new oe(0)};var o=ZS(new oe(r).sub(t).div(n-1),i,s),l;t<=0&&r>=0?l=new oe(0):(l=new oe(t).add(r).div(2),l=l.sub(new oe(l).mod(o)));var c=Math.ceil(l.sub(t).div(o).toNumber()),d=Math.ceil(new oe(r).sub(l).div(o).toNumber()),u=c+d+1;return u>n?XS(t,r,n,i,s+1):(u0?d+(n-u):d,c=r>0?c:c+(n-u)),{step:o,tickMin:l.sub(new oe(c).mul(o)),tickMax:l.add(new oe(d).mul(o))})};function fM(e){var[t,r]=e,n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:6,i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,s=Math.max(n,2),[o,l]=GS([t,r]);if(o===-1/0||l===1/0){var c=l===1/0?[o,...sp(0,n-1).map(()=>1/0)]:[...sp(0,n-1).map(()=>-1/0),l];return t>r?op(c):c}if(o===l)return dM(o,n,i);var{step:d,tickMin:u,tickMax:f}=XS(o,l,s,i,0),p=YS(u,f.add(new oe(.1).mul(d)),d);return t>r?op(p):p}function pM(e,t){var[r,n]=e,i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,[s,o]=GS([r,n]);if(s===-1/0||o===1/0)return[r,n];if(s===o)return[s];var l=Math.max(t,2),c=ZS(new oe(o).sub(s).div(l-1),i,0),d=[...YS(new oe(s),new oe(o),c),o];return i===!1&&(d=d.map(u=>Math.round(u))),r>n?op(d):d}var hM=KS(fM),mM=KS(pM),gM=e=>e.rootProps.barCategoryGap,wu=e=>e.rootProps.stackOffset,km=e=>e.options.chartName,Pm=e=>e.rootProps.syncId,JS=e=>e.rootProps.syncMethod,_m=e=>e.options.eventEmitter,xM=e=>e.rootProps.baseValue,lt={grid:-100,barBackground:-50,area:100,cursorRectangle:200,bar:300,line:400,axis:500,scatter:600,activeBar:1e3,cursorLine:1100,activeDot:1200,label:2e3},zr={allowDuplicatedCategory:!0,angleAxisId:0,reversed:!1,scale:"auto",tick:!0,type:"category"},$t={allowDataOverflow:!1,allowDuplicatedCategory:!0,radiusAxisId:0,scale:"auto",tick:!0,tickCount:5,type:"number"},Su=(e,t)=>{if(!(!e||!t))return e!=null&&e.reversed?[t[1],t[0]]:t},yM={allowDataOverflow:!1,allowDecimals:!1,allowDuplicatedCategory:!1,dataKey:void 0,domain:void 0,id:zr.angleAxisId,includeHidden:!1,name:void 0,reversed:zr.reversed,scale:zr.scale,tick:zr.tick,tickCount:void 0,ticks:void 0,type:zr.type,unit:void 0},vM={allowDataOverflow:$t.allowDataOverflow,allowDecimals:!1,allowDuplicatedCategory:$t.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:$t.radiusAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:$t.scale,tick:$t.tick,tickCount:$t.tickCount,ticks:void 0,type:$t.type,unit:void 0},bM={allowDataOverflow:!1,allowDecimals:!1,allowDuplicatedCategory:zr.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:zr.angleAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:zr.scale,tick:zr.tick,tickCount:void 0,ticks:void 0,type:"number",unit:void 0},jM={allowDataOverflow:$t.allowDataOverflow,allowDecimals:!1,allowDuplicatedCategory:$t.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:$t.radiusAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:$t.scale,tick:$t.tick,tickCount:$t.tickCount,ticks:void 0,type:"category",unit:void 0},Cm=(e,t)=>e.polarAxis.angleAxis[t]!=null?e.polarAxis.angleAxis[t]:e.layout.layoutType==="radial"?bM:yM,Am=(e,t)=>e.polarAxis.radiusAxis[t]!=null?e.polarAxis.radiusAxis[t]:e.layout.layoutType==="radial"?jM:vM,Nu=e=>e.polarOptions,Om=I([ln,cn,rt],yD),QS=I([Nu,Om],(e,t)=>{if(e!=null)return Rn(e.innerRadius,t,0)}),e2=I([Nu,Om],(e,t)=>{if(e!=null)return Rn(e.outerRadius,t,t*.8)}),wM=e=>{if(e==null)return[0,0];var{startAngle:t,endAngle:r}=e;return[t,r]},t2=I([Nu],wM);I([Cm,t2],Su);var r2=I([Om,QS,e2],(e,t,r)=>{if(!(e==null||t==null||r==null))return[t,r]});I([Am,r2],Su);var n2=I([ue,Nu,QS,e2,ln,cn],(e,t,r,n,i,s)=>{if(!(e!=="centric"&&e!=="radial"||t==null||r==null||n==null)){var{cx:o,cy:l,startAngle:c,endAngle:d}=t;return{cx:Rn(o,i,i/2),cy:Rn(l,s,s/2),innerRadius:r,outerRadius:n,startAngle:c,endAngle:d,clockWise:!1}}}),Fe=(e,t)=>t,ku=(e,t,r)=>r;function Em(e){return e==null?void 0:e.id}function i2(e,t,r){var{chartData:n=[]}=t,{allowDuplicatedCategory:i,dataKey:s}=r,o=new Map;return e.forEach(l=>{var c,d=(c=l.data)!==null&&c!==void 0?c:n;if(!(d==null||d.length===0)){var u=Em(l);d.forEach((f,p)=>{var m=s==null||i?p:String(et(f,s,null)),x=et(f,l.dataKey,0),g;o.has(m)?g=o.get(m):g={},Object.assign(g,{[u]:x}),o.set(m,g)})}}),Array.from(o.values())}function Dm(e){return e.stackId!=null&&e.dataKey!=null}var Pu=(e,t)=>e===t?!0:e==null||t==null?!1:e[0]===t[0]&&e[1]===t[1];function _u(e,t){return Array.isArray(e)&&Array.isArray(t)&&e.length===0&&t.length===0?!0:e===t}function SM(e,t){if(e.length===t.length){for(var r=0;r{var t=ue(e);return t==="horizontal"?"xAxis":t==="vertical"?"yAxis":t==="centric"?"angleAxis":"radiusAxis"},Aa=e=>e.tooltip.settings.axisId;function wy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function dc(e){for(var t=1;te.cartesianAxis.xAxis[t],dn=(e,t)=>{var r=a2(e,t);return r??Tt},Mt={allowDataOverflow:!1,allowDecimals:!0,allowDuplicatedCategory:!0,angle:0,dataKey:void 0,domain:lp,hide:!0,id:0,includeHidden:!1,interval:"preserveEnd",minTickGap:5,mirror:!1,name:void 0,orientation:"left",padding:{top:0,bottom:0},reversed:!1,scale:"auto",tick:!0,tickCount:5,tickFormatter:void 0,ticks:void 0,type:"number",unit:void 0,width:eo},s2=(e,t)=>e.cartesianAxis.yAxis[t],fn=(e,t)=>{var r=s2(e,t);return r??Mt},_M={domain:[0,"auto"],includeHidden:!1,reversed:!1,allowDataOverflow:!1,allowDuplicatedCategory:!1,dataKey:void 0,id:0,name:"",range:[64,64],scale:"auto",type:"number",unit:""},Tm=(e,t)=>{var r=e.cartesianAxis.zAxis[t];return r??_M},vt=(e,t,r)=>{switch(t){case"xAxis":return dn(e,r);case"yAxis":return fn(e,r);case"zAxis":return Tm(e,r);case"angleAxis":return Cm(e,r);case"radiusAxis":return Am(e,r);default:throw new Error("Unexpected axis type: ".concat(t))}},CM=(e,t,r)=>{switch(t){case"xAxis":return dn(e,r);case"yAxis":return fn(e,r);default:throw new Error("Unexpected axis type: ".concat(t))}},so=(e,t,r)=>{switch(t){case"xAxis":return dn(e,r);case"yAxis":return fn(e,r);case"angleAxis":return Cm(e,r);case"radiusAxis":return Am(e,r);default:throw new Error("Unexpected axis type: ".concat(t))}},o2=e=>e.graphicalItems.cartesianItems.some(t=>t.type==="bar")||e.graphicalItems.polarItems.some(t=>t.type==="radialBar");function l2(e,t){return r=>{switch(e){case"xAxis":return"xAxisId"in r&&r.xAxisId===t;case"yAxis":return"yAxisId"in r&&r.yAxisId===t;case"zAxis":return"zAxisId"in r&&r.zAxisId===t;case"angleAxis":return"angleAxisId"in r&&r.angleAxisId===t;case"radiusAxis":return"radiusAxisId"in r&&r.radiusAxisId===t;default:return!1}}}var Mm=e=>e.graphicalItems.cartesianItems,AM=I([Fe,ku],l2),c2=(e,t,r)=>e.filter(r).filter(n=>(t==null?void 0:t.includeHidden)===!0?!0:!n.hide),oo=I([Mm,vt,AM],c2,{memoizeOptions:{resultEqualityCheck:_u}}),u2=I([oo],e=>e.filter(t=>t.type==="area"||t.type==="bar").filter(Dm)),d2=e=>e.filter(t=>!("stackId"in t)||t.stackId===void 0),OM=I([oo],d2),f2=e=>e.map(t=>t.data).filter(Boolean).flat(1),EM=I([oo],f2,{memoizeOptions:{resultEqualityCheck:_u}}),p2=(e,t)=>{var{chartData:r=[],dataStartIndex:n,dataEndIndex:i}=t;return e.length>0?e:r.slice(n,i+1)},Im=I([EM,bu],p2),h2=(e,t,r)=>(t==null?void 0:t.dataKey)!=null?e.map(n=>({value:et(n,t.dataKey)})):r.length>0?r.map(n=>n.dataKey).flatMap(n=>e.map(i=>({value:et(i,n)}))):e.map(n=>({value:n})),Cu=I([Im,vt,oo],h2);function m2(e,t){switch(e){case"xAxis":return t.direction==="x";case"yAxis":return t.direction==="y";default:return!1}}function ll(e){if(Or(e)||e instanceof Date){var t=Number(e);if(_e(t))return t}}function Sy(e){if(Array.isArray(e)){var t=[ll(e[0]),ll(e[1])];return ji(t)?t:void 0}var r=ll(e);if(r!=null)return[r,r]}function an(e){return e.map(ll).filter(F6)}function DM(e,t,r){return!r||typeof t!="number"||yr(t)?[]:r.length?an(r.flatMap(n=>{var i=et(e,n.dataKey),s,o;if(Array.isArray(i)?[s,o]=i:s=o=i,!(!_e(s)||!_e(o)))return[t-s,t+o]})):[]}var Ue=e=>{var t=We(e),r=Aa(e);return so(e,t,r)},g2=I([Ue],e=>e==null?void 0:e.dataKey),TM=I([u2,bu,Ue],i2),x2=(e,t,r)=>{var n={},i=t.reduce((s,o)=>(o.stackId==null||(s[o.stackId]==null&&(s[o.stackId]=[]),s[o.stackId].push(o)),s),n);return Object.fromEntries(Object.entries(i).map(s=>{var[o,l]=s,c=l.map(Em);return[o,{stackedData:_E(e,c,r),graphicalItems:l}]}))},cp=I([TM,u2,wu],x2),y2=(e,t,r,n)=>{var{dataStartIndex:i,dataEndIndex:s}=t;if(n==null&&r!=="zAxis"){var o=EE(e,i,s);if(!(o!=null&&o[0]===0&&o[1]===0))return o}},MM=I([vt],e=>e.allowDataOverflow),$m=e=>{var t;if(e==null||!("domain"in e))return lp;if(e.domain!=null)return e.domain;if(e.ticks!=null){if(e.type==="number"){var r=an(e.ticks);return[Math.min(...r),Math.max(...r)]}if(e.type==="category")return e.ticks.map(String)}return(t=e==null?void 0:e.domain)!==null&&t!==void 0?t:lp},v2=I([vt],$m),b2=I([v2,MM],LS),IM=I([cp,Kn,Fe,b2],y2,{memoizeOptions:{resultEqualityCheck:Pu}}),Lm=e=>e.errorBars,$M=(e,t,r)=>e.flatMap(n=>t[n.id]).filter(Boolean).filter(n=>m2(r,n)),fc=function(){for(var t=arguments.length,r=new Array(t),n=0;n{var s,o;if(r.length>0&&e.forEach(l=>{r.forEach(c=>{var d,u,f=(d=n[c.id])===null||d===void 0?void 0:d.filter(v=>m2(i,v)),p=et(l,(u=t.dataKey)!==null&&u!==void 0?u:c.dataKey),m=DM(l,p,f);if(m.length>=2){var x=Math.min(...m),g=Math.max(...m);(s==null||xo)&&(o=g)}var b=Sy(p);b!=null&&(s=s==null?b[0]:Math.min(s,b[0]),o=o==null?b[1]:Math.max(o,b[1]))})}),(t==null?void 0:t.dataKey)!=null&&e.forEach(l=>{var c=Sy(et(l,t.dataKey));c!=null&&(s=s==null?c[0]:Math.min(s,c[0]),o=o==null?c[1]:Math.max(o,c[1]))}),_e(s)&&_e(o))return[s,o]},LM=I([Im,vt,OM,Lm,Fe],j2,{memoizeOptions:{resultEqualityCheck:Pu}});function zM(e){var{value:t}=e;if(Or(t)||t instanceof Date)return t}var RM=(e,t,r)=>{var n=e.map(zM).filter(i=>i!=null);return r&&(t.dataKey==null||t.allowDuplicatedCategory&&_j(n))?rS(0,e.length):t.allowDuplicatedCategory?n:Array.from(new Set(n))},w2=e=>e.referenceElements.dots,Oa=(e,t,r)=>e.filter(n=>n.ifOverflow==="extendDomain").filter(n=>t==="xAxis"?n.xAxisId===r:n.yAxisId===r),BM=I([w2,Fe,ku],Oa),S2=e=>e.referenceElements.areas,FM=I([S2,Fe,ku],Oa),N2=e=>e.referenceElements.lines,WM=I([N2,Fe,ku],Oa),k2=(e,t)=>{var r=an(e.map(n=>t==="xAxis"?n.x:n.y));if(r.length!==0)return[Math.min(...r),Math.max(...r)]},UM=I(BM,Fe,k2),P2=(e,t)=>{var r=an(e.flatMap(n=>[t==="xAxis"?n.x1:n.y1,t==="xAxis"?n.x2:n.y2]));if(r.length!==0)return[Math.min(...r),Math.max(...r)]},qM=I([FM,Fe],P2);function HM(e){var t;if(e.x!=null)return an([e.x]);var r=(t=e.segment)===null||t===void 0?void 0:t.map(n=>n.x);return r==null||r.length===0?[]:an(r)}function KM(e){var t;if(e.y!=null)return an([e.y]);var r=(t=e.segment)===null||t===void 0?void 0:t.map(n=>n.y);return r==null||r.length===0?[]:an(r)}var _2=(e,t)=>{var r=e.flatMap(n=>t==="xAxis"?HM(n):KM(n));if(r.length!==0)return[Math.min(...r),Math.max(...r)]},VM=I([WM,Fe],_2),YM=I(UM,VM,qM,(e,t,r)=>fc(e,r,t)),C2=(e,t,r,n,i,s,o,l)=>{if(r!=null)return r;var c=o==="vertical"&&l==="xAxis"||o==="horizontal"&&l==="yAxis",d=c?fc(n,s,i):fc(s,i);return iM(t,d,e.allowDataOverflow)},GM=I([vt,v2,b2,IM,LM,YM,ue,Fe],C2,{memoizeOptions:{resultEqualityCheck:Pu}}),ZM=[0,1],A2=(e,t,r,n,i,s,o)=>{if(!((e==null||r==null||r.length===0)&&o===void 0)){var{dataKey:l,type:c}=e,d=Mr(t,s);if(d&&l==null){var u;return rS(0,(u=r==null?void 0:r.length)!==null&&u!==void 0?u:0)}return c==="category"?RM(n,e,d):i==="expand"?ZM:o}},zm=I([vt,ue,Im,Cu,wu,Fe,GM],A2),O2=(e,t,r,n,i)=>{if(e!=null){var{scale:s,type:o}=e;if(s==="auto")return t==="radial"&&i==="radiusAxis"?"band":t==="radial"&&i==="angleAxis"?"linear":o==="category"&&n&&(n.indexOf("LineChart")>=0||n.indexOf("AreaChart")>=0||n.indexOf("ComposedChart")>=0&&!r)?"point":o==="category"?"band":"linear";if(typeof s=="string"){var l="scale".concat(Xs(s));return l in es?l:"point"}}},lo=I([vt,ue,o2,km,Fe],O2);function XM(e){if(e!=null){if(e in es)return es[e]();var t="scale".concat(Xs(e));if(t in es)return es[t]()}}function Rm(e,t,r,n){if(!(r==null||n==null)){if(typeof e.scale=="function")return e.scale.copy().domain(r).range(n);var i=XM(t);if(i!=null){var s=i.domain(r).range(n);return SE(s),s}}}var E2=(e,t,r)=>{var n=$m(t);if(!(r!=="auto"&&r!=="linear")){if(t!=null&&t.tickCount&&Array.isArray(n)&&(n[0]==="auto"||n[1]==="auto")&&ji(e))return hM(e,t.tickCount,t.allowDecimals);if(t!=null&&t.tickCount&&t.type==="number"&&ji(e))return mM(e,t.tickCount,t.allowDecimals)}},Bm=I([zm,so,lo],E2),D2=(e,t,r,n)=>{if(n!=="angleAxis"&&(e==null?void 0:e.type)==="number"&&ji(t)&&Array.isArray(r)&&r.length>0){var i=t[0],s=r[0],o=t[1],l=r[r.length-1];return[Math.min(i,s),Math.max(o,l)]}return t},JM=I([vt,zm,Bm,Fe],D2),QM=I(Cu,vt,(e,t)=>{if(!(!t||t.type!=="number")){var r=1/0,n=Array.from(an(e.map(l=>l.value))).sort((l,c)=>l-c);if(n.length<2)return 1/0;var i=n[n.length-1]-n[0];if(i===0)return 1/0;for(var s=0;sn,(e,t,r,n,i)=>{if(!_e(e))return 0;var s=t==="vertical"?n.height:n.width;if(i==="gap")return e*s/2;if(i==="no-gap"){var o=Rn(r,e*s),l=e*s/2;return l-o-(l-o)/s*o}return 0}),eI=(e,t)=>{var r=dn(e,t);return r==null||typeof r.padding!="string"?0:T2(e,"xAxis",t,r.padding)},tI=(e,t)=>{var r=fn(e,t);return r==null||typeof r.padding!="string"?0:T2(e,"yAxis",t,r.padding)},rI=I(dn,eI,(e,t)=>{var r,n;if(e==null)return{left:0,right:0};var{padding:i}=e;return typeof i=="string"?{left:t,right:t}:{left:((r=i.left)!==null&&r!==void 0?r:0)+t,right:((n=i.right)!==null&&n!==void 0?n:0)+t}}),nI=I(fn,tI,(e,t)=>{var r,n;if(e==null)return{top:0,bottom:0};var{padding:i}=e;return typeof i=="string"?{top:t,bottom:t}:{top:((r=i.top)!==null&&r!==void 0?r:0)+t,bottom:((n=i.bottom)!==null&&n!==void 0?n:0)+t}}),iI=I([rt,rI,cu,lu,(e,t,r)=>r],(e,t,r,n,i)=>{var{padding:s}=n;return i?[s.left,r.width-s.right]:[e.left+t.left,e.left+e.width-t.right]}),aI=I([rt,ue,nI,cu,lu,(e,t,r)=>r],(e,t,r,n,i,s)=>{var{padding:o}=i;return s?[n.height-o.bottom,o.top]:t==="horizontal"?[e.top+e.height-r.bottom,e.top+r.top]:[e.top+r.top,e.top+e.height-r.bottom]}),co=(e,t,r,n)=>{var i;switch(t){case"xAxis":return iI(e,r,n);case"yAxis":return aI(e,r,n);case"zAxis":return(i=Tm(e,r))===null||i===void 0?void 0:i.range;case"angleAxis":return t2(e);case"radiusAxis":return r2(e,r);default:return}},M2=I([vt,co],Su),Ea=I([vt,lo,JM,M2],Rm);I([oo,Lm,Fe],$M);function I2(e,t){return e.idt.id?1:0}var Au=(e,t)=>t,Ou=(e,t,r)=>r,sI=I(su,Au,Ou,(e,t,r)=>e.filter(n=>n.orientation===t).filter(n=>n.mirror===r).sort(I2)),oI=I(ou,Au,Ou,(e,t,r)=>e.filter(n=>n.orientation===t).filter(n=>n.mirror===r).sort(I2)),$2=(e,t)=>({width:e.width,height:t.height}),lI=(e,t)=>{var r=typeof t.width=="number"?t.width:eo;return{width:r,height:e.height}},cI=I(rt,dn,$2),uI=(e,t,r)=>{switch(t){case"top":return e.top;case"bottom":return r-e.bottom;default:return 0}},dI=(e,t,r)=>{switch(t){case"left":return e.left;case"right":return r-e.right;default:return 0}},fI=I(cn,rt,sI,Au,Ou,(e,t,r,n,i)=>{var s={},o;return r.forEach(l=>{var c=$2(t,l);o==null&&(o=uI(t,n,e));var d=n==="top"&&!i||n==="bottom"&&i;s[l.id]=o-Number(d)*c.height,o+=(d?-1:1)*c.height}),s}),pI=I(ln,rt,oI,Au,Ou,(e,t,r,n,i)=>{var s={},o;return r.forEach(l=>{var c=lI(t,l);o==null&&(o=dI(t,n,e));var d=n==="left"&&!i||n==="right"&&i;s[l.id]=o-Number(d)*c.width,o+=(d?-1:1)*c.width}),s}),hI=(e,t)=>{var r=dn(e,t);if(r!=null)return fI(e,r.orientation,r.mirror)},mI=I([rt,dn,hI,(e,t)=>t],(e,t,r,n)=>{if(t!=null){var i=r==null?void 0:r[n];return i==null?{x:e.left,y:0}:{x:e.left,y:i}}}),gI=(e,t)=>{var r=fn(e,t);if(r!=null)return pI(e,r.orientation,r.mirror)},xI=I([rt,fn,gI,(e,t)=>t],(e,t,r,n)=>{if(t!=null){var i=r==null?void 0:r[n];return i==null?{x:0,y:e.top}:{x:i,y:e.top}}}),yI=I(rt,fn,(e,t)=>{var r=typeof t.width=="number"?t.width:eo;return{width:r,height:e.height}}),L2=(e,t,r,n)=>{if(r!=null){var{allowDuplicatedCategory:i,type:s,dataKey:o}=r,l=Mr(e,n),c=t.map(d=>d.value);if(o&&l&&s==="category"&&i&&_j(c))return c}},Fm=I([ue,Cu,vt,Fe],L2),z2=(e,t,r,n)=>{if(!(r==null||r.dataKey==null)){var{type:i,scale:s}=r,o=Mr(e,n);if(o&&(i==="number"||s!=="auto"))return t.map(l=>l.value)}},Wm=I([ue,Cu,so,Fe],z2),Ny=I([ue,CM,lo,Ea,Fm,Wm,co,Bm,Fe],(e,t,r,n,i,s,o,l,c)=>{if(t!=null){var d=Mr(e,c);return{angle:t.angle,interval:t.interval,minTickGap:t.minTickGap,orientation:t.orientation,tick:t.tick,tickCount:t.tickCount,tickFormatter:t.tickFormatter,ticks:t.ticks,type:t.type,unit:t.unit,axisType:c,categoricalDomain:s,duplicateDomain:i,isCategorical:d,niceTicks:l,range:o,realScaleType:r,scale:n}}}),vI=(e,t,r,n,i,s,o,l,c)=>{if(!(t==null||n==null)){var d=Mr(e,c),{type:u,ticks:f,tickCount:p}=t,m=r==="scaleBand"&&typeof n.bandwidth=="function"?n.bandwidth()/2:2,x=u==="category"&&n.bandwidth?n.bandwidth()/m:0;x=c==="angleAxis"&&s!=null&&s.length>=2?Jt(s[0]-s[1])*2*x:x;var g=f||i;if(g){var b=g.map((v,j)=>{var y=o?o.indexOf(v):v;return{index:j,coordinate:n(y)+x,value:v,offset:x}});return b.filter(v=>_e(v.coordinate))}return d&&l?l.map((v,j)=>({coordinate:n(v)+x,value:v,index:j,offset:x})).filter(v=>_e(v.coordinate)):n.ticks?n.ticks(p).map(v=>({coordinate:n(v)+x,value:v,offset:x})):n.domain().map((v,j)=>({coordinate:n(v)+x,value:o?o[v]:v,index:j,offset:x}))}},R2=I([ue,so,lo,Ea,Bm,co,Fm,Wm,Fe],vI),bI=(e,t,r,n,i,s,o)=>{if(!(t==null||r==null||n==null||n[0]===n[1])){var l=Mr(e,o),{tickCount:c}=t,d=0;return d=o==="angleAxis"&&(n==null?void 0:n.length)>=2?Jt(n[0]-n[1])*2*d:d,l&&s?s.map((u,f)=>({coordinate:r(u)+d,value:u,index:f,offset:d})):r.ticks?r.ticks(c).map(u=>({coordinate:r(u)+d,value:u,offset:d})):r.domain().map((u,f)=>({coordinate:r(u)+d,value:i?i[u]:u,index:f,offset:d}))}},Eu=I([ue,so,Ea,co,Fm,Wm,Fe],bI),Du=I(vt,Ea,(e,t)=>{if(!(e==null||t==null))return dc(dc({},e),{},{scale:t})}),jI=I([vt,lo,zm,M2],Rm);I((e,t,r)=>Tm(e,r),jI,(e,t)=>{if(!(e==null||t==null))return dc(dc({},e),{},{scale:t})});var wI=I([ue,su,ou],(e,t,r)=>{switch(e){case"horizontal":return t.some(n=>n.reversed)?"right-to-left":"left-to-right";case"vertical":return r.some(n=>n.reversed)?"bottom-to-top":"top-to-bottom";case"centric":case"radial":return"left-to-right";default:return}}),B2=e=>e.options.defaultTooltipEventType,F2=e=>e.options.validateTooltipEventTypes;function W2(e,t,r){if(e==null)return t;var n=e?"axis":"item";return r==null?t:r.includes(n)?n:t}function Um(e,t){var r=B2(e),n=F2(e);return W2(t,r,n)}function SI(e){return G(t=>Um(t,e))}var U2=(e,t)=>{var r,n=Number(t);if(!(yr(n)||t==null))return n>=0?e==null||(r=e[n])===null||r===void 0?void 0:r.value:void 0},NI=e=>e.tooltip.settings,jn={active:!1,index:null,dataKey:void 0,coordinate:void 0},kI={itemInteraction:{click:jn,hover:jn},axisInteraction:{click:jn,hover:jn},keyboardInteraction:jn,syncInteraction:{active:!1,index:null,dataKey:void 0,label:void 0,coordinate:void 0,sourceViewBox:void 0},tooltipItemPayloads:[],settings:{shared:void 0,trigger:"hover",axisId:0,active:!1,defaultIndex:void 0}},q2=At({name:"tooltip",initialState:kI,reducers:{addTooltipEntrySettings:{reducer(e,t){e.tooltipItemPayloads.push(t.payload)},prepare:Le()},removeTooltipEntrySettings:{reducer(e,t){var r=Kr(e).tooltipItemPayloads.indexOf(t.payload);r>-1&&e.tooltipItemPayloads.splice(r,1)},prepare:Le()},setTooltipSettingsState(e,t){e.settings=t.payload},setActiveMouseOverItemIndex(e,t){e.syncInteraction.active=!1,e.keyboardInteraction.active=!1,e.itemInteraction.hover.active=!0,e.itemInteraction.hover.index=t.payload.activeIndex,e.itemInteraction.hover.dataKey=t.payload.activeDataKey,e.itemInteraction.hover.coordinate=t.payload.activeCoordinate},mouseLeaveChart(e){e.itemInteraction.hover.active=!1,e.axisInteraction.hover.active=!1},mouseLeaveItem(e){e.itemInteraction.hover.active=!1},setActiveClickItemIndex(e,t){e.syncInteraction.active=!1,e.itemInteraction.click.active=!0,e.keyboardInteraction.active=!1,e.itemInteraction.click.index=t.payload.activeIndex,e.itemInteraction.click.dataKey=t.payload.activeDataKey,e.itemInteraction.click.coordinate=t.payload.activeCoordinate},setMouseOverAxisIndex(e,t){e.syncInteraction.active=!1,e.axisInteraction.hover.active=!0,e.keyboardInteraction.active=!1,e.axisInteraction.hover.index=t.payload.activeIndex,e.axisInteraction.hover.dataKey=t.payload.activeDataKey,e.axisInteraction.hover.coordinate=t.payload.activeCoordinate},setMouseClickAxisIndex(e,t){e.syncInteraction.active=!1,e.keyboardInteraction.active=!1,e.axisInteraction.click.active=!0,e.axisInteraction.click.index=t.payload.activeIndex,e.axisInteraction.click.dataKey=t.payload.activeDataKey,e.axisInteraction.click.coordinate=t.payload.activeCoordinate},setSyncInteraction(e,t){e.syncInteraction=t.payload},setKeyboardInteraction(e,t){e.keyboardInteraction.active=t.payload.active,e.keyboardInteraction.index=t.payload.activeIndex,e.keyboardInteraction.coordinate=t.payload.activeCoordinate,e.keyboardInteraction.dataKey=t.payload.activeDataKey}}}),{addTooltipEntrySettings:PI,removeTooltipEntrySettings:_I,setTooltipSettingsState:CI,setActiveMouseOverItemIndex:AI,mouseLeaveItem:A7,mouseLeaveChart:H2,setActiveClickItemIndex:O7,setMouseOverAxisIndex:K2,setMouseClickAxisIndex:OI,setSyncInteraction:up,setKeyboardInteraction:dp}=q2.actions,EI=q2.reducer;function ky(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ho(e){for(var t=1;t{if(t==null)return jn;var i=II(e,t,r);if(i==null)return jn;if(i.active)return i;if(e.keyboardInteraction.active)return e.keyboardInteraction;if(e.syncInteraction.active&&e.syncInteraction.index!=null)return e.syncInteraction;var s=e.settings.active===!0;if($I(i)){if(s)return Ho(Ho({},i),{},{active:!0})}else if(n!=null)return{active:!0,coordinate:void 0,dataKey:void 0,index:n};return Ho(Ho({},jn),{},{coordinate:i.coordinate})},qm=(e,t)=>{var r=e==null?void 0:e.index;if(r==null)return null;var n=Number(r);if(!_e(n))return r;var i=0,s=1/0;return t.length>0&&(s=t.length-1),String(Math.max(i,Math.min(n,s)))},Y2=(e,t,r,n,i,s,o,l)=>{if(!(s==null||l==null)){var c=o[0],d=c==null?void 0:l(c.positions,s);if(d!=null)return d;var u=i==null?void 0:i[Number(s)];if(u)switch(r){case"horizontal":return{x:u.coordinate,y:(n.top+t)/2};default:return{x:(n.left+e)/2,y:u.coordinate}}}},G2=(e,t,r,n)=>{if(t==="axis")return e.tooltipItemPayloads;if(e.tooltipItemPayloads.length===0)return[];var i;return r==="hover"?i=e.itemInteraction.hover.dataKey:i=e.itemInteraction.click.dataKey,i==null&&n!=null?[e.tooltipItemPayloads[0]]:e.tooltipItemPayloads.filter(s=>{var o;return((o=s.settings)===null||o===void 0?void 0:o.dataKey)===i})},uo=e=>e.options.tooltipPayloadSearcher,Da=e=>e.tooltip;function Py(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function _y(e){for(var t=1;t{if(!(t==null||s==null)){var{chartData:l,computedData:c,dataStartIndex:d,dataEndIndex:u}=r,f=[];return e.reduce((p,m)=>{var x,{dataDefinedOnItem:g,settings:b}=m,v=BI(g,l),j=Array.isArray(v)?Cw(v,d,u):v,y=(x=b==null?void 0:b.dataKey)!==null&&x!==void 0?x:n,w=b==null?void 0:b.nameKey,S;if(n&&Array.isArray(j)&&!Array.isArray(j[0])&&o==="axis"?S=Cj(j,n,i):S=s(j,t,c,w),Array.isArray(S))S.forEach(P=>{var _=_y(_y({},b),{},{name:P.name,unit:P.unit,color:void 0,fill:void 0});p.push(c0({tooltipEntrySettings:_,dataKey:P.dataKey,payload:P.payload,value:et(P.payload,P.dataKey),name:P.name}))});else{var N;p.push(c0({tooltipEntrySettings:b,dataKey:y,payload:S,value:et(S,y),name:(N=et(S,w))!==null&&N!==void 0?N:b==null?void 0:b.name}))}return p},f)}},Hm=I([Ue,ue,o2,km,We],O2),FI=I([e=>e.graphicalItems.cartesianItems,e=>e.graphicalItems.polarItems],(e,t)=>[...e,...t]),WI=I([We,Aa],l2),fo=I([FI,Ue,WI],c2,{memoizeOptions:{resultEqualityCheck:_u}}),UI=I([fo],e=>e.filter(Dm)),qI=I([fo],f2,{memoizeOptions:{resultEqualityCheck:_u}}),Ta=I([qI,Kn],p2),HI=I([UI,Kn,Ue],i2),Km=I([Ta,Ue,fo],h2),X2=I([Ue],$m),KI=I([Ue],e=>e.allowDataOverflow),J2=I([X2,KI],LS),VI=I([fo],e=>e.filter(Dm)),YI=I([HI,VI,wu],x2),GI=I([YI,Kn,We,J2],y2),ZI=I([fo],d2),XI=I([Ta,Ue,ZI,Lm,We],j2,{memoizeOptions:{resultEqualityCheck:Pu}}),JI=I([w2,We,Aa],Oa),QI=I([JI,We],k2),e$=I([S2,We,Aa],Oa),t$=I([e$,We],P2),r$=I([N2,We,Aa],Oa),n$=I([r$,We],_2),i$=I([QI,n$,t$],fc),a$=I([Ue,X2,J2,GI,XI,i$,ue,We],C2),Q2=I([Ue,ue,Ta,Km,wu,We,a$],A2),s$=I([Q2,Ue,Hm],E2),o$=I([Ue,Q2,s$,We],D2),eN=e=>{var t=We(e),r=Aa(e),n=!1;return co(e,t,r,n)},tN=I([Ue,eN],Su),rN=I([Ue,Hm,o$,tN],Rm),l$=I([ue,Km,Ue,We],L2),c$=I([ue,Km,Ue,We],z2),u$=(e,t,r,n,i,s,o,l)=>{if(t){var{type:c}=t,d=Mr(e,l);if(n){var u=r==="scaleBand"&&n.bandwidth?n.bandwidth()/2:2,f=c==="category"&&n.bandwidth?n.bandwidth()/u:0;return f=l==="angleAxis"&&i!=null&&(i==null?void 0:i.length)>=2?Jt(i[0]-i[1])*2*f:f,d&&o?o.map((p,m)=>({coordinate:n(p)+f,value:p,index:m,offset:f})):n.domain().map((p,m)=>({coordinate:n(p)+f,value:s?s[p]:p,index:m,offset:f}))}}},pn=I([ue,Ue,Hm,rN,eN,l$,c$,We],u$),Vm=I([B2,F2,NI],(e,t,r)=>W2(r.shared,e,t)),nN=e=>e.tooltip.settings.trigger,Ym=e=>e.tooltip.settings.defaultIndex,Tu=I([Da,Vm,nN,Ym],V2),Us=I([Tu,Ta],qm),iN=I([pn,Us],U2),d$=I([Tu],e=>{if(e)return e.dataKey}),aN=I([Da,Vm,nN,Ym],G2),f$=I([ln,cn,ue,rt,pn,Ym,aN,uo],Y2),p$=I([Tu,f$],(e,t)=>e!=null&&e.coordinate?e.coordinate:t),h$=I([Tu],e=>e.active),m$=I([aN,Us,Kn,g2,iN,uo,Vm],Z2),g$=I([m$],e=>{if(e!=null){var t=e.map(r=>r.payload).filter(r=>r!=null);return Array.from(new Set(t))}});function Cy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ay(e){for(var t=1;tG(Ue),j$=()=>{var e=b$(),t=G(pn),r=G(rN);return ha(!e||!r?void 0:Ay(Ay({},e),{},{scale:r}),t)};function Oy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ti(e){for(var t=1;t{var i=t.find(s=>s&&s.index===r);if(i){if(e==="horizontal")return{x:i.coordinate,y:n.chartY};if(e==="vertical")return{x:n.chartX,y:i.coordinate}}return{x:0,y:0}},P$=(e,t,r,n)=>{var i=t.find(d=>d&&d.index===r);if(i){if(e==="centric"){var s=i.coordinate,{radius:o}=n;return Ti(Ti(Ti({},n),Je(n.cx,n.cy,o,s)),{},{angle:s,radius:o})}var l=i.coordinate,{angle:c}=n;return Ti(Ti(Ti({},n),Je(n.cx,n.cy,l,c)),{},{angle:c,radius:l})}return{angle:0,clockWise:!1,cx:0,cy:0,endAngle:0,innerRadius:0,outerRadius:0,radius:0,startAngle:0,x:0,y:0}};function _$(e,t){var{chartX:r,chartY:n}=e;return r>=t.left&&r<=t.left+t.width&&n>=t.top&&n<=t.top+t.height}var sN=(e,t,r,n,i)=>{var s,o=-1,l=(s=t==null?void 0:t.length)!==null&&s!==void 0?s:0;if(l<=1||e==null)return 0;if(n==="angleAxis"&&i!=null&&Math.abs(Math.abs(i[1]-i[0])-360)<=1e-6)for(var c=0;c0?r[c-1].coordinate:r[l-1].coordinate,u=r[c].coordinate,f=c>=l-1?r[0].coordinate:r[c+1].coordinate,p=void 0;if(Jt(u-d)!==Jt(f-u)){var m=[];if(Jt(f-u)===Jt(i[1]-i[0])){p=f;var x=u+i[1]-i[0];m[0]=Math.min(x,(x+d)/2),m[1]=Math.max(x,(x+d)/2)}else{p=d;var g=f+i[1]-i[0];m[0]=Math.min(u,(g+u)/2),m[1]=Math.max(u,(g+u)/2)}var b=[Math.min(u,(p+u)/2),Math.max(u,(p+u)/2)];if(e>b[0]&&e<=b[1]||e>=m[0]&&e<=m[1]){({index:o}=r[c]);break}}else{var v=Math.min(d,f),j=Math.max(d,f);if(e>(v+u)/2&&e<=(j+u)/2){({index:o}=r[c]);break}}}else if(t){for(var y=0;y0&&y(t[y].coordinate+t[y-1].coordinate)/2&&e<=(t[y].coordinate+t[y+1].coordinate)/2||y===l-1&&e>(t[y].coordinate+t[y-1].coordinate)/2){({index:o}=t[y]);break}}return o},oN=()=>G(km),Gm=(e,t)=>t,lN=(e,t,r)=>r,Zm=(e,t,r,n)=>n,C$=I(pn,e=>Qc(e,t=>t.coordinate)),Xm=I([Da,Gm,lN,Zm],V2),cN=I([Xm,Ta],qm),A$=(e,t,r)=>{if(t!=null){var n=Da(e);return t==="axis"?r==="hover"?n.axisInteraction.hover.dataKey:n.axisInteraction.click.dataKey:r==="hover"?n.itemInteraction.hover.dataKey:n.itemInteraction.click.dataKey}},uN=I([Da,Gm,lN,Zm],G2),pc=I([ln,cn,ue,rt,pn,Zm,uN,uo],Y2),O$=I([Xm,pc],(e,t)=>{var r;return(r=e.coordinate)!==null&&r!==void 0?r:t}),dN=I([pn,cN],U2),E$=I([uN,cN,Kn,g2,dN,uo,Gm],Z2),D$=I([Xm],e=>({isActive:e.active,activeIndex:e.index})),T$=(e,t,r,n,i,s,o)=>{if(!(!e||!r||!n||!i)&&_$(e,o)){var l=DE(e,t),c=sN(l,s,i,r,n),d=k$(t,i,c,e);return{activeIndex:String(c),activeCoordinate:d}}},M$=(e,t,r,n,i,s,o)=>{if(!(!e||!n||!i||!s||!r)){var l=SD(e,r);if(l){var c=TE(l,t),d=sN(c,o,s,n,i),u=P$(t,s,d,l);return{activeIndex:String(d),activeCoordinate:u}}}},I$=(e,t,r,n,i,s,o,l)=>{if(!(!e||!t||!n||!i||!s))return t==="horizontal"||t==="vertical"?T$(e,t,n,i,s,o,l):M$(e,t,r,n,i,s,o)},$$=I(e=>e.zIndex.zIndexMap,(e,t)=>t,(e,t,r)=>r,(e,t,r)=>{if(t!=null){var n=e[t];if(n!=null)return r?n.panoramaElementId:n.elementId}}),L$=I(e=>e.zIndex.zIndexMap,e=>{var t=Object.keys(e).map(n=>parseInt(n,10)).concat(Object.values(lt)),r=Array.from(new Set(t));return r.sort((n,i)=>n-i)},{memoizeOptions:{resultEqualityCheck:SM}});function Ey(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Dy(e){for(var t=1;tDy(Dy({},e),{},{[t]:{elementId:void 0,panoramaElementId:void 0,consumers:0}}),F$)},U$=new Set(Object.values(lt));function q$(e){return U$.has(e)}var fN=At({name:"zIndex",initialState:W$,reducers:{registerZIndexPortal:{reducer:(e,t)=>{var{zIndex:r}=t.payload;e.zIndexMap[r]?e.zIndexMap[r].consumers+=1:e.zIndexMap[r]={consumers:1,elementId:void 0,panoramaElementId:void 0}},prepare:Le()},unregisterZIndexPortal:{reducer:(e,t)=>{var{zIndex:r}=t.payload;e.zIndexMap[r]&&(e.zIndexMap[r].consumers-=1,e.zIndexMap[r].consumers<=0&&!q$(r)&&delete e.zIndexMap[r])},prepare:Le()},registerZIndexPortalId:{reducer:(e,t)=>{var{zIndex:r,elementId:n,isPanorama:i}=t.payload;e.zIndexMap[r]?i?e.zIndexMap[r].panoramaElementId=n:e.zIndexMap[r].elementId=n:e.zIndexMap[r]={consumers:0,elementId:i?void 0:n,panoramaElementId:i?n:void 0}},prepare:Le()},unregisterZIndexPortalId:{reducer:(e,t)=>{var{zIndex:r}=t.payload;e.zIndexMap[r]&&(t.payload.isPanorama?e.zIndexMap[r].panoramaElementId=void 0:e.zIndexMap[r].elementId=void 0)},prepare:Le()}}}),{registerZIndexPortal:H$,unregisterZIndexPortal:K$,registerZIndexPortalId:V$,unregisterZIndexPortalId:Y$}=fN.actions,G$=fN.reducer;function Ir(e){var{zIndex:t,children:r}=e,n=o3(),i=n&&t!==void 0&&t!==0,s=pt(),o=Ve();h.useLayoutEffect(()=>i?(o(H$({zIndex:t})),()=>{o(K$({zIndex:t}))}):ka,[o,t,i]);var l=G(d=>$$(d,t,s));if(!i)return r;if(!l)return null;var c=document.getElementById(l);return c?bh.createPortal(r,c):null}function fp(){return fp=Object.assign?Object.assign.bind():function(e){for(var t=1;th.useContext(pN),hN={exports:{}};(function(e){var t=Object.prototype.hasOwnProperty,r="~";function n(){}Object.create&&(n.prototype=Object.create(null),new n().__proto__||(r=!1));function i(c,d,u){this.fn=c,this.context=d,this.once=u||!1}function s(c,d,u,f,p){if(typeof u!="function")throw new TypeError("The listener must be a function");var m=new i(u,f||c,p),x=r?r+d:d;return c._events[x]?c._events[x].fn?c._events[x]=[c._events[x],m]:c._events[x].push(m):(c._events[x]=m,c._eventsCount++),c}function o(c,d){--c._eventsCount===0?c._events=new n:delete c._events[d]}function l(){this._events=new n,this._eventsCount=0}l.prototype.eventNames=function(){var d=[],u,f;if(this._eventsCount===0)return d;for(f in u=this._events)t.call(u,f)&&d.push(r?f.slice(1):f);return Object.getOwnPropertySymbols?d.concat(Object.getOwnPropertySymbols(u)):d},l.prototype.listeners=function(d){var u=r?r+d:d,f=this._events[u];if(!f)return[];if(f.fn)return[f.fn];for(var p=0,m=f.length,x=new Array(m);p{e.eventEmitter==null&&(e.eventEmitter=Symbol("rechartsEventEmitter"))}}}),sL=gN.reducer,{createEventEmitter:oL}=gN.actions;function lL(e){return e.tooltip.syncInteraction}var cL={chartData:void 0,computedData:void 0,dataStartIndex:0,dataEndIndex:0},xN=At({name:"chartData",initialState:cL,reducers:{setChartData(e,t){if(e.chartData=t.payload,t.payload==null){e.dataStartIndex=0,e.dataEndIndex=0;return}t.payload.length>0&&e.dataEndIndex!==t.payload.length-1&&(e.dataEndIndex=t.payload.length-1)},setComputedData(e,t){e.computedData=t.payload},setDataStartEndIndexes(e,t){var{startIndex:r,endIndex:n}=t.payload;r!=null&&(e.dataStartIndex=r),n!=null&&(e.dataEndIndex=n)}}}),{setChartData:Iy,setDataStartEndIndexes:uL,setComputedData:E7}=xN.actions,dL=xN.reducer,fL=["x","y"];function $y(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Mi(e){for(var t=1;tc.rootProps.className);h.useEffect(()=>{if(e==null)return ka;var c=(d,u,f)=>{if(t!==f&&e===d){if(n==="index"){var p;if(o&&u!==null&&u!==void 0&&(p=u.payload)!==null&&p!==void 0&&p.coordinate&&u.payload.sourceViewBox){var m=u.payload.coordinate,{x,y:g}=m,b=gL(m,fL),{x:v,y:j,width:y,height:w}=u.payload.sourceViewBox,S=Mi(Mi({},b),{},{x:o.x+(y?(x-v)/y:0)*o.width,y:o.y+(w?(g-j)/w:0)*o.height});r(Mi(Mi({},u),{},{payload:Mi(Mi({},u.payload),{},{coordinate:S})}))}else r(u);return}if(i!=null){var N;if(typeof n=="function"){var P={activeTooltipIndex:u.payload.index==null?void 0:Number(u.payload.index),isTooltipActive:u.payload.active,activeIndex:u.payload.index==null?void 0:Number(u.payload.index),activeLabel:u.payload.label,activeDataKey:u.payload.dataKey,activeCoordinate:u.payload.coordinate},_=n(i,P);N=i[_]}else n==="value"&&(N=i.find(E=>String(E.value)===u.payload.label));var{coordinate:T}=u.payload;if(N==null||u.payload.active===!1||T==null||o==null){r(up({active:!1,coordinate:void 0,dataKey:void 0,index:null,label:void 0,sourceViewBox:void 0}));return}var{x:$,y:M}=T,C=Math.min($,o.x+o.width),R=Math.min(M,o.y+o.height),q={x:s==="horizontal"?N.coordinate:C,y:s==="horizontal"?R:N.coordinate},Z=up({active:u.payload.active,coordinate:q,dataKey:u.payload.dataKey,index:String(N.index),label:u.payload.label,sourceViewBox:u.payload.sourceViewBox});r(Z)}}};return qs.on(pp,c),()=>{qs.off(pp,c)}},[l,r,t,e,n,i,s,o])}function vL(){var e=G(Pm),t=G(_m),r=Ve();h.useEffect(()=>{if(e==null)return ka;var n=(i,s,o)=>{t!==o&&e===i&&r(uL(s))};return qs.on(My,n),()=>{qs.off(My,n)}},[r,t,e])}function bL(){var e=Ve();h.useEffect(()=>{e(oL())},[e]),yL(),vL()}function jL(e,t,r,n,i,s){var o=G(m=>A$(m,e,t)),l=G(_m),c=G(Pm),d=G(JS),u=G(lL),f=u==null?void 0:u.active,p=uu();h.useEffect(()=>{if(!f&&c!=null&&l!=null){var m=up({active:s,coordinate:r,dataKey:o,index:i,label:typeof n=="number"?String(n):n,sourceViewBox:p});qs.emit(pp,c,m,l)}},[f,r,o,i,n,l,c,d,s,p])}function Ly(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function zy(e){for(var t=1;t{P(CI({shared:j,trigger:y,axisId:N,active:i,defaultIndex:_}))},[P,j,y,N,i,_]);var T=uu(),$=qw(),M=SI(j),{activeIndex:C,isActive:R}=(t=G(J=>D$(J,M,y,_)))!==null&&t!==void 0?t:{},q=G(J=>E$(J,M,y,_)),Z=G(J=>dN(J,M,y,_)),E=G(J=>O$(J,M,y,_)),D=q,O=rL(),k=(r=i??R)!==null&&r!==void 0?r:!1,[L,U]=w4([D,k]),H=M==="axis"?Z:void 0;jL(M,y,E,H,C,k);var te=S??O;if(te==null||T==null||M==null)return null;var re=D??Ry;k||(re=Ry),d&&re.length&&(re=a4(re.filter(J=>J.value!=null&&(J.hide!==!0||n.includeHidden)),p,kL));var we=re.length>0,A=h.createElement(S3,{allowEscapeViewBox:s,animationDuration:o,animationEasing:l,isAnimationActive:u,active:k,coordinate:E,hasPayload:we,offset:f,position:m,reverseDirection:x,useTranslate3d:g,viewBox:T,wrapperStyle:b,lastBoundingBox:L,innerRef:U,hasPortalFromProps:!!S},PL(c,zy(zy({},n),{},{payload:re,label:H,active:k,activeIndex:C,coordinate:E,accessibilityLayer:$})));return h.createElement(h.Fragment,null,bh.createPortal(A,te),k&&h.createElement(tL,{cursor:v,tooltipEventType:M,coordinate:E,payload:re,index:C}))}function CL(e,t,r){return(t=AL(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function AL(e){var t=OL(e,"string");return typeof t=="symbol"?t:t+""}function OL(e,t){if(typeof e!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(typeof n!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}class EL{constructor(t){CL(this,"cache",new Map),this.maxSize=t}get(t){var r=this.cache.get(t);return r!==void 0&&(this.cache.delete(t),this.cache.set(t,r)),r}set(t,r){if(this.cache.has(t))this.cache.delete(t);else if(this.cache.size>=this.maxSize){var n=this.cache.keys().next().value;this.cache.delete(n)}this.cache.set(t,r)}clear(){this.cache.clear()}size(){return this.cache.size}}function Fy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function DL(e){for(var t=1;t{try{var r=document.getElementById(Uy);r||(r=document.createElement("span"),r.setAttribute("id",Uy),r.setAttribute("aria-hidden","true"),document.body.appendChild(r)),Object.assign(r.style,LL,t),r.textContent="".concat(e);var n=r.getBoundingClientRect();return{width:n.width,height:n.height}}catch{return{width:0,height:0}}},ds=function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(t==null||Ci.isSsr)return{width:0,height:0};if(!yN.enableCache)return qy(t,r);var n=zL(t,r),i=Wy.get(n);if(i)return i;var s=qy(t,r);return Wy.set(n,s),s},Hy=/(-?\d+(?:\.\d+)?[a-zA-Z%]*)([*/])(-?\d+(?:\.\d+)?[a-zA-Z%]*)/,Ky=/(-?\d+(?:\.\d+)?[a-zA-Z%]*)([+-])(-?\d+(?:\.\d+)?[a-zA-Z%]*)/,RL=/^px|cm|vh|vw|em|rem|%|mm|in|pt|pc|ex|ch|vmin|vmax|Q$/,BL=/(-?\d+(?:\.\d+)?)([a-zA-Z%]+)?/,vN={cm:96/2.54,mm:96/25.4,pt:96/72,pc:96/6,in:96,Q:96/(2.54*40),px:1},FL=Object.keys(vN),Gi="NaN";function WL(e,t){return e*vN[t]}class jt{static parse(t){var r,[,n,i]=(r=BL.exec(t))!==null&&r!==void 0?r:[];return new jt(parseFloat(n),i??"")}constructor(t,r){this.num=t,this.unit=r,this.num=t,this.unit=r,yr(t)&&(this.unit=""),r!==""&&!RL.test(r)&&(this.num=NaN,this.unit=""),FL.includes(r)&&(this.num=WL(t,r),this.unit="px")}add(t){return this.unit!==t.unit?new jt(NaN,""):new jt(this.num+t.num,this.unit)}subtract(t){return this.unit!==t.unit?new jt(NaN,""):new jt(this.num-t.num,this.unit)}multiply(t){return this.unit!==""&&t.unit!==""&&this.unit!==t.unit?new jt(NaN,""):new jt(this.num*t.num,this.unit||t.unit)}divide(t){return this.unit!==""&&t.unit!==""&&this.unit!==t.unit?new jt(NaN,""):new jt(this.num/t.num,this.unit||t.unit)}toString(){return"".concat(this.num).concat(this.unit)}isNaN(){return yr(this.num)}}function bN(e){if(e.includes(Gi))return Gi;for(var t=e;t.includes("*")||t.includes("/");){var r,[,n,i,s]=(r=Hy.exec(t))!==null&&r!==void 0?r:[],o=jt.parse(n??""),l=jt.parse(s??""),c=i==="*"?o.multiply(l):o.divide(l);if(c.isNaN())return Gi;t=t.replace(Hy,c.toString())}for(;t.includes("+")||/.-\d+(?:\.\d+)?/.test(t);){var d,[,u,f,p]=(d=Ky.exec(t))!==null&&d!==void 0?d:[],m=jt.parse(u??""),x=jt.parse(p??""),g=f==="+"?m.add(x):m.subtract(x);if(g.isNaN())return Gi;t=t.replace(Ky,g.toString())}return t}var Vy=/\(([^()]*)\)/;function UL(e){for(var t=e,r;(r=Vy.exec(t))!=null;){var[,n]=r;t=t.replace(Vy,bN(n))}return t}function qL(e){var t=e.replace(/\s+/g,"");return t=UL(t),t=bN(t),t}function HL(e){try{return qL(e)}catch{return Gi}}function Nd(e){var t=HL(e.slice(5,-1));return t===Gi?"":t}var KL=["x","y","lineHeight","capHeight","fill","scaleToFit","textAnchor","verticalAnchor"],VL=["dx","dy","angle","className","breakAll"];function hp(){return hp=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:t,breakAll:r,style:n}=e;try{var i=[];Re(t)||(r?i=t.toString().split(""):i=t.toString().split(jN));var s=i.map(l=>({word:l,width:ds(l,n).width})),o=r?0:ds(" ",n).width;return{wordsWithComputedWidth:s,spaceWidth:o}}catch{return null}};function GL(e){return e==="start"||e==="middle"||e==="end"||e==="inherit"}var SN=(e,t,r,n)=>e.reduce((i,s)=>{var{word:o,width:l}=s,c=i[i.length-1];if(c&&l!=null&&(t==null||n||c.width+l+re.reduce((t,r)=>t.width>r.width?t:r),ZL="…",Gy=(e,t,r,n,i,s,o,l)=>{var c=e.slice(0,t),d=wN({breakAll:r,style:n,children:c+ZL});if(!d)return[!1,[]];var u=SN(d.wordsWithComputedWidth,s,o,l),f=u.length>i||NN(u).width>Number(s);return[f,u]},XL=(e,t,r,n,i)=>{var{maxLines:s,children:o,style:l,breakAll:c}=e,d=Y(s),u=String(o),f=SN(t,n,r,i);if(!d||i)return f;var p=f.length>s||NN(f).width>Number(n);if(!p)return f;for(var m=0,x=u.length-1,g=0,b;m<=x&&g<=u.length-1;){var v=Math.floor((m+x)/2),j=v-1,[y,w]=Gy(u,j,c,l,s,n,r,i),[S]=Gy(u,v,c,l,s,n,r,i);if(!y&&!S&&(m=v+1),y&&S&&(x=v-1),!y&&S){b=w;break}g++}return b||f},Zy=e=>{var t=Re(e)?[]:e.toString().split(jN);return[{words:t,width:void 0}]},JL=e=>{var{width:t,scaleToFit:r,children:n,style:i,breakAll:s,maxLines:o}=e;if((t||r)&&!Ci.isSsr){var l,c,d=wN({breakAll:s,children:n,style:i});if(d){var{wordsWithComputedWidth:u,spaceWidth:f}=d;l=u,c=f}else return Zy(n);return XL({breakAll:s,children:n,maxLines:o,style:i},l,c,t,!!r)}return Zy(n)},kN="#808080",QL={breakAll:!1,capHeight:"0.71em",fill:kN,lineHeight:"1em",scaleToFit:!1,textAnchor:"start",verticalAnchor:"end",x:0,y:0},Jm=h.forwardRef((e,t)=>{var r=ft(e,QL),{x:n,y:i,lineHeight:s,capHeight:o,fill:l,scaleToFit:c,textAnchor:d,verticalAnchor:u}=r,f=Yy(r,KL),p=h.useMemo(()=>JL({breakAll:f.breakAll,children:f.children,maxLines:f.maxLines,scaleToFit:c,style:f.style,width:f.width}),[f.breakAll,f.children,f.maxLines,c,f.style,f.width]),{dx:m,dy:x,angle:g,className:b,breakAll:v}=f,j=Yy(f,VL);if(!Or(n)||!Or(i)||p.length===0)return null;var y=Number(n)+(Y(m)?m:0),w=Number(i)+(Y(x)?x:0);if(!_e(y)||!_e(w))return null;var S;switch(u){case"start":S=Nd("calc(".concat(o,")"));break;case"middle":S=Nd("calc(".concat((p.length-1)/2," * -").concat(s," + (").concat(o," / 2))"));break;default:S=Nd("calc(".concat(p.length-1," * -").concat(s,")"));break}var N=[];if(c){var P=p[0].width,{width:_}=f;N.push("scale(".concat(Y(_)&&Y(P)?_/P:1,")"))}return g&&N.push("rotate(".concat(g,", ").concat(y,", ").concat(w,")")),N.length&&(j.transform=N.join(" ")),h.createElement("text",hp({},ut(j),{ref:t,x:y,y:w,className:ce("recharts-text",b),textAnchor:d,fill:l.includes("url")?kN:l}),p.map((T,$)=>{var M=T.words.join(v?"":" ");return h.createElement("tspan",{x:y,dy:$===0?S:s,key:"".concat(M,"-").concat($)},M)}))});Jm.displayName="Text";var e8=["labelRef"];function t8(e,t){if(e==null)return{};var r,n,i=r8(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var{x:t,y:r,upperWidth:n,lowerWidth:i,width:s,height:o,children:l}=e,c=h.useMemo(()=>({x:t,y:r,upperWidth:n,lowerWidth:i,width:s,height:o}),[t,r,n,i,s,o]);return h.createElement(PN.Provider,{value:c},l)},_N=()=>{var e=h.useContext(PN),t=uu();return e||Rw(t)},o8=h.createContext(null),l8=()=>{var e=h.useContext(o8),t=G(n2);return e||t},c8=e=>{var{value:t,formatter:r}=e,n=Re(e.children)?t:e.children;return typeof r=="function"?r(n):n},Qm=e=>e!=null&&typeof e=="function",u8=(e,t)=>{var r=Jt(t-e),n=Math.min(Math.abs(t-e),360);return r*n},d8=(e,t,r,n,i)=>{var{offset:s,className:o}=e,{cx:l,cy:c,innerRadius:d,outerRadius:u,startAngle:f,endAngle:p,clockWise:m}=i,x=(d+u)/2,g=u8(f,p),b=g>=0?1:-1,v,j;switch(t){case"insideStart":v=f+b*s,j=m;break;case"insideEnd":v=p-b*s,j=!m;break;case"end":v=p+b*s,j=m;break;default:throw new Error("Unsupported position ".concat(t))}j=g<=0?j:!j;var y=Je(l,c,x,v),w=Je(l,c,x,v+(j?1:-1)*359),S="M".concat(y.x,",").concat(y.y,` + A`).concat(x,",").concat(x,",0,1,").concat(j?0:1,`, + `).concat(w.x,",").concat(w.y),N=Re(e.id)?Ts("recharts-radial-line-"):e.id;return h.createElement("text",Rr({},n,{dominantBaseline:"central",className:ce("recharts-radial-bar-label",o)}),h.createElement("defs",null,h.createElement("path",{id:N,d:S})),h.createElement("textPath",{xlinkHref:"#".concat(N)},r))},f8=(e,t,r)=>{var{cx:n,cy:i,innerRadius:s,outerRadius:o,startAngle:l,endAngle:c}=e,d=(l+c)/2;if(r==="outside"){var{x:u,y:f}=Je(n,i,o+t,d);return{x:u,y:f,textAnchor:u>=n?"start":"end",verticalAnchor:"middle"}}if(r==="center")return{x:n,y:i,textAnchor:"middle",verticalAnchor:"middle"};if(r==="centerTop")return{x:n,y:i,textAnchor:"middle",verticalAnchor:"start"};if(r==="centerBottom")return{x:n,y:i,textAnchor:"middle",verticalAnchor:"end"};var p=(s+o)/2,{x:m,y:x}=Je(n,i,p,d);return{x:m,y:x,textAnchor:"middle",verticalAnchor:"middle"}},mp=e=>"cx"in e&&Y(e.cx),p8=(e,t)=>{var{parentViewBox:r,offset:n,position:i}=e,s;r!=null&&!mp(r)&&(s=r);var{x:o,y:l,upperWidth:c,lowerWidth:d,height:u}=t,f=o,p=o+(c-d)/2,m=(f+p)/2,x=(c+d)/2,g=f+c/2,b=u>=0?1:-1,v=b*n,j=b>0?"end":"start",y=b>0?"start":"end",w=c>=0?1:-1,S=w*n,N=w>0?"end":"start",P=w>0?"start":"end";if(i==="top"){var _={x:f+c/2,y:l-v,textAnchor:"middle",verticalAnchor:j};return Ce(Ce({},_),s?{height:Math.max(l-s.y,0),width:c}:{})}if(i==="bottom"){var T={x:p+d/2,y:l+u+v,textAnchor:"middle",verticalAnchor:y};return Ce(Ce({},T),s?{height:Math.max(s.y+s.height-(l+u),0),width:d}:{})}if(i==="left"){var $={x:m-S,y:l+u/2,textAnchor:N,verticalAnchor:"middle"};return Ce(Ce({},$),s?{width:Math.max($.x-s.x,0),height:u}:{})}if(i==="right"){var M={x:m+x+S,y:l+u/2,textAnchor:P,verticalAnchor:"middle"};return Ce(Ce({},M),s?{width:Math.max(s.x+s.width-M.x,0),height:u}:{})}var C=s?{width:x,height:u}:{};return i==="insideLeft"?Ce({x:m+S,y:l+u/2,textAnchor:P,verticalAnchor:"middle"},C):i==="insideRight"?Ce({x:m+x-S,y:l+u/2,textAnchor:N,verticalAnchor:"middle"},C):i==="insideTop"?Ce({x:f+c/2,y:l+v,textAnchor:"middle",verticalAnchor:y},C):i==="insideBottom"?Ce({x:p+d/2,y:l+u-v,textAnchor:"middle",verticalAnchor:j},C):i==="insideTopLeft"?Ce({x:f+S,y:l+v,textAnchor:P,verticalAnchor:y},C):i==="insideTopRight"?Ce({x:f+c-S,y:l+v,textAnchor:N,verticalAnchor:y},C):i==="insideBottomLeft"?Ce({x:p+S,y:l+u-v,textAnchor:P,verticalAnchor:j},C):i==="insideBottomRight"?Ce({x:p+d-S,y:l+u-v,textAnchor:N,verticalAnchor:j},C):i&&typeof i=="object"&&(Y(i.x)||Qr(i.x))&&(Y(i.y)||Qr(i.y))?Ce({x:o+Rn(i.x,x),y:l+Rn(i.y,u),textAnchor:"end",verticalAnchor:"end"},C):Ce({x:g,y:l+u/2,textAnchor:"middle",verticalAnchor:"middle"},C)},h8={offset:5,zIndex:lt.label};function yn(e){var t=ft(e,h8),{viewBox:r,position:n,value:i,children:s,content:o,className:l="",textBreakAll:c,labelRef:d}=t,u=l8(),f=_N(),p=n==="center"?f:u??f,m,x,g;if(r==null?m=p:mp(r)?m=r:m=Rw(r),!m||Re(i)&&Re(s)&&!h.isValidElement(o)&&typeof o!="function")return null;var b=Ce(Ce({},t),{},{viewBox:m});if(h.isValidElement(o)){var{labelRef:v}=b,j=t8(b,e8);return h.cloneElement(o,j)}if(typeof o=="function"){if(x=h.createElement(o,b),h.isValidElement(x))return x}else x=c8(t);var y=ut(t);if(mp(m)){if(n==="insideStart"||n==="insideEnd"||n==="end")return d8(t,n,x,y,m);g=f8(m,t.offset,t.position)}else g=p8(t,m);return h.createElement(Ir,{zIndex:t.zIndex},h.createElement(Jm,Rr({ref:d,className:ce("recharts-label",l)},y,g,{textAnchor:GL(y.textAnchor)?y.textAnchor:g.textAnchor,breakAll:c}),x))}yn.displayName="Label";var m8=(e,t,r)=>{if(!e)return null;var n={viewBox:t,labelRef:r};return e===!0?h.createElement(yn,Rr({key:"label-implicit"},n)):Or(e)?h.createElement(yn,Rr({key:"label-implicit",value:e},n)):h.isValidElement(e)?e.type===yn?h.cloneElement(e,Ce({key:"label-implicit"},n)):h.createElement(yn,Rr({key:"label-implicit",content:e},n)):Qm(e)?h.createElement(yn,Rr({key:"label-implicit",content:e},n)):e&&typeof e=="object"?h.createElement(yn,Rr({},e,{key:"label-implicit"},n)):null};function g8(e){var{label:t,labelRef:r}=e,n=_N();return m8(t,n,r)||null}var CN={},AN={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r[r.length-1]}e.last=t})(AN);var ON={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return Array.isArray(r)?r:Array.from(r)}e.toArray=t})(ON);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=AN,r=ON,n=Jc;function i(s){if(n.isArrayLike(s))return t.last(r.toArray(s))}e.last=i})(CN);var x8=CN.last;const y8=Tr(x8);var v8=["valueAccessor"],b8=["dataKey","clockWise","id","textBreakAll","zIndex"];function hc(){return hc=Object.assign?Object.assign.bind():function(e){for(var t=1;tArray.isArray(e.value)?y8(e.value):e.value,EN=h.createContext(void 0),DN=EN.Provider,TN=h.createContext(void 0);TN.Provider;function S8(){return h.useContext(EN)}function N8(){return h.useContext(TN)}function cl(e){var{valueAccessor:t=w8}=e,r=Jy(e,v8),{dataKey:n,clockWise:i,id:s,textBreakAll:o,zIndex:l}=r,c=Jy(r,b8),d=S8(),u=N8(),f=d||u;return!f||!f.length?null:h.createElement(Ir,{zIndex:l??lt.label},h.createElement(ir,{className:"recharts-label-list"},f.map((p,m)=>{var x,g=Re(n)?t(p,m):et(p&&p.payload,n),b=Re(s)?{}:{id:"".concat(s,"-").concat(m)};return h.createElement(yn,hc({key:"label-".concat(m)},ut(p),c,b,{fill:(x=r.fill)!==null&&x!==void 0?x:p.fill,parentViewBox:p.parentViewBox,value:g,textBreakAll:o,viewBox:p.viewBox,index:m,zIndex:0}))})))}cl.displayName="LabelList";function MN(e){var{label:t}=e;return t?t===!0?h.createElement(cl,{key:"labelList-implicit"}):h.isValidElement(t)||Qm(t)?h.createElement(cl,{key:"labelList-implicit",content:t}):typeof t=="object"?h.createElement(cl,hc({key:"labelList-implicit"},t,{type:String(t.type)})):null:null}function gp(){return gp=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{cx:t,cy:r,r:n,className:i}=e,s=ce("recharts-dot",i);return Y(t)&&Y(r)&&Y(n)?h.createElement("circle",gp({},nr(e),Ih(e),{className:s,cx:t,cy:r,r:n})):null},k8={radiusAxis:{},angleAxis:{}},$N=At({name:"polarAxis",initialState:k8,reducers:{addRadiusAxis(e,t){e.radiusAxis[t.payload.id]=t.payload},removeRadiusAxis(e,t){delete e.radiusAxis[t.payload.id]},addAngleAxis(e,t){e.angleAxis[t.payload.id]=t.payload},removeAngleAxis(e,t){delete e.angleAxis[t.payload.id]}}}),{addRadiusAxis:D7,removeRadiusAxis:T7,addAngleAxis:M7,removeAngleAxis:I7}=$N.actions,P8=$N.reducer,eg=e=>e&&typeof e=="object"&&"clipDot"in e?!!e.clipDot:!0,LN={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){var i;if(typeof r!="object"||r==null)return!1;if(Object.getPrototypeOf(r)===null)return!0;if(Object.prototype.toString.call(r)!=="[object Object]"){const s=r[Symbol.toStringTag];return s==null||!((i=Object.getOwnPropertyDescriptor(r,Symbol.toStringTag))!=null&&i.writable)?!1:r.toString()===`[object ${s}]`}let n=r;for(;Object.getPrototypeOf(n)!==null;)n=Object.getPrototypeOf(n);return Object.getPrototypeOf(r)===n}e.isPlainObject=t})(LN);var _8=LN.isPlainObject;const C8=Tr(_8);function Qy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function ev(e){for(var t=1;t{var s=r-n,o;return o="M ".concat(e,",").concat(t),o+="L ".concat(e+r,",").concat(t),o+="L ".concat(e+r-s/2,",").concat(t+i),o+="L ".concat(e+r-s/2-n,",").concat(t+i),o+="L ".concat(e,",").concat(t," Z"),o},D8={x:0,y:0,upperWidth:0,lowerWidth:0,height:0,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},T8=e=>{var t=ft(e,D8),{x:r,y:n,upperWidth:i,lowerWidth:s,height:o,className:l}=t,{animationEasing:c,animationDuration:d,animationBegin:u,isUpdateAnimationActive:f}=t,p=h.useRef(null),[m,x]=h.useState(-1),g=h.useRef(i),b=h.useRef(s),v=h.useRef(o),j=h.useRef(r),y=h.useRef(n),w=pu(e,"trapezoid-");if(h.useEffect(()=>{if(p.current&&p.current.getTotalLength)try{var q=p.current.getTotalLength();q&&x(q)}catch{}},[]),r!==+r||n!==+n||i!==+i||s!==+s||o!==+o||i===0&&s===0||o===0)return null;var S=ce("recharts-trapezoid",l);if(!f)return h.createElement("g",null,h.createElement("path",mc({},ut(t),{className:S,d:tv(r,n,i,s,o)})));var N=g.current,P=b.current,_=v.current,T=j.current,$=y.current,M="0px ".concat(m===-1?1:m,"px"),C="".concat(m,"px 0px"),R=Hw(["strokeDasharray"],d,c);return h.createElement(fu,{animationId:w,key:w,canBegin:m>0,duration:d,easing:c,isActive:f,begin:u},q=>{var Z=De(N,i,q),E=De(P,s,q),D=De(_,o,q),O=De(T,r,q),k=De($,n,q);p.current&&(g.current=Z,b.current=E,v.current=D,j.current=O,y.current=k);var L=q>0?{transition:R,strokeDasharray:C}:{strokeDasharray:M};return h.createElement("path",mc({},ut(t),{className:S,d:tv(O,k,Z,E,D),ref:p,style:ev(ev({},L),t.style)}))})},M8=["option","shapeType","propTransformer","activeClassName","isActive"];function I8(e,t){if(e==null)return{};var r,n,i=$8(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{if(!i){var s=t(r);return n(PI(s)),()=>{n(_I(s))}}},[t,r,n,i]),null}function RN(e){var{legendPayload:t}=e,r=Ve(),n=pt();return h.useLayoutEffect(()=>n?ka:(r(c3(t)),()=>{r(u3(t))}),[r,n,t]),null}var kd,q8=()=>{var[e]=h.useState(()=>Ts("uid-"));return e},H8=(kd=Tv.useId)!==null&&kd!==void 0?kd:q8;function BN(e,t){var r=H8();return t||(e?"".concat(e,"-").concat(r):r)}var K8=h.createContext(void 0),FN=e=>{var{id:t,type:r,children:n}=e,i=BN("recharts-".concat(r),t);return h.createElement(K8.Provider,{value:i},n(i))},V8={cartesianItems:[],polarItems:[]},WN=At({name:"graphicalItems",initialState:V8,reducers:{addCartesianGraphicalItem:{reducer(e,t){e.cartesianItems.push(t.payload)},prepare:Le()},replaceCartesianGraphicalItem:{reducer(e,t){var{prev:r,next:n}=t.payload,i=Kr(e).cartesianItems.indexOf(r);i>-1&&(e.cartesianItems[i]=n)},prepare:Le()},removeCartesianGraphicalItem:{reducer(e,t){var r=Kr(e).cartesianItems.indexOf(t.payload);r>-1&&e.cartesianItems.splice(r,1)},prepare:Le()},addPolarGraphicalItem:{reducer(e,t){e.polarItems.push(t.payload)},prepare:Le()},removePolarGraphicalItem:{reducer(e,t){var r=Kr(e).polarItems.indexOf(t.payload);r>-1&&e.polarItems.splice(r,1)},prepare:Le()}}}),{addCartesianGraphicalItem:Y8,replaceCartesianGraphicalItem:G8,removeCartesianGraphicalItem:Z8,addPolarGraphicalItem:$7,removePolarGraphicalItem:L7}=WN.actions,X8=WN.reducer;function UN(e){var t=Ve(),r=h.useRef(null);return h.useLayoutEffect(()=>{r.current===null?t(Y8(e)):r.current!==e&&t(G8({prev:r.current,next:e})),r.current=e},[t,e]),h.useLayoutEffect(()=>()=>{r.current&&(t(Z8(r.current)),r.current=null)},[t]),null}var J8=["points"];function iv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Pd(e){for(var t=1;t{var b,v,j=Pd(Pd(Pd({r:3},o),f),{},{index:g,cx:(b=x.x)!==null&&b!==void 0?b:void 0,cy:(v=x.y)!==null&&v!==void 0?v:void 0,dataKey:s,value:x.value,payload:x.payload,points:t});return h.createElement(iz,{key:"dot-".concat(g),option:r,dotProps:j,className:i})}),m={};return l&&c!=null&&(m.clipPath="url(#clipPath-".concat(u?"":"dots-").concat(c,")")),h.createElement(Ir,{zIndex:d},h.createElement(ir,xc({className:n},m),p))}function av(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function sv(e){for(var t=1;t({top:e.top,bottom:e.bottom,left:e.left,right:e.right})),xz=I([gz,ln,cn],(e,t,r)=>{if(!(!e||t==null||r==null))return{x:e.left,y:e.top,width:Math.max(0,t-e.left-e.right),height:Math.max(0,r-e.top-e.bottom)}}),Mu=()=>G(xz),yz=()=>G(g$);function ov(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function _d(e){for(var t=1;t{var{point:t,childIndex:r,mainColor:n,activeDot:i,dataKey:s}=e;if(i===!1||t.x==null||t.y==null)return null;var o={index:r,dataKey:s,cx:t.x,cy:t.y,r:4,fill:n??"none",strokeWidth:2,stroke:"#fff",payload:t.payload,value:t.value},l=_d(_d(_d({},o),qc(i)),Ih(i)),c;return h.isValidElement(i)?c=h.cloneElement(i,l):typeof i=="function"?c=i(l):c=h.createElement(IN,l),h.createElement(ir,{className:"recharts-active-dot"},c)};function xp(e){var{points:t,mainColor:r,activeDot:n,itemDataKey:i,zIndex:s=lt.activeDot}=e,o=G(Us),l=yz();if(t==null||l==null)return null;var c=t.find(d=>l.includes(d.payload));return Re(c)?null:h.createElement(Ir,{zIndex:s},h.createElement(wz,{point:c,childIndex:Number(o),mainColor:r,dataKey:i,activeDot:n}))}var Sz={},KN=At({name:"errorBars",initialState:Sz,reducers:{addErrorBar:(e,t)=>{var{itemId:r,errorBar:n}=t.payload;e[r]||(e[r]=[]),e[r].push(n)},replaceErrorBar:(e,t)=>{var{itemId:r,prev:n,next:i}=t.payload;e[r]&&(e[r]=e[r].map(s=>s.dataKey===n.dataKey&&s.direction===n.direction?i:s))},removeErrorBar:(e,t)=>{var{itemId:r,errorBar:n}=t.payload;e[r]&&(e[r]=e[r].filter(i=>i.dataKey!==n.dataKey||i.direction!==n.direction))}}}),{addErrorBar:B7,replaceErrorBar:F7,removeErrorBar:W7}=KN.actions,Nz=KN.reducer,kz=["children"];function Pz(e,t){if(e==null)return{};var r,n,i=_z(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n({x:0,y:0,value:0}),errorBarOffset:0},Az=h.createContext(Cz);function Oz(e){var{children:t}=e,r=Pz(e,kz);return h.createElement(Az.Provider,{value:r},t)}function tg(e,t){var r,n,i=G(d=>dn(d,e)),s=G(d=>fn(d,t)),o=(r=i==null?void 0:i.allowDataOverflow)!==null&&r!==void 0?r:Tt.allowDataOverflow,l=(n=s==null?void 0:s.allowDataOverflow)!==null&&n!==void 0?n:Mt.allowDataOverflow,c=o||l;return{needClip:c,needClipX:o,needClipY:l}}function VN(e){var{xAxisId:t,yAxisId:r,clipPathId:n}=e,i=Mu(),{needClipX:s,needClipY:o,needClip:l}=tg(t,r);if(!l||!i)return null;var{x:c,y:d,width:u,height:f}=i;return h.createElement("clipPath",{id:"clipPath-".concat(n)},h.createElement("rect",{x:s?c:c-u/2,y:o?d:d-f/2,width:s?u:u*2,height:o?f:f*2}))}var Ez=e=>{var{chartData:t}=e,r=Ve(),n=pt();return h.useEffect(()=>n?()=>{}:(r(Iy(t)),()=>{r(Iy(void 0))}),[t,r,n]),null},lv={x:0,y:0,width:0,height:0,padding:{top:0,right:0,bottom:0,left:0}},YN=At({name:"brush",initialState:lv,reducers:{setBrushSettings(e,t){return t.payload==null?lv:t.payload}}}),{setBrushSettings:U7}=YN.actions,Dz=YN.reducer;function Tz(e,t,r){return(t=Mz(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function Mz(e){var t=Iz(e,"string");return typeof t=="symbol"?t:t+""}function Iz(e,t){if(typeof e!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(typeof n!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}class rg{static create(t){return new rg(t)}constructor(t){this.scale=t}get domain(){return this.scale.domain}get range(){return this.scale.range}get rangeMin(){return this.range()[0]}get rangeMax(){return this.range()[1]}get bandwidth(){return this.scale.bandwidth}apply(t){var{bandAware:r,position:n}=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(t!==void 0){if(n)switch(n){case"start":return this.scale(t);case"middle":{var i=this.bandwidth?this.bandwidth()/2:0;return this.scale(t)+i}case"end":{var s=this.bandwidth?this.bandwidth():0;return this.scale(t)+s}default:return this.scale(t)}if(r){var o=this.bandwidth?this.bandwidth()/2:0;return this.scale(t)+o}return this.scale(t)}}isInRange(t){var r=this.range(),n=r[0],i=r[r.length-1];return n<=i?t>=n&&t<=i:t>=i&&t<=n}}Tz(rg,"EPS",1e-4);function $z(e){return(e%180+180)%180}var Lz=function(t){var{width:r,height:n}=t,i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,s=$z(i),o=s*Math.PI/180,l=Math.atan(n/r),c=o>l&&o{e.dots.push(t.payload)},removeDot:(e,t)=>{var r=Kr(e).dots.findIndex(n=>n===t.payload);r!==-1&&e.dots.splice(r,1)},addArea:(e,t)=>{e.areas.push(t.payload)},removeArea:(e,t)=>{var r=Kr(e).areas.findIndex(n=>n===t.payload);r!==-1&&e.areas.splice(r,1)},addLine:(e,t)=>{e.lines.push(t.payload)},removeLine:(e,t)=>{var r=Kr(e).lines.findIndex(n=>n===t.payload);r!==-1&&e.lines.splice(r,1)}}}),{addDot:q7,removeDot:H7,addArea:K7,removeArea:V7,addLine:Y7,removeLine:G7}=GN.actions,Rz=GN.reducer,Bz=h.createContext(void 0),Fz=e=>{var{children:t}=e,[r]=h.useState("".concat(Ts("recharts"),"-clip")),n=Mu();if(n==null)return null;var{x:i,y:s,width:o,height:l}=n;return h.createElement(Bz.Provider,{value:r},h.createElement("defs",null,h.createElement("clipPath",{id:r},h.createElement("rect",{x:i,y:s,height:l,width:o}))),t)};function ya(e,t){for(var r in e)if({}.hasOwnProperty.call(e,r)&&(!{}.hasOwnProperty.call(t,r)||e[r]!==t[r]))return!1;for(var n in t)if({}.hasOwnProperty.call(t,n)&&!{}.hasOwnProperty.call(e,n))return!1;return!0}function ZN(e,t){if(t<1)return[];if(t===1)return e;for(var r=[],n=0;ne*i)return!1;var s=r();return e*(t-e*s/2-n)>=0&&e*(t+e*s/2-i)<=0}function qz(e,t){return ZN(e,t+1)}function Hz(e,t,r,n,i){for(var s=(n||[]).slice(),{start:o,end:l}=t,c=0,d=1,u=o,f=function(){var x=n==null?void 0:n[c];if(x===void 0)return{v:ZN(n,d)};var g=c,b,v=()=>(b===void 0&&(b=r(x,g)),b),j=x.coordinate,y=c===0||yc(e,j,v,u,l);y||(c=0,u=o,d+=1),y&&(u=j+e*(v()/2+i),c+=d)},p;d<=s.length;)if(p=f(),p)return p.v;return[]}function cv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function at(e){for(var t=1;t(x===void 0&&(x=r(m,p)),x);if(p===o-1){var b=e*(m.coordinate+e*g()/2-c);s[p]=m=at(at({},m),{},{tickCoord:b>0?m.coordinate-b*e:m.coordinate})}else s[p]=m=at(at({},m),{},{tickCoord:m.coordinate});if(m.tickCoord!=null){var v=yc(e,m.tickCoord,g,l,c);v&&(c=m.tickCoord-e*(g()/2+i),s[p]=at(at({},m),{},{isShow:!0}))}},u=o-1;u>=0;u--)d(u);return s}function Zz(e,t,r,n,i,s){var o=(n||[]).slice(),l=o.length,{start:c,end:d}=t;if(s){var u=n[l-1],f=r(u,l-1),p=e*(u.coordinate+e*f/2-d);if(o[l-1]=u=at(at({},u),{},{tickCoord:p>0?u.coordinate-p*e:u.coordinate}),u.tickCoord!=null){var m=yc(e,u.tickCoord,()=>f,c,d);m&&(d=u.tickCoord-e*(f/2+i),o[l-1]=at(at({},u),{},{isShow:!0}))}}for(var x=s?l-1:l,g=function(j){var y=o[j],w,S=()=>(w===void 0&&(w=r(y,j)),w);if(j===0){var N=e*(y.coordinate-e*S()/2-c);o[j]=y=at(at({},y),{},{tickCoord:N<0?y.coordinate-N*e:y.coordinate})}else o[j]=y=at(at({},y),{},{tickCoord:y.coordinate});if(y.tickCoord!=null){var P=yc(e,y.tickCoord,S,c,d);P&&(c=y.tickCoord+e*(S()/2+i),o[j]=at(at({},y),{},{isShow:!0}))}},b=0;b{var S=typeof d=="function"?d(y.value,w):y.value;return x==="width"?Wz(ds(S,{fontSize:t,letterSpacing:r}),g,f):ds(S,{fontSize:t,letterSpacing:r})[x]},v=i.length>=2?Jt(i[1].coordinate-i[0].coordinate):1,j=Uz(s,v,x);return c==="equidistantPreserveStart"?Hz(v,j,b,i,o):(c==="preserveStart"||c==="preserveStartEnd"?m=Zz(v,j,b,i,o,c==="preserveStartEnd"):m=Gz(v,j,b,i,o),m.filter(y=>y.isShow))}var Xz=e=>{var{ticks:t,label:r,labelGapWithTick:n=5,tickSize:i=0,tickMargin:s=0}=e,o=0;if(t){Array.from(t).forEach(u=>{if(u){var f=u.getBoundingClientRect();f.width>o&&(o=f.width)}});var l=r?r.getBoundingClientRect().width:0,c=i+s,d=o+c+l+(r?n:0);return Math.round(d)}return 0},Jz=["axisLine","width","height","className","hide","ticks","axisType"],Qz=["viewBox"],eR=["viewBox"];function yp(e,t){if(e==null)return{};var r,n,i=tR(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var{ticks:r=[],tick:n,tickLine:i,stroke:s,tickFormatter:o,unit:l,padding:c,tickTextProps:d,orientation:u,mirror:f,x:p,y:m,width:x,height:g,tickSize:b,tickMargin:v,fontSize:j,letterSpacing:y,getTicksConfig:w,events:S,axisType:N}=e,P=ng(Ee(Ee({},w),{},{ticks:r}),j,y),_=oR(u,f),T=lR(u,f),$=nr(w),M=qc(n),C={};typeof i=="object"&&(C=i);var R=Ee(Ee({},$),{},{fill:"none"},C),q=P.map(D=>Ee({entry:D},sR(D,p,m,x,g,u,b,f,v))),Z=q.map(D=>{var{entry:O,line:k}=D;return h.createElement(ir,{className:"recharts-cartesian-axis-tick",key:"tick-".concat(O.value,"-").concat(O.coordinate,"-").concat(O.tickCoord)},i&&h.createElement("line",Si({},R,k,{className:ce("recharts-cartesian-axis-tick-line",Xc(i,"className"))})))}),E=q.map((D,O)=>{var{entry:k,tick:L}=D,U=Ee(Ee(Ee(Ee({textAnchor:_,verticalAnchor:T},$),{},{stroke:"none",fill:s},M),L),{},{index:O,payload:k,visibleTicksCount:P.length,tickFormatter:o,padding:c},d);return h.createElement(ir,Si({className:"recharts-cartesian-axis-tick-label",key:"tick-label-".concat(k.value,"-").concat(k.coordinate,"-").concat(k.tickCoord)},Q6(S,k,O)),n&&h.createElement(cR,{option:n,tickProps:U,value:"".concat(typeof o=="function"?o(k.value,O):k.value).concat(l||"")}))});return h.createElement("g",{className:"recharts-cartesian-axis-ticks recharts-".concat(N,"-ticks")},E.length>0&&h.createElement(Ir,{zIndex:lt.label},h.createElement("g",{className:"recharts-cartesian-axis-tick-labels recharts-".concat(N,"-tick-labels"),ref:t},E)),Z.length>0&&h.createElement("g",{className:"recharts-cartesian-axis-tick-lines recharts-".concat(N,"-tick-lines")},Z))}),dR=h.forwardRef((e,t)=>{var{axisLine:r,width:n,height:i,className:s,hide:o,ticks:l,axisType:c}=e,d=yp(e,Jz),[u,f]=h.useState(""),[p,m]=h.useState(""),x=h.useRef(null);h.useImperativeHandle(t,()=>({getCalculatedWidth:()=>{var b;return Xz({ticks:x.current,label:(b=e.labelRef)===null||b===void 0?void 0:b.current,labelGapWithTick:5,tickSize:e.tickSize,tickMargin:e.tickMargin})}}));var g=h.useCallback(b=>{if(b){var v=b.getElementsByClassName("recharts-cartesian-axis-tick-value");x.current=v;var j=v[0];if(j){var y=window.getComputedStyle(j),w=y.fontSize,S=y.letterSpacing;(w!==u||S!==p)&&(f(w),m(S))}}},[u,p]);return o||n!=null&&n<=0||i!=null&&i<=0?null:h.createElement(Ir,{zIndex:e.zIndex},h.createElement(ir,{className:ce("recharts-cartesian-axis",s)},h.createElement(aR,{x:e.x,y:e.y,width:n,height:i,orientation:e.orientation,mirror:e.mirror,axisLine:r,otherSvgProps:nr(e)}),h.createElement(uR,{ref:g,axisType:c,events:d,fontSize:u,getTicksConfig:e,height:e.height,letterSpacing:p,mirror:e.mirror,orientation:e.orientation,padding:e.padding,stroke:e.stroke,tick:e.tick,tickFormatter:e.tickFormatter,tickLine:e.tickLine,tickMargin:e.tickMargin,tickSize:e.tickSize,tickTextProps:e.tickTextProps,ticks:l,unit:e.unit,width:e.width,x:e.x,y:e.y}),h.createElement(s8,{x:e.x,y:e.y,width:e.width,height:e.height,lowerWidth:e.width,upperWidth:e.width},h.createElement(g8,{label:e.label,labelRef:e.labelRef}),e.children)))}),fR=h.memo(dR,(e,t)=>{var{viewBox:r}=e,n=yp(e,Qz),{viewBox:i}=t,s=yp(t,eR);return ya(r,i)&&ya(n,s)}),ag=h.forwardRef((e,t)=>{var r=ft(e,ig);return h.createElement(fR,Si({},r,{ref:t}))});ag.displayName="CartesianAxis";var pR=["x1","y1","x2","y2","key"],hR=["offset"],mR=["xAxisId","yAxisId"],gR=["xAxisId","yAxisId"];function dv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function ot(e){for(var t=1;t{var{fill:t}=e;if(!t||t==="none")return null;var{fillOpacity:r,x:n,y:i,width:s,height:o,ry:l}=e;return h.createElement("rect",{x:n,y:i,ry:l,width:s,height:o,stroke:"none",fill:t,fillOpacity:r,className:"recharts-cartesian-grid-bg"})};function XN(e){var{option:t,lineItemProps:r}=e,n;if(h.isValidElement(t))n=h.cloneElement(t,r);else if(typeof t=="function")n=t(r);else{var i,{x1:s,y1:o,x2:l,y2:c,key:d}=r,u=vc(r,pR),f=(i=nr(u))!==null&&i!==void 0?i:{},{offset:p}=f,m=vc(f,hR);n=h.createElement("line",ai({},m,{x1:s,y1:o,x2:l,y2:c,fill:"none",key:d}))}return n}function wR(e){var{x:t,width:r,horizontal:n=!0,horizontalPoints:i}=e;if(!n||!i||!i.length)return null;var{xAxisId:s,yAxisId:o}=e,l=vc(e,mR),c=i.map((d,u)=>{var f=ot(ot({},l),{},{x1:t,y1:d,x2:t+r,y2:d,key:"line-".concat(u),index:u});return h.createElement(XN,{key:"line-".concat(u),option:n,lineItemProps:f})});return h.createElement("g",{className:"recharts-cartesian-grid-horizontal"},c)}function SR(e){var{y:t,height:r,vertical:n=!0,verticalPoints:i}=e;if(!n||!i||!i.length)return null;var{xAxisId:s,yAxisId:o}=e,l=vc(e,gR),c=i.map((d,u)=>{var f=ot(ot({},l),{},{x1:d,y1:t,x2:d,y2:t+r,key:"line-".concat(u),index:u});return h.createElement(XN,{option:n,lineItemProps:f,key:"line-".concat(u)})});return h.createElement("g",{className:"recharts-cartesian-grid-vertical"},c)}function NR(e){var{horizontalFill:t,fillOpacity:r,x:n,y:i,width:s,height:o,horizontalPoints:l,horizontal:c=!0}=e;if(!c||!t||!t.length||l==null)return null;var d=l.map(f=>Math.round(f+i-i)).sort((f,p)=>f-p);i!==d[0]&&d.unshift(0);var u=d.map((f,p)=>{var m=!d[p+1],x=m?i+o-f:d[p+1]-f;if(x<=0)return null;var g=p%t.length;return h.createElement("rect",{key:"react-".concat(p),y:f,x:n,height:x,width:s,stroke:"none",fill:t[g],fillOpacity:r,className:"recharts-cartesian-grid-bg"})});return h.createElement("g",{className:"recharts-cartesian-gridstripes-horizontal"},u)}function kR(e){var{vertical:t=!0,verticalFill:r,fillOpacity:n,x:i,y:s,width:o,height:l,verticalPoints:c}=e;if(!t||!r||!r.length)return null;var d=c.map(f=>Math.round(f+i-i)).sort((f,p)=>f-p);i!==d[0]&&d.unshift(0);var u=d.map((f,p)=>{var m=!d[p+1],x=m?i+o-f:d[p+1]-f;if(x<=0)return null;var g=p%r.length;return h.createElement("rect",{key:"react-".concat(p),x:f,y:s,width:x,height:l,stroke:"none",fill:r[g],fillOpacity:n,className:"recharts-cartesian-grid-bg"})});return h.createElement("g",{className:"recharts-cartesian-gridstripes-vertical"},u)}var PR=(e,t)=>{var{xAxis:r,width:n,height:i,offset:s}=e;return Aw(ng(ot(ot(ot({},ig),r),{},{ticks:Ow(r),viewBox:{x:0,y:0,width:n,height:i}})),s.left,s.left+s.width,t)},_R=(e,t)=>{var{yAxis:r,width:n,height:i,offset:s}=e;return Aw(ng(ot(ot(ot({},ig),r),{},{ticks:Ow(r),viewBox:{x:0,y:0,width:n,height:i}})),s.top,s.top+s.height,t)},CR={horizontal:!0,vertical:!0,horizontalPoints:[],verticalPoints:[],stroke:"#ccc",fill:"none",verticalFill:[],horizontalFill:[],xAxisId:0,yAxisId:0,syncWithTicks:!1,zIndex:lt.grid};function vp(e){var t=Fw(),r=Ww(),n=Bw(),i=ot(ot({},ft(e,CR)),{},{x:Y(e.x)?e.x:n.left,y:Y(e.y)?e.y:n.top,width:Y(e.width)?e.width:n.width,height:Y(e.height)?e.height:n.height}),{xAxisId:s,yAxisId:o,x:l,y:c,width:d,height:u,syncWithTicks:f,horizontalValues:p,verticalValues:m}=i,x=pt(),g=G(T=>Ny(T,"xAxis",s,x)),b=G(T=>Ny(T,"yAxis",o,x));if(!Er(d)||!Er(u)||!Y(l)||!Y(c))return null;var v=i.verticalCoordinatesGenerator||PR,j=i.horizontalCoordinatesGenerator||_R,{horizontalPoints:y,verticalPoints:w}=i;if((!y||!y.length)&&typeof j=="function"){var S=p&&p.length,N=j({yAxis:b?ot(ot({},b),{},{ticks:S?p:b.ticks}):void 0,width:t??d,height:r??u,offset:n},S?!0:f);Gl(Array.isArray(N),"horizontalCoordinatesGenerator should return Array but instead it returned [".concat(typeof N,"]")),Array.isArray(N)&&(y=N)}if((!w||!w.length)&&typeof v=="function"){var P=m&&m.length,_=v({xAxis:g?ot(ot({},g),{},{ticks:P?m:g.ticks}):void 0,width:t??d,height:r??u,offset:n},P?!0:f);Gl(Array.isArray(_),"verticalCoordinatesGenerator should return Array but instead it returned [".concat(typeof _,"]")),Array.isArray(_)&&(w=_)}return h.createElement(Ir,{zIndex:i.zIndex},h.createElement("g",{className:"recharts-cartesian-grid"},h.createElement(jR,{fill:i.fill,fillOpacity:i.fillOpacity,x:i.x,y:i.y,width:i.width,height:i.height,ry:i.ry}),h.createElement(NR,ai({},i,{horizontalPoints:y})),h.createElement(kR,ai({},i,{verticalPoints:w})),h.createElement(wR,ai({},i,{offset:n,horizontalPoints:y,xAxis:g,yAxis:b})),h.createElement(SR,ai({},i,{offset:n,verticalPoints:w,xAxis:g,yAxis:b}))))}vp.displayName="CartesianGrid";var JN=(e,t,r,n)=>Du(e,"xAxis",t,n),QN=(e,t,r,n)=>Eu(e,"xAxis",t,n),ek=(e,t,r,n)=>Du(e,"yAxis",r,n),tk=(e,t,r,n)=>Eu(e,"yAxis",r,n),AR=I([ue,JN,ek,QN,tk],(e,t,r,n,i)=>Mr(e,"xAxis")?ha(t,n,!1):ha(r,i,!1)),OR=(e,t,r,n,i)=>i;function ER(e){return e.type==="line"}var DR=I([Mm,OR],(e,t)=>e.filter(ER).find(r=>r.id===t)),TR=I([ue,JN,ek,QN,tk,DR,AR,bu],(e,t,r,n,i,s,o,l)=>{var{chartData:c,dataStartIndex:d,dataEndIndex:u}=l;if(!(s==null||t==null||r==null||n==null||i==null||n.length===0||i.length===0||o==null)){var{dataKey:f,data:p}=s,m;if(p!=null&&p.length>0?m=p:m=c==null?void 0:c.slice(d,u+1),m!=null)return QR({layout:e,xAxis:t,yAxis:r,xAxisTicks:n,yAxisTicks:i,dataKey:f,bandSize:o,displayedData:m})}});function rk(e){var t=qc(e),r=3,n=2;if(t!=null){var{r:i,strokeWidth:s}=t,o=Number(i),l=Number(s);return(Number.isNaN(o)||o<0)&&(o=r),(Number.isNaN(l)||l<0)&&(l=n),{r:o,strokeWidth:l}}return{r,strokeWidth:n}}var MR=["id"],IR=["type","layout","connectNulls","needClip","shape"],$R=["activeDot","animateNewValues","animationBegin","animationDuration","animationEasing","connectNulls","dot","hide","isAnimationActive","label","legendType","xAxisId","yAxisId","id"];function Hs(){return Hs=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{dataKey:t,name:r,stroke:n,legendType:i,hide:s}=e;return[{inactive:s,dataKey:t,type:i,color:n,value:au(r,t),payload:e}]};function WR(e){var{dataKey:t,data:r,stroke:n,strokeWidth:i,fill:s,name:o,hide:l,unit:c}=e;return{dataDefinedOnItem:r,positions:void 0,settings:{stroke:n,strokeWidth:i,fill:s,dataKey:t,nameKey:void 0,name:au(o,t),hide:l,type:e.tooltipType,color:e.stroke,unit:c}}}var nk=(e,t)=>"".concat(t,"px ").concat(e-t,"px");function UR(e,t){for(var r=e.length%2!==0?[...e,0]:e,n=[],i=0;i{var n=r.reduce((f,p)=>f+p);if(!n)return nk(t,e);for(var i=Math.floor(e/n),s=e%n,o=t-e,l=[],c=0,d=0;cs){l=[...r.slice(0,c),s-d];break}var u=l.length%2===0?[0,o]:[o];return[...UR(r,i),...l,...u].map(f=>"".concat(f,"px")).join(", ")};function HR(e){var{clipPathId:t,points:r,props:n}=e,{dot:i,dataKey:s,needClip:o}=n,{id:l}=n,c=sg(n,MR),d=nr(c);return h.createElement(qN,{points:r,dot:i,className:"recharts-line-dots",dotClassName:"recharts-line-dot",dataKey:s,baseProps:d,needClip:o,clipPathId:t})}function KR(e){var{showLabels:t,children:r,points:n}=e,i=h.useMemo(()=>n==null?void 0:n.map(s=>{var o,l,c={x:(o=s.x)!==null&&o!==void 0?o:0,y:(l=s.y)!==null&&l!==void 0?l:0,width:0,lowerWidth:0,upperWidth:0,height:0};return wr(wr({},c),{},{value:s.value,payload:s.payload,viewBox:c,parentViewBox:void 0,fill:void 0})}),[n]);return h.createElement(DN,{value:t?i:void 0},r)}function pv(e){var{clipPathId:t,pathRef:r,points:n,strokeDasharray:i,props:s}=e,{type:o,layout:l,connectNulls:c,needClip:d,shape:u}=s,f=sg(s,IR),p=wr(wr({},ut(f)),{},{fill:"none",className:"recharts-line-curve",clipPath:d?"url(#clipPath-".concat(t,")"):void 0,points:n,type:o,layout:l,connectNulls:c,strokeDasharray:i??s.strokeDasharray});return h.createElement(h.Fragment,null,(n==null?void 0:n.length)>1&&h.createElement(U8,Hs({shapeType:"curve",option:u},p,{pathRef:r})),h.createElement(HR,{points:n,clipPathId:t,props:s}))}function VR(e){try{return e&&e.getTotalLength&&e.getTotalLength()||0}catch{return 0}}function YR(e){var{clipPathId:t,props:r,pathRef:n,previousPointsRef:i,longestAnimatedLengthRef:s}=e,{points:o,strokeDasharray:l,isAnimationActive:c,animationBegin:d,animationDuration:u,animationEasing:f,animateNewValues:p,width:m,height:x,onAnimationEnd:g,onAnimationStart:b}=r,v=i.current,j=pu(r,"recharts-line-"),[y,w]=h.useState(!1),S=!y,N=h.useCallback(()=>{typeof g=="function"&&g(),w(!1)},[g]),P=h.useCallback(()=>{typeof b=="function"&&b(),w(!0)},[b]),_=VR(n.current),T=s.current;return h.createElement(KR,{points:o,showLabels:S},r.children,h.createElement(fu,{animationId:j,begin:d,duration:u,isActive:c,easing:f,onAnimationEnd:N,onAnimationStart:P,key:j},$=>{var M=De(T,_+T,$),C=Math.min(M,_),R;if(c)if(l){var q="".concat(l).split(/[,\s]+/gim).map(D=>parseFloat(D));R=qR(C,_,q)}else R=nk(_,C);else R=l==null?void 0:String(l);if(v){var Z=v.length/o.length,E=$===1?o:o.map((D,O)=>{var k=Math.floor(O*Z);if(v[k]){var L=v[k];return wr(wr({},D),{},{x:De(L.x,D.x,$),y:De(L.y,D.y,$)})}return p?wr(wr({},D),{},{x:De(m*2,D.x,$),y:De(x/2,D.y,$)}):wr(wr({},D),{},{x:D.x,y:D.y})});return i.current=E,h.createElement(pv,{props:r,points:E,clipPathId:t,pathRef:n,strokeDasharray:R})}return $>0&&_>0&&(i.current=o,s.current=C),h.createElement(pv,{props:r,points:o,clipPathId:t,pathRef:n,strokeDasharray:R})}),h.createElement(MN,{label:r.label}))}function GR(e){var{clipPathId:t,props:r}=e,n=h.useRef(null),i=h.useRef(0),s=h.useRef(null);return h.createElement(YR,{props:r,clipPathId:t,previousPointsRef:n,longestAnimatedLengthRef:i,pathRef:s})}var ZR=(e,t)=>{var r,n;return{x:(r=e.x)!==null&&r!==void 0?r:void 0,y:(n=e.y)!==null&&n!==void 0?n:void 0,value:e.value,errorVal:et(e.payload,t)}};class XR extends h.Component{render(){var{hide:t,dot:r,points:n,className:i,xAxisId:s,yAxisId:o,top:l,left:c,width:d,height:u,id:f,needClip:p,zIndex:m}=this.props;if(t)return null;var x=ce("recharts-line",i),g=f,{r:b,strokeWidth:v}=rk(r),j=eg(r),y=b*2+v;return h.createElement(Ir,{zIndex:m},h.createElement(ir,{className:x},p&&h.createElement("defs",null,h.createElement(VN,{clipPathId:g,xAxisId:s,yAxisId:o}),!j&&h.createElement("clipPath",{id:"clipPath-dots-".concat(g)},h.createElement("rect",{x:c-y/2,y:l-y/2,width:d+y,height:u+y}))),h.createElement(Oz,{xAxisId:s,yAxisId:o,data:n,dataPointFormatter:ZR,errorBarOffset:0},h.createElement(GR,{props:this.props,clipPathId:g}))),h.createElement(xp,{activeDot:this.props.activeDot,points:n,mainColor:this.props.stroke,itemDataKey:this.props.dataKey}))}}var ik={activeDot:!0,animateNewValues:!0,animationBegin:0,animationDuration:1500,animationEasing:"ease",connectNulls:!1,dot:!0,fill:"#fff",hide:!1,isAnimationActive:!Ci.isSsr,label:!1,legendType:"line",stroke:"#3182bd",strokeWidth:1,xAxisId:0,yAxisId:0,zIndex:lt.line};function JR(e){var t=ft(e,ik),{activeDot:r,animateNewValues:n,animationBegin:i,animationDuration:s,animationEasing:o,connectNulls:l,dot:c,hide:d,isAnimationActive:u,label:f,legendType:p,xAxisId:m,yAxisId:x,id:g}=t,b=sg(t,$R),{needClip:v}=tg(m,x),j=Mu(),y=to(),w=pt(),S=G($=>TR($,m,x,w,g));if(y!=="horizontal"&&y!=="vertical"||S==null||j==null)return null;var{height:N,width:P,x:_,y:T}=j;return h.createElement(XR,Hs({},b,{id:g,connectNulls:l,dot:c,activeDot:r,animateNewValues:n,animationBegin:i,animationDuration:s,animationEasing:o,isAnimationActive:u,hide:d,label:f,legendType:p,xAxisId:m,yAxisId:x,points:S,layout:y,height:N,width:P,left:_,top:T,needClip:v}))}function QR(e){var{layout:t,xAxis:r,yAxis:n,xAxisTicks:i,yAxisTicks:s,dataKey:o,bandSize:l,displayedData:c}=e;return c.map((d,u)=>{var f=et(d,o);if(t==="horizontal"){var p=Yl({axis:r,ticks:i,bandSize:l,entry:d,index:u}),m=Re(f)?null:n.scale(f);return{x:p,y:m,value:f,payload:d}}var x=Re(f)?null:r.scale(f),g=Yl({axis:n,ticks:s,bandSize:l,entry:d,index:u});return x==null||g==null?null:{x,y:g,value:f,payload:d}}).filter(Boolean)}function e9(e){var t=ft(e,ik),r=pt();return h.createElement(FN,{id:t.id,type:"line"},n=>h.createElement(h.Fragment,null,h.createElement(RN,{legendPayload:FR(t)}),h.createElement(zN,{fn:WR,args:t}),h.createElement(UN,{type:"line",id:n,data:t.data,xAxisId:t.xAxisId,yAxisId:t.yAxisId,zAxisId:0,dataKey:t.dataKey,hide:t.hide,isPanorama:r}),h.createElement(JR,Hs({},t,{id:n}))))}var ak=h.memo(e9);ak.displayName="Line";var sk=(e,t,r,n)=>Du(e,"xAxis",t,n),ok=(e,t,r,n)=>Eu(e,"xAxis",t,n),lk=(e,t,r,n)=>Du(e,"yAxis",r,n),ck=(e,t,r,n)=>Eu(e,"yAxis",r,n),t9=I([ue,sk,lk,ok,ck],(e,t,r,n,i)=>Mr(e,"xAxis")?ha(t,n,!1):ha(r,i,!1)),r9=(e,t,r,n,i)=>i,uk=I([Mm,r9],(e,t)=>e.filter(r=>r.type==="area").find(r=>r.id===t)),n9=(e,t,r,n,i)=>{var s,o=uk(e,t,r,n,i);if(o!=null){var l=ue(e),c=Mr(l,"xAxis"),d;if(c?d=cp(e,"yAxis",r,n):d=cp(e,"xAxis",t,n),d!=null){var{stackId:u}=o,f=Em(o);if(!(u==null||f==null)){var p=(s=d[u])===null||s===void 0?void 0:s.stackedData;return p==null?void 0:p.find(m=>m.key===f)}}}},i9=I([ue,sk,lk,ok,ck,n9,bu,t9,uk,xM],(e,t,r,n,i,s,o,l,c,d)=>{var{chartData:u,dataStartIndex:f,dataEndIndex:p}=o;if(!(c==null||e!=="horizontal"&&e!=="vertical"||t==null||r==null||n==null||i==null||n.length===0||i.length===0||l==null)){var{data:m}=c,x;if(m&&m.length>0?x=m:x=u==null?void 0:u.slice(f,p+1),x!=null)return S9({layout:e,xAxis:t,yAxis:r,xAxisTicks:n,yAxisTicks:i,dataStartIndex:f,areaSettings:c,stackedData:s,displayedData:x,chartBaseValue:d,bandSize:l})}}),a9=["id"],s9=["activeDot","animationBegin","animationDuration","animationEasing","connectNulls","dot","fill","fillOpacity","hide","isAnimationActive","legendType","stroke","xAxisId","yAxisId"];function fi(){return fi=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{dataKey:t,name:r,stroke:n,fill:i,legendType:s,hide:o}=e;return[{inactive:o,dataKey:t,type:s,color:bc(n,i),value:au(r,t),payload:e}]};function f9(e){var{dataKey:t,data:r,stroke:n,strokeWidth:i,fill:s,name:o,hide:l,unit:c}=e;return{dataDefinedOnItem:r,positions:void 0,settings:{stroke:n,strokeWidth:i,fill:s,dataKey:t,nameKey:void 0,name:au(o,t),hide:l,type:e.tooltipType,color:bc(n,s),unit:c}}}function p9(e){var{clipPathId:t,points:r,props:n}=e,{needClip:i,dot:s,dataKey:o}=n,l=nr(n);return h.createElement(qN,{points:r,dot:s,className:"recharts-area-dots",dotClassName:"recharts-area-dot",dataKey:o,baseProps:l,needClip:i,clipPathId:t})}function h9(e){var{showLabels:t,children:r,points:n}=e,i=n.map(s=>{var o,l,c={x:(o=s.x)!==null&&o!==void 0?o:0,y:(l=s.y)!==null&&l!==void 0?l:0,width:0,lowerWidth:0,upperWidth:0,height:0};return Zi(Zi({},c),{},{value:s.value,payload:s.payload,parentViewBox:void 0,viewBox:c,fill:void 0})});return h.createElement(DN,{value:t?i:void 0},r)}function mv(e){var{points:t,baseLine:r,needClip:n,clipPathId:i,props:s}=e,{layout:o,type:l,stroke:c,connectNulls:d,isRange:u}=s,{id:f}=s,p=dk(s,a9),m=nr(p),x=ut(p);return h.createElement(h.Fragment,null,(t==null?void 0:t.length)>1&&h.createElement(ir,{clipPath:n?"url(#clipPath-".concat(i,")"):void 0},h.createElement(us,fi({},x,{id:f,points:t,connectNulls:d,type:l,baseLine:r,layout:o,stroke:"none",className:"recharts-area-area"})),c!=="none"&&h.createElement(us,fi({},m,{className:"recharts-area-curve",layout:o,type:l,connectNulls:d,fill:"none",points:t})),c!=="none"&&u&&h.createElement(us,fi({},m,{className:"recharts-area-curve",layout:o,type:l,connectNulls:d,fill:"none",points:r}))),h.createElement(p9,{points:t,props:p,clipPathId:i}))}function m9(e){var{alpha:t,baseLine:r,points:n,strokeWidth:i}=e,s=n[0].y,o=n[n.length-1].y;if(!_e(s)||!_e(o))return null;var l=t*Math.abs(s-o),c=Math.max(...n.map(d=>d.x||0));return Y(r)?c=Math.max(r,c):r&&Array.isArray(r)&&r.length&&(c=Math.max(...r.map(d=>d.x||0),c)),Y(c)?h.createElement("rect",{x:0,y:sd.y||0));return Y(r)?c=Math.max(r,c):r&&Array.isArray(r)&&r.length&&(c=Math.max(...r.map(d=>d.y||0),c)),Y(c)?h.createElement("rect",{x:s{typeof m=="function"&&m(),b(!1)},[m]),y=h.useCallback(()=>{typeof p=="function"&&p(),b(!0)},[p]),w=i.current,S=s.current;return h.createElement(h9,{showLabels:v,points:o},n.children,h.createElement(fu,{animationId:x,begin:d,duration:u,isActive:c,easing:f,onAnimationEnd:j,onAnimationStart:y,key:x},N=>{if(w){var P=w.length/o.length,_=N===1?o:o.map(($,M)=>{var C=Math.floor(M*P);if(w[C]){var R=w[C];return Zi(Zi({},$),{},{x:De(R.x,$.x,N),y:De(R.y,$.y,N)})}return $}),T;return Y(l)?T=De(S,l,N):Re(l)||yr(l)?T=De(S,0,N):T=l.map(($,M)=>{var C=Math.floor(M*P);if(Array.isArray(S)&&S[C]){var R=S[C];return Zi(Zi({},$),{},{x:De(R.x,$.x,N),y:De(R.y,$.y,N)})}return $}),N>0&&(i.current=_,s.current=T),h.createElement(mv,{points:_,baseLine:T,needClip:t,clipPathId:r,props:n})}return N>0&&(i.current=o,s.current=l),h.createElement(ir,null,c&&h.createElement("defs",null,h.createElement("clipPath",{id:"animationClipPath-".concat(r)},h.createElement(x9,{alpha:N,points:o,baseLine:l,layout:n.layout,strokeWidth:n.strokeWidth}))),h.createElement(ir,{clipPath:"url(#animationClipPath-".concat(r,")")},h.createElement(mv,{points:o,baseLine:l,needClip:t,clipPathId:r,props:n})))}),h.createElement(MN,{label:n.label}))}function v9(e){var{needClip:t,clipPathId:r,props:n}=e,i=h.useRef(null),s=h.useRef();return h.createElement(y9,{needClip:t,clipPathId:r,props:n,previousPointsRef:i,previousBaselineRef:s})}class b9 extends h.PureComponent{render(){var{hide:t,dot:r,points:n,className:i,top:s,left:o,needClip:l,xAxisId:c,yAxisId:d,width:u,height:f,id:p,baseLine:m,zIndex:x}=this.props;if(t)return null;var g=ce("recharts-area",i),b=p,{r:v,strokeWidth:j}=rk(r),y=eg(r),w=v*2+j;return h.createElement(Ir,{zIndex:x},h.createElement(ir,{className:g},l&&h.createElement("defs",null,h.createElement(VN,{clipPathId:b,xAxisId:c,yAxisId:d}),!y&&h.createElement("clipPath",{id:"clipPath-dots-".concat(b)},h.createElement("rect",{x:o-w/2,y:s-w/2,width:u+w,height:f+w}))),h.createElement(v9,{needClip:l,clipPathId:b,props:this.props})),h.createElement(xp,{points:n,mainColor:bc(this.props.stroke,this.props.fill),itemDataKey:this.props.dataKey,activeDot:this.props.activeDot}),this.props.isRange&&Array.isArray(m)&&h.createElement(xp,{points:m,mainColor:bc(this.props.stroke,this.props.fill),itemDataKey:this.props.dataKey,activeDot:this.props.activeDot}))}}var fk={activeDot:!0,animationBegin:0,animationDuration:1500,animationEasing:"ease",connectNulls:!1,dot:!1,fill:"#3182bd",fillOpacity:.6,hide:!1,isAnimationActive:!Ci.isSsr,legendType:"line",stroke:"#3182bd",xAxisId:0,yAxisId:0,zIndex:lt.area};function j9(e){var t,r=ft(e,fk),{activeDot:n,animationBegin:i,animationDuration:s,animationEasing:o,connectNulls:l,dot:c,fill:d,fillOpacity:u,hide:f,isAnimationActive:p,legendType:m,stroke:x,xAxisId:g,yAxisId:b}=r,v=dk(r,s9),j=to(),y=oN(),{needClip:w}=tg(g,b),S=pt(),{points:N,isRange:P,baseLine:_}=(t=G(q=>i9(q,g,b,S,e.id)))!==null&&t!==void 0?t:{},T=Mu();if(j!=="horizontal"&&j!=="vertical"||T==null||y!=="AreaChart"&&y!=="ComposedChart")return null;var{height:$,width:M,x:C,y:R}=T;return!N||!N.length?null:h.createElement(b9,fi({},v,{activeDot:n,animationBegin:i,animationDuration:s,animationEasing:o,baseLine:_,connectNulls:l,dot:c,fill:d,fillOpacity:u,height:$,hide:f,layout:j,isAnimationActive:p,isRange:P,legendType:m,needClip:w,points:N,stroke:x,width:M,left:C,top:R,xAxisId:g,yAxisId:b}))}var w9=(e,t,r,n,i)=>{var s=r??t;if(Y(s))return s;var o=e==="horizontal"?i:n,l=o.scale.domain();if(o.type==="number"){var c=Math.max(l[0],l[1]),d=Math.min(l[0],l[1]);return s==="dataMin"?d:s==="dataMax"||c<0?c:Math.max(Math.min(l[0],l[1]),0)}return s==="dataMin"?l[0]:s==="dataMax"?l[1]:l[0]};function S9(e){var{areaSettings:{connectNulls:t,baseValue:r,dataKey:n},stackedData:i,layout:s,chartBaseValue:o,xAxis:l,yAxis:c,displayedData:d,dataStartIndex:u,xAxisTicks:f,yAxisTicks:p,bandSize:m}=e,x=i&&i.length,g=w9(s,o,r,l,c),b=s==="horizontal",v=!1,j=d.map((w,S)=>{var N;x?N=i[u+S]:(N=et(w,n),Array.isArray(N)?v=!0:N=[g,N]);var P=N[1]==null||x&&!t&&et(w,n)==null;return b?{x:Yl({axis:l,ticks:f,bandSize:m,entry:w,index:S}),y:P?null:c.scale(N[1]),value:N,payload:w}:{x:P?null:l.scale(N[1]),y:Yl({axis:c,ticks:p,bandSize:m,entry:w,index:S}),value:N,payload:w}}),y;return x||v?y=j.map(w=>{var S=Array.isArray(w.value)?w.value[0]:null;return b?{x:w.x,y:S!=null&&w.y!=null?c.scale(S):null,payload:w.payload}:{x:S!=null?l.scale(S):null,y:w.y,payload:w.payload}}):y=b?c.scale(g):l.scale(g),{points:j,baseLine:y,isRange:v}}function N9(e){var t=ft(e,fk),r=pt();return h.createElement(FN,{id:t.id,type:"area"},n=>h.createElement(h.Fragment,null,h.createElement(RN,{legendPayload:d9(t)}),h.createElement(zN,{fn:f9,args:t}),h.createElement(UN,{type:"area",id:n,data:t.data,dataKey:t.dataKey,xAxisId:t.xAxisId,yAxisId:t.yAxisId,zAxisId:0,stackId:CE(t.stackId),hide:t.hide,barSize:void 0,baseValue:t.baseValue,isPanorama:r,connectNulls:t.connectNulls}),h.createElement(j9,fi({},t,{id:n}))))}var pk=h.memo(N9);pk.displayName="Area";var k9=["dangerouslySetInnerHTML","ticks"],P9=["id"],_9=["domain"],C9=["domain"];function bp(){return bp=Object.assign?Object.assign.bind():function(e){for(var t=1;t(t(uz(e)),()=>{t(dz(e))}),[e,t]),null}var E9=e=>{var{xAxisId:t,className:r}=e,n=G(Dw),i=pt(),s="xAxis",o=G(b=>Ea(b,s,t,i)),l=G(b=>R2(b,s,t,i)),c=G(b=>cI(b,t)),d=G(b=>mI(b,t)),u=G(b=>a2(b,t));if(c==null||d==null||u==null)return null;var{dangerouslySetInnerHTML:f,ticks:p}=e,m=jc(e,k9),{id:x}=u,g=jc(u,P9);return h.createElement(ag,bp({},m,g,{scale:o,x:d.x,y:d.y,width:c.width,height:c.height,className:ce("recharts-".concat(s," ").concat(s),r),viewBox:n,ticks:l,axisType:s}))},D9={allowDataOverflow:Tt.allowDataOverflow,allowDecimals:Tt.allowDecimals,allowDuplicatedCategory:Tt.allowDuplicatedCategory,height:Tt.height,hide:!1,mirror:Tt.mirror,orientation:Tt.orientation,padding:Tt.padding,reversed:Tt.reversed,scale:Tt.scale,tickCount:Tt.tickCount,type:Tt.type,xAxisId:0},T9=e=>{var t,r,n,i,s,o=ft(e,D9);return h.createElement(h.Fragment,null,h.createElement(O9,{interval:(t=o.interval)!==null&&t!==void 0?t:"preserveEnd",id:o.xAxisId,scale:o.scale,type:o.type,padding:o.padding,allowDataOverflow:o.allowDataOverflow,domain:o.domain,dataKey:o.dataKey,allowDuplicatedCategory:o.allowDuplicatedCategory,allowDecimals:o.allowDecimals,tickCount:o.tickCount,includeHidden:(r=o.includeHidden)!==null&&r!==void 0?r:!1,reversed:o.reversed,ticks:o.ticks,height:o.height,orientation:o.orientation,mirror:o.mirror,hide:o.hide,unit:o.unit,name:o.name,angle:(n=o.angle)!==null&&n!==void 0?n:0,minTickGap:(i=o.minTickGap)!==null&&i!==void 0?i:5,tick:(s=o.tick)!==null&&s!==void 0?s:!0,tickFormatter:o.tickFormatter}),h.createElement(E9,o))},M9=(e,t)=>{var{domain:r}=e,n=jc(e,_9),{domain:i}=t,s=jc(t,C9);return ya(n,s)?Array.isArray(r)&&r.length===2&&Array.isArray(i)&&i.length===2?r[0]===i[0]&&r[1]===i[1]:ya({domain:r},{domain:i}):!1},jp=h.memo(T9,M9);jp.displayName="XAxis";var I9=["dangerouslySetInnerHTML","ticks"],$9=["id"],L9=["domain"],z9=["domain"];function wp(){return wp=Object.assign?Object.assign.bind():function(e){for(var t=1;t(t(fz(e)),()=>{t(pz(e))}),[e,t]),null}var F9=e=>{var{yAxisId:t,className:r,width:n,label:i}=e,s=h.useRef(null),o=h.useRef(null),l=G(Dw),c=pt(),d=Ve(),u="yAxis",f=G(S=>Ea(S,u,t,c)),p=G(S=>yI(S,t)),m=G(S=>xI(S,t)),x=G(S=>R2(S,u,t,c)),g=G(S=>s2(S,t));if(h.useLayoutEffect(()=>{if(!(n!=="auto"||!p||Qm(i)||h.isValidElement(i)||g==null)){var S=s.current;if(S){var N=S.getCalculatedWidth();Math.round(p.width)!==Math.round(N)&&d(hz({id:t,width:N}))}}},[x,p,d,i,t,n,g]),p==null||m==null||g==null)return null;var{dangerouslySetInnerHTML:b,ticks:v}=e,j=wc(e,I9),{id:y}=g,w=wc(g,$9);return h.createElement(ag,wp({},j,w,{ref:s,labelRef:o,scale:f,x:m.x,y:m.y,tickTextProps:n==="auto"?{width:void 0}:{width:n},width:p.width,height:p.height,className:ce("recharts-".concat(u," ").concat(u),r),viewBox:l,ticks:x,axisType:u}))},W9={allowDataOverflow:Mt.allowDataOverflow,allowDecimals:Mt.allowDecimals,allowDuplicatedCategory:Mt.allowDuplicatedCategory,hide:!1,mirror:Mt.mirror,orientation:Mt.orientation,padding:Mt.padding,reversed:Mt.reversed,scale:Mt.scale,tickCount:Mt.tickCount,type:Mt.type,width:Mt.width,yAxisId:0},U9=e=>{var t,r,n,i,s,o=ft(e,W9);return h.createElement(h.Fragment,null,h.createElement(B9,{interval:(t=o.interval)!==null&&t!==void 0?t:"preserveEnd",id:o.yAxisId,scale:o.scale,type:o.type,domain:o.domain,allowDataOverflow:o.allowDataOverflow,dataKey:o.dataKey,allowDuplicatedCategory:o.allowDuplicatedCategory,allowDecimals:o.allowDecimals,tickCount:o.tickCount,padding:o.padding,includeHidden:(r=o.includeHidden)!==null&&r!==void 0?r:!1,reversed:o.reversed,ticks:o.ticks,width:o.width,orientation:o.orientation,mirror:o.mirror,hide:o.hide,unit:o.unit,name:o.name,angle:(n=o.angle)!==null&&n!==void 0?n:0,minTickGap:(i=o.minTickGap)!==null&&i!==void 0?i:5,tick:(s=o.tick)!==null&&s!==void 0?s:!0,tickFormatter:o.tickFormatter}),h.createElement(F9,o))},q9=(e,t)=>{var{domain:r}=e,n=wc(e,L9),{domain:i}=t,s=wc(t,z9);return ya(n,s)?Array.isArray(r)&&r.length===2&&Array.isArray(i)&&i.length===2?r[0]===i[0]&&r[1]===i[1]:ya({domain:r},{domain:i}):!1},Sp=h.memo(U9,q9);Sp.displayName="YAxis";var H9={};/** + * @license React + * use-sync-external-store-with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var po=h;function K9(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var V9=typeof Object.is=="function"?Object.is:K9,Y9=po.useSyncExternalStore,G9=po.useRef,Z9=po.useEffect,X9=po.useMemo,J9=po.useDebugValue;H9.useSyncExternalStoreWithSelector=function(e,t,r,n,i){var s=G9(null);if(s.current===null){var o={hasValue:!1,value:null};s.current=o}else o=s.current;s=X9(function(){function c(m){if(!d){if(d=!0,u=m,m=n(m),i!==void 0&&o.hasValue){var x=o.value;if(i(x,m))return f=x}return f=m}if(x=f,V9(u,m))return x;var g=n(m);return i!==void 0&&i(x,g)?(u=m,x):(u=m,f=g)}var d=!1,u,f,p=r===void 0?null:r;return[function(){return c(t())},p===null?void 0:function(){return c(p())}]},[t,r,n,i]);var l=Y9(e,s[0],s[1]);return Z9(function(){o.hasValue=!0,o.value=l},[l]),J9(l),l};function Q9(e){e()}function eB(){let e=null,t=null;return{clear(){e=null,t=null},notify(){Q9(()=>{let r=e;for(;r;)r.callback(),r=r.next})},get(){const r=[];let n=e;for(;n;)r.push(n),n=n.next;return r},subscribe(r){let n=!0;const i=t={callback:r,next:null,prev:t};return i.prev?i.prev.next=i:e=i,function(){!n||e===null||(n=!1,i.next?i.next.prev=i.prev:t=i.prev,i.prev?i.prev.next=i.next:e=i.next)}}}}var gv={notify(){},get:()=>[]};function tB(e,t){let r,n=gv,i=0,s=!1;function o(g){u();const b=n.subscribe(g);let v=!1;return()=>{v||(v=!0,b(),f())}}function l(){n.notify()}function c(){x.onStateChange&&x.onStateChange()}function d(){return s}function u(){i++,r||(r=e.subscribe(c),n=eB())}function f(){i--,r&&i===0&&(r(),r=void 0,n.clear(),n=gv)}function p(){s||(s=!0,u())}function m(){s&&(s=!1,f())}const x={addNestedSub:o,notifyNestedSubs:l,handleChangeWrapper:c,isSubscribed:d,trySubscribe:p,tryUnsubscribe:m,getListeners:()=>n};return x}var rB=()=>typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",nB=rB(),iB=()=>typeof navigator<"u"&&navigator.product==="ReactNative",aB=iB(),sB=()=>nB||aB?h.useLayoutEffect:h.useEffect,oB=sB(),Cd=Symbol.for("react-redux-context"),Ad=typeof globalThis<"u"?globalThis:{};function lB(){if(!h.createContext)return{};const e=Ad[Cd]??(Ad[Cd]=new Map);let t=e.get(h.createContext);return t||(t=h.createContext(null),e.set(h.createContext,t)),t}var cB=lB();function uB(e){const{children:t,context:r,serverState:n,store:i}=e,s=h.useMemo(()=>{const c=tB(i);return{store:i,subscription:c,getServerState:n?()=>n:void 0}},[i,n]),o=h.useMemo(()=>i.getState(),[i]);oB(()=>{const{subscription:c}=s;return c.onStateChange=c.notifyNestedSubs,c.trySubscribe(),o!==i.getState()&&c.notifyNestedSubs(),()=>{c.tryUnsubscribe(),c.onStateChange=void 0}},[s,o]);const l=r||cB;return h.createElement(l.Provider,{value:s},t)}var dB=uB,fB=(e,t)=>t,og=I([fB,ue,n2,We,tN,pn,C$,rt],I$),lg=e=>{var t=e.currentTarget.getBoundingClientRect(),r=t.width/e.currentTarget.offsetWidth,n=t.height/e.currentTarget.offsetHeight;return{chartX:Math.round((e.clientX-t.left)/r),chartY:Math.round((e.clientY-t.top)/n)}},hk=ar("mouseClick"),mk=Qs();mk.startListening({actionCreator:hk,effect:(e,t)=>{var r=e.payload,n=og(t.getState(),lg(r));(n==null?void 0:n.activeIndex)!=null&&t.dispatch(OI({activeIndex:n.activeIndex,activeDataKey:void 0,activeCoordinate:n.activeCoordinate}))}});var Np=ar("mouseMove"),gk=Qs();gk.startListening({actionCreator:Np,effect:(e,t)=>{var r=e.payload,n=t.getState(),i=Um(n,n.tooltip.settings.shared),s=og(n,lg(r));i==="axis"&&((s==null?void 0:s.activeIndex)!=null?t.dispatch(K2({activeIndex:s.activeIndex,activeDataKey:void 0,activeCoordinate:s.activeCoordinate})):t.dispatch(H2()))}});var xv={accessibilityLayer:!0,barCategoryGap:"10%",barGap:4,barSize:void 0,className:void 0,maxBarSize:void 0,stackOffset:"none",syncId:void 0,syncMethod:"index",baseValue:void 0},xk=At({name:"rootProps",initialState:xv,reducers:{updateOptions:(e,t)=>{var r;e.accessibilityLayer=t.payload.accessibilityLayer,e.barCategoryGap=t.payload.barCategoryGap,e.barGap=(r=t.payload.barGap)!==null&&r!==void 0?r:xv.barGap,e.barSize=t.payload.barSize,e.maxBarSize=t.payload.maxBarSize,e.stackOffset=t.payload.stackOffset,e.syncId=t.payload.syncId,e.syncMethod=t.payload.syncMethod,e.className=t.payload.className,e.baseValue=t.payload.baseValue}}}),pB=xk.reducer,{updateOptions:hB}=xk.actions,yk=At({name:"polarOptions",initialState:null,reducers:{updatePolarOptions:(e,t)=>t.payload}}),{updatePolarOptions:Z7}=yk.actions,mB=yk.reducer,vk=ar("keyDown"),bk=ar("focus"),cg=Qs();cg.startListening({actionCreator:vk,effect:(e,t)=>{var r=t.getState(),n=r.rootProps.accessibilityLayer!==!1;if(n){var{keyboardInteraction:i}=r.tooltip,s=e.payload;if(!(s!=="ArrowRight"&&s!=="ArrowLeft"&&s!=="Enter")){var o=Number(qm(i,Ta(r))),l=pn(r);if(s==="Enter"){var c=pc(r,"axis","hover",String(i.index));t.dispatch(dp({active:!i.active,activeIndex:i.index,activeDataKey:i.dataKey,activeCoordinate:c}));return}var d=wI(r),u=d==="left-to-right"?1:-1,f=s==="ArrowRight"?1:-1,p=o+f*u;if(!(l==null||p>=l.length||p<0)){var m=pc(r,"axis","hover",String(p));t.dispatch(dp({active:!0,activeIndex:p.toString(),activeDataKey:void 0,activeCoordinate:m}))}}}}});cg.startListening({actionCreator:bk,effect:(e,t)=>{var r=t.getState(),n=r.rootProps.accessibilityLayer!==!1;if(n){var{keyboardInteraction:i}=r.tooltip;if(!i.active&&i.index==null){var s="0",o=pc(r,"axis","hover",String(s));t.dispatch(dp({activeDataKey:void 0,active:!0,activeIndex:s,activeCoordinate:o}))}}}});var Vt=ar("externalEvent"),jk=Qs();jk.startListening({actionCreator:Vt,effect:(e,t)=>{if(e.payload.handler!=null){var r=t.getState(),n={activeCoordinate:p$(r),activeDataKey:d$(r),activeIndex:Us(r),activeLabel:iN(r),activeTooltipIndex:Us(r),isTooltipActive:h$(r)};e.payload.handler(n,e.payload.reactEvent)}}});var gB=I([Da],e=>e.tooltipItemPayloads),xB=I([gB,uo,(e,t,r)=>t,(e,t,r)=>r],(e,t,r,n)=>{var i=e.find(l=>l.settings.dataKey===n);if(i!=null){var{positions:s}=i;if(s!=null){var o=t(s,r);return o}}}),wk=ar("touchMove"),Sk=Qs();Sk.startListening({actionCreator:wk,effect:(e,t)=>{var r=e.payload;if(!(r.touches==null||r.touches.length===0)){var n=t.getState(),i=Um(n,n.tooltip.settings.shared);if(i==="axis"){var s=og(n,lg({clientX:r.touches[0].clientX,clientY:r.touches[0].clientY,currentTarget:r.currentTarget}));(s==null?void 0:s.activeIndex)!=null&&t.dispatch(K2({activeIndex:s.activeIndex,activeDataKey:void 0,activeCoordinate:s.activeCoordinate}))}else if(i==="item"){var o,l=r.touches[0];if(document.elementFromPoint==null)return;var c=document.elementFromPoint(l.clientX,l.clientY);if(!c||!c.getAttribute)return;var d=c.getAttribute(IE),u=(o=c.getAttribute($E))!==null&&o!==void 0?o:void 0,f=xB(t.getState(),d,u);t.dispatch(AI({activeDataKey:u,activeIndex:d,activeCoordinate:f}))}}}});var yB=nw({brush:Dz,cartesianAxis:mz,chartData:dL,errorBars:Nz,graphicalItems:X8,layout:yE,legend:d3,options:sL,polarAxis:P8,polarOptions:mB,referenceElements:Rz,rootProps:pB,tooltip:EI,zIndex:G$}),vB=function(t){return U4({reducer:yB,preloadedState:t,middleware:r=>r({serializableCheck:!1}).concat([mk.middleware,gk.middleware,cg.middleware,jk.middleware,Sk.middleware]),enhancers:r=>{var n=r;return typeof r=="function"&&(n=r()),n.concat(mw({type:"raf"}))},devTools:Ci.devToolsEnabled})};function bB(e){var{preloadedState:t,children:r,reduxStoreName:n}=e,i=pt(),s=h.useRef(null);if(i)return r;s.current==null&&(s.current=vB(t));var o=Hh;return h.createElement(dB,{context:o,store:s.current},r)}function jB(e){var{layout:t,margin:r}=e,n=Ve(),i=pt();return h.useEffect(()=>{i||(n(mE(t)),n(hE(r)))},[n,i,t,r]),null}function wB(e){var t=Ve();return h.useEffect(()=>{t(hB(e))},[t,e]),null}function yv(e){var{zIndex:t,isPanorama:r}=e,n=r?"recharts-zindex-panorama-":"recharts-zindex-",i=BN("".concat(n).concat(t)),s=Ve();return h.useLayoutEffect(()=>(s(V$({zIndex:t,elementId:i,isPanorama:r})),()=>{s(Y$({zIndex:t,isPanorama:r}))}),[s,t,i,r]),h.createElement("g",{id:i})}function vv(e){var{children:t,isPanorama:r}=e,n=G(L$);if(!n||n.length===0)return t;var i=n.filter(o=>o<0),s=n.filter(o=>o>0);return h.createElement(h.Fragment,null,i.map(o=>h.createElement(yv,{key:o,zIndex:o,isPanorama:r})),t,s.map(o=>h.createElement(yv,{key:o,zIndex:o,isPanorama:r})))}var SB=["children"];function NB(e,t){if(e==null)return{};var r,n,i=kB(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var r=Fw(),n=Ww(),i=qw();if(!Er(r)||!Er(n))return null;var{children:s,otherAttributes:o,title:l,desc:c}=e,d,u;return o!=null&&(typeof o.tabIndex=="number"?d=o.tabIndex:d=i?0:void 0,typeof o.role=="string"?u=o.role:u=i?"application":void 0),h.createElement(lj,Sc({},o,{title:l,desc:c,role:u,tabIndex:d,width:r,height:n,style:PB,ref:t}),s)}),CB=e=>{var{children:t}=e,r=G(cu);if(!r)return null;var{width:n,height:i,y:s,x:o}=r;return h.createElement(lj,{width:n,height:i,x:o,y:s},t)},bv=h.forwardRef((e,t)=>{var{children:r}=e,n=NB(e,SB),i=pt();return i?h.createElement(CB,null,h.createElement(vv,{isPanorama:!0},r)):h.createElement(_B,Sc({ref:t},n),h.createElement(vv,{isPanorama:!1},r))});function AB(){var e=Ve(),[t,r]=h.useState(null),n=G(ME);return h.useEffect(()=>{if(t!=null){var i=t.getBoundingClientRect(),s=i.width/t.offsetWidth;_e(s)&&s!==n&&e(xE(s))}},[t,e,n]),r}function jv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function OB(e){for(var t=1;t(bL(),null);function Nc(e){if(typeof e=="number")return e;if(typeof e=="string"){var t=parseFloat(e);if(!Number.isNaN(t))return t}return 0}var IB=h.forwardRef((e,t)=>{var r,n,i=h.useRef(null),[s,o]=h.useState({containerWidth:Nc((r=e.style)===null||r===void 0?void 0:r.width),containerHeight:Nc((n=e.style)===null||n===void 0?void 0:n.height)}),l=h.useCallback((d,u)=>{o(f=>{var p=Math.round(d),m=Math.round(u);return f.containerWidth===p&&f.containerHeight===m?f:{containerWidth:p,containerHeight:m}})},[]),c=h.useCallback(d=>{if(typeof t=="function"&&t(d),d!=null&&typeof ResizeObserver<"u"){var{width:u,height:f}=d.getBoundingClientRect();l(u,f);var p=x=>{var{width:g,height:b}=x[0].contentRect;l(g,b)},m=new ResizeObserver(p);m.observe(d),i.current=m}},[t,l]);return h.useEffect(()=>()=>{var d=i.current;d!=null&&d.disconnect()},[l]),h.createElement(h.Fragment,null,h.createElement(du,{width:s.containerWidth,height:s.containerHeight}),h.createElement("div",Ni({ref:c},e)))}),$B=h.forwardRef((e,t)=>{var{width:r,height:n}=e,[i,s]=h.useState({containerWidth:Nc(r),containerHeight:Nc(n)}),o=h.useCallback((c,d)=>{s(u=>{var f=Math.round(c),p=Math.round(d);return u.containerWidth===f&&u.containerHeight===p?u:{containerWidth:f,containerHeight:p}})},[]),l=h.useCallback(c=>{if(typeof t=="function"&&t(c),c!=null){var{width:d,height:u}=c.getBoundingClientRect();o(d,u)}},[t,o]);return h.createElement(h.Fragment,null,h.createElement(du,{width:i.containerWidth,height:i.containerHeight}),h.createElement("div",Ni({ref:l},e)))}),LB=h.forwardRef((e,t)=>{var{width:r,height:n}=e;return h.createElement(h.Fragment,null,h.createElement(du,{width:r,height:n}),h.createElement("div",Ni({ref:t},e)))}),zB=h.forwardRef((e,t)=>{var{width:r,height:n}=e;return Qr(r)||Qr(n)?h.createElement($B,Ni({},e,{ref:t})):h.createElement(LB,Ni({},e,{ref:t}))});function RB(e){return e===!0?IB:zB}var BB=h.forwardRef((e,t)=>{var{children:r,className:n,height:i,onClick:s,onContextMenu:o,onDoubleClick:l,onMouseDown:c,onMouseEnter:d,onMouseLeave:u,onMouseMove:f,onMouseUp:p,onTouchEnd:m,onTouchMove:x,onTouchStart:g,style:b,width:v,responsive:j,dispatchTouchEvents:y=!0}=e,w=h.useRef(null),S=Ve(),[N,P]=h.useState(null),[_,T]=h.useState(null),$=AB(),M=Qh(),C=(M==null?void 0:M.width)>0?M.width:v,R=(M==null?void 0:M.height)>0?M.height:i,q=h.useCallback(z=>{$(z),typeof t=="function"&&t(z),P(z),T(z),z!=null&&(w.current=z)},[$,t,P,T]),Z=h.useCallback(z=>{S(hk(z)),S(Vt({handler:s,reactEvent:z}))},[S,s]),E=h.useCallback(z=>{S(Np(z)),S(Vt({handler:d,reactEvent:z}))},[S,d]),D=h.useCallback(z=>{S(H2()),S(Vt({handler:u,reactEvent:z}))},[S,u]),O=h.useCallback(z=>{S(Np(z)),S(Vt({handler:f,reactEvent:z}))},[S,f]),k=h.useCallback(()=>{S(bk())},[S]),L=h.useCallback(z=>{S(vk(z.key))},[S]),U=h.useCallback(z=>{S(Vt({handler:o,reactEvent:z}))},[S,o]),H=h.useCallback(z=>{S(Vt({handler:l,reactEvent:z}))},[S,l]),te=h.useCallback(z=>{S(Vt({handler:c,reactEvent:z}))},[S,c]),re=h.useCallback(z=>{S(Vt({handler:p,reactEvent:z}))},[S,p]),we=h.useCallback(z=>{S(Vt({handler:g,reactEvent:z}))},[S,g]),A=h.useCallback(z=>{y&&S(wk(z)),S(Vt({handler:x,reactEvent:z}))},[S,y,x]),J=h.useCallback(z=>{S(Vt({handler:m,reactEvent:z}))},[S,m]),Ot=RB(j);return h.createElement(pN.Provider,{value:N},h.createElement(a6.Provider,{value:_},h.createElement(Ot,{width:C??(b==null?void 0:b.width),height:R??(b==null?void 0:b.height),className:ce("recharts-wrapper",n),style:OB({position:"relative",cursor:"default",width:C,height:R},b),onClick:Z,onContextMenu:U,onDoubleClick:H,onFocus:k,onKeyDown:L,onMouseDown:te,onMouseEnter:E,onMouseLeave:D,onMouseMove:O,onMouseUp:re,onTouchEnd:J,onTouchMove:A,onTouchStart:we,ref:q},h.createElement(MB,null),r)))}),FB=["width","height","responsive","children","className","style","compact","title","desc"];function WB(e,t){if(e==null)return{};var r,n,i=UB(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var{width:r,height:n,responsive:i,children:s,className:o,style:l,compact:c,title:d,desc:u}=e,f=WB(e,FB),p=nr(f);return c?h.createElement(h.Fragment,null,h.createElement(du,{width:r,height:n}),h.createElement(bv,{otherAttributes:p,title:d,desc:u},s)):h.createElement(BB,{className:o,style:l,width:r,height:n,responsive:i??!1,onClick:e.onClick,onMouseLeave:e.onMouseLeave,onMouseEnter:e.onMouseEnter,onMouseMove:e.onMouseMove,onMouseDown:e.onMouseDown,onMouseUp:e.onMouseUp,onContextMenu:e.onContextMenu,onDoubleClick:e.onDoubleClick,onTouchStart:e.onTouchStart,onTouchMove:e.onTouchMove,onTouchEnd:e.onTouchEnd},h.createElement(bv,{otherAttributes:p,title:d,desc:u,ref:t},h.createElement(Fz,null,s)))});function kp(){return kp=Object.assign?Object.assign.bind():function(e){for(var t=1;th.createElement(Nk,{chartName:"LineChart",defaultTooltipEventType:"axis",validateTooltipEventTypes:VB,tooltipPayloadSearcher:mN,categoricalChartProps:e,ref:t})),GB=["axis"],ZB=h.forwardRef((e,t)=>h.createElement(Nk,{chartName:"AreaChart",defaultTooltipEventType:"axis",validateTooltipEventTypes:GB,tooltipPayloadSearcher:mN,categoricalChartProps:e,ref:t}));function XB(){var b,v,j,y,w,S,N,P,_,T,$,M,C,R,q,Z,E,D,O,k,L;const[e,t]=h.useState(null),[r,n]=h.useState(null),[i,s]=h.useState(!0),[o,l]=h.useState(0),[c,d]=h.useState(!1);h.useEffect(()=>{f(),u()},[]);const u=async()=>{try{const H=(await B.getChangeStats()).pending_count;l(H);const te=localStorage.getItem("dismissedPendingChangesCount"),re=te&&parseInt(te)>=H;d(H>0&&!re)}catch(U){console.error("Failed to load change stats:",U),d(!1)}},f=async()=>{try{const[U,H]=await Promise.all([B.getDashboardStats(),B.getDashboardActivity()]);t(U),n(H)}catch(U){console.error("Failed to load dashboard:",U)}finally{s(!1)}},p=()=>{localStorage.setItem("dismissedPendingChangesCount",o.toString()),d(!1)};if(i)return a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx(Xt,{className:"w-8 h-8 animate-spin text-gray-400"})})});const m=Math.round(((b=e==null?void 0:e.products)==null?void 0:b.with_images)/((v=e==null?void 0:e.products)==null?void 0:v.total)*100)||0;Math.round(((j=e==null?void 0:e.stores)==null?void 0:j.active)/((y=e==null?void 0:e.stores)==null?void 0:y.total)*100);const x=[{date:"Mon",products:120},{date:"Tue",products:145},{date:"Wed",products:132},{date:"Thu",products:178},{date:"Fri",products:195},{date:"Sat",products:210},{date:"Sun",products:((w=e==null?void 0:e.products)==null?void 0:w.total)||225}],g=[{time:"00:00",scrapes:5},{time:"04:00",scrapes:12},{time:"08:00",scrapes:18},{time:"12:00",scrapes:25},{time:"16:00",scrapes:30},{time:"20:00",scrapes:22},{time:"24:00",scrapes:((S=r==null?void 0:r.recent_scrapes)==null?void 0:S.length)||15}];return a.jsxs(X,{children:[c&&a.jsx("div",{className:"mb-6 bg-amber-50 border-l-4 border-amber-500 rounded-lg p-4",children:a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("div",{className:"flex items-center gap-3 flex-1",children:[a.jsx(nj,{className:"w-5 h-5 text-amber-600 flex-shrink-0"}),a.jsxs("div",{className:"flex-1",children:[a.jsxs("h3",{className:"text-sm font-semibold text-amber-900",children:[o," pending change",o!==1?"s":""," require review"]}),a.jsx("p",{className:"text-sm text-amber-700 mt-0.5",children:"Proposed changes to dispensary data are waiting for approval"})]})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("button",{className:"btn btn-sm bg-amber-600 hover:bg-amber-700 text-white border-none",children:"Review Changes"}),a.jsx("button",{onClick:p,className:"btn btn-sm btn-ghost text-amber-900 hover:bg-amber-100","aria-label":"Dismiss notification",children:a.jsx(ij,{className:"w-4 h-4"})})]})]})}),a.jsxs("div",{className:"space-y-8",children:[a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:"Dashboard"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Monitor your dispensary data aggregation"})]}),a.jsxs("button",{onClick:f,className:"inline-flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium text-gray-700",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-blue-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-green-600",children:[a.jsx(zn,{className:"w-3 h-3"}),a.jsx("span",{children:"12.5%"})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((P=(N=e==null?void 0:e.products)==null?void 0:N.total)==null?void 0:P.toLocaleString())||0}),a.jsxs("p",{className:"text-xs text-gray-500",children:[((_=e==null?void 0:e.products)==null?void 0:_.in_stock)||0," in stock"]})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-emerald-50 rounded-lg",children:a.jsx(Il,{className:"w-5 h-5 text-emerald-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-green-600",children:[a.jsx(zn,{className:"w-3 h-3"}),a.jsx("span",{children:"8.2%"})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Total Dispensaries"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((T=e==null?void 0:e.stores)==null?void 0:T.total)||0}),a.jsxs("p",{className:"text-xs text-gray-500",children:[(($=e==null?void 0:e.stores)==null?void 0:$.active)||0," active (crawlable)"]})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(rj,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-red-600",children:[a.jsx(FO,{className:"w-3 h-3"}),a.jsx("span",{children:"3.1%"})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Active Campaigns"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((M=e==null?void 0:e.campaigns)==null?void 0:M.active)||0}),a.jsxs("p",{className:"text-xs text-gray-500",children:[((C=e==null?void 0:e.campaigns)==null?void 0:C.total)||0," total campaigns"]})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-amber-50 rounded-lg",children:a.jsx(lO,{className:"w-5 h-5 text-amber-600"})}),a.jsxs("span",{className:"text-xs font-medium text-gray-600",children:[m,"%"]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Images Downloaded"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((q=(R=e==null?void 0:e.products)==null?void 0:R.with_images)==null?void 0:q.toLocaleString())||0}),a.jsx("div",{className:"mt-3",children:a.jsx("div",{className:"w-full bg-gray-100 rounded-full h-1.5",children:a.jsx("div",{className:"bg-amber-500 h-1.5 rounded-full transition-all",style:{width:`${m}%`}})})})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-cyan-50 rounded-lg",children:a.jsx(vO,{className:"w-5 h-5 text-cyan-600"})}),a.jsx(xr,{className:"w-4 h-4 text-gray-400"})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Clicks (24h)"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((E=(Z=e==null?void 0:e.clicks)==null?void 0:Z.clicks_24h)==null?void 0:E.toLocaleString())||0}),a.jsx("p",{className:"text-xs text-gray-500",children:"Last 24 hours"})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-indigo-50 rounded-lg",children:a.jsx(Cr,{className:"w-5 h-5 text-indigo-600"})}),a.jsx(Ds,{className:"w-4 h-4 text-gray-400"})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((D=e==null?void 0:e.brands)==null?void 0:D.total)||((O=e==null?void 0:e.products)==null?void 0:O.unique_brands)||0}),a.jsx("p",{className:"text-xs text-gray-500",children:"Unique brands tracked"})]})]})]}),a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"mb-6",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Product Growth"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Weekly product count trend"})]}),a.jsx(p0,{width:"100%",height:200,children:a.jsxs(ZB,{data:x,children:[a.jsx("defs",{children:a.jsxs("linearGradient",{id:"productGradient",x1:"0",y1:"0",x2:"0",y2:"1",children:[a.jsx("stop",{offset:"5%",stopColor:"#3b82f6",stopOpacity:.1}),a.jsx("stop",{offset:"95%",stopColor:"#3b82f6",stopOpacity:0})]})}),a.jsx(vp,{strokeDasharray:"3 3",stroke:"#f1f5f9"}),a.jsx(jp,{dataKey:"date",tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(Sp,{tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(By,{contentStyle:{backgroundColor:"#ffffff",border:"1px solid #e2e8f0",borderRadius:"8px",fontSize:"12px"}}),a.jsx(pk,{type:"monotone",dataKey:"products",stroke:"#3b82f6",strokeWidth:2,fill:"url(#productGradient)"})]})})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"mb-6",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Scrape Activity"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Scrapes over the last 24 hours"})]}),a.jsx(p0,{width:"100%",height:200,children:a.jsxs(YB,{data:g,children:[a.jsx(vp,{strokeDasharray:"3 3",stroke:"#f1f5f9"}),a.jsx(jp,{dataKey:"time",tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(Sp,{tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(By,{contentStyle:{backgroundColor:"#ffffff",border:"1px solid #e2e8f0",borderRadius:"8px",fontSize:"12px"}}),a.jsx(ak,{type:"monotone",dataKey:"scrapes",stroke:"#10b981",strokeWidth:2,dot:{fill:"#10b981",r:4}})]})})]})]}),a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200",children:[a.jsxs("div",{className:"px-6 py-4 border-b border-gray-200",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Recent Scrapes"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Latest data collection activities"})]}),a.jsx("div",{className:"divide-y divide-gray-100",children:((k=r==null?void 0:r.recent_scrapes)==null?void 0:k.length)>0?r.recent_scrapes.slice(0,5).map((U,H)=>a.jsx("div",{className:"px-6 py-4 hover:bg-gray-50 transition-colors",children:a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsx("p",{className:"text-sm font-medium text-gray-900 truncate",children:U.name}),a.jsx("p",{className:"text-xs text-gray-500 mt-1",children:new Date(U.last_scraped_at).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})})]}),a.jsx("div",{className:"ml-4 flex-shrink-0",children:a.jsxs("span",{className:"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700",children:[U.product_count," products"]})})]})},H)):a.jsxs("div",{className:"px-6 py-12 text-center",children:[a.jsx(Ds,{className:"w-8 h-8 text-gray-300 mx-auto mb-2"}),a.jsx("p",{className:"text-sm text-gray-500",children:"No recent scrapes"})]})})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200",children:[a.jsxs("div",{className:"px-6 py-4 border-b border-gray-200",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Recent Products"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Newly added to inventory"})]}),a.jsx("div",{className:"divide-y divide-gray-100",children:((L=r==null?void 0:r.recent_products)==null?void 0:L.length)>0?r.recent_products.slice(0,5).map((U,H)=>a.jsx("div",{className:"px-6 py-4 hover:bg-gray-50 transition-colors",children:a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsx("p",{className:"text-sm font-medium text-gray-900 truncate",children:U.name}),a.jsx("p",{className:"text-xs text-gray-500 mt-1",children:U.store_name})]}),U.price&&a.jsx("div",{className:"ml-4 flex-shrink-0",children:a.jsxs("span",{className:"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700",children:["$",U.price]})})]})},H)):a.jsxs("div",{className:"px-6 py-12 text-center",children:[a.jsx(Ct,{className:"w-8 h-8 text-gray-300 mx-auto mb-2"}),a.jsx("p",{className:"text-sm text-gray-500",children:"No recent products"})]})})]})]})]})]})}function JB(){const[e,t]=nA(),r=dt(),[n,i]=h.useState([]),[s,o]=h.useState([]),[l,c]=h.useState([]),[d,u]=h.useState(!1),[f,p]=h.useState(""),[m,x]=h.useState(""),[g,b]=h.useState(""),[v,j]=h.useState(""),[y,w]=h.useState(0),[S,N]=h.useState(0),P=50;h.useEffect(()=>{const k=e.get("store");k&&x(k),T()},[]),h.useEffect(()=>{m&&($(),_())},[f,m,g,v,S]);const _=async()=>{try{const k=await B.getCategoryTree(parseInt(m));c(k.categories||[])}catch(k){console.error("Failed to load categories:",k)}},T=async()=>{try{const k=await B.getStores();o(k.stores)}catch(k){console.error("Failed to load stores:",k)}},$=async()=>{u(!0);try{const k={limit:P,offset:S,store_id:m};f&&(k.search=f),g&&(k.category_id=g),v&&(k.in_stock=v);const L=await B.getProducts(k);i(L.products),w(L.total)}catch(k){console.error("Failed to load products:",k)}finally{u(!1)}},M=k=>{p(k),N(0)},C=k=>{x(k),b(""),N(0),p(""),t(k?{store:k}:{})},R=k=>{b(k),N(0)},q=(k,L=0)=>k.map(U=>a.jsxs("div",{style:{marginLeft:`${L*20}px`},children:[a.jsxs("button",{onClick:()=>R(U.id.toString()),style:{width:"100%",textAlign:"left",padding:"10px 15px",background:g===U.id.toString()?"#667eea":"transparent",color:g===U.id.toString()?"white":"#333",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:L===0?"600":"400",fontSize:L===0?"15px":"14px",marginBottom:"4px",transition:"all 0.2s"},onMouseEnter:H=>{g!==U.id.toString()&&(H.currentTarget.style.background="#f5f5f5")},onMouseLeave:H=>{g!==U.id.toString()&&(H.currentTarget.style.background="transparent")},children:[U.name," (",U.product_count||0,")"]}),U.children&&U.children.length>0&&q(U.children,L+1)]},U.id)),Z=l.find(k=>k.id.toString()===g),E=()=>{S+P{S>0&&N(Math.max(0,S-P))},O=s.find(k=>k.id.toString()===m);return a.jsx(X,{children:a.jsxs("div",{children:[a.jsx("h1",{style:{fontSize:"32px",marginBottom:"30px"},children:"Products"}),a.jsxs("div",{style:{background:"white",padding:"30px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"15px",fontSize:"18px",fontWeight:"600",color:"#333"},children:"Select a Store to View Products:"}),a.jsxs("select",{value:m,onChange:k=>C(k.target.value),style:{width:"100%",padding:"15px",border:"2px solid #667eea",borderRadius:"8px",fontSize:"16px",fontWeight:"500",cursor:"pointer",background:"white"},children:[a.jsx("option",{value:"",children:"-- Select a Store --"}),s.map(k=>a.jsx("option",{value:k.id,children:k.name},k.id))]})]}),m?a.jsxs(a.Fragment,{children:[a.jsxs("div",{style:{background:"#667eea",color:"white",padding:"20px",borderRadius:"8px",marginBottom:"20px",display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{children:[a.jsx("h2",{style:{margin:0,fontSize:"24px"},children:O==null?void 0:O.name}),Z&&a.jsx("div",{style:{marginTop:"8px",fontSize:"14px",opacity:.9},children:Z.name})]}),a.jsxs("div",{style:{fontSize:"18px",fontWeight:"bold"},children:[y," Products"]})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"280px 1fr",gap:"20px"},children:[a.jsx("div",{children:a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",position:"sticky",top:"20px"},children:[a.jsx("h3",{style:{margin:"0 0 16px 0",fontSize:"18px",fontWeight:"600",color:"#333"},children:"Categories"}),a.jsxs("button",{onClick:()=>R(""),style:{width:"100%",textAlign:"left",padding:"10px 15px",background:g===""?"#667eea":"transparent",color:g===""?"white":"#333",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600",fontSize:"15px",marginBottom:"12px"},children:["All Products (",y,")"]}),a.jsx("div",{style:{borderTop:"1px solid #eee",marginBottom:"12px"}}),q(l)]})}),a.jsxs("div",{children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"20px",display:"flex",gap:"15px",flexWrap:"wrap"},children:[a.jsx("input",{type:"text",placeholder:"Search products...",value:f,onChange:k=>M(k.target.value),style:{flex:"1",minWidth:"200px",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}}),a.jsxs("select",{value:v,onChange:k=>{j(k.target.value),N(0)},style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"",children:"All Products"}),a.jsx("option",{value:"true",children:"In Stock"}),a.jsx("option",{value:"false",children:"Out of Stock"})]})]}),d?a.jsx("div",{style:{textAlign:"center",padding:"40px"},children:"Loading..."}):a.jsxs(a.Fragment,{children:[a.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(250px, 1fr))",gap:"20px",marginBottom:"20px"},children:n.map(k=>a.jsx(QB,{product:k,onViewDetails:()=>r(`/products/${k.id}`)},k.id))}),n.length===0&&a.jsx("div",{style:{background:"white",padding:"40px",borderRadius:"8px",textAlign:"center",color:"#999"},children:"No products found"}),y>P&&a.jsxs("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",gap:"15px",marginTop:"30px"},children:[a.jsx("button",{onClick:D,disabled:S===0,style:{padding:"10px 20px",background:S===0?"#ddd":"#667eea",color:S===0?"#999":"white",border:"none",borderRadius:"6px",cursor:S===0?"not-allowed":"pointer"},children:"Previous"}),a.jsxs("span",{children:["Showing ",S+1," - ",Math.min(S+P,y)," of ",y]}),a.jsx("button",{onClick:E,disabled:S+P>=y,style:{padding:"10px 20px",background:S+P>=y?"#ddd":"#667eea",color:S+P>=y?"#999":"white",border:"none",borderRadius:"6px",cursor:S+P>=y?"not-allowed":"pointer"},children:"Next"})]})]})]})]})]}):a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"🏪"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"Please select a store from the dropdown above to view products"})]})]})})}function QB({product:e,onViewDetails:t}){const r=n=>{if(!n)return"Never";const i=new Date(n),o=new Date().getTime()-i.getTime(),l=Math.floor(o/(1e3*60*60*24));return l===0?"Today":l===1?"Yesterday":l<7?`${l} days ago`:i.toLocaleDateString()};return a.jsxs("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden",transition:"transform 0.2s"},onMouseEnter:n=>n.currentTarget.style.transform="translateY(-4px)",onMouseLeave:n=>n.currentTarget.style.transform="translateY(0)",children:[e.image_url_full?a.jsx("img",{src:e.image_url_full,alt:e.name,style:{width:"100%",height:"200px",objectFit:"cover",background:"#f5f5f5"},onError:n=>{n.currentTarget.src='data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="200" height="200"%3E%3Crect fill="%23ddd" width="200" height="200"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23999"%3ENo Image%3C/text%3E%3C/svg%3E'}}):a.jsx("div",{style:{width:"100%",height:"200px",background:"#f5f5f5",display:"flex",alignItems:"center",justifyContent:"center",color:"#999"},children:"No Image"}),a.jsxs("div",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"500",marginBottom:"8px",fontSize:"14px",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.name}),a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"8px"},children:[a.jsx("div",{style:{fontWeight:"bold",color:"#667eea"},children:e.price?`$${e.price}`:"N/A"}),a.jsx("div",{style:{padding:"4px 8px",borderRadius:"4px",fontSize:"12px",background:e.in_stock?"#d4edda":"#f8d7da",color:e.in_stock?"#155724":"#721c24"},children:e.in_stock?"In Stock":"Out of Stock"})]}),a.jsxs("div",{style:{fontSize:"11px",color:"#888",marginBottom:"12px",borderTop:"1px solid #eee",paddingTop:"8px"},children:["Last Updated: ",r(e.last_seen_at)]}),a.jsxs("div",{style:{display:"flex",gap:"8px"},children:[e.dutchie_url&&a.jsx("a",{href:e.dutchie_url,target:"_blank",rel:"noopener noreferrer",style:{flex:1,padding:"8px 12px",background:"#f0f0f0",color:"#333",textDecoration:"none",borderRadius:"6px",fontSize:"12px",fontWeight:"500",textAlign:"center",border:"1px solid #ddd"},onClick:n=>n.stopPropagation(),children:"Dutchie"}),a.jsx("button",{onClick:n=>{n.stopPropagation(),t()},style:{flex:1,padding:"8px 12px",background:"#667eea",color:"white",border:"none",borderRadius:"6px",fontSize:"12px",fontWeight:"500",cursor:"pointer"},children:"Details"})]})]})]})}function e7(){const{id:e}=Na(),t=dt(),[r,n]=h.useState(null),[i,s]=h.useState(!0),[o,l]=h.useState(null);h.useEffect(()=>{c()},[e]);const c=async()=>{if(e){s(!0),l(null);try{const p=await B.getProduct(parseInt(e));n(p.product)}catch(p){l(p.message||"Failed to load product")}finally{s(!1)}}};if(i)return a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})});if(o||!r)return a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx(Ct,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Product not found"}),a.jsx("p",{className:"text-gray-500 mb-4",children:o}),a.jsx("button",{onClick:()=>t(-1),className:"text-blue-600 hover:text-blue-700",children:"← Go back"})]})});const d=r.metadata||{},f=r.image_url_full?r.image_url_full:r.medium_path?`http://localhost:9020/dutchie/${r.medium_path}`:r.thumbnail_path?`http://localhost:9020/dutchie/${r.thumbnail_path}`:null;return a.jsx(X,{children:a.jsxs("div",{className:"max-w-6xl mx-auto",children:[a.jsxs("button",{onClick:()=>t(-1),className:"flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6",children:[a.jsx(_h,{className:"w-4 h-4"}),"Back"]}),a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 overflow-hidden",children:a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-8 p-6",children:[a.jsx("div",{className:"aspect-square bg-gray-50 rounded-lg overflow-hidden",children:f?a.jsx("img",{src:f,alt:r.name,className:"w-full h-full object-contain"}):a.jsx("div",{className:"w-full h-full flex items-center justify-center text-gray-400",children:a.jsx(Ct,{className:"w-24 h-24"})})}),a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[r.in_stock?a.jsx("span",{className:"px-2 py-1 bg-green-100 text-green-700 text-xs font-medium rounded",children:"In Stock"}):a.jsx("span",{className:"px-2 py-1 bg-red-100 text-red-700 text-xs font-medium rounded",children:"Out of Stock"}),r.strain_type&&a.jsx("span",{className:"px-2 py-1 bg-purple-100 text-purple-700 text-xs font-medium rounded capitalize",children:r.strain_type})]}),a.jsx("h1",{className:"text-2xl font-bold text-gray-900 mb-2",children:r.name}),r.brand&&a.jsx("p",{className:"text-lg text-gray-600 font-medium",children:r.brand}),a.jsxs("div",{className:"flex items-center gap-4 mt-2 text-sm text-gray-500",children:[r.store_name&&a.jsx("span",{children:r.store_name}),r.category_name&&a.jsxs(a.Fragment,{children:[a.jsx("span",{children:"•"}),a.jsx("span",{children:r.category_name})]})]})]}),r.price!==null&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsxs("div",{className:"text-3xl font-bold text-blue-600",children:["$",parseFloat(r.price).toFixed(2)]}),r.weight&&a.jsx("div",{className:"text-sm text-gray-500 mt-1",children:r.weight})]}),(r.thc_percentage||r.cbd_percentage)&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-3",children:"Cannabinoid Content"}),a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[r.thc_percentage!==null&&a.jsxs("div",{className:"bg-green-50 rounded-lg p-3",children:[a.jsx("div",{className:"text-xs text-gray-500 uppercase",children:"THC"}),a.jsxs("div",{className:"text-xl font-bold text-green-600",children:[r.thc_percentage,"%"]})]}),r.cbd_percentage!==null&&a.jsxs("div",{className:"bg-blue-50 rounded-lg p-3",children:[a.jsx("div",{className:"text-xs text-gray-500 uppercase",children:"CBD"}),a.jsxs("div",{className:"text-xl font-bold text-blue-600",children:[r.cbd_percentage,"%"]})]})]})]}),r.description&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Description"}),a.jsx("p",{className:"text-gray-600 text-sm leading-relaxed",children:r.description})]}),d.terpenes&&d.terpenes.length>0&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Terpenes"}),a.jsx("div",{className:"flex flex-wrap gap-2",children:d.terpenes.map(p=>a.jsx("span",{className:"px-2 py-1 bg-amber-100 text-amber-700 text-xs font-medium rounded",children:p},p))})]}),d.effects&&d.effects.length>0&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Effects"}),a.jsx("div",{className:"flex flex-wrap gap-2",children:d.effects.map(p=>a.jsx("span",{className:"px-2 py-1 bg-indigo-100 text-indigo-700 text-xs font-medium rounded",children:p},p))})]}),d.flavors&&d.flavors.length>0&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Flavors"}),a.jsx("div",{className:"flex flex-wrap gap-2",children:d.flavors.map(p=>a.jsx("span",{className:"px-2 py-1 bg-pink-100 text-pink-700 text-xs font-medium rounded",children:p},p))})]}),d.lineage&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Lineage"}),a.jsx("p",{className:"text-gray-600 text-sm",children:d.lineage})]}),r.dutchie_url&&a.jsx("div",{className:"border-t border-gray-100 pt-4",children:a.jsxs("a",{href:r.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700",children:["View on Dutchie",a.jsx(Jr,{className:"w-4 h-4"})]})}),r.last_seen_at&&a.jsxs("div",{className:"text-xs text-gray-400 pt-4 border-t border-gray-100",children:["Last updated: ",new Date(r.last_seen_at).toLocaleString()]})]})]})})]})})}function t7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(new Set),[o,l]=h.useState(new Set),c=dt();h.useEffect(()=>{d()},[]);const d=async()=>{n(!0);try{const v=await B.getStores();t(v.stores)}catch(v){console.error("Failed to load stores:",v)}finally{n(!1)}},u=v=>{const j=v.match(/(?:^|-)(al|ak|az|ar|ca|co|ct|de|fl|ga|hi|id|il|in|ia|ks|ky|la|me|md|ma|mi|mn|ms|mo|mt|ne|nv|nh|nj|nm|ny|nc|nd|oh|ok|or|pa|ri|sc|sd|tn|tx|ut|vt|va|wa|wv|wi|wy)-/i),y={peoria:"AZ"};let w=j?j[1].toUpperCase():null;if(!w){for(const[S,N]of Object.entries(y))if(v.toLowerCase().includes(S)){w=N;break}}return w||"UNKNOWN"},f=v=>{const j=u(v.slug).toLowerCase(),y=v.name.match(/^([^-]+)/),w=y?y[1].trim().toLowerCase().replace(/\s+/g,"-"):"other";return`/stores/${j}/${w}/${v.slug}`},p=e.reduce((v,j)=>{const y=j.name.match(/^([^-]+)/),w=y?y[1].trim():"Other",S=u(j.slug),P={AZ:"Arizona",FL:"Florida",PA:"Pennsylvania",NJ:"New Jersey",MA:"Massachusetts",IL:"Illinois",NY:"New York",MD:"Maryland",MI:"Michigan",OH:"Ohio",CT:"Connecticut",ME:"Maine",MO:"Missouri",NV:"Nevada",OR:"Oregon",UT:"Utah"}[S]||S;return v[w]||(v[w]={}),v[w][P]||(v[w][P]=[]),v[w][P].push(j),v},{}),m=async(v,j,y)=>{y.stopPropagation();try{await B.updateStore(v,{scrape_enabled:!j}),t(e.map(w=>w.id===v?{...w,scrape_enabled:!j}:w))}catch(w){console.error("Failed to update scraping status:",w)}},x=v=>v?new Date(v).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"2-digit",minute:"2-digit"}):"Never",g=v=>{const j=new Set(i);j.has(v)?j.delete(v):j.add(v),s(j)},b=(v,j)=>{const y=`${v}-${j}`,w=new Set(o);w.has(y)?w.delete(y):w.add(y),l(w)};return r?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsx("div",{className:"flex items-center justify-between",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Il,{className:"w-6 h-6 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:"Stores"}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:[e.length," total stores"]})]})]})}),a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 overflow-hidden",children:a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50 border-b border-gray-200",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Store Name"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Type"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"View Online"}),a.jsx("th",{className:"px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Categories"}),a.jsx("th",{className:"px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Products"}),a.jsx("th",{className:"px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Scraping"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Last Scraped"})]})}),a.jsx("tbody",{children:Object.entries(p).map(([v,j])=>{const y=Object.values(j).flat().length,w=y===1,S=i.has(v);if(w){const _=Object.values(j).flat()[0];return a.jsxs("tr",{className:"border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors",onClick:()=>c(f(_)),title:"Click to view store",children:[a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[_.logo_url?a.jsx("img",{src:_.logo_url,alt:`${_.name} logo`,className:"w-8 h-8 object-contain flex-shrink-0",onError:T=>{T.target.style.display="none"}}):null,a.jsxs("div",{children:[a.jsx("div",{className:"font-semibold text-gray-900",children:_.name}),a.jsx("div",{className:"text-xs text-gray-500",children:_.slug})]})]})}),a.jsx("td",{className:"px-6 py-4",children:a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-blue-50 text-blue-700 rounded",children:"Dutchie"})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("a",{href:_.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700",onClick:T=>T.stopPropagation(),children:[a.jsx("span",{children:"View Online"}),a.jsx(Jr,{className:"w-3 h-3"})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Cr,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:_.category_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Ct,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:_.product_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",onClick:T=>T.stopPropagation(),children:a.jsx("button",{onClick:T=>m(_.id,_.scrape_enabled,T),className:"inline-flex items-center gap-1 text-sm font-medium transition-colors",children:_.scrape_enabled?a.jsxs(a.Fragment,{children:[a.jsx(Ix,{className:"w-5 h-5 text-green-600"}),a.jsx("span",{className:"text-green-600",children:"On"})]}):a.jsxs(a.Fragment,{children:[a.jsx(Mx,{className:"w-5 h-5 text-gray-400"}),a.jsx("span",{className:"text-gray-500",children:"Off"})]})})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(xr,{className:"w-4 h-4 text-gray-400"}),x(_.last_scraped_at)]})})]},_.id)}const N=Object.values(j).flat()[0],P=N==null?void 0:N.logo_url;return a.jsxs(fs.Fragment,{children:[a.jsx("tr",{className:"bg-gray-100 border-b border-gray-200 cursor-pointer hover:bg-gray-150 transition-colors",onClick:()=>g(v),children:a.jsx("td",{colSpan:7,className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx(Cf,{className:`w-5 h-5 text-gray-600 transition-transform ${S?"rotate-90":""}`}),P&&a.jsx("img",{src:P,alt:`${v} logo`,className:"w-8 h-8 object-contain flex-shrink-0",onError:_=>{_.target.style.display="none"}}),a.jsx("span",{className:"text-base font-semibold text-gray-900",children:v}),a.jsxs("span",{className:"text-sm text-gray-500",children:["(",y," stores)"]})]})})}),S&&Object.entries(j).map(([_,T])=>{const $=`${v}-${_}`,M=o.has($);return a.jsxs(fs.Fragment,{children:[a.jsx("tr",{className:"bg-gray-50 border-b border-gray-100 cursor-pointer hover:bg-gray-100 transition-colors",onClick:()=>b(v,_),children:a.jsx("td",{colSpan:7,className:"px-6 py-3 pl-12",children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Cf,{className:`w-4 h-4 text-gray-500 transition-transform ${M?"rotate-90":""}`}),a.jsx("span",{className:"text-sm font-medium text-gray-700",children:_}),a.jsxs("span",{className:"text-xs text-gray-500",children:["(",T.length," locations)"]})]})})}),M&&T.map(C=>a.jsxs("tr",{className:"border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors",onClick:()=>c(f(C)),title:"Click to view store",children:[a.jsx("td",{className:"px-6 py-4 pl-16",children:a.jsxs("div",{children:[a.jsx("div",{className:"font-semibold text-gray-900",children:C.name}),a.jsx("div",{className:"text-xs text-gray-500",children:C.slug})]})}),a.jsx("td",{className:"px-6 py-4",children:a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-blue-50 text-blue-700 rounded",children:"Dutchie"})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("a",{href:C.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700",onClick:R=>R.stopPropagation(),children:[a.jsx("span",{children:"View Online"}),a.jsx(Jr,{className:"w-3 h-3"})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Cr,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:C.category_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Ct,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:C.product_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",onClick:R=>R.stopPropagation(),children:a.jsx("button",{onClick:R=>m(C.id,C.scrape_enabled,R),className:"inline-flex items-center gap-1 text-sm font-medium transition-colors",children:C.scrape_enabled?a.jsxs(a.Fragment,{children:[a.jsx(Ix,{className:"w-5 h-5 text-green-600"}),a.jsx("span",{className:"text-green-600",children:"On"})]}):a.jsxs(a.Fragment,{children:[a.jsx(Mx,{className:"w-5 h-5 text-gray-400"}),a.jsx("span",{className:"text-gray-500",children:"Off"})]})})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(xr,{className:"w-4 h-4 text-gray-400"}),x(C.last_scraped_at)]})})]},C.id))]},`state-${$}`)})]},`chain-${v}`)})})]})})}),e.length===0&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-12 text-center",children:[a.jsx(Il,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-2",children:"No stores found"}),a.jsx("p",{className:"text-gray-500",children:"Start by adding stores to your database"})]})]})})}function r7(){const e=dt(),[t,r]=h.useState([]),[n,i]=h.useState(!0),[s,o]=h.useState(""),[l,c]=h.useState(""),[d,u]=h.useState(null),[f,p]=h.useState({});h.useEffect(()=>{m()},[]);const m=async()=>{i(!0);try{const y=await B.getDispensaries();r(y.dispensaries)}catch(y){console.error("Failed to load dispensaries:",y)}finally{i(!1)}},x=y=>{u(y),p({dba_name:y.dba_name||"",website:y.website||"",phone:y.phone||"",google_rating:y.google_rating||"",google_review_count:y.google_review_count||""})},g=async()=>{if(d)try{await B.updateDispensary(d.id,f),await m(),u(null),p({})}catch(y){console.error("Failed to update dispensary:",y),alert("Failed to update dispensary")}},b=()=>{u(null),p({})},v=t.filter(y=>{const w=s.toLowerCase(),S=!s||y.name.toLowerCase().includes(w)||y.company_name&&y.company_name.toLowerCase().includes(w)||y.dba_name&&y.dba_name.toLowerCase().includes(w),N=!l||y.city===l;return S&&N}),j=Array.from(new Set(t.map(y=>y.city).filter(Boolean))).sort();return a.jsxs(X,{children:[a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Dispensaries"}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:["AZDHS official dispensary directory (",t.length," total)"]})]}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Search"}),a.jsxs("div",{className:"relative",children:[a.jsx(AO,{className:"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"}),a.jsx("input",{type:"text",value:s,onChange:y=>o(y.target.value),placeholder:"Search by name or company...",className:"w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Filter by City"}),a.jsxs("select",{value:l,onChange:y=>c(y.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"",children:"All Cities"}),j.map(y=>a.jsx("option",{value:y,children:y},y))]})]})]})}),n?a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading dispensaries..."})]}):a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 overflow-hidden",children:[a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50 border-b border-gray-200",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Name"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Company"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Address"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"City"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Phone"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Email"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Website"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Actions"})]})}),a.jsx("tbody",{className:"divide-y divide-gray-200",children:v.length===0?a.jsx("tr",{children:a.jsx("td",{colSpan:8,className:"px-4 py-8 text-center text-sm text-gray-500",children:"No dispensaries found"})}):v.map(y=>a.jsxs("tr",{className:"hover:bg-gray-50",children:[a.jsx("td",{className:"px-4 py-3",children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Ln,{className:"w-4 h-4 text-gray-400 flex-shrink-0"}),a.jsxs("div",{className:"flex flex-col",children:[a.jsx("span",{className:"text-sm font-medium text-gray-900",children:y.dba_name||y.name}),y.dba_name&&y.google_rating&&a.jsxs("span",{className:"text-xs text-gray-500",children:["⭐ ",y.google_rating," (",y.google_review_count," reviews)"]})]})]})}),a.jsx("td",{className:"px-4 py-3",children:a.jsx("span",{className:"text-sm text-gray-600",children:y.company_name||"-"})}),a.jsx("td",{className:"px-4 py-3",children:a.jsxs("div",{className:"flex items-start gap-1",children:[a.jsx(yi,{className:"w-3 h-3 text-gray-400 flex-shrink-0 mt-0.5"}),a.jsx("span",{className:"text-sm text-gray-600",children:y.address||"-"})]})}),a.jsxs("td",{className:"px-4 py-3",children:[a.jsx("span",{className:"text-sm text-gray-600",children:y.city||"-"}),y.zip&&a.jsxs("span",{className:"text-xs text-gray-400 ml-1",children:["(",y.zip,")"]})]}),a.jsx("td",{className:"px-4 py-3",children:y.phone?a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx(Ah,{className:"w-3 h-3 text-gray-400"}),a.jsx("span",{className:"text-sm text-gray-600",children:y.phone.replace(/(\d{3})(\d{3})(\d{4})/,"($1) $2-$3")})]}):a.jsx("span",{className:"text-sm text-gray-400",children:"-"})}),a.jsx("td",{className:"px-4 py-3",children:y.email?a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx(tj,{className:"w-3 h-3 text-gray-400"}),a.jsx("a",{href:`mailto:${y.email}`,className:"text-sm text-blue-600 hover:text-blue-800",children:y.email})]}):a.jsx("span",{className:"text-sm text-gray-400",children:"-"})}),a.jsx("td",{className:"px-4 py-3",children:y.website?a.jsxs("a",{href:y.website,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(Jr,{className:"w-3 h-3"}),a.jsx("span",{children:"Visit Site"})]}):a.jsx("span",{className:"text-sm text-gray-400",children:"-"})}),a.jsx("td",{className:"px-4 py-3",children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("button",{onClick:()=>x(y),className:"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-lg transition-colors",title:"Edit",children:a.jsx(wO,{className:"w-4 h-4"})}),a.jsx("button",{onClick:()=>{const w=y.city.toLowerCase().replace(/\s+/g,"-");e(`/dispensaries/${y.state}/${w}/${y.slug}`)},className:"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors",title:"View",children:a.jsx(rO,{className:"w-4 h-4"})})]})})]},y.id))})]})}),a.jsx("div",{className:"bg-gray-50 px-4 py-3 border-t border-gray-200",children:a.jsxs("div",{className:"text-sm text-gray-600",children:["Showing ",v.length," of ",t.length," dispensaries"]})})]})]}),d&&a.jsx("div",{className:"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50",children:a.jsxs("div",{className:"bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto",children:[a.jsxs("div",{className:"flex items-center justify-between p-6 border-b border-gray-200",children:[a.jsxs("h2",{className:"text-xl font-bold text-gray-900",children:["Edit Dispensary: ",d.name]}),a.jsx("button",{onClick:b,className:"text-gray-400 hover:text-gray-600",children:a.jsx(ij,{className:"w-6 h-6"})})]}),a.jsxs("div",{className:"p-6 space-y-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"DBA Name (Display Name)"}),a.jsx("input",{type:"text",value:f.dba_name,onChange:y=>p({...f,dba_name:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"e.g., Green Med Wellness"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Website"}),a.jsx("input",{type:"url",value:f.website,onChange:y=>p({...f,website:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"https://example.com"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Phone Number"}),a.jsx("input",{type:"tel",value:f.phone,onChange:y=>p({...f,phone:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"5551234567"})]}),a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Google Rating"}),a.jsx("input",{type:"number",step:"0.1",min:"0",max:"5",value:f.google_rating,onChange:y=>p({...f,google_rating:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"4.5"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Review Count"}),a.jsx("input",{type:"number",min:"0",value:f.google_review_count,onChange:y=>p({...f,google_review_count:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"123"})]})]}),a.jsxs("div",{className:"bg-gray-50 p-4 rounded-lg space-y-2",children:[a.jsxs("div",{className:"text-sm",children:[a.jsx("span",{className:"font-medium text-gray-700",children:"AZDHS Name:"})," ",a.jsx("span",{className:"text-gray-600",children:d.name})]}),a.jsxs("div",{className:"text-sm",children:[a.jsx("span",{className:"font-medium text-gray-700",children:"Address:"})," ",a.jsxs("span",{className:"text-gray-600",children:[d.address,", ",d.city,", ",d.state," ",d.zip]})]})]})]}),a.jsxs("div",{className:"flex items-center justify-end gap-3 p-6 border-t border-gray-200",children:[a.jsx("button",{onClick:b,className:"px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",children:"Cancel"}),a.jsxs("button",{onClick:g,className:"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors",children:[a.jsx(_O,{className:"w-4 h-4"}),"Save Changes"]})]})]})})]})}function n7(){const{state:e,city:t,slug:r}=Na(),n=dt(),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState([]),[u,f]=h.useState([]),[p,m]=h.useState(!0),[x,g]=h.useState("products"),[b,v]=h.useState(!1),[j,y]=h.useState(!1),[w,S]=h.useState(""),[N,P]=h.useState(1),[_]=h.useState(25),T=D=>{if(!D)return"Never";const O=new Date(D),L=new Date().getTime()-O.getTime(),U=Math.floor(L/(1e3*60*60*24));return U===0?"Today":U===1?"Yesterday":U<7?`${U} days ago`:O.toLocaleDateString()};h.useEffect(()=>{$()},[r]);const $=async()=>{m(!0);try{const[D,O,k,L]=await Promise.all([B.getDispensary(r),B.getDispensaryProducts(r).catch(()=>({products:[]})),B.getDispensaryBrands(r).catch(()=>({brands:[]})),B.getDispensarySpecials(r).catch(()=>({specials:[]}))]);s(D),l(O.products),d(k.brands),f(L.specials)}catch(D){console.error("Failed to load dispensary:",D)}finally{m(!1)}},M=async D=>{v(!1),y(!0);try{const O=await fetch(`https://dispos.crawlsy.com/api/dispensaries/${r}/scrape`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${localStorage.getItem("token")}`},body:JSON.stringify({type:D})});if(!O.ok)throw new Error("Failed to trigger scraping");const k=await O.json();alert(`${D.charAt(0).toUpperCase()+D.slice(1)} update started! ${k.message||""}`)}catch(O){console.error("Failed to trigger scraping:",O),alert("Failed to start update. Please try again.")}finally{y(!1)}},C=o.filter(D=>{var k,L,U,H,te;if(!w)return!0;const O=w.toLowerCase();return((k=D.name)==null?void 0:k.toLowerCase().includes(O))||((L=D.brand)==null?void 0:L.toLowerCase().includes(O))||((U=D.variant)==null?void 0:U.toLowerCase().includes(O))||((H=D.description)==null?void 0:H.toLowerCase().includes(O))||((te=D.strain_type)==null?void 0:te.toLowerCase().includes(O))}),R=Math.ceil(C.length/_),q=(N-1)*_,Z=q+_,E=C.slice(q,Z);return h.useEffect(()=>{P(1)},[w]),p?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading dispensary..."})]})}):i?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("button",{onClick:()=>n("/dispensaries"),className:"flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900",children:[a.jsx(_h,{className:"w-4 h-4"}),"Back to Dispensaries"]}),a.jsxs("div",{className:"relative",children:[a.jsxs("button",{onClick:()=>v(!b),disabled:j,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed",children:[a.jsx(Xt,{className:`w-4 h-4 ${j?"animate-spin":""}`}),j?"Updating...":"Update",!j&&a.jsx(ej,{className:"w-4 h-4"})]}),b&&!j&&a.jsxs("div",{className:"absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-10",children:[a.jsx("button",{onClick:()=>M("products"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-t-lg",children:"Products"}),a.jsx("button",{onClick:()=>M("brands"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100",children:"Brands"}),a.jsx("button",{onClick:()=>M("specials"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100",children:"Specials"}),a.jsx("button",{onClick:()=>M("all"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-b-lg border-t border-gray-200",children:"All"})]})]})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-start justify-between gap-4 mb-4",children:[a.jsxs("div",{className:"flex items-start gap-4",children:[a.jsx("div",{className:"p-3 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-8 h-8 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:i.dba_name||i.name}),i.company_name&&a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:i.company_name})]})]}),a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600 bg-gray-50 px-4 py-2 rounded-lg",children:[a.jsx(Ch,{className:"w-4 h-4"}),a.jsxs("div",{children:[a.jsx("span",{className:"font-medium",children:"Last Crawl Date:"}),a.jsx("span",{className:"ml-2",children:i.last_menu_scrape?new Date(i.last_menu_scrape).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):"Never"})]})]})]}),a.jsxs("div",{className:"flex flex-wrap gap-4",children:[i.address&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(yi,{className:"w-4 h-4"}),a.jsxs("span",{children:[i.address,", ",i.city,", ",i.state," ",i.zip]})]}),i.phone&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(Ah,{className:"w-4 h-4"}),a.jsx("span",{children:i.phone.replace(/(\d{3})(\d{3})(\d{4})/,"($1) $2-$3")})]}),a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(Jr,{className:"w-4 h-4"}),i.website?a.jsx("a",{href:i.website,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:text-blue-800",children:"Website"}):a.jsx("span",{children:"Website N/A"})]}),i.email&&a.jsxs("div",{className:"flex items-center gap-2 text-sm",children:[a.jsx(tj,{className:"w-4 h-4 text-gray-400"}),a.jsx("a",{href:`mailto:${i.email}`,className:"text-blue-600 hover:text-blue-800",children:i.email})]}),i.azdhs_url&&a.jsxs("a",{href:i.azdhs_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(Jr,{className:"w-4 h-4"}),a.jsx("span",{children:"AZDHS Profile"})]}),a.jsxs(K1,{to:"/schedule",className:"flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(xr,{className:"w-4 h-4"}),a.jsx("span",{children:"View Schedule"})]})]})]}),a.jsxs("div",{className:"grid grid-cols-4 gap-6",children:[a.jsx("button",{onClick:()=>{g("products"),S("")},className:"bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:o.length})]})]})}),a.jsx("button",{onClick:()=>g("brands"),className:"bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(Cr,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:c.length})]})]})}),a.jsx("button",{onClick:()=>g("specials"),className:"bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(zn,{className:"w-5 h-5 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Active Specials"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:u.length})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(QA,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Avg Price"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:o.length>0?`$${(o.reduce((D,O)=>D+(O.sale_price||O.regular_price||0),0)/o.length).toFixed(2)}`:"-"})]})]})})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"border-b border-gray-200",children:a.jsxs("div",{className:"flex gap-4 px-6",children:[a.jsxs("button",{onClick:()=>g("products"),className:`py-4 px-2 text-sm font-medium border-b-2 ${x==="products"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Products (",o.length,")"]}),a.jsxs("button",{onClick:()=>g("brands"),className:`py-4 px-2 text-sm font-medium border-b-2 ${x==="brands"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Brands (",c.length,")"]}),a.jsxs("button",{onClick:()=>g("specials"),className:`py-4 px-2 text-sm font-medium border-b-2 ${x==="specials"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Specials (",u.length,")"]})]})}),a.jsxs("div",{className:"p-6",children:[x==="products"&&a.jsx("div",{className:"space-y-4",children:o.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No products available"}):a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:"flex items-center gap-4 mb-4",children:[a.jsx("input",{type:"text",placeholder:"Search products by name, brand, variant, description, or strain type...",value:w,onChange:D=>S(D.target.value),className:"input input-bordered input-sm flex-1"}),w&&a.jsx("button",{onClick:()=>S(""),className:"btn btn-sm btn-ghost",children:"Clear"}),a.jsxs("div",{className:"text-sm text-gray-600",children:["Showing ",q+1,"-",Math.min(Z,C.length)," of ",C.length," products"]})]}),a.jsx("div",{className:"overflow-x-auto -mx-6 px-6",children:a.jsxs("table",{className:"table table-xs table-zebra table-pin-rows w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Image"}),a.jsx("th",{children:"Product Name"}),a.jsx("th",{children:"Brand"}),a.jsx("th",{children:"Variant"}),a.jsx("th",{children:"Description"}),a.jsx("th",{className:"text-right",children:"Price"}),a.jsx("th",{className:"text-center",children:"THC %"}),a.jsx("th",{className:"text-center",children:"CBD %"}),a.jsx("th",{className:"text-center",children:"Strain Type"}),a.jsx("th",{className:"text-center",children:"In Stock"}),a.jsx("th",{children:"Last Updated"}),a.jsx("th",{children:"Actions"})]})}),a.jsx("tbody",{children:E.map(D=>a.jsxs("tr",{children:[a.jsx("td",{className:"whitespace-nowrap",children:D.image_url?a.jsx("img",{src:D.image_url,alt:D.name,className:"w-12 h-12 object-cover rounded",onError:O=>O.currentTarget.style.display="none"}):"-"}),a.jsx("td",{className:"font-medium max-w-[150px]",children:a.jsx("div",{className:"line-clamp-2",title:D.name,children:D.name})}),a.jsx("td",{className:"max-w-[120px]",children:a.jsx("div",{className:"line-clamp-2",title:D.brand||"-",children:D.brand||"-"})}),a.jsx("td",{className:"max-w-[100px]",children:a.jsx("div",{className:"line-clamp-2",title:D.variant||"-",children:D.variant||"-"})}),a.jsx("td",{className:"w-[120px]",children:a.jsx("span",{title:D.description,children:D.description?D.description.length>15?D.description.substring(0,15)+"...":D.description:"-"})}),a.jsx("td",{className:"text-right font-semibold whitespace-nowrap",children:D.sale_price?a.jsxs("div",{className:"flex flex-col items-end",children:[a.jsxs("span",{className:"text-error",children:["$",D.sale_price]}),a.jsxs("span",{className:"text-gray-400 line-through text-xs",children:["$",D.regular_price]})]}):D.regular_price?`$${D.regular_price}`:"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.thc_percentage?a.jsxs("span",{className:"badge badge-success badge-sm",children:[D.thc_percentage,"%"]}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.cbd_percentage?a.jsxs("span",{className:"badge badge-info badge-sm",children:[D.cbd_percentage,"%"]}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.strain_type?a.jsx("span",{className:"badge badge-ghost badge-sm",children:D.strain_type}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.in_stock?a.jsx("span",{className:"badge badge-success badge-sm",children:"Yes"}):D.in_stock===!1?a.jsx("span",{className:"badge badge-error badge-sm",children:"No"}):"-"}),a.jsx("td",{className:"whitespace-nowrap text-xs text-gray-500",children:D.updated_at?T(D.updated_at):"-"}),a.jsx("td",{children:a.jsxs("div",{className:"flex gap-1",children:[D.dutchie_url&&a.jsx("a",{href:D.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"btn btn-xs btn-outline",children:"Dutchie"}),a.jsx("button",{onClick:()=>n(`/products/${D.id}`),className:"btn btn-xs btn-primary",children:"Details"})]})})]},D.id))})]})}),R>1&&a.jsxs("div",{className:"flex justify-center items-center gap-2 mt-4",children:[a.jsx("button",{onClick:()=>P(D=>Math.max(1,D-1)),disabled:N===1,className:"btn btn-sm btn-outline",children:"Previous"}),a.jsx("div",{className:"flex gap-1",children:Array.from({length:R},(D,O)=>O+1).map(D=>{const O=D===1||D===R||D>=N-1&&D<=N+1;return D===2&&N>3||D===R-1&&NP(D),className:`btn btn-sm ${N===D?"btn-primary":"btn-outline"}`,children:D},D):null})}),a.jsx("button",{onClick:()=>P(D=>Math.min(R,D+1)),disabled:N===R,className:"btn btn-sm btn-outline",children:"Next"})]})]})}),x==="brands"&&a.jsx("div",{className:"space-y-4",children:c.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No brands available"}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:c.map(D=>a.jsxs("button",{onClick:()=>{g("products"),S(D.brand)},className:"border border-gray-200 rounded-lg p-4 text-center hover:border-blue-300 hover:shadow-md transition-all cursor-pointer",children:[a.jsx("p",{className:"font-medium text-gray-900 line-clamp-2",children:D.brand}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:[D.product_count," product",D.product_count!==1?"s":""]})]},D.brand))})}),x==="specials"&&a.jsx("div",{className:"space-y-4",children:u.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No active specials"}):a.jsx("div",{className:"space-y-3",children:u.map(D=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4",children:[a.jsx("h4",{className:"font-medium text-gray-900",children:D.name}),D.description&&a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:D.description}),a.jsxs("div",{className:"flex items-center gap-4 mt-2 text-sm text-gray-500",children:[a.jsxs("span",{children:[new Date(D.start_date).toLocaleDateString()," -"," ",D.end_date?new Date(D.end_date).toLocaleDateString():"Ongoing"]}),a.jsxs("span",{children:[D.product_count," products"]})]})]},D.id))})})]})]})]})}):a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-600",children:"Dispensary not found"})})})}function i7(){var $,M;const{slug:e}=Na(),t=dt(),[r,n]=h.useState(null),[i,s]=h.useState([]),[o,l]=h.useState([]),[c,d]=h.useState([]),[u,f]=h.useState(!0),[p,m]=h.useState(null),[x,g]=h.useState(""),[b,v]=h.useState("products"),[j,y]=h.useState("name");h.useEffect(()=>{w()},[e]),h.useEffect(()=>{r&&S()},[p,x,j,r]);const w=async()=>{f(!0);try{const R=(await B.getStores()).stores.find(D=>D.slug===e);if(!R)throw new Error("Store not found");const[q,Z,E]=await Promise.all([B.getStore(R.id),B.getCategories(R.id),B.getStoreBrands(R.id)]);n(q),l(Z.categories||[]),d(E.brands||[])}catch(C){console.error("Failed to load store data:",C)}finally{f(!1)}},S=async()=>{if(r)try{const C={store_id:r.id,limit:1e3};p&&(C.category_id=p),x&&(C.brand=x);let q=(await B.getProducts(C)).products||[];q.sort((Z,E)=>{switch(j){case"name":return(Z.name||"").localeCompare(E.name||"");case"price_asc":return(Z.price||0)-(E.price||0);case"price_desc":return(E.price||0)-(Z.price||0);case"thc":return(E.thc_percentage||0)-(Z.thc_percentage||0);default:return 0}}),s(q)}catch(C){console.error("Failed to load products:",C)}},N=C=>C.image_url_full?C.image_url_full:C.medium_path?`http://localhost:9020/dutchie/${C.medium_path}`:C.thumbnail_path?`http://localhost:9020/dutchie/${C.thumbnail_path}`:"https://via.placeholder.com/300x300?text=No+Image",P=C=>C?new Date(C).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"2-digit",minute:"2-digit"}):"Never",_=C=>{switch(C==null?void 0:C.toLowerCase()){case"dutchie":return"bg-green-100 text-green-700";case"jane":return"bg-purple-100 text-purple-700";case"treez":return"bg-blue-100 text-blue-700";case"weedmaps":return"bg-orange-100 text-orange-700";case"leafly":return"bg-emerald-100 text-emerald-700";default:return"bg-gray-100 text-gray-700"}},T=C=>{switch(C){case"completed":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full flex items-center gap-1",children:[a.jsx(_r,{className:"w-3 h-3"})," Completed"]});case"running":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 rounded-full flex items-center gap-1",children:[a.jsx(Xt,{className:"w-3 h-3 animate-spin"})," Running"]});case"failed":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded-full flex items-center gap-1",children:[a.jsx(Hr,{className:"w-3 h-3"})," Failed"]});case"pending":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-700 rounded-full flex items-center gap-1",children:[a.jsx(xr,{className:"w-3 h-3"})," Pending"]});default:return a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-gray-100 text-gray-700 rounded-full",children:C})}};return u?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})}):r?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-start justify-between mb-6",children:[a.jsxs("div",{className:"flex items-start gap-4",children:[a.jsx("button",{onClick:()=>t("/stores"),className:"text-gray-600 hover:text-gray-900 mt-1",children:"← Back"}),a.jsxs("div",{children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:r.name}),a.jsx("span",{className:`px-2 py-1 text-xs font-medium rounded ${_(r.provider)}`,children:r.provider||"Unknown"})]}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:["Store ID: ",r.id]})]})]}),a.jsxs("a",{href:r.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700",children:["View Menu ",a.jsx(Jr,{className:"w-4 h-4"})]})]}),a.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-6",children:[a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[a.jsx(Ct,{className:"w-4 h-4"}),"Products"]}),a.jsx("p",{className:"text-xl font-semibold text-gray-900",children:r.product_count||0})]}),a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[a.jsx(Cr,{className:"w-4 h-4"}),"Categories"]}),a.jsx("p",{className:"text-xl font-semibold text-gray-900",children:r.category_count||0})]}),a.jsxs("div",{className:"p-4 bg-green-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-green-600 text-xs mb-1",children:[a.jsx(_r,{className:"w-4 h-4"}),"In Stock"]}),a.jsx("p",{className:"text-xl font-semibold text-green-700",children:r.in_stock_count||0})]}),a.jsxs("div",{className:"p-4 bg-red-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-red-600 text-xs mb-1",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Out of Stock"]}),a.jsx("p",{className:"text-xl font-semibold text-red-700",children:r.out_of_stock_count||0})]}),a.jsxs("div",{className:`p-4 rounded-lg ${r.is_stale?"bg-yellow-50":"bg-blue-50"}`,children:[a.jsxs("div",{className:`flex items-center gap-2 text-xs mb-1 ${r.is_stale?"text-yellow-600":"text-blue-600"}`,children:[a.jsx(xr,{className:"w-4 h-4"}),"Freshness"]}),a.jsx("p",{className:`text-sm font-semibold ${r.is_stale?"text-yellow-700":"text-blue-700"}`,children:r.freshness||"Never scraped"})]}),a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[a.jsx(Ch,{className:"w-4 h-4"}),"Next Crawl"]}),a.jsx("p",{className:"text-sm font-semibold text-gray-700",children:($=r.schedule)!=null&&$.next_run_at?P(r.schedule.next_run_at):"Not scheduled"})]})]}),r.linked_dispensary&&a.jsxs("div",{className:"p-4 bg-indigo-50 rounded-lg mb-6",children:[a.jsxs("div",{className:"flex items-center gap-2 text-indigo-600 text-xs mb-2",children:[a.jsx(WA,{className:"w-4 h-4"}),"Linked Dispensary"]}),a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("p",{className:"font-semibold text-indigo-900",children:r.linked_dispensary.name}),a.jsxs("p",{className:"text-sm text-indigo-700 flex items-center gap-1",children:[a.jsx(yi,{className:"w-3 h-3"}),r.linked_dispensary.city,", ",r.linked_dispensary.state,r.linked_dispensary.address&&` - ${r.linked_dispensary.address}`]})]}),a.jsx("button",{onClick:()=>t(`/dispensaries/${r.linked_dispensary.slug}`),className:"text-sm text-indigo-600 hover:text-indigo-700 font-medium",children:"View Dispensary →"})]})]}),a.jsxs("div",{className:"flex gap-2 border-b border-gray-200",children:[a.jsx("button",{onClick:()=>v("products"),className:`px-4 py-2 border-b-2 transition-colors ${b==="products"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Ct,{className:"w-4 h-4"}),"Products (",i.length,")"]})}),a.jsx("button",{onClick:()=>v("brands"),className:`px-4 py-2 border-b-2 transition-colors ${b==="brands"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Cr,{className:"w-4 h-4"}),"Brands (",c.length,")"]})}),a.jsx("button",{onClick:()=>v("specials"),className:`px-4 py-2 border-b-2 transition-colors ${b==="specials"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx($x,{className:"w-4 h-4"}),"Specials"]})}),a.jsx("button",{onClick:()=>v("crawl-history"),className:`px-4 py-2 border-b-2 transition-colors ${b==="crawl-history"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Ds,{className:"w-4 h-4"}),"Crawl History (",((M=r.recent_jobs)==null?void 0:M.length)||0,")"]})})]})]}),b==="crawl-history"&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 overflow-hidden",children:[a.jsxs("div",{className:"p-4 border-b border-gray-200",children:[a.jsx("h2",{className:"text-lg font-semibold text-gray-900",children:"Recent Crawl Jobs"}),a.jsx("p",{className:"text-sm text-gray-500",children:"Last 10 crawl jobs for this store"})]}),r.recent_jobs&&r.recent_jobs.length>0?a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Status"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Type"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Started"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Completed"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"Found"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"New"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"Updated"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"In Stock"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"Out of Stock"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Error"})]})}),a.jsx("tbody",{className:"divide-y divide-gray-100",children:r.recent_jobs.map(C=>a.jsxs("tr",{className:"hover:bg-gray-50",children:[a.jsx("td",{className:"px-4 py-3",children:T(C.status)}),a.jsx("td",{className:"px-4 py-3 text-sm text-gray-700",children:C.job_type||"-"}),a.jsx("td",{className:"px-4 py-3 text-sm text-gray-700",children:P(C.started_at)}),a.jsx("td",{className:"px-4 py-3 text-sm text-gray-700",children:P(C.completed_at)}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-gray-900",children:C.products_found??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-green-600",children:C.products_new??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-blue-600",children:C.products_updated??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-green-600",children:C.in_stock_count??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-red-600",children:C.out_of_stock_count??"-"}),a.jsx("td",{className:"px-4 py-3 text-sm text-red-600 max-w-xs truncate",title:C.error_message||"",children:C.error_message||"-"})]},C.id))})]})}):a.jsxs("div",{className:"text-center py-12",children:[a.jsx(Ds,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("p",{className:"text-gray-500",children:"No crawl history available"})]})]}),b==="products"&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 p-4",children:a.jsxs("div",{className:"flex flex-wrap gap-4",children:[a.jsxs("div",{className:"flex-1 min-w-[200px]",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Category"}),a.jsxs("select",{value:p||"",onChange:C=>m(C.target.value?parseInt(C.target.value):null),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"",children:"All Categories"}),o.map(C=>a.jsxs("option",{value:C.id,children:[C.name," (",i.filter(R=>R.category_id===C.id).length,")"]},C.id))]})]}),a.jsxs("div",{className:"flex-1 min-w-[200px]",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Brand"}),a.jsxs("select",{value:x,onChange:C=>g(C.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"",children:"All Brands"}),c.map(C=>a.jsx("option",{value:C,children:C},C))]})]}),a.jsxs("div",{className:"flex-1 min-w-[200px]",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Sort By"}),a.jsxs("select",{value:j,onChange:C=>y(C.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"name",children:"Name (A-Z)"}),a.jsx("option",{value:"price_asc",children:"Price (Low to High)"}),a.jsx("option",{value:"price_desc",children:"Price (High to Low)"}),a.jsx("option",{value:"thc",children:"THC % (High to Low)"})]})]})]})}),i.length>0?a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",children:i.map(C=>a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow",children:[a.jsxs("div",{className:"aspect-square bg-gray-50 relative",children:[a.jsx("img",{src:N(C),alt:C.name,className:"w-full h-full object-cover"}),C.in_stock?a.jsx("span",{className:"absolute top-2 right-2 px-2 py-1 bg-green-500 text-white text-xs font-medium rounded",children:"In Stock"}):a.jsx("span",{className:"absolute top-2 right-2 px-2 py-1 bg-red-500 text-white text-xs font-medium rounded",children:"Out of Stock"})]}),a.jsxs("div",{className:"p-3 space-y-2",children:[a.jsx("h3",{className:"font-semibold text-sm text-gray-900 line-clamp-2",children:C.name}),C.brand&&a.jsx("p",{className:"text-xs text-gray-600 font-medium",children:C.brand}),C.category_name&&a.jsx("p",{className:"text-xs text-gray-500",children:C.category_name}),a.jsxs("div",{className:"grid grid-cols-2 gap-2 pt-2 border-t border-gray-100",children:[C.price!==null&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Price:"}),a.jsxs("span",{className:"ml-1 font-semibold text-blue-600",children:["$",parseFloat(C.price).toFixed(2)]})]}),C.weight&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Weight:"}),a.jsx("span",{className:"ml-1 font-medium",children:C.weight})]}),C.thc_percentage!==null&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"THC:"}),a.jsxs("span",{className:"ml-1 font-medium text-green-600",children:[C.thc_percentage,"%"]})]}),C.cbd_percentage!==null&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"CBD:"}),a.jsxs("span",{className:"ml-1 font-medium text-blue-600",children:[C.cbd_percentage,"%"]})]}),C.strain_type&&a.jsxs("div",{className:"text-xs col-span-2",children:[a.jsx("span",{className:"text-gray-500",children:"Type:"}),a.jsx("span",{className:"ml-1 font-medium capitalize",children:C.strain_type})]})]}),C.description&&a.jsx("p",{className:"text-xs text-gray-600 line-clamp-2 pt-2 border-t border-gray-100",children:C.description}),C.last_seen_at&&a.jsxs("p",{className:"text-xs text-gray-400 pt-2 border-t border-gray-100",children:["Updated: ",new Date(C.last_seen_at).toLocaleDateString()]}),a.jsxs("div",{className:"flex gap-2 mt-3 pt-3 border-t border-gray-100",children:[C.dutchie_url&&a.jsx("a",{href:C.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex-1 px-3 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors text-center border border-gray-200",children:"Dutchie"}),a.jsx("button",{onClick:()=>t(`/products/${C.id}`),className:"flex-1 px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors",children:"Details"})]})]})]},C.id))}):a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-12 text-center",children:[a.jsx(Ct,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-2",children:"No products found"}),a.jsx("p",{className:"text-gray-500",children:"Try adjusting your filters"})]})]}),b==="brands"&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("h2",{className:"text-lg font-semibold text-gray-900 mb-4",children:["All Brands (",c.length,")"]}),c.length>0?a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3",children:c.map(C=>{const R=i.filter(q=>q.brand===C);return a.jsxs("div",{className:"p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all cursor-pointer",onClick:()=>{v("products"),g(C)},children:[a.jsx("p",{className:"font-medium text-gray-900 text-sm",children:C}),a.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:[R.length," products"]})]},C)})}):a.jsxs("div",{className:"text-center py-12",children:[a.jsx(Cr,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("p",{className:"text-gray-500",children:"No brands found"})]})]}),b==="specials"&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsx("h2",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Daily Specials"}),a.jsxs("div",{className:"text-center py-12",children:[a.jsx($x,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("p",{className:"text-gray-500",children:"No specials available"})]})]})]})}):a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Store not found"}),a.jsx("button",{onClick:()=>t("/stores"),className:"text-blue-600 hover:text-blue-700",children:"← Back to stores"})]})})}function a7(){const{state:e,storeName:t,slug:r}=Na(),n=dt(),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState(!0);h.useEffect(()=>{u()},[r]);const u=async()=>{d(!0);try{const p=(await B.getStores()).stores.find(x=>x.slug===r);if(!p)throw new Error("Store not found");s(p);const m=await B.getStoreBrands(p.id);l(m.brands)}catch(f){console.error("Failed to load brands:",f)}finally{d(!1)}};return c?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("span",{className:"loading loading-spinner loading-lg"})})}):i?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsx("button",{onClick:()=>n(`/stores/${e}/${t}/${r}`),className:"btn btn-ghost btn-sm",children:"← Back to Store"}),a.jsxs("div",{className:"flex-1",children:[a.jsxs("h1",{className:"text-3xl font-bold",children:[i.name," - Brands"]}),a.jsxs("div",{className:"flex gap-3 mt-2",children:[a.jsx("a",{href:`${i.dutchie_url}/brands`,target:"_blank",rel:"noopener noreferrer",className:"link link-primary text-sm",children:"View Live Brands Page"}),a.jsx("span",{className:"text-sm text-gray-500",children:"•"}),a.jsxs("span",{className:"text-sm text-gray-500",children:[o.length," ",o.length===1?"Brand":"Brands"]})]})]})]}),o.length>0?a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",children:o.map((f,p)=>a.jsx("div",{className:"card bg-base-100 shadow-md hover:shadow-lg transition-shadow",children:a.jsx("div",{className:"card-body p-6",children:a.jsx("h3",{className:"text-lg font-semibold text-center",children:f})})},p))}):a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body text-center py-12",children:[a.jsx("p",{className:"text-gray-500",children:"No brands found for this store"}),a.jsx("p",{className:"text-sm text-gray-400 mt-2",children:"Brands are automatically extracted from products"})]})})]})}):a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-500",children:"Store not found"})})})}function s7(){const{state:e,storeName:t,slug:r}=Na(),n=dt(),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState(new Date().toISOString().split("T")[0]),[u,f]=h.useState(!0);h.useEffect(()=>{p()},[r,c]);const p=async()=>{f(!0);try{const x=(await B.getStores()).stores.find(b=>b.slug===r);if(!x)throw new Error("Store not found");s(x);const g=await B.getStoreSpecials(x.id,c);l(g.specials)}catch(m){console.error("Failed to load specials:",m)}finally{f(!1)}};return u?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("span",{className:"loading loading-spinner loading-lg"})})}):i?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsx("button",{onClick:()=>n(`/stores/${e}/${t}/${r}`),className:"btn btn-ghost btn-sm",children:"← Back to Store"}),a.jsxs("div",{className:"flex-1",children:[a.jsxs("h1",{className:"text-3xl font-bold",children:[i.name," - Specials"]}),a.jsxs("div",{className:"flex gap-3 mt-2",children:[a.jsx("a",{href:`${i.dutchie_url}/specials`,target:"_blank",rel:"noopener noreferrer",className:"link link-primary text-sm",children:"View Live Specials Page"}),a.jsx("span",{className:"text-sm text-gray-500",children:"•"}),a.jsxs("span",{className:"text-sm text-gray-500",children:[o.length," ",o.length===1?"Special":"Specials"]})]})]})]}),a.jsx("div",{className:"card bg-base-100 shadow-md",children:a.jsx("div",{className:"card-body p-4",children:a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsx("label",{className:"font-semibold",children:"Select Date:"}),a.jsx("input",{type:"date",value:c,onChange:m=>d(m.target.value),className:"input input-bordered input-sm"})]})})}),o.length>0?a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:o.map((m,x)=>a.jsx("div",{className:"card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h3",{className:"card-title text-lg",children:m.name}),m.description&&a.jsx("p",{className:"text-sm text-gray-600",children:m.description}),a.jsxs("div",{className:"space-y-2 mt-2",children:[m.original_price&&a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsx("span",{className:"text-sm opacity-60",children:"Original Price:"}),a.jsxs("span",{className:"line-through text-gray-500",children:["$",parseFloat(m.original_price).toFixed(2)]})]}),m.special_price&&a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsx("span",{className:"text-sm opacity-60",children:"Special Price:"}),a.jsxs("span",{className:"text-lg font-bold text-success",children:["$",parseFloat(m.special_price).toFixed(2)]})]}),m.discount_percentage&&a.jsxs("div",{className:"badge badge-success badge-lg",children:[m.discount_percentage,"% OFF"]}),m.discount_amount&&a.jsxs("div",{className:"badge badge-info badge-lg",children:["Save $",parseFloat(m.discount_amount).toFixed(2)]})]})]})},x))}):a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body text-center py-12",children:[a.jsxs("p",{className:"text-gray-500",children:["No specials found for ",c]}),a.jsx("p",{className:"text-sm text-gray-400 mt-2",children:"Try selecting a different date"})]})})]})}):a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-500",children:"Store not found"})})})}function o7(){const[e,t]=h.useState([]),[r,n]=h.useState(null),[i,s]=h.useState([]),[o,l]=h.useState(!0);h.useEffect(()=>{c()},[]),h.useEffect(()=>{r&&d()},[r]);const c=async()=>{try{const f=await B.getStores();t(f.stores),f.stores.length>0&&n(f.stores[0].id)}catch(f){console.error("Failed to load stores:",f)}finally{l(!1)}},d=async()=>{if(r)try{const f=await B.getCategoryTree(r);s(f.tree)}catch(f){console.error("Failed to load categories:",f)}};if(o)return a.jsx(X,{children:a.jsx("div",{children:"Loading..."})});const u=e.find(f=>f.id===r);return a.jsx(X,{children:a.jsxs("div",{children:[a.jsx("h1",{style:{fontSize:"32px",marginBottom:"30px"},children:"Categories"}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"10px",fontWeight:"600"},children:"Select Store:"}),a.jsx("select",{value:r||"",onChange:f=>n(parseInt(f.target.value)),style:{width:"100%",padding:"12px",border:"2px solid #667eea",borderRadius:"6px",fontSize:"16px",cursor:"pointer"},children:e.map(f=>a.jsxs("option",{value:f.id,children:[f.name," (",f.category_count||0," categories)"]},f.id))})]}),u&&a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsxs("h2",{style:{marginBottom:"20px",fontSize:"24px"},children:[u.name," - Categories"]}),i.length===0?a.jsx("div",{style:{color:"#999",textAlign:"center",padding:"40px"},children:'No categories found. Run "Discover Categories" from the Stores page.'}):a.jsx("div",{children:i.map(f=>a.jsx(kk,{category:f},f.id))})]})]})})}function kk({category:e,level:t=0}){return a.jsxs("div",{style:{marginLeft:t*30+"px",marginBottom:"10px"},children:[a.jsxs("div",{style:{padding:"12px 15px",background:t===0?"#f0f0ff":"#f8f9fa",borderRadius:"6px",borderLeft:`4px solid ${t===0?"#667eea":"#999"}`,display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{children:[a.jsxs("strong",{style:{fontSize:t===0?"16px":"14px"},children:[t>0&&"└── ",e.name]}),a.jsxs("span",{style:{marginLeft:"10px",color:"#666",fontSize:"12px"},children:["(",e.product_count||0," products)"]})]}),a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:a.jsx("code",{children:e.path})})]}),e.children&&e.children.length>0&&a.jsx("div",{style:{marginTop:"5px"},children:e.children.map(r=>a.jsx(kk,{category:r,level:t+1},r.id))})]})}function Vn({message:e,type:t,onClose:r,duration:n=4e3}){h.useEffect(()=>{const s=setTimeout(r,n);return()=>clearTimeout(s)},[n,r]);const i={success:"#10b981",error:"#ef4444",info:"#3b82f6"};return a.jsxs("div",{style:{position:"fixed",top:"20px",right:"20px",background:i[t],color:"white",padding:"16px 24px",borderRadius:"8px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:9999,maxWidth:"400px",animation:"slideIn 0.3s ease-out",fontSize:"14px",fontWeight:"500"},children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"12px"},children:[a.jsx("div",{style:{flex:1,whiteSpace:"pre-wrap"},children:e}),a.jsx("button",{onClick:r,style:{background:"transparent",border:"none",color:"white",cursor:"pointer",fontSize:"18px",padding:"0 4px",opacity:.8},children:"×"})]}),a.jsx("style",{children:` + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + `})]})}function l7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(!1),[o,l]=h.useState(null);h.useEffect(()=>{c()},[]);const c=async()=>{n(!0);try{const u=await B.getCampaigns();t(u.campaigns)}catch(u){console.error("Failed to load campaigns:",u)}finally{n(!1)}},d=async(u,f)=>{if(confirm(`Are you sure you want to delete campaign "${f}"?`))try{await B.deleteCampaign(u),c()}catch(p){l({message:"Failed to delete campaign: "+p.message,type:"error"})}};return r?a.jsx(X,{children:a.jsx("div",{children:"Loading..."})}):a.jsxs(X,{children:[o&&a.jsx(Vn,{message:o.message,type:o.type,onClose:()=>l(null)}),a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Campaigns"}),a.jsx("button",{onClick:()=>s(!0),style:{padding:"12px 24px",background:"#667eea",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontSize:"14px",fontWeight:"500"},children:"+ Create Campaign"})]}),i&&a.jsx(c7,{onClose:()=>s(!1),onSuccess:()=>{s(!1),c()}}),a.jsx("div",{style:{display:"grid",gap:"20px"},children:e.map(u=>a.jsx("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsxs("h3",{style:{marginBottom:"10px",fontSize:"20px"},children:[u.name,a.jsx("span",{style:{marginLeft:"10px",padding:"4px 8px",borderRadius:"4px",fontSize:"12px",background:u.active?"#d4edda":"#f8d7da",color:u.active?"#155724":"#721c24"},children:u.active?"Active":"Inactive"})]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"5px"},children:[a.jsx("strong",{children:"Slug:"})," ",u.slug]}),u.description&&a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"5px"},children:[a.jsx("strong",{children:"Description:"})," ",u.description]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"5px"},children:[a.jsx("strong",{children:"Display Style:"})," ",u.display_style]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666"},children:[a.jsx("strong",{children:"Products:"})," ",u.product_count||0]})]}),a.jsx("div",{style:{display:"flex",gap:"10px"},children:a.jsx("button",{onClick:()=>d(u.id,u.name),style:{padding:"8px 16px",background:"#e74c3c",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontSize:"14px"},children:"Delete"})})]})},u.id))}),e.length===0&&!i&&a.jsx("div",{style:{background:"white",padding:"40px",borderRadius:"8px",textAlign:"center",color:"#999"},children:"No campaigns found"})]})]})}function c7({onClose:e,onSuccess:t}){const[r,n]=h.useState(""),[i,s]=h.useState(""),[o,l]=h.useState(""),[c,d]=h.useState("grid"),[u,f]=h.useState(!0),[p,m]=h.useState(!1),[x,g]=h.useState(null),b=async v=>{v.preventDefault(),m(!0);try{await B.createCampaign({name:r,slug:i,description:o,display_style:c,active:u}),t()}catch(j){g({message:"Failed to create campaign: "+j.message,type:"error"})}finally{m(!1)}};return a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"20px"},children:[x&&a.jsx(Vn,{message:x.message,type:x.type,onClose:()=>g(null)}),a.jsx("h3",{style:{marginBottom:"20px"},children:"Create New Campaign"}),a.jsxs("form",{onSubmit:b,children:[a.jsxs("div",{style:{display:"grid",gap:"15px"},children:[a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Name *"}),a.jsx("input",{type:"text",value:r,onChange:v=>n(v.target.value),required:!0,style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Slug *"}),a.jsx("input",{type:"text",value:i,onChange:v=>s(v.target.value),required:!0,placeholder:"e.g., summer-sale",style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Description"}),a.jsx("textarea",{value:o,onChange:v=>l(v.target.value),rows:3,style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Display Style"}),a.jsxs("select",{value:c,onChange:v=>d(v.target.value),style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"grid",children:"Grid"}),a.jsx("option",{value:"list",children:"List"}),a.jsx("option",{value:"carousel",children:"Carousel"})]})]}),a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px"},children:[a.jsx("input",{type:"checkbox",id:"active",checked:u,onChange:v=>f(v.target.checked)}),a.jsx("label",{htmlFor:"active",children:"Active"})]})]}),a.jsxs("div",{style:{display:"flex",gap:"10px",marginTop:"20px"},children:[a.jsx("button",{type:"submit",disabled:p,style:{padding:"10px 20px",background:p?"#999":"#667eea",color:"white",border:"none",borderRadius:"6px",cursor:p?"not-allowed":"pointer",fontSize:"14px"},children:p?"Creating...":"Create Campaign"}),a.jsx("button",{type:"button",onClick:e,style:{padding:"10px 20px",background:"#ddd",color:"#333",border:"none",borderRadius:"6px",cursor:"pointer",fontSize:"14px"},children:"Cancel"})]})]})]})}function u7(){var l,c,d,u;const[e,t]=h.useState(null),[r,n]=h.useState(!0),[i,s]=h.useState(30);h.useEffect(()=>{o()},[i]);const o=async()=>{n(!0);try{const f=await B.getAnalyticsOverview(i);t(f)}catch(f){console.error("Failed to load analytics:",f)}finally{n(!1)}};return r?a.jsx(X,{children:a.jsx("div",{children:"Loading..."})}):a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Analytics"}),a.jsxs("select",{value:i,onChange:f=>s(parseInt(f.target.value)),style:{padding:"10px 15px",border:"1px solid #ddd",borderRadius:"6px",fontSize:"14px"},children:[a.jsx("option",{value:7,children:"Last 7 days"}),a.jsx("option",{value:30,children:"Last 30 days"}),a.jsx("option",{value:90,children:"Last 90 days"}),a.jsx("option",{value:365,children:"Last year"})]})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(250px, 1fr))",gap:"20px",marginBottom:"30px"},children:[a.jsx(wv,{title:"Total Clicks",value:((l=e==null?void 0:e.overview)==null?void 0:l.total_clicks)||0,icon:"👆",color:"#3498db"}),a.jsx(wv,{title:"Unique Products Clicked",value:((c=e==null?void 0:e.overview)==null?void 0:c.unique_products)||0,icon:"📦",color:"#2ecc71"})]}),a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px"},children:[a.jsx("h3",{style:{marginBottom:"20px"},children:"Clicks Over Time"}),((d=e==null?void 0:e.clicks_by_day)==null?void 0:d.length)>0?a.jsx("div",{style:{overflowX:"auto"},children:a.jsx("div",{style:{display:"flex",alignItems:"flex-end",gap:"8px",minWidth:"600px",height:"200px"},children:e.clicks_by_day.slice().reverse().map((f,p)=>{const m=Math.max(...e.clicks_by_day.map(g=>g.clicks)),x=f.clicks/m*180;return a.jsxs("div",{style:{flex:1,display:"flex",flexDirection:"column",alignItems:"center"},children:[a.jsx("div",{style:{width:"100%",height:`${x}px`,background:"#667eea",borderRadius:"4px 4px 0 0",position:"relative",minHeight:"2px"},title:`${f.clicks} clicks`,children:a.jsx("div",{style:{position:"absolute",top:"-25px",left:"50%",transform:"translateX(-50%)",fontSize:"12px",fontWeight:"bold"},children:f.clicks})}),a.jsx("div",{style:{fontSize:"10px",marginTop:"5px",color:"#666",transform:"rotate(-45deg)",transformOrigin:"left",whiteSpace:"nowrap"},children:new Date(f.date).toLocaleDateString("en-US",{month:"short",day:"numeric"})})]},p)})})}):a.jsx("div",{style:{color:"#999",textAlign:"center",padding:"40px"},children:"No click data for this period"})]}),a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("h3",{style:{marginBottom:"20px"},children:"Top Products"}),((u=e==null?void 0:e.top_products)==null?void 0:u.length)>0?a.jsx("div",{children:e.top_products.map((f,p)=>a.jsxs("div",{style:{padding:"15px 0",borderBottom:p{u()},[]);const u=async()=>{n(!0);try{const x=await B.getSettings();t(x.settings)}catch(x){console.error("Failed to load settings:",x)}finally{n(!1)}},f=(x,g)=>{l(b=>({...b,[x]:g}))},p=async()=>{s(!0);try{const x=Object.entries(o).map(([g,b])=>({key:g,value:b}));await B.updateSettings(x),l({}),u(),d({message:"Settings saved successfully!",type:"success"})}catch(x){d({message:"Failed to save settings: "+x.message,type:"error"})}finally{s(!1)}},m=Object.keys(o).length>0;return r?a.jsx(X,{children:a.jsx("div",{children:"Loading..."})}):a.jsxs(X,{children:[c&&a.jsx(Vn,{message:c.message,type:c.type,onClose:()=>d(null)}),a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Settings"}),m&&a.jsx("button",{onClick:p,disabled:i,style:{padding:"12px 24px",background:i?"#999":"#2ecc71",color:"white",border:"none",borderRadius:"6px",cursor:i?"not-allowed":"pointer",fontSize:"14px",fontWeight:"500"},children:i?"Saving...":"Save Changes"})]}),a.jsx("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:a.jsx("div",{style:{display:"grid",gap:"25px"},children:e.map(x=>{const g=o[x.key]!==void 0?o[x.key]:x.value;return a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"500",fontSize:"16px"},children:f7(x.key)}),x.description&&a.jsx("div",{style:{fontSize:"13px",color:"#666",marginBottom:"8px"},children:x.description}),a.jsx("input",{type:"text",value:g,onChange:b=>f(x.key,b.target.value),style:{width:"100%",maxWidth:"500px",padding:"10px",border:o[x.key]!==void 0?"2px solid #667eea":"1px solid #ddd",borderRadius:"6px",fontSize:"14px"}}),a.jsxs("div",{style:{fontSize:"12px",color:"#999",marginTop:"5px"},children:["Last updated: ",new Date(x.updated_at).toLocaleString()]})]},x.key)})})}),m&&a.jsx("div",{style:{marginTop:"20px",padding:"15px",background:"#fff3cd",border:"1px solid #ffc107",borderRadius:"6px",color:"#856404"},children:'⚠️ You have unsaved changes. Click "Save Changes" to apply them.'})]})]})}function f7(e){return e.split("_").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function p7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(!1),[o,l]=h.useState({}),[c,d]=h.useState(null),[u,f]=h.useState(null);h.useEffect(()=>{p(),m()},[]),h.useEffect(()=>{if(!(c!=null&&c.id))return;if(c.status==="completed"||c.status==="cancelled"||c.status==="failed"){p();return}const P=setInterval(async()=>{try{const _=await B.getProxyTestJob(c.id);d(_.job),(_.job.status==="completed"||_.job.status==="cancelled"||_.job.status==="failed")&&(clearInterval(P),p())}catch(_){console.error("Failed to poll job status:",_)}},2e3);return()=>clearInterval(P)},[c==null?void 0:c.id]);const p=async()=>{n(!0);try{const N=await B.getProxies();t(N.proxies)}catch(N){console.error("Failed to load proxies:",N)}finally{n(!1)}},m=async()=>{try{const N=await B.getActiveProxyTestJob();N.job&&d(N.job)}catch{console.log("No active job found")}},x=async N=>{l(P=>({...P,[N]:!0}));try{await B.testProxy(N),p()}catch(P){f({message:"Test failed: "+P.message,type:"error"})}finally{l(P=>({...P,[N]:!1}))}},g=async N=>{l(P=>({...P,[N]:!0})),B.testProxy(N).then(()=>{p(),l(P=>({...P,[N]:!1}))}).catch(()=>{l(P=>({...P,[N]:!1}))})},b=async()=>{try{const N=await B.testAllProxies();f({message:"Proxy testing job started",type:"success"}),d({id:N.jobId,status:"pending",tested_proxies:0,total_proxies:e.length,passed_proxies:0,failed_proxies:0})}catch(N){f({message:"Failed to start testing: "+N.message,type:"error"})}},v=async()=>{if(c!=null&&c.id)try{await B.cancelProxyTestJob(c.id),f({message:"Job cancelled",type:"info"})}catch(N){f({message:"Failed to cancel job: "+N.message,type:"error"})}},j=async()=>{try{await B.updateProxyLocations(),f({message:"Location update job started",type:"success"})}catch(N){f({message:"Failed to start location update: "+N.message,type:"error"})}},y=async N=>{if(confirm("Delete this proxy?"))try{await B.deleteProxy(N),p()}catch(P){f({message:"Failed to delete proxy: "+P.message,type:"error"})}};if(r)return a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})});const w={total:e.length,passed:e.filter(N=>N.active).length,failed:e.filter(N=>!N.active).length},S=w.total>0?Math.round(w.passed/w.total*100):0;return a.jsxs(X,{children:[u&&a.jsx(Vn,{message:u.message,type:u.type,onClose:()=>f(null)}),a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(sl,{className:"w-6 h-6 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:"Proxies"}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:[w.total," total • ",w.passed," active • ",w.failed," inactive"]})]})]}),a.jsxs("div",{className:"flex gap-2",children:[a.jsxs("button",{onClick:b,disabled:!!c&&c.status!=="completed"&&c.status!=="cancelled"&&c.status!=="failed",className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Test All"]}),a.jsxs("button",{onClick:j,className:"inline-flex items-center gap-2 px-4 py-2 bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors text-sm font-medium",children:[a.jsx(yi,{className:"w-4 h-4"}),"Update Locations"]}),a.jsxs("button",{onClick:()=>s(!0),className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium",children:[a.jsx(Af,{className:"w-4 h-4"}),"Add Proxy"]})]})]}),a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-4 gap-4",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(sl,{className:"w-5 h-5 text-blue-600"})}),a.jsx("div",{className:"flex items-center gap-1 text-xs text-gray-500",children:a.jsxs("span",{children:[S,"% pass rate"]})})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Total Proxies"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:w.total})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-green-600",children:[a.jsx(zn,{className:"w-3 h-3"}),a.jsxs("span",{children:[Math.round(w.passed/w.total*100)||0,"%"]})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Active"}),a.jsx("p",{className:"text-3xl font-semibold text-green-600",children:w.passed}),a.jsx("p",{className:"text-xs text-gray-500",children:"Passing health checks"})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-red-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-red-600"})}),a.jsx("div",{className:"flex items-center gap-1 text-xs text-red-600",children:a.jsxs("span",{children:[Math.round(w.failed/w.total*100)||0,"%"]})})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Inactive"}),a.jsx("p",{className:"text-3xl font-semibold text-red-600",children:w.failed}),a.jsx("p",{className:"text-xs text-gray-500",children:"Failed health checks"})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsx("div",{className:"flex items-center justify-between mb-4",children:a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(zn,{className:"w-5 h-5 text-purple-600"})})}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Success Rate"}),a.jsxs("p",{className:"text-3xl font-semibold text-gray-900",children:[S,"%"]}),a.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2 mt-3",children:a.jsx("div",{className:"bg-green-600 h-2 rounded-full transition-all",style:{width:`${S}%`}})})]})]})]}),c&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex justify-between items-center mb-4",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Xt,{className:`w-5 h-5 text-blue-600 ${c.status==="running"?"animate-spin":""}`})}),a.jsxs("div",{children:[a.jsx("h3",{className:"font-semibold text-gray-900",children:"Proxy Testing Job"}),a.jsx("p",{className:"text-sm text-gray-500",children:c.status.charAt(0).toUpperCase()+c.status.slice(1)})]})]}),a.jsxs("div",{className:"flex gap-2",children:[c.status==="running"&&a.jsx("button",{onClick:v,className:"px-3 py-1.5 bg-red-50 text-red-700 rounded-lg hover:bg-red-100 transition-colors text-sm font-medium",children:"Cancel"}),(c.status==="completed"||c.status==="cancelled"||c.status==="failed")&&a.jsx("button",{onClick:()=>d(null),className:"px-3 py-1.5 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors text-sm font-medium",children:"Dismiss"})]})]}),a.jsxs("div",{className:"mb-4",children:[a.jsxs("div",{className:"text-sm text-gray-600 mb-2",children:["Progress: ",c.tested_proxies||0," / ",c.total_proxies||0," proxies tested"]}),a.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2",children:a.jsx("div",{className:`h-2 rounded-full transition-all ${c.status==="completed"?"bg-green-600":c.status==="cancelled"||c.status==="failed"?"bg-red-600":"bg-blue-600"}`,style:{width:`${(c.tested_proxies||0)/(c.total_proxies||100)*100}%`}})})]}),a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{className:"text-sm text-gray-600",children:"Passed:"}),a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-green-50 text-green-700 rounded",children:c.passed_proxies||0})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{className:"text-sm text-gray-600",children:"Failed:"}),a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-red-50 text-red-700 rounded",children:c.failed_proxies||0})]})]}),c.error&&a.jsxs("div",{className:"mt-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2",children:[a.jsx(Uc,{className:"w-5 h-5 text-red-600 flex-shrink-0 mt-0.5"}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm font-medium text-red-900",children:"Error"}),a.jsx("p",{className:"text-sm text-red-700",children:c.error})]})]})]}),i&&a.jsx(h7,{onClose:()=>s(!1),onSuccess:()=>{s(!1),p()}}),a.jsx("div",{className:"space-y-3",children:e.map(N=>a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 p-4 hover:shadow-lg transition-shadow",children:a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsxs("div",{className:"flex-1",children:[a.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[a.jsxs("h3",{className:"font-semibold text-gray-900",children:[N.protocol,"://",N.host,":",N.port]}),a.jsx("span",{className:`px-2 py-1 text-xs font-medium rounded ${N.active?"bg-green-50 text-green-700":"bg-red-50 text-red-700"}`,children:N.active?"Active":"Inactive"}),N.is_anonymous&&a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-blue-50 text-blue-700 rounded",children:"Anonymous"}),(N.city||N.state||N.country)&&a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-purple-50 text-purple-700 rounded flex items-center gap-1",children:[a.jsx(yi,{className:"w-3 h-3"}),N.city&&`${N.city}/`,N.state&&`${N.state.substring(0,2).toUpperCase()} `,N.country]})]}),a.jsx("div",{className:"text-sm text-gray-600",children:N.last_tested_at?a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx(xr,{className:"w-4 h-4 text-gray-400"}),a.jsxs("span",{children:["Last tested: ",new Date(N.last_tested_at).toLocaleString()]})]}),N.test_result==="success"?a.jsxs("span",{className:"flex items-center gap-1 text-green-600",children:[a.jsx(_r,{className:"w-4 h-4"}),"Success (",N.response_time_ms,"ms)"]}):a.jsxs("span",{className:"flex items-center gap-1 text-red-600",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Failed"]})]}):"Not tested yet"})]}),a.jsxs("div",{className:"flex gap-2",children:[N.active?a.jsx("button",{onClick:()=>x(N.id),disabled:o[N.id],className:"inline-flex items-center gap-1 px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors text-sm font-medium disabled:opacity-50",children:o[N.id]?a.jsx("div",{className:"w-4 h-4 border-2 border-blue-700 border-t-transparent rounded-full animate-spin"}):a.jsxs(a.Fragment,{children:[a.jsx(Xt,{className:"w-4 h-4"}),"Test"]})}):a.jsx("button",{onClick:()=>g(N.id),disabled:o[N.id],className:"inline-flex items-center gap-1 px-3 py-1.5 bg-yellow-50 text-yellow-700 rounded-lg hover:bg-yellow-100 transition-colors text-sm font-medium disabled:opacity-50",children:o[N.id]?a.jsx("div",{className:"w-4 h-4 border-2 border-yellow-700 border-t-transparent rounded-full animate-spin"}):a.jsxs(a.Fragment,{children:[a.jsx(Xt,{className:"w-4 h-4"}),"Retest"]})}),a.jsxs("button",{onClick:()=>y(N.id),className:"inline-flex items-center gap-1 px-3 py-1.5 bg-red-50 text-red-700 rounded-lg hover:bg-red-100 transition-colors text-sm font-medium",children:[a.jsx(RO,{className:"w-4 h-4"}),"Delete"]})]})]})},N.id))}),e.length===0&&!i&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-12 text-center",children:[a.jsx(sl,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-2",children:"No proxies configured"}),a.jsx("p",{className:"text-gray-500 mb-6",children:"Add your first proxy to get started with scraping"}),a.jsxs("button",{onClick:()=>s(!0),className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium",children:[a.jsx(Af,{className:"w-4 h-4"}),"Add Proxy"]})]})]})]})}function h7({onClose:e,onSuccess:t}){const[r,n]=h.useState("single"),[i,s]=h.useState(""),[o,l]=h.useState(""),[c,d]=h.useState("http"),[u,f]=h.useState(""),[p,m]=h.useState(""),[x,g]=h.useState(""),[b,v]=h.useState(!1),[j,y]=h.useState(null),w=_=>{if(_=_.trim(),!_||_.startsWith("#"))return null;let T;return T=_.match(/^(https?|socks5):\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/),T?{protocol:T[1],username:T[2],password:T[3],host:T[4],port:parseInt(T[5])}:(T=_.match(/^(https?|socks5):\/\/([^:]+):(\d+)$/),T?{protocol:T[1],host:T[2],port:parseInt(T[3])}:(T=_.match(/^([^:]+):(\d+):([^:]+):(.+)$/),T?{protocol:"http",host:T[1],port:parseInt(T[2]),username:T[3],password:T[4]}:(T=_.match(/^([^:]+):(\d+)$/),T?{protocol:"http",host:T[1],port:parseInt(T[2])}:null)))},S=async()=>{const T=x.split(` +`).map($=>w($)).filter($=>$!==null);if(T.length===0){y({message:"No valid proxies found. Please check the format.",type:"error"});return}v(!0);try{const $=await B.addProxiesBulk(T),M=`Import complete! + +Added: ${$.added} +Duplicates: ${$.duplicates||0} +Failed: ${$.failed} + +Proxies are inactive by default. Use "Test All Proxies" to verify and activate them.`;y({message:M,type:"success"}),t()}catch($){y({message:"Failed to import proxies: "+$.message,type:"error"})}finally{v(!1)}},N=async _=>{var M;const T=(M=_.target.files)==null?void 0:M[0];if(!T)return;const $=await T.text();g($)},P=async _=>{if(_.preventDefault(),r==="bulk"){await S();return}v(!0);try{await B.addProxy({host:i,port:parseInt(o),protocol:c,username:u||void 0,password:p||void 0}),t()}catch(T){y({message:"Failed to add proxy: "+T.message,type:"error"})}finally{v(!1)}};return a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200",children:[j&&a.jsx(Vn,{message:j.message,type:j.type,onClose:()=>y(null)}),a.jsxs("div",{className:"p-6",children:[a.jsxs("div",{className:"flex justify-between items-center mb-6",children:[a.jsx("h2",{className:"text-xl font-semibold text-gray-900",children:"Add Proxies"}),a.jsxs("div",{className:"flex bg-gray-100 rounded-lg p-1",children:[a.jsx("button",{type:"button",onClick:()=>n("single"),className:`px-4 py-2 rounded-md text-sm font-medium transition-colors ${r==="single"?"bg-white text-gray-900 shadow-sm":"text-gray-600 hover:text-gray-900"}`,children:"Single"}),a.jsx("button",{type:"button",onClick:()=>n("bulk"),className:`px-4 py-2 rounded-md text-sm font-medium transition-colors ${r==="bulk"?"bg-white text-gray-900 shadow-sm":"text-gray-600 hover:text-gray-900"}`,children:"Bulk Import"})]})]}),a.jsxs("form",{onSubmit:P,children:[r==="single"?a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Host *"}),a.jsx("input",{type:"text",value:i,onChange:_=>s(_.target.value),required:!0,placeholder:"proxy.example.com",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Port *"}),a.jsx("input",{type:"number",value:o,onChange:_=>l(_.target.value),required:!0,placeholder:"8080",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Protocol *"}),a.jsxs("select",{value:c,onChange:_=>d(_.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"http",children:"HTTP"}),a.jsx("option",{value:"https",children:"HTTPS"}),a.jsx("option",{value:"socks5",children:"SOCKS5"})]})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Username"}),a.jsx("input",{type:"text",value:u,onChange:_=>f(_.target.value),placeholder:"Optional",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]}),a.jsxs("div",{className:"md:col-span-2",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Password"}),a.jsx("input",{type:"password",value:p,onChange:_=>m(_.target.value),placeholder:"Optional",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]})]}):a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Upload File"}),a.jsxs("label",{className:"flex items-center justify-center w-full px-4 py-6 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition-colors",children:[a.jsxs("div",{className:"flex flex-col items-center",children:[a.jsx(HO,{className:"w-8 h-8 text-gray-400 mb-2"}),a.jsx("span",{className:"text-sm text-gray-600",children:"Click to upload or drag and drop"}),a.jsx("span",{className:"text-xs text-gray-500 mt-1",children:".txt or .list files"})]}),a.jsx("input",{type:"file",accept:".txt,.list",onChange:N,className:"hidden"})]})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Or Paste Proxies (one per line)"}),a.jsx("textarea",{value:x,onChange:_=>g(_.target.value),placeholder:`Supported formats: +host:port +protocol://host:port +host:port:username:password +protocol://username:password@host:port + +Example: +192.168.1.1:8080 +http://proxy.example.com:3128 +10.0.0.1:8080:user:pass +socks5://user:pass@proxy.example.com:1080`,rows:12,className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"})]}),a.jsxs("div",{className:"p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-2",children:[a.jsx(Uc,{className:"w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5"}),a.jsx("span",{className:"text-sm text-blue-900",children:"Lines starting with # are treated as comments and ignored."})]})]}),a.jsxs("div",{className:"flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200",children:[a.jsx("button",{type:"button",onClick:e,className:"px-4 py-2 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors font-medium",children:"Cancel"}),a.jsx("button",{type:"submit",disabled:b,className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed",children:b?a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"}),a.jsx("span",{children:"Processing..."})]}):a.jsxs(a.Fragment,{children:[a.jsx(Af,{className:"w-4 h-4"}),r==="bulk"?"Import Proxies":"Add Proxy"]})})]})]})]})]})}function m7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(!0),[o,l]=h.useState(""),[c,d]=h.useState(""),[u,f]=h.useState(200),[p,m]=h.useState(null),x=h.useRef(null);h.useEffect(()=>{g()},[o,c,u]),h.useEffect(()=>{if(!i)return;const S=setInterval(()=>{g()},2e3);return()=>clearInterval(S)},[i,o,c,u]);const g=async()=>{try{const S=await B.getLogs(u,o,c);t(S.logs)}catch(S){console.error("Failed to load logs:",S)}finally{n(!1)}},b=async()=>{if(confirm("Are you sure you want to clear all logs?"))try{await B.clearLogs(),t([]),m({message:"Logs cleared successfully",type:"success"})}catch(S){m({message:"Failed to clear logs: "+S.message,type:"error"})}},v=()=>{var S;(S=x.current)==null||S.scrollIntoView({behavior:"smooth"})},j=S=>{switch(S){case"error":return"#dc3545";case"warn":return"#ffc107";case"info":return"#17a2b8";case"debug":return"#6c757d";default:return"#333"}},y=S=>{switch(S){case"error":return"#f8d7da";case"warn":return"#fff3cd";case"info":return"#d1ecf1";case"debug":return"#e2e3e5";default:return"#f8f9fa"}},w=S=>{switch(S){case"scraper":return"🔍";case"images":return"📸";case"categories":return"📂";case"system":return"⚙️";case"api":return"🌐";default:return"📝"}};return r?a.jsx(X,{children:a.jsx("div",{children:"Loading logs..."})}):a.jsxs(X,{children:[p&&a.jsx(Vn,{message:p.message,type:p.type,onClose:()=>m(null)}),a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"20px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"System Logs"}),a.jsxs("div",{style:{display:"flex",gap:"10px",alignItems:"center"},children:[a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"5px"},children:[a.jsx("input",{type:"checkbox",checked:i,onChange:S=>s(S.target.checked)}),"Auto-refresh (2s)"]}),a.jsx("button",{onClick:v,style:{padding:"8px 16px",background:"#6c757d",color:"white",border:"none",borderRadius:"6px",cursor:"pointer"},children:"⬇️ Scroll to Bottom"}),a.jsx("button",{onClick:b,style:{padding:"8px 16px",background:"#dc3545",color:"white",border:"none",borderRadius:"6px",cursor:"pointer"},children:"🗑️ Clear Logs"})]})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"20px",display:"flex",gap:"15px",flexWrap:"wrap"},children:[a.jsxs("select",{value:o,onChange:S=>l(S.target.value),style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"",children:"All Levels"}),a.jsx("option",{value:"info",children:"Info"}),a.jsx("option",{value:"warn",children:"Warning"}),a.jsx("option",{value:"error",children:"Error"}),a.jsx("option",{value:"debug",children:"Debug"})]}),a.jsxs("select",{value:c,onChange:S=>d(S.target.value),style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"",children:"All Categories"}),a.jsx("option",{value:"scraper",children:"🔍 Scraper"}),a.jsx("option",{value:"images",children:"📸 Images"}),a.jsx("option",{value:"categories",children:"📂 Categories"}),a.jsx("option",{value:"system",children:"⚙️ System"}),a.jsx("option",{value:"api",children:"🌐 API"})]}),a.jsxs("select",{value:u,onChange:S=>f(parseInt(S.target.value)),style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"50",children:"Last 50"}),a.jsx("option",{value:"100",children:"Last 100"}),a.jsx("option",{value:"200",children:"Last 200"}),a.jsx("option",{value:"500",children:"Last 500"}),a.jsx("option",{value:"1000",children:"Last 1000"})]}),a.jsx("div",{style:{marginLeft:"auto",display:"flex",alignItems:"center",gap:"10px"},children:a.jsxs("span",{style:{fontSize:"14px",color:"#666"},children:["Showing ",e.length," logs"]})})]}),a.jsxs("div",{style:{background:"#1e1e1e",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",maxHeight:"70vh",overflowY:"auto",fontFamily:"monospace",fontSize:"13px"},children:[e.length===0?a.jsx("div",{style:{color:"#999",textAlign:"center",padding:"40px"},children:"No logs to display"}):e.map((S,N)=>a.jsxs("div",{style:{padding:"8px 12px",marginBottom:"4px",borderRadius:"4px",background:y(S.level),borderLeft:`4px solid ${j(S.level)}`,display:"flex",gap:"10px",alignItems:"flex-start"},children:[a.jsx("span",{style:{color:"#666",fontSize:"11px",whiteSpace:"nowrap"},children:new Date(S.timestamp).toLocaleTimeString()}),a.jsx("span",{style:{fontSize:"14px"},children:w(S.category)}),a.jsx("span",{style:{padding:"2px 6px",borderRadius:"3px",fontSize:"10px",fontWeight:"bold",background:j(S.level),color:"white",textTransform:"uppercase"},children:S.level}),a.jsx("span",{style:{padding:"2px 6px",borderRadius:"3px",fontSize:"10px",background:"#e2e3e5",color:"#333"},children:S.category}),a.jsx("span",{style:{flex:1,color:"#333",wordBreak:"break-word"},children:S.message})]},N)),a.jsx("div",{ref:x})]})]})]})}function g7(){const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState([]),[u,f]=h.useState([]),[p,m]=h.useState(!0),[x,g]=h.useState(!0),[b,v]=h.useState("az-live"),[j,y]=h.useState(null),[w,S]=h.useState(""),[N,P]=h.useState(null),[_,T]=h.useState({scheduledJobs:[],crawlJobs:[],inMemoryScrapers:[],totalActive:0}),[$,M]=h.useState({jobLogs:[],crawlJobs:[]}),[C,R]=h.useState([]);h.useEffect(()=>{if(q(),x){const E=setInterval(q,3e3);return()=>clearInterval(E)}},[x]);const q=async()=>{try{const[E,D,O,k,L,U]=await Promise.all([B.getActiveScrapers(),B.getScraperHistory(),B.getJobStats(),B.getActiveJobs(),B.getWorkerStats(),B.getRecentJobs({limit:50})]);t(E.scrapers||[]),n(D.history||[]),s(O),l(k.jobs||[]),d(L.workers||[]),f(U.jobs||[]);const[H,te,re,we]=await Promise.all([B.getAZMonitorSummary().catch(()=>null),B.getAZMonitorActiveJobs().catch(()=>({scheduledJobs:[],crawlJobs:[],inMemoryScrapers:[],totalActive:0})),B.getAZMonitorRecentJobs(30).catch(()=>({jobLogs:[],crawlJobs:[]})),B.getAZMonitorErrors({limit:10,hours:24}).catch(()=>({errors:[]}))]);P(H),T(te),M(re),R((we==null?void 0:we.errors)||[])}catch(E){console.error("Failed to load scraper data:",E)}finally{m(!1)}},Z=E=>{const D=Math.floor(E/1e3),O=Math.floor(D/60),k=Math.floor(O/60);return k>0?`${k}h ${O%60}m ${D%60}s`:O>0?`${O}m ${D%60}s`:`${D}s`};return a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Scraper Monitor"}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:x,onChange:E=>g(E.target.checked),style:{width:"18px",height:"18px",cursor:"pointer"}}),a.jsx("span",{children:"Auto-refresh (3s)"})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"10px",borderBottom:"2px solid #eee"},children:[a.jsxs("button",{onClick:()=>v("az-live"),style:{padding:"12px 24px",background:b==="az-live"?"white":"transparent",border:"none",borderBottom:b==="az-live"?"3px solid #10b981":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:b==="az-live"?"600":"400",color:b==="az-live"?"#10b981":"#666",marginBottom:"-2px"},children:["AZ Live ",_.totalActive>0&&a.jsx("span",{style:{marginLeft:"8px",padding:"2px 8px",background:"#10b981",color:"white",borderRadius:"10px",fontSize:"12px"},children:_.totalActive})]}),a.jsx("button",{onClick:()=>v("jobs"),style:{padding:"12px 24px",background:b==="jobs"?"white":"transparent",border:"none",borderBottom:b==="jobs"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:b==="jobs"?"600":"400",color:b==="jobs"?"#2563eb":"#666",marginBottom:"-2px"},children:"Dispensary Jobs"}),a.jsx("button",{onClick:()=>v("scrapers"),style:{padding:"12px 24px",background:b==="scrapers"?"white":"transparent",border:"none",borderBottom:b==="scrapers"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:b==="scrapers"?"600":"400",color:b==="scrapers"?"#2563eb":"#666",marginBottom:"-2px"},children:"Crawl History"})]}),b==="az-live"&&a.jsxs(a.Fragment,{children:[N&&a.jsx("div",{style:{marginBottom:"30px"},children:a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(180px, 1fr))",gap:"15px"},children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Running Jobs"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:_.totalActive>0?"#10b981":"#666"},children:_.totalActive})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Successful (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#10b981"},children:(N.successful_jobs_24h||0)+(N.successful_crawls_24h||0)})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Failed (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:(N.failed_jobs_24h||0)+(N.failed_crawls_24h||0)>0?"#ef4444":"#666"},children:(N.failed_jobs_24h||0)+(N.failed_crawls_24h||0)})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Products (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#8b5cf6"},children:N.products_found_24h||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Snapshots (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#06b6d4"},children:N.snapshots_created_24h||0})]})]})}),a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px",display:"flex",alignItems:"center",gap:"10px"},children:["Active Jobs",_.totalActive>0&&a.jsxs("span",{style:{padding:"4px 12px",background:"#d1fae5",color:"#065f46",borderRadius:"12px",fontSize:"14px",fontWeight:"600"},children:[_.totalActive," running"]})]}),_.totalActive===0?a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"😴"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"No jobs currently running"})]}):a.jsxs("div",{style:{display:"grid",gap:"15px"},children:[_.scheduledJobs.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #10b981"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsx("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:E.job_name}),a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:E.job_description||"Scheduled job"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(120px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Processed"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600"},children:E.items_processed||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Succeeded"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#10b981"},children:E.items_succeeded||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Failed"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:E.items_failed>0?"#ef4444":"#666"},children:E.items_failed||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[Math.floor((E.duration_seconds||0)/60),"m ",Math.floor((E.duration_seconds||0)%60),"s"]})]})]})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#d1fae5",color:"#065f46"},children:"RUNNING"})]})},`sched-${E.id}`)),_.crawlJobs.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #3b82f6"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsx("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:E.dispensary_name||"Unknown Store"}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:[E.city," | ",E.job_type||"crawl"]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(120px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#8b5cf6"},children:E.products_found||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Snapshots"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#06b6d4"},children:E.snapshots_created||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[Math.floor((E.duration_seconds||0)/60),"m ",Math.floor((E.duration_seconds||0)%60),"s"]})]})]})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#dbeafe",color:"#1e40af"},children:"CRAWLING"})]})},`crawl-${E.id}`))]})]}),(N==null?void 0:N.nextRuns)&&N.nextRuns.length>0&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:"Next Scheduled Runs"}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Job"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Next Run"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Last Status"})]})}),a.jsx("tbody",{children:N.nextRuns.map(E=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:E.job_name}),a.jsx("div",{style:{fontSize:"13px",color:"#666"},children:E.description})]}),a.jsx("td",{style:{padding:"15px"},children:a.jsx("div",{style:{fontWeight:"600",color:"#2563eb"},children:E.next_run_at?new Date(E.next_run_at).toLocaleString():"-"})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.last_status==="success"?"#d1fae5":E.last_status==="error"?"#fee2e2":"#fef3c7",color:E.last_status==="success"?"#065f46":E.last_status==="error"?"#991b1b":"#92400e"},children:E.last_status||"never"})})]},E.id))})]})})]}),C.length>0&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h2",{style:{fontSize:"24px",marginBottom:"20px",color:"#ef4444"},children:"Recent Errors (24h)"}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:C.map((E,D)=>a.jsxs("div",{style:{padding:"15px",borderBottom:Da.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:E.job_name}),a.jsxs("div",{style:{fontSize:"12px",color:"#999"},children:["Log #",E.id]})]}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.status==="success"?"#d1fae5":E.status==="running"?"#dbeafe":E.status==="error"?"#fee2e2":"#fef3c7",color:E.status==="success"?"#065f46":E.status==="running"?"#1e40af":E.status==="error"?"#991b1b":"#92400e"},children:E.status})}),a.jsxs("td",{style:{padding:"15px",textAlign:"right"},children:[a.jsx("span",{style:{color:"#10b981"},children:E.items_succeeded||0})," / ",a.jsx("span",{children:E.items_processed||0})]}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:E.duration_ms?`${Math.floor(E.duration_ms/6e4)}m ${Math.floor(E.duration_ms%6e4/1e3)}s`:"-"}),a.jsx("td",{style:{padding:"15px",color:"#666"},children:E.completed_at?new Date(E.completed_at).toLocaleString():"-"})]},`log-${E.id}`))})]})})]})]}),b==="jobs"&&a.jsxs(a.Fragment,{children:[i&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:"Job Statistics"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(200px, 1fr))",gap:"15px"},children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Pending"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#f59e0b"},children:i.pending||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"In Progress"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#3b82f6"},children:i.in_progress||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Completed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#10b981"},children:i.completed||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Failed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#ef4444"},children:i.failed||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#8b5cf6"},children:i.total_products_found||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Products Saved"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#06b6d4"},children:i.total_products_saved||0})]})]})]}),c.length>0&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Active Workers (",c.length,")"]}),a.jsx("div",{style:{display:"grid",gap:"15px"},children:c.map(E=>a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #10b981"},children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"12px"},children:["Worker: ",E.worker_id]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#d1fae5",color:"#065f46"},children:"ACTIVE"})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Active Jobs"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600"},children:E.active_jobs})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#8b5cf6"},children:E.total_products_found||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Saved"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#10b981"},children:E.total_products_saved||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Running Since"}),a.jsx("div",{style:{fontSize:"14px"},children:new Date(E.earliest_start).toLocaleTimeString()})]})]})]},E.worker_id))})]}),a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Active Jobs (",o.length,")"]}),o.length===0?a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"😴"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"No jobs currently running"})]}):a.jsx("div",{style:{display:"grid",gap:"15px"},children:o.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #3b82f6"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsx("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:E.dispensary_name||E.brand_name}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:[E.job_type||"crawl"," | Job #",E.id]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#8b5cf6"},children:E.products_found||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Saved"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#10b981"},children:E.products_saved||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[Math.floor(E.duration_seconds/60),"m ",Math.floor(E.duration_seconds%60),"s"]})]})]})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#dbeafe",color:"#1e40af"},children:"IN PROGRESS"})]})},E.id))})]}),a.jsxs("div",{children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Recent Jobs (",u.length,")"]}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Type"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Found"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Saved"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Duration"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Completed"})]})}),a.jsx("tbody",{children:u.map(E=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsx("td",{style:{padding:"15px"},children:E.dispensary_name||E.brand_name}),a.jsx("td",{style:{padding:"15px",fontSize:"14px",color:"#666"},children:E.job_type||"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.status==="completed"?"#d1fae5":E.status==="in_progress"?"#dbeafe":E.status==="failed"?"#fee2e2":"#fef3c7",color:E.status==="completed"?"#065f46":E.status==="in_progress"?"#1e40af":E.status==="failed"?"#991b1b":"#92400e"},children:E.status})}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E.products_found||0}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600",color:"#10b981"},children:E.products_saved||0}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:E.duration_seconds?`${Math.floor(E.duration_seconds/60)}m ${Math.floor(E.duration_seconds%60)}s`:"-"}),a.jsx("td",{style:{padding:"15px",color:"#666"},children:E.completed_at?new Date(E.completed_at).toLocaleString():"-"})]},E.id))})]})})]})]}),b==="scrapers"&&a.jsxs(a.Fragment,{children:[a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Active Scrapers (",e.length,")"]}),e.length===0?a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"🛌"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"No scrapers currently running"})]}):a.jsx("div",{style:{display:"grid",gap:"15px"},children:e.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:`4px solid ${E.status==="running"?E.isStale?"#ff9800":"#2ecc71":E.status==="error"?"#e74c3c":"#95a5a6"}`},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsxs("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:[E.storeName," - ",E.categoryName]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:["ID: ",E.id]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Requests"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[E.stats.requestsSuccess," / ",E.stats.requestsTotal]})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Items Saved"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#2ecc71"},children:E.stats.itemsSaved})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Items Dropped"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#e74c3c"},children:E.stats.itemsDropped})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Errors"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:E.stats.errorsCount>0?"#ff9800":"#999"},children:E.stats.errorsCount})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600"},children:Z(E.duration)})]})]}),E.currentActivity&&a.jsxs("div",{style:{marginTop:"12px",padding:"8px 12px",background:"#f8f8f8",borderRadius:"4px",fontSize:"14px",color:"#666"},children:["📍 ",E.currentActivity]}),E.isStale&&a.jsx("div",{style:{marginTop:"12px",padding:"8px 12px",background:"#fff3cd",borderRadius:"4px",fontSize:"14px",color:"#856404"},children:"⚠️ No update in over 1 minute - scraper may be stuck"})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:E.status==="running"?"#d4edda":E.status==="error"?"#f8d7da":"#e7e7e7",color:E.status==="running"?"#155724":E.status==="error"?"#721c24":"#666"},children:E.status.toUpperCase()})]})},E.id))})]}),a.jsxs("div",{children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Recent Scrapes (",r.length,")"]}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Found"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Products"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Last Crawled"})]})}),a.jsx("tbody",{children:r.map((E,D)=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsx("td",{style:{padding:"15px"},children:E.dispensary_name||E.store_name}),a.jsx("td",{style:{padding:"15px"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.status==="completed"?"#d1fae5":E.status==="failed"?"#fee2e2":"#fef3c7",color:E.status==="completed"?"#065f46":E.status==="failed"?"#991b1b":"#92400e"},children:E.status||"-"})}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E.products_found||"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E.product_count}),a.jsx("td",{style:{padding:"15px",color:"#666"},children:E.last_scraped_at?new Date(E.last_scraped_at).toLocaleString():"-"})]},D))})]})})]})]})]})})}function x7(){var we;const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState([]),[o,l]=h.useState(!0),[c,d]=h.useState(!0),[u,f]=h.useState("dispensaries"),[p,m]=h.useState(null),[x,g]=h.useState(null),[b,v]=h.useState(null),[j,y]=h.useState(null),[w,S]=h.useState(!1),[N,P]=h.useState("all"),[_,T]=h.useState(""),[$,M]=h.useState("");h.useEffect(()=>{const A=setTimeout(()=>{T($)},300);return()=>clearTimeout(A)},[$]),h.useEffect(()=>{if(C(),c){const A=setInterval(C,5e3);return()=>clearInterval(A)}},[c,N,_]);const C=async()=>{try{const A={};N==="AZ"&&(A.state="AZ"),_.trim()&&(A.search=_.trim());const[J,Ot,z]=await Promise.all([B.getGlobalSchedule(),B.getDispensarySchedules(Object.keys(A).length>0?A:void 0),B.getDispensaryCrawlJobs(100)]);t(J.schedules||[]),n(Ot.dispensaries||[]),s(z.jobs||[])}catch(A){console.error("Failed to load schedule data:",A)}finally{l(!1)}},R=async A=>{m(A);try{await B.triggerDispensaryCrawl(A),await C()}catch(J){console.error("Failed to trigger crawl:",J)}finally{m(null)}},q=async()=>{if(confirm("This will create crawl jobs for ALL active stores. Continue?"))try{const A=await B.triggerAllCrawls();alert(`Created ${A.jobs_created} crawl jobs`),await C()}catch(A){console.error("Failed to trigger all crawls:",A)}},Z=async A=>{try{await B.cancelCrawlJob(A),await C()}catch(J){console.error("Failed to cancel job:",J)}},E=async A=>{g(A);try{const J=await B.resolvePlatformId(A);J.success?alert(J.message):alert(`Failed: ${J.error||J.message}`),await C()}catch(J){console.error("Failed to resolve platform ID:",J),alert(`Error: ${J.message}`)}finally{g(null)}},D=async A=>{v(A);try{const J=await B.refreshDetection(A);alert(`Detected: ${J.menu_type}${J.platform_dispensary_id?`, Platform ID: ${J.platform_dispensary_id}`:""}`),await C()}catch(J){console.error("Failed to refresh detection:",J),alert(`Error: ${J.message}`)}finally{v(null)}},O=async(A,J)=>{y(A);try{await B.toggleDispensarySchedule(A,!J),await C()}catch(Ot){console.error("Failed to toggle schedule:",Ot),alert(`Error: ${Ot.message}`)}finally{y(null)}},k=async(A,J)=>{try{await B.updateGlobalSchedule(A,J),await C()}catch(Ot){console.error("Failed to update global schedule:",Ot)}},L=A=>{if(!A)return"Never";const J=new Date(A),z=new Date().getTime()-J.getTime(),ee=Math.floor(z/6e4),ne=Math.floor(ee/60),W=Math.floor(ne/24);return ee<1?"Just now":ee<60?`${ee}m ago`:ne<24?`${ne}h ago`:`${W}d ago`},U=A=>{const J=new Date(A),Ot=new Date,z=J.getTime()-Ot.getTime();if(z<0)return"Overdue";const ee=Math.floor(z/6e4),ne=Math.floor(ee/60);return ee<60?`${ee}m`:`${ne}h ${ee%60}m`},H=A=>{switch(A){case"completed":case"success":return{bg:"#d1fae5",color:"#065f46"};case"running":return{bg:"#dbeafe",color:"#1e40af"};case"failed":case"error":return{bg:"#fee2e2",color:"#991b1b"};case"cancelled":return{bg:"#f3f4f6",color:"#374151"};case"pending":return{bg:"#fef3c7",color:"#92400e"};case"sandbox_only":return{bg:"#e0e7ff",color:"#3730a3"};case"detection_only":return{bg:"#fce7f3",color:"#9d174d"};default:return{bg:"#f3f4f6",color:"#374151"}}},te=e.find(A=>A.schedule_type==="global_interval"),re=e.find(A=>A.schedule_type==="daily_special");return a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Crawler Schedule"}),a.jsxs("div",{style:{display:"flex",gap:"15px",alignItems:"center"},children:[a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:c,onChange:A=>d(A.target.checked),style:{width:"18px",height:"18px",cursor:"pointer"}}),a.jsx("span",{children:"Auto-refresh (5s)"})]}),a.jsx("button",{onClick:q,style:{padding:"10px 20px",background:"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:"Crawl All Stores"})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"10px",borderBottom:"2px solid #eee"},children:[a.jsxs("button",{onClick:()=>f("dispensaries"),style:{padding:"12px 24px",background:u==="dispensaries"?"white":"transparent",border:"none",borderBottom:u==="dispensaries"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:u==="dispensaries"?"600":"400",color:u==="dispensaries"?"#2563eb":"#666",marginBottom:"-2px"},children:["Dispensary Schedules (",r.length,")"]}),a.jsxs("button",{onClick:()=>f("jobs"),style:{padding:"12px 24px",background:u==="jobs"?"white":"transparent",border:"none",borderBottom:u==="jobs"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:u==="jobs"?"600":"400",color:u==="jobs"?"#2563eb":"#666",marginBottom:"-2px"},children:["Job Queue (",i.filter(A=>A.status==="pending"||A.status==="running").length,")"]}),a.jsx("button",{onClick:()=>f("global"),style:{padding:"12px 24px",background:u==="global"?"white":"transparent",border:"none",borderBottom:u==="global"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:u==="global"?"600":"400",color:u==="global"?"#2563eb":"#666",marginBottom:"-2px"},children:"Global Settings"})]}),u==="global"&&a.jsxs("div",{style:{display:"grid",gap:"20px"},children:[a.jsxs("div",{style:{background:"white",padding:"24px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start",marginBottom:"20px"},children:[a.jsxs("div",{children:[a.jsx("h2",{style:{fontSize:"20px",margin:0,marginBottom:"8px"},children:"Interval Crawl Schedule"}),a.jsx("p",{style:{color:"#666",margin:0},children:"Crawl all stores periodically"})]}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("span",{style:{color:"#666"},children:"Enabled"}),a.jsx("input",{type:"checkbox",checked:(te==null?void 0:te.enabled)??!0,onChange:A=>k("global_interval",{enabled:A.target.checked}),style:{width:"20px",height:"20px",cursor:"pointer"}})]})]}),a.jsx("div",{style:{display:"flex",alignItems:"center",gap:"15px"},children:a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px"},children:[a.jsx("span",{children:"Crawl every"}),a.jsxs("select",{value:(te==null?void 0:te.interval_hours)??4,onChange:A=>k("global_interval",{interval_hours:parseInt(A.target.value)}),style:{padding:"8px 12px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"16px"},children:[a.jsx("option",{value:1,children:"1 hour"}),a.jsx("option",{value:2,children:"2 hours"}),a.jsx("option",{value:4,children:"4 hours"}),a.jsx("option",{value:6,children:"6 hours"}),a.jsx("option",{value:8,children:"8 hours"}),a.jsx("option",{value:12,children:"12 hours"}),a.jsx("option",{value:24,children:"24 hours"})]})]})})]}),a.jsxs("div",{style:{background:"white",padding:"24px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start",marginBottom:"20px"},children:[a.jsxs("div",{children:[a.jsx("h2",{style:{fontSize:"20px",margin:0,marginBottom:"8px"},children:"Daily Special Crawl"}),a.jsx("p",{style:{color:"#666",margin:0},children:"Crawl stores at local midnight to capture daily specials"})]}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("span",{style:{color:"#666"},children:"Enabled"}),a.jsx("input",{type:"checkbox",checked:(re==null?void 0:re.enabled)??!0,onChange:A=>k("daily_special",{enabled:A.target.checked}),style:{width:"20px",height:"20px",cursor:"pointer"}})]})]}),a.jsx("div",{style:{display:"flex",alignItems:"center",gap:"15px"},children:a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px"},children:[a.jsx("span",{children:"Run at"}),a.jsx("input",{type:"time",value:((we=re==null?void 0:re.run_time)==null?void 0:we.slice(0,5))??"00:01",onChange:A=>k("daily_special",{run_time:A.target.value}),style:{padding:"8px 12px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"16px"}}),a.jsx("span",{style:{color:"#666"},children:"(store local time)"})]})})]})]}),u==="dispensaries"&&a.jsxs("div",{children:[a.jsxs("div",{style:{marginBottom:"15px",display:"flex",gap:"20px",alignItems:"center",flexWrap:"wrap"},children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[a.jsx("span",{style:{fontWeight:"500",color:"#374151"},children:"State:"}),a.jsxs("div",{style:{display:"flex",borderRadius:"6px",overflow:"hidden",border:"1px solid #d1d5db"},children:[a.jsx("button",{onClick:()=>P("all"),style:{padding:"6px 14px",background:N==="all"?"#2563eb":"white",color:N==="all"?"white":"#374151",border:"none",cursor:"pointer",fontSize:"14px",fontWeight:"500"},children:"All"}),a.jsx("button",{onClick:()=>P("AZ"),style:{padding:"6px 14px",background:N==="AZ"?"#2563eb":"white",color:N==="AZ"?"white":"#374151",border:"none",borderLeft:"1px solid #d1d5db",cursor:"pointer",fontSize:"14px",fontWeight:"500"},children:"AZ Only"})]})]}),a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[a.jsx("span",{style:{fontWeight:"500",color:"#374151"},children:"Search:"}),a.jsx("input",{type:"text",placeholder:"Store name or slug...",value:$,onChange:A=>M(A.target.value),style:{padding:"6px 12px",borderRadius:"6px",border:"1px solid #d1d5db",fontSize:"14px",width:"200px"}}),$&&a.jsx("button",{onClick:()=>{M(""),T("")},style:{padding:"4px 8px",background:"#f3f4f6",border:"1px solid #d1d5db",borderRadius:"4px",cursor:"pointer",fontSize:"12px"},children:"Clear"})]}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"8px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:w,onChange:A=>S(A.target.checked),style:{width:"16px",height:"16px",cursor:"pointer"}}),a.jsx("span",{children:"Dutchie only"})]}),a.jsxs("span",{style:{color:"#666",fontSize:"14px",marginLeft:"auto"},children:["Showing ",(w?r.filter(A=>A.menu_type==="dutchie"):r).length," dispensaries"]})]}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"auto"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",minWidth:"1200px"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600"},children:"Menu Type"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600"},children:"Platform ID"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Last Run"}),a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Next Run"}),a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Last Result"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600",minWidth:"220px"},children:"Actions"})]})}),a.jsx("tbody",{children:(w?r.filter(A=>A.menu_type==="dutchie"):r).map(A=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"12px"},children:[a.jsx("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:A.state&&A.city&&(A.dispensary_slug||A.slug)?a.jsx(K1,{to:`/dispensaries/${A.state}/${A.city.toLowerCase().replace(/\s+/g,"-")}/${A.dispensary_slug||A.slug}`,style:{fontWeight:"600",color:"#2563eb",textDecoration:"none"},children:A.dispensary_name}):a.jsx("span",{style:{fontWeight:"600"},children:A.dispensary_name})}),a.jsx("div",{style:{fontSize:"12px",color:"#666"},children:A.city?`${A.city}, ${A.state}`:A.state})]}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:A.menu_type?a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"11px",fontWeight:"600",background:A.menu_type==="dutchie"?"#d1fae5":"#e0e7ff",color:A.menu_type==="dutchie"?"#065f46":"#3730a3"},children:A.menu_type}):a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"11px",fontWeight:"600",background:"#f3f4f6",color:"#666"},children:"unknown"})}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:A.platform_dispensary_id?a.jsx("span",{style:{padding:"4px 8px",borderRadius:"4px",fontSize:"10px",fontFamily:"monospace",background:"#d1fae5",color:"#065f46"},title:A.platform_dispensary_id,children:A.platform_dispensary_id.length>12?`${A.platform_dispensary_id.slice(0,6)}...${A.platform_dispensary_id.slice(-4)}`:A.platform_dispensary_id}):a.jsx("span",{style:{padding:"4px 8px",borderRadius:"4px",fontSize:"10px",background:"#fee2e2",color:"#991b1b"},children:"missing"})}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:a.jsxs("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",gap:"4px"},children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"11px",fontWeight:"600",background:A.can_crawl?"#d1fae5":A.is_active!==!1?"#fef3c7":"#fee2e2",color:A.can_crawl?"#065f46":A.is_active!==!1?"#92400e":"#991b1b"},children:A.can_crawl?"Ready":A.is_active!==!1?"Not Ready":"Disabled"}),A.schedule_status_reason&&A.schedule_status_reason!=="ready"&&a.jsx("span",{style:{fontSize:"10px",color:"#666",maxWidth:"100px",textAlign:"center"},children:A.schedule_status_reason}),A.interval_minutes&&a.jsxs("span",{style:{fontSize:"10px",color:"#999"},children:["Every ",Math.round(A.interval_minutes/60),"h"]})]})}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{children:L(A.last_run_at)}),A.last_run_at&&a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:new Date(A.last_run_at).toLocaleString()})]}),a.jsx("td",{style:{padding:"15px"},children:a.jsx("div",{style:{fontWeight:"600",color:"#2563eb"},children:A.next_run_at?U(A.next_run_at):"Not scheduled"})}),a.jsx("td",{style:{padding:"15px"},children:A.last_status||A.latest_job_status?a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px",marginBottom:"4px"},children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...H(A.last_status||A.latest_job_status||"pending")},children:A.last_status||A.latest_job_status}),A.last_error&&a.jsx("button",{onClick:()=>alert(A.last_error),style:{padding:"2px 6px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"10px"},children:"Error"})]}),A.last_summary?a.jsx("div",{style:{fontSize:"12px",color:"#666",maxWidth:"250px"},children:A.last_summary}):A.latest_products_found!==null?a.jsxs("div",{style:{fontSize:"12px",color:"#666"},children:[A.latest_products_found," products"]}):null]}):a.jsx("span",{style:{color:"#999",fontSize:"13px"},children:"No runs yet"})}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:a.jsxs("div",{style:{display:"flex",gap:"6px",justifyContent:"center",flexWrap:"wrap"},children:[a.jsx("button",{onClick:()=>D(A.dispensary_id),disabled:b===A.dispensary_id,style:{padding:"4px 8px",background:b===A.dispensary_id?"#94a3b8":"#f3f4f6",color:"#374151",border:"1px solid #d1d5db",borderRadius:"4px",cursor:b===A.dispensary_id?"wait":"pointer",fontSize:"11px"},title:"Re-detect menu type and resolve platform ID",children:b===A.dispensary_id?"...":"Refresh"}),A.menu_type==="dutchie"&&!A.platform_dispensary_id&&a.jsx("button",{onClick:()=>E(A.dispensary_id),disabled:x===A.dispensary_id,style:{padding:"4px 8px",background:x===A.dispensary_id?"#94a3b8":"#fef3c7",color:"#92400e",border:"1px solid #fcd34d",borderRadius:"4px",cursor:x===A.dispensary_id?"wait":"pointer",fontSize:"11px"},title:"Resolve platform dispensary ID via GraphQL",children:x===A.dispensary_id?"...":"Resolve ID"}),a.jsx("button",{onClick:()=>R(A.dispensary_id),disabled:p===A.dispensary_id||!A.can_crawl,style:{padding:"4px 8px",background:p===A.dispensary_id?"#94a3b8":A.can_crawl?"#2563eb":"#e5e7eb",color:A.can_crawl?"white":"#9ca3af",border:"none",borderRadius:"4px",cursor:p===A.dispensary_id||!A.can_crawl?"not-allowed":"pointer",fontSize:"11px"},title:A.can_crawl?"Trigger immediate crawl":`Cannot crawl: ${A.schedule_status_reason}`,children:p===A.dispensary_id?"...":"Run"}),a.jsx("button",{onClick:()=>O(A.dispensary_id,A.is_active),disabled:j===A.dispensary_id,style:{padding:"4px 8px",background:j===A.dispensary_id?"#94a3b8":A.is_active?"#fee2e2":"#d1fae5",color:A.is_active?"#991b1b":"#065f46",border:"none",borderRadius:"4px",cursor:j===A.dispensary_id?"wait":"pointer",fontSize:"11px"},title:A.is_active?"Disable scheduled crawling":"Enable scheduled crawling",children:j===A.dispensary_id?"...":A.is_active?"Disable":"Enable"})]})})]},A.dispensary_id))})]})})]}),u==="jobs"&&a.jsxs(a.Fragment,{children:[a.jsx("div",{style:{marginBottom:"30px"},children:a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"15px"},children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Pending"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#f59e0b"},children:i.filter(A=>A.status==="pending").length})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Running"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#3b82f6"},children:i.filter(A=>A.status==="running").length})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Completed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#10b981"},children:i.filter(A=>A.status==="completed").length})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Failed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#ef4444"},children:i.filter(A=>A.status==="failed").length})]})]})}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Type"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Trigger"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Products"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Started"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Completed"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Actions"})]})}),a.jsx("tbody",{children:i.length===0?a.jsx("tr",{children:a.jsx("td",{colSpan:8,style:{padding:"40px",textAlign:"center",color:"#666"},children:"No crawl jobs found"})}):i.map(A=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:A.dispensary_name}),a.jsxs("div",{style:{fontSize:"12px",color:"#999"},children:["Job #",A.id]})]}),a.jsx("td",{style:{padding:"15px",textAlign:"center",fontSize:"13px"},children:A.job_type}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"3px 8px",borderRadius:"4px",fontSize:"12px",background:A.trigger_type==="manual"?"#e0e7ff":A.trigger_type==="daily_special"?"#fce7f3":"#f3f4f6",color:A.trigger_type==="manual"?"#3730a3":A.trigger_type==="daily_special"?"#9d174d":"#374151"},children:A.trigger_type})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...H(A.status)},children:A.status})}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:A.products_found!==null?a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600"},children:A.products_found}),A.products_new!==null&&A.products_updated!==null&&a.jsxs("div",{style:{fontSize:"12px",color:"#666"},children:["+",A.products_new," / ~",A.products_updated]})]}):"-"}),a.jsx("td",{style:{padding:"15px",fontSize:"13px"},children:A.started_at?new Date(A.started_at).toLocaleString():"-"}),a.jsx("td",{style:{padding:"15px",fontSize:"13px"},children:A.completed_at?new Date(A.completed_at).toLocaleString():"-"}),a.jsxs("td",{style:{padding:"15px",textAlign:"center"},children:[A.status==="pending"&&a.jsx("button",{onClick:()=>Z(A.id),style:{padding:"4px 10px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"12px"},children:"Cancel"}),A.error_message&&a.jsx("button",{onClick:()=>alert(A.error_message),style:{padding:"4px 10px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"12px"},children:"View Error"})]})]},A.id))})]})})]})]})})}function y7(){const[e,t]=h.useState([]),[r,n]=h.useState(null),[i,s]=h.useState(3),[o,l]=h.useState("rotate-desktop"),[c,d]=h.useState(!1),[u,f]=h.useState(!1),[p,m]=h.useState(null),[x,g]=h.useState(!0);h.useEffect(()=>{b()},[]);const b=async()=>{g(!0);try{const S=(await B.getDispensaries()).dispensaries.filter(N=>N.menu_url&&N.scrape_enabled);t(S),S.length>0&&n(S[0].id)}catch(w){console.error("Failed to load dispensaries:",w)}finally{g(!1)}},v=async()=>{if(!(!r||c)){d(!0);try{await B.triggerDispensaryCrawl(r),m({message:"Crawl started for dispensary! Check the Scraper Monitor for progress.",type:"success"})}catch(w){m({message:"Failed to start crawl: "+w.message,type:"error"})}finally{d(!1)}}},j=async()=>{if(!(!r||u)){f(!0);try{m({message:"Image download feature coming soon!",type:"info"})}catch(w){m({message:"Failed to start image download: "+w.message,type:"error"})}finally{f(!1)}}},y=e.find(w=>w.id===r);return x?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("span",{className:"loading loading-spinner loading-lg"})})}):a.jsxs(X,{children:[p&&a.jsx(Vn,{message:p.message,type:p.type,onClose:()=>m(null)}),a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-3xl font-bold",children:"Scraper Tools"}),a.jsx("p",{className:"text-gray-500 mt-2",children:"Manage crawling operations for dispensaries"})]}),a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h2",{className:"card-title",children:"Select Dispensary"}),a.jsx("select",{className:"select select-bordered w-full max-w-md",value:r||"",onChange:w=>n(parseInt(w.target.value)),children:e.map(w=>a.jsxs("option",{value:w.id,children:[w.dba_name||w.name," - ",w.city,", ",w.state]},w.id))}),y&&a.jsx("div",{className:"mt-4 p-4 bg-base-200 rounded-lg",children:a.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 gap-4 text-sm",children:[a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Status"}),a.jsx("div",{className:"font-semibold",children:y.scrape_enabled?a.jsx("span",{className:"badge badge-success",children:"Enabled"}):a.jsx("span",{className:"badge badge-error",children:"Disabled"})})]}),a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Provider"}),a.jsx("div",{className:"font-semibold",children:y.provider_type||"Unknown"})]}),a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Products"}),a.jsx("div",{className:"font-semibold",children:y.product_count||0})]}),a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Last Crawled"}),a.jsx("div",{className:"font-semibold",children:y.last_crawl_at?new Date(y.last_crawl_at).toLocaleDateString():"Never"})]})]})})]})}),a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6",children:[a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h2",{className:"card-title",children:"Crawl Dispensary"}),a.jsx("p",{className:"text-sm text-gray-500",children:"Start crawling products from the selected dispensary menu"}),a.jsx("div",{className:"card-actions justify-end mt-4",children:a.jsx("button",{onClick:v,disabled:!r||c,className:`btn btn-primary ${c?"loading":""}`,children:c?"Starting...":"Start Crawl"})})]})}),a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h2",{className:"card-title",children:"Download Images"}),a.jsx("p",{className:"text-sm text-gray-500",children:"Download missing product images for the selected dispensary"}),a.jsx("div",{className:"card-actions justify-end mt-auto",children:a.jsx("button",{onClick:j,disabled:!r||u,className:`btn btn-secondary ${u?"loading":""}`,children:u?"Downloading...":"Download Missing Images"})})]})})]}),a.jsxs("div",{className:"alert alert-info",children:[a.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",className:"stroke-current shrink-0 w-6 h-6",children:a.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"})}),a.jsxs("span",{children:["After starting scraper operations, check the"," ",a.jsx("a",{href:"/scraper-monitor",className:"link",children:"Scraper Monitor"})," for real-time progress and ",a.jsx("a",{href:"/logs",className:"link",children:"Logs"})," for detailed output."]})]})]})]})}function v7(){const[e,t]=h.useState([]),[r,n]=h.useState(null),[i,s]=h.useState(!0),[o,l]=h.useState("pending"),[c,d]=h.useState(null);h.useEffect(()=>{u()},[o]);const u=async()=>{s(!0);try{const[g,b]=await Promise.all([B.getChanges(o==="all"?void 0:o),B.getChangeStats()]);t(g.changes),n(b)}catch(g){console.error("Failed to load changes:",g)}finally{s(!1)}},f=async g=>{d(g);try{(await B.approveChange(g)).requires_recrawl&&alert("Change approved! This dispensary requires a menu recrawl."),await u()}catch(b){console.error("Failed to approve change:",b),alert("Failed to approve change. Please try again.")}finally{d(null)}},p=async g=>{const b=prompt("Enter rejection reason (optional):");d(g);try{await B.rejectChange(g,b||void 0),await u()}catch(v){console.error("Failed to reject change:",v),alert("Failed to reject change. Please try again.")}finally{d(null)}},m=g=>({dba_name:"DBA Name",website:"Website",phone:"Phone",email:"Email",google_rating:"Google Rating",google_review_count:"Google Review Count",menu_url:"Menu URL"})[g]||g,x=g=>({high:"bg-green-100 text-green-800",medium:"bg-yellow-100 text-yellow-800",low:"bg-red-100 text-red-800"})[g]||"bg-gray-100 text-gray-800";return i?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading changes..."})]})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Change Approval"}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"Review and approve proposed changes to dispensary data"})]}),a.jsxs("button",{onClick:u,className:"flex items-center gap-2 px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),r&&a.jsxs("div",{className:"grid grid-cols-4 gap-6",children:[a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-yellow-50 rounded-lg",children:a.jsx(xr,{className:"w-5 h-5 text-yellow-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Pending"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.pending_count})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(nj,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Needs Recrawl"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.pending_recrawl_count})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Approved"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.approved_count})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-red-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-red-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Rejected"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.rejected_count})]})]})})]}),a.jsx("div",{className:"flex gap-2",children:["all","pending","approved","rejected"].map(g=>a.jsx("button",{onClick:()=>l(g),className:`px-4 py-2 text-sm font-medium rounded-lg ${o===g?"bg-blue-600 text-white":"bg-white text-gray-700 border border-gray-300 hover:bg-gray-50"}`,children:g.charAt(0).toUpperCase()+g.slice(1)},g))}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200",children:e.length===0?a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-600",children:"No changes found"})}):a.jsx("div",{className:"divide-y divide-gray-200",children:e.map(g=>a.jsx("div",{className:"p-6",children:a.jsxs("div",{className:"flex items-start justify-between",children:[a.jsxs("div",{className:"flex-1",children:[a.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900",children:g.dispensary_name}),a.jsx("span",{className:`px-2 py-1 text-xs font-medium rounded ${x(g.confidence_score)}`,children:g.confidence_score||"N/A"}),g.requires_recrawl&&a.jsx("span",{className:"px-2 py-1 text-xs font-medium rounded bg-orange-100 text-orange-800",children:"Requires Recrawl"})]}),a.jsxs("p",{className:"text-sm text-gray-600 mb-3",children:[g.city,", ",g.state," • Source: ",g.source]}),a.jsxs("div",{className:"grid grid-cols-2 gap-4 mb-3",children:[a.jsxs("div",{children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"Field"}),a.jsx("p",{className:"text-sm text-gray-900",children:m(g.field_name)})]}),a.jsxs("div",{children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"Old Value"}),a.jsx("p",{className:"text-sm text-gray-900",children:g.old_value||a.jsx("em",{className:"text-gray-400",children:"None"})})]}),a.jsxs("div",{className:"col-span-2",children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"New Value"}),a.jsx("p",{className:"text-sm font-medium text-blue-600",children:g.new_value})]}),g.change_notes&&a.jsxs("div",{className:"col-span-2",children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"Notes"}),a.jsx("p",{className:"text-sm text-gray-700",children:g.change_notes})]})]}),a.jsxs("p",{className:"text-xs text-gray-500",children:["Created ",new Date(g.created_at).toLocaleString()]}),g.status==="rejected"&&g.rejection_reason&&a.jsxs("div",{className:"mt-2 p-3 bg-red-50 rounded border border-red-200",children:[a.jsx("p",{className:"text-xs font-medium text-red-800",children:"Rejection Reason:"}),a.jsx("p",{className:"text-sm text-red-700",children:g.rejection_reason})]})]}),a.jsxs("div",{className:"flex items-center gap-2 ml-4",children:[g.status==="pending"&&a.jsxs(a.Fragment,{children:[a.jsxs("button",{onClick:()=>f(g.id),disabled:c===g.id,className:"flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 disabled:opacity-50",children:[a.jsx(_r,{className:"w-4 h-4"}),"Approve"]}),a.jsxs("button",{onClick:()=>p(g.id),disabled:c===g.id,className:"flex items-center gap-2 px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 disabled:opacity-50",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Reject"]})]}),g.status==="approved"&&a.jsxs("span",{className:"flex items-center gap-2 px-4 py-2 bg-green-100 text-green-800 text-sm font-medium rounded-lg",children:[a.jsx(_r,{className:"w-4 h-4"}),"Approved"]}),g.status==="rejected"&&a.jsxs("span",{className:"flex items-center gap-2 px-4 py-2 bg-red-100 text-red-800 text-sm font-medium rounded-lg",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Rejected"]}),a.jsx("a",{href:`/dispensaries/${g.dispensary_slug}`,target:"_blank",rel:"noopener noreferrer",className:"p-2 text-gray-400 hover:text-gray-600",children:a.jsx(Jr,{className:"w-4 h-4"})})]})]})},g.id))})})]})})}function b7(){const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState(!0),[o,l]=h.useState(!1),[c,d]=h.useState({user_name:"",store_id:"",allowed_ips:"",allowed_domains:""}),[u,f]=h.useState(null);h.useEffect(()=>{m(),p()},[]);const p=async()=>{try{const y=await B.getApiPermissionDispensaries();n(y.dispensaries)}catch(y){console.error("Failed to load dispensaries:",y)}},m=async()=>{s(!0);try{const y=await B.getApiPermissions();t(y.permissions)}catch(y){f({message:"Failed to load API permissions: "+y.message,type:"error"})}finally{s(!1)}},x=async y=>{if(y.preventDefault(),!c.user_name.trim()){f({message:"User name is required",type:"error"});return}if(!c.store_id){f({message:"Store is required",type:"error"});return}try{const w=await B.createApiPermission({...c,store_id:parseInt(c.store_id)});f({message:w.message,type:"success"}),d({user_name:"",store_id:"",allowed_ips:"",allowed_domains:""}),l(!1),m()}catch(w){f({message:"Failed to create permission: "+w.message,type:"error"})}},g=async y=>{try{await B.toggleApiPermission(y),f({message:"Permission status updated",type:"success"}),m()}catch(w){f({message:"Failed to toggle permission: "+w.message,type:"error"})}},b=async y=>{if(confirm("Are you sure you want to delete this API permission?"))try{await B.deleteApiPermission(y),f({message:"Permission deleted successfully",type:"success"}),m()}catch(w){f({message:"Failed to delete permission: "+w.message,type:"error"})}},v=y=>{navigator.clipboard.writeText(y),f({message:"API key copied to clipboard!",type:"success"})},j=y=>{if(!y)return"Never";const w=new Date(y);return w.toLocaleDateString()+" "+w.toLocaleTimeString()};return i?a.jsx(X,{children:a.jsx("div",{className:"p-6",children:a.jsx("div",{className:"text-center text-gray-600",children:"Loading API permissions..."})})}):a.jsx(X,{children:a.jsxs("div",{className:"p-6",children:[u&&a.jsx(Vn,{message:u.message,type:u.type,onClose:()=>f(null)}),a.jsxs("div",{className:"flex justify-between items-center mb-6",children:[a.jsx("h1",{className:"text-2xl font-bold",children:"API Permissions"}),a.jsx("button",{onClick:()=>l(!o),className:"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700",children:o?"Cancel":"Add New Permission"})]}),a.jsxs("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6",children:[a.jsx("h3",{className:"font-semibold text-blue-900 mb-2",children:"How it works:"}),a.jsx("p",{className:"text-blue-800 text-sm",children:"Users with valid permissions can access your API without entering tokens. Access is automatically validated based on their IP address and/or domain name."})]}),o&&a.jsxs("div",{className:"bg-white rounded-lg shadow-md p-6 mb-6",children:[a.jsx("h2",{className:"text-xl font-semibold mb-4",children:"Add New API User"}),a.jsxs("form",{onSubmit:x,children:[a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"User/Client Name *"}),a.jsx("input",{type:"text",value:c.user_name,onChange:y=>d({...c,user_name:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",placeholder:"e.g., My Website",required:!0}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"A friendly name to identify this API user"})]}),a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Store *"}),a.jsxs("select",{value:c.store_id,onChange:y=>d({...c,store_id:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",required:!0,children:[a.jsx("option",{value:"",children:"Select a store..."}),r.map(y=>a.jsx("option",{value:y.id,children:y.name},y.id))]}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"The store this API token can access"})]}),a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Allowed IP Addresses"}),a.jsx("textarea",{value:c.allowed_ips,onChange:y=>d({...c,allowed_ips:y.target.value}),rows:3,className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm",placeholder:`192.168.1.1 +10.0.0.0/8 +2001:db8::/32`}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:["One IP address or CIDR range per line. Leave empty to allow any IP.",a.jsx("br",{}),"Supports IPv4, IPv6, and CIDR notation (e.g., 192.168.0.0/24)"]})]}),a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Allowed Domains"}),a.jsx("textarea",{value:c.allowed_domains,onChange:y=>d({...c,allowed_domains:y.target.value}),rows:3,className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm",placeholder:`example.com +*.example.com +subdomain.example.com`}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"One domain per line. Wildcards supported (e.g., *.example.com). Leave empty to allow any domain."})]}),a.jsx("button",{type:"submit",className:"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700",children:"Create API Permission"})]})]}),a.jsxs("div",{className:"bg-white rounded-lg shadow-md overflow-hidden",children:[a.jsx("div",{className:"px-6 py-4 border-b border-gray-200",children:a.jsx("h2",{className:"text-xl font-semibold",children:"Active API Users"})}),e.length===0?a.jsx("div",{className:"p-6 text-center text-gray-600",children:"No API permissions configured yet. Add your first user above."}):a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"User Name"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Store"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"API Key"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Allowed IPs"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Allowed Domains"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Status"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Last Used"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Actions"})]})}),a.jsx("tbody",{className:"bg-white divide-y divide-gray-200",children:e.map(y=>a.jsxs("tr",{children:[a.jsx("td",{className:"px-6 py-4 whitespace-nowrap",children:a.jsx("div",{className:"font-medium text-gray-900",children:y.user_name})}),a.jsx("td",{className:"px-6 py-4 whitespace-nowrap",children:a.jsx("div",{className:"text-sm text-gray-900",children:y.store_name||a.jsx("span",{className:"text-gray-400 italic",children:"No store"})})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center space-x-2",children:[a.jsxs("code",{className:"text-xs bg-gray-100 px-2 py-1 rounded",children:[y.api_key.substring(0,16),"..."]}),a.jsx("button",{onClick:()=>v(y.api_key),className:"text-blue-600 hover:text-blue-800 text-sm",children:"Copy"})]})}),a.jsx("td",{className:"px-6 py-4",children:y.allowed_ips?a.jsxs("div",{className:"text-sm text-gray-600",children:[y.allowed_ips.split(` +`).slice(0,2).join(", "),y.allowed_ips.split(` +`).length>2&&a.jsxs("span",{className:"text-gray-400",children:[" +",y.allowed_ips.split(` +`).length-2," more"]})]}):a.jsx("span",{className:"text-gray-400 italic",children:"Any IP"})}),a.jsx("td",{className:"px-6 py-4",children:y.allowed_domains?a.jsxs("div",{className:"text-sm text-gray-600",children:[y.allowed_domains.split(` +`).slice(0,2).join(", "),y.allowed_domains.split(` +`).length>2&&a.jsxs("span",{className:"text-gray-400",children:[" +",y.allowed_domains.split(` +`).length-2," more"]})]}):a.jsx("span",{className:"text-gray-400 italic",children:"Any domain"})}),a.jsx("td",{className:"px-6 py-4 whitespace-nowrap",children:y.is_active?a.jsx("span",{className:"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800",children:"Active"}):a.jsx("span",{className:"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800",children:"Disabled"})}),a.jsx("td",{className:"px-6 py-4 whitespace-nowrap text-sm text-gray-600",children:j(y.last_used_at)}),a.jsxs("td",{className:"px-6 py-4 whitespace-nowrap text-sm space-x-2",children:[a.jsx("button",{onClick:()=>g(y.id),className:"text-blue-600 hover:text-blue-800",children:y.is_active?"Disable":"Enable"}),a.jsx("button",{onClick:()=>b(y.id),className:"text-red-600 hover:text-red-800",children:"Delete"})]})]},y.id))})]})})]})]})})}function j7(){const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState(null),[o,l]=h.useState(null),[c,d]=h.useState(!0),[u,f]=h.useState(!0),[p,m]=h.useState("schedules"),[x,g]=h.useState(null),[b,v]=h.useState(!1),[j,y]=h.useState(!1),[w,S]=h.useState(null);h.useEffect(()=>{if(N(),u){const k=setInterval(N,1e4);return()=>clearInterval(k)}},[u]);const N=async()=>{try{const[k,L,U,H]=await Promise.all([B.getDutchieAZSchedules(),B.getDutchieAZRunLogs({limit:50}),B.getDutchieAZSchedulerStatus(),B.getDetectionStats().catch(()=>null)]);t(k.schedules||[]),n(L.logs||[]),s(U),l(H)}catch(k){console.error("Failed to load schedule data:",k)}finally{d(!1)}},P=async()=>{try{i!=null&&i.running?await B.stopDutchieAZScheduler():await B.startDutchieAZScheduler(),await N()}catch(k){console.error("Failed to toggle scheduler:",k)}},_=async()=>{try{await B.initDutchieAZSchedules(),await N()}catch(k){console.error("Failed to initialize schedules:",k)}},T=async k=>{try{await B.triggerDutchieAZSchedule(k),await N()}catch(L){console.error("Failed to trigger schedule:",L)}},$=async k=>{try{await B.updateDutchieAZSchedule(k.id,{enabled:!k.enabled}),await N()}catch(L){console.error("Failed to toggle schedule:",L)}},M=async(k,L)=>{try{const U={description:L.description??void 0,enabled:L.enabled,baseIntervalMinutes:L.baseIntervalMinutes,jitterMinutes:L.jitterMinutes,jobConfig:L.jobConfig??void 0};await B.updateDutchieAZSchedule(k,U),g(null),await N()}catch(U){console.error("Failed to update schedule:",U)}},C=async()=>{if(confirm("Run menu detection on all dispensaries with unknown/missing menu_type?")){y(!0),S(null);try{const k=await B.detectAllDispensaries({state:"AZ",onlyUnknown:!0});S(k),await N()}catch(k){console.error("Failed to run bulk detection:",k)}finally{y(!1)}}},R=async()=>{if(confirm("Resolve platform IDs for all Dutchie dispensaries missing them?")){y(!0),S(null);try{const k=await B.detectAllDispensaries({state:"AZ",onlyMissingPlatformId:!0,onlyUnknown:!1});S(k),await N()}catch(k){console.error("Failed to resolve platform IDs:",k)}finally{y(!1)}}},q=k=>{if(!k)return"Never";const L=new Date(k),H=new Date().getTime()-L.getTime(),te=Math.floor(H/6e4),re=Math.floor(te/60),we=Math.floor(re/24);return te<1?"Just now":te<60?`${te}m ago`:re<24?`${re}h ago`:`${we}d ago`},Z=k=>{if(!k)return"Not scheduled";const L=new Date(k),U=new Date,H=L.getTime()-U.getTime();if(H<0)return"Overdue";const te=Math.floor(H/6e4),re=Math.floor(te/60);return te<60?`${te}m`:`${re}h ${te%60}m`},E=k=>{if(!k)return"-";if(k<1e3)return`${k}ms`;const L=Math.floor(k/1e3),U=Math.floor(L/60);return U<1?`${L}s`:`${U}m ${L%60}s`},D=(k,L)=>{const U=Math.floor(k/60),H=k%60,te=Math.floor(L/60),re=L%60;let we=U>0?`${U}h`:"";H>0&&(we+=`${H}m`);let A=te>0?`${te}h`:"";return re>0&&(A+=`${re}m`),`${we} +/- ${A}`},O=k=>{switch(k){case"success":return{bg:"#d1fae5",color:"#065f46"};case"running":return{bg:"#dbeafe",color:"#1e40af"};case"error":return{bg:"#fee2e2",color:"#991b1b"};case"partial":return{bg:"#fef3c7",color:"#92400e"};default:return{bg:"#f3f4f6",color:"#374151"}}};return a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsxs("div",{children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Dutchie AZ Schedule"}),a.jsx("p",{style:{color:"#666",margin:"8px 0 0 0"},children:"Jittered scheduling for Arizona Dutchie product crawls"})]}),a.jsx("div",{style:{display:"flex",gap:"15px",alignItems:"center"},children:a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:u,onChange:k=>f(k.target.checked),style:{width:"18px",height:"18px",cursor:"pointer"}}),a.jsx("span",{children:"Auto-refresh (10s)"})]})})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px",display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"20px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"4px"},children:"Scheduler Status"}),a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[a.jsx("span",{style:{width:"12px",height:"12px",borderRadius:"50%",background:i!=null&&i.running?"#10b981":"#ef4444",display:"inline-block"}}),a.jsx("span",{style:{fontWeight:"600",fontSize:"18px"},children:i!=null&&i.running?"Running":"Stopped"})]})]}),a.jsxs("div",{style:{borderLeft:"1px solid #eee",paddingLeft:"20px"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"4px"},children:"Poll Interval"}),a.jsx("div",{style:{fontWeight:"600"},children:i?`${i.pollIntervalMs/1e3}s`:"-"})]}),a.jsxs("div",{style:{borderLeft:"1px solid #eee",paddingLeft:"20px"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"4px"},children:"Active Schedules"}),a.jsxs("div",{style:{fontWeight:"600"},children:[e.filter(k=>k.enabled).length," / ",e.length]})]})]}),a.jsxs("div",{style:{display:"flex",gap:"10px"},children:[a.jsx("button",{onClick:P,style:{padding:"10px 20px",background:i!=null&&i.running?"#ef4444":"#10b981",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:i!=null&&i.running?"Stop Scheduler":"Start Scheduler"}),e.length===0&&a.jsx("button",{onClick:_,style:{padding:"10px 20px",background:"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:"Initialize Default Schedules"})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"10px",borderBottom:"2px solid #eee"},children:[a.jsxs("button",{onClick:()=>m("schedules"),style:{padding:"12px 24px",background:p==="schedules"?"white":"transparent",border:"none",borderBottom:p==="schedules"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:p==="schedules"?"600":"400",color:p==="schedules"?"#2563eb":"#666",marginBottom:"-2px"},children:["Schedule Configs (",e.length,")"]}),a.jsxs("button",{onClick:()=>m("logs"),style:{padding:"12px 24px",background:p==="logs"?"white":"transparent",border:"none",borderBottom:p==="logs"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:p==="logs"?"600":"400",color:p==="logs"?"#2563eb":"#666",marginBottom:"-2px"},children:["Run Logs (",r.length,")"]}),a.jsxs("button",{onClick:()=>m("detection"),style:{padding:"12px 24px",background:p==="detection"?"white":"transparent",border:"none",borderBottom:p==="detection"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:p==="detection"?"600":"400",color:p==="detection"?"#2563eb":"#666",marginBottom:"-2px"},children:["Menu Detection ",o!=null&&o.needsDetection?`(${o.needsDetection} pending)`:""]})]}),p==="schedules"&&a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:e.length===0?a.jsx("div",{style:{padding:"40px",textAlign:"center",color:"#666"},children:'No schedules configured. Click "Initialize Default Schedules" to create the default crawl schedule.'}):a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Job Name"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Enabled"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Interval (Jitter)"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Last Run"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Next Run"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Last Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Actions"})]})}),a.jsx("tbody",{children:e.map(k=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:k.jobName}),k.description&&a.jsx("div",{style:{fontSize:"13px",color:"#666",marginTop:"4px"},children:k.description}),k.jobConfig&&a.jsxs("div",{style:{fontSize:"11px",color:"#999",marginTop:"4px"},children:["Config: ",JSON.stringify(k.jobConfig)]})]}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("button",{onClick:()=>$(k),style:{padding:"4px 12px",borderRadius:"12px",border:"none",cursor:"pointer",fontWeight:"600",fontSize:"12px",background:k.enabled?"#d1fae5":"#fee2e2",color:k.enabled?"#065f46":"#991b1b"},children:k.enabled?"ON":"OFF"})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("div",{style:{fontWeight:"600"},children:D(k.baseIntervalMinutes,k.jitterMinutes)})}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{children:q(k.lastRunAt)}),k.lastDurationMs&&a.jsxs("div",{style:{fontSize:"12px",color:"#666"},children:["Duration: ",E(k.lastDurationMs)]})]}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600",color:"#2563eb"},children:Z(k.nextRunAt)}),k.nextRunAt&&a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:new Date(k.nextRunAt).toLocaleString()})]}),a.jsx("td",{style:{padding:"15px"},children:k.lastStatus?a.jsxs("div",{children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...O(k.lastStatus)},children:k.lastStatus}),k.lastErrorMessage&&a.jsx("button",{onClick:()=>alert(k.lastErrorMessage),style:{marginLeft:"8px",padding:"2px 6px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"10px"},children:"Error"})]}):a.jsx("span",{style:{color:"#999"},children:"Never run"})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsxs("div",{style:{display:"flex",gap:"8px",justifyContent:"center"},children:[a.jsx("button",{onClick:()=>T(k.id),disabled:k.lastStatus==="running",style:{padding:"6px 12px",background:k.lastStatus==="running"?"#94a3b8":"#2563eb",color:"white",border:"none",borderRadius:"4px",cursor:k.lastStatus==="running"?"not-allowed":"pointer",fontSize:"13px"},children:"Run Now"}),a.jsx("button",{onClick:()=>g(k),style:{padding:"6px 12px",background:"#f3f4f6",color:"#374151",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"13px"},children:"Edit"})]})})]},k.id))})]})}),p==="logs"&&a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:r.length===0?a.jsx("div",{style:{padding:"40px",textAlign:"center",color:"#666"},children:"No run logs yet. Logs will appear here after jobs execute."}):a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Job"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Started"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Duration"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Processed"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Succeeded"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Failed"})]})}),a.jsx("tbody",{children:r.map(k=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:k.job_name}),a.jsxs("div",{style:{fontSize:"12px",color:"#999"},children:["Run #",k.id]})]}),a.jsxs("td",{style:{padding:"15px",textAlign:"center"},children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...O(k.status)},children:k.status}),k.error_message&&a.jsx("button",{onClick:()=>alert(k.error_message),style:{marginLeft:"8px",padding:"2px 6px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"10px"},children:"Error"})]}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{children:k.started_at?new Date(k.started_at).toLocaleString():"-"}),a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:q(k.started_at)})]}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E(k.duration_ms)}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:k.items_processed??"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"right",color:"#10b981"},children:k.items_succeeded??"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"right",color:k.items_failed?"#ef4444":"inherit"},children:k.items_failed??"-"})]},k.id))})]})}),p==="detection"&&a.jsxs("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",padding:"30px"},children:[o&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h3",{style:{margin:"0 0 20px 0"},children:"Detection Statistics"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"20px"},children:[a.jsxs("div",{style:{padding:"20px",background:"#f8f8f8",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#2563eb"},children:o.totalDispensaries}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"Total Dispensaries"})]}),a.jsxs("div",{style:{padding:"20px",background:"#f8f8f8",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#10b981"},children:o.withMenuType}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"With Menu Type"})]}),a.jsxs("div",{style:{padding:"20px",background:"#f8f8f8",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#10b981"},children:o.withPlatformId}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"With Platform ID"})]}),a.jsxs("div",{style:{padding:"20px",background:"#fef3c7",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#92400e"},children:o.needsDetection}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"Needs Detection"})]})]}),Object.keys(o.byProvider).length>0&&a.jsxs("div",{style:{marginTop:"20px"},children:[a.jsx("h4",{style:{margin:"0 0 10px 0"},children:"By Provider"}),a.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"10px"},children:Object.entries(o.byProvider).map(([k,L])=>a.jsxs("span",{style:{padding:"6px 14px",background:k==="dutchie"?"#dbeafe":"#f3f4f6",borderRadius:"16px",fontSize:"14px",fontWeight:"600"},children:[k,": ",L]},k))})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"15px",flexWrap:"wrap"},children:[a.jsx("button",{onClick:C,disabled:j||!(o!=null&&o.needsDetection),style:{padding:"12px 24px",background:j?"#94a3b8":"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:j?"not-allowed":"pointer",fontWeight:"600",fontSize:"14px"},children:j?"Detecting...":"Detect All Unknown"}),a.jsx("button",{onClick:R,disabled:j,style:{padding:"12px 24px",background:j?"#94a3b8":"#10b981",color:"white",border:"none",borderRadius:"6px",cursor:j?"not-allowed":"pointer",fontWeight:"600",fontSize:"14px"},children:j?"Resolving...":"Resolve Missing Platform IDs"})]}),w&&a.jsxs("div",{style:{marginBottom:"30px",padding:"20px",background:"#f8f8f8",borderRadius:"8px"},children:[a.jsx("h4",{style:{margin:"0 0 15px 0"},children:"Detection Results"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(4, 1fr)",gap:"15px",marginBottom:"15px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px"},children:w.totalProcessed}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Processed"})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px",color:"#10b981"},children:w.totalSucceeded}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Succeeded"})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px",color:"#ef4444"},children:w.totalFailed}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Failed"})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px",color:"#666"},children:w.totalSkipped}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Skipped"})]})]}),w.errors&&w.errors.length>0&&a.jsxs("div",{style:{marginTop:"15px"},children:[a.jsx("div",{style:{fontWeight:"600",marginBottom:"8px",color:"#991b1b"},children:"Errors:"}),a.jsxs("div",{style:{maxHeight:"150px",overflow:"auto",background:"#fee2e2",padding:"10px",borderRadius:"4px",fontSize:"12px"},children:[w.errors.slice(0,10).map((k,L)=>a.jsx("div",{style:{marginBottom:"4px"},children:k},L)),w.errors.length>10&&a.jsxs("div",{style:{fontStyle:"italic",marginTop:"8px"},children:["...and ",w.errors.length-10," more"]})]})]})]}),a.jsxs("div",{style:{padding:"20px",background:"#f0f9ff",borderRadius:"8px",fontSize:"14px"},children:[a.jsx("h4",{style:{margin:"0 0 10px 0",color:"#1e40af"},children:"About Menu Detection"}),a.jsxs("ul",{style:{margin:0,paddingLeft:"20px",color:"#1e40af"},children:[a.jsxs("li",{style:{marginBottom:"8px"},children:[a.jsx("strong",{children:"Detect All Unknown:"})," Scans dispensaries with no menu_type set and detects the provider (dutchie, treez, jane, etc.) from their menu_url."]}),a.jsxs("li",{style:{marginBottom:"8px"},children:[a.jsx("strong",{children:"Resolve Missing Platform IDs:"}),' For dispensaries already detected as "dutchie", extracts the cName from menu_url and resolves the platform_dispensary_id via GraphQL.']}),a.jsxs("li",{children:[a.jsx("strong",{children:"Automatic scheduling:"}),' A "Menu Detection" job runs daily (24h +/- 1h jitter) to detect new dispensaries.']})]})]})]}),x&&a.jsx("div",{style:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.5)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e3},children:a.jsxs("div",{style:{background:"white",padding:"30px",borderRadius:"12px",width:"500px",maxWidth:"90vw"},children:[a.jsxs("h2",{style:{margin:"0 0 20px 0"},children:["Edit Schedule: ",x.jobName]}),a.jsxs("div",{style:{marginBottom:"20px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"600"},children:"Description"}),a.jsx("input",{type:"text",value:x.description||"",onChange:k=>g({...x,description:k.target.value}),style:{width:"100%",padding:"10px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"14px"}})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"20px",marginBottom:"20px"},children:[a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"600"},children:"Base Interval (minutes)"}),a.jsx("input",{type:"number",value:x.baseIntervalMinutes,onChange:k=>g({...x,baseIntervalMinutes:parseInt(k.target.value)||240}),style:{width:"100%",padding:"10px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"14px"}}),a.jsxs("div",{style:{fontSize:"12px",color:"#666",marginTop:"4px"},children:["= ",Math.floor(x.baseIntervalMinutes/60),"h ",x.baseIntervalMinutes%60,"m"]})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"600"},children:"Jitter (minutes)"}),a.jsx("input",{type:"number",value:x.jitterMinutes,onChange:k=>g({...x,jitterMinutes:parseInt(k.target.value)||30}),style:{width:"100%",padding:"10px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"14px"}}),a.jsxs("div",{style:{fontSize:"12px",color:"#666",marginTop:"4px"},children:["+/- ",x.jitterMinutes,"m random offset"]})]})]}),a.jsxs("div",{style:{fontSize:"13px",color:"#666",marginBottom:"20px",padding:"15px",background:"#f8f8f8",borderRadius:"6px"},children:[a.jsx("strong",{children:"Effective range:"})," ",Math.floor((x.baseIntervalMinutes-x.jitterMinutes)/60),"h ",(x.baseIntervalMinutes-x.jitterMinutes)%60,"m"," to ",Math.floor((x.baseIntervalMinutes+x.jitterMinutes)/60),"h ",(x.baseIntervalMinutes+x.jitterMinutes)%60,"m"]}),a.jsxs("div",{style:{display:"flex",gap:"10px",justifyContent:"flex-end"},children:[a.jsx("button",{onClick:()=>g(null),style:{padding:"10px 20px",background:"#f3f4f6",color:"#374151",border:"none",borderRadius:"6px",cursor:"pointer"},children:"Cancel"}),a.jsx("button",{onClick:()=>M(x.id,{description:x.description,baseIntervalMinutes:x.baseIntervalMinutes,jitterMinutes:x.jitterMinutes}),style:{padding:"10px 20px",background:"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:"Save Changes"})]})]})})]})})}function w7(){const e=dt(),[t,r]=h.useState([]),[n,i]=h.useState(0),[s,o]=h.useState(!0),[l,c]=h.useState(null);h.useEffect(()=>{d()},[]);const d=async()=>{o(!0);try{const[u,f]=await Promise.all([B.getDutchieAZStores({limit:200}),B.getDutchieAZDashboard()]);r(u.stores),i(u.total),c(f)}catch(u){console.error("Failed to load data:",u)}finally{o(!1)}};return s?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading stores..."})]})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Dutchie AZ Stores"}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"Arizona dispensaries using the Dutchie platform - data from the new pipeline"})]}),a.jsxs("button",{onClick:d,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),l&&a.jsxs("div",{className:"grid grid-cols-4 gap-4",children:[a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-5 h-5 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Dispensaries"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.dispensaryCount})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.productCount.toLocaleString()})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.brandCount})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Failed Jobs (24h)"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.failedJobCount})]})]})})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"p-4 border-b border-gray-200",children:a.jsxs("h2",{className:"text-lg font-semibold text-gray-900",children:["All Stores (",n,")"]})}),a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"table table-zebra w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Name"}),a.jsx("th",{children:"City"}),a.jsx("th",{children:"Platform ID"}),a.jsx("th",{children:"Status"}),a.jsx("th",{children:"Actions"})]})}),a.jsx("tbody",{children:t.map(u=>a.jsxs("tr",{children:[a.jsx("td",{children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-4 h-4 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"font-medium text-gray-900",children:u.dba_name||u.name}),u.company_name&&u.company_name!==u.name&&a.jsx("p",{className:"text-xs text-gray-500",children:u.company_name})]})]})}),a.jsx("td",{children:a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(yi,{className:"w-4 h-4"}),u.city,", ",u.state]})}),a.jsx("td",{children:u.platform_dispensary_id?a.jsx("span",{className:"text-xs font-mono text-gray-600",children:u.platform_dispensary_id}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Not Resolved"})}),a.jsx("td",{children:u.platform_dispensary_id?a.jsx("span",{className:"badge badge-success badge-sm",children:"Ready"}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Pending"})}),a.jsx("td",{children:a.jsx("button",{onClick:()=>e(`/az/stores/${u.id}`),className:"btn btn-sm btn-primary",disabled:!u.platform_dispensary_id,children:"View Products"})})]},u.id))})]})})]})]})})}function S7(){const{id:e}=Na(),t=dt(),[r,n]=h.useState(null),[i,s]=h.useState([]),[o,l]=h.useState(!0),[c,d]=h.useState(!1),[u,f]=h.useState("products"),[p,m]=h.useState(!1),[x,g]=h.useState(!1),[b,v]=h.useState(""),[j,y]=h.useState(1),[w,S]=h.useState(0),[N]=h.useState(25),[P,_]=h.useState(""),T=O=>{if(!O)return"Never";const k=new Date(O),U=new Date().getTime()-k.getTime(),H=Math.floor(U/(1e3*60*60*24));return H===0?"Today":H===1?"Yesterday":H<7?`${H} days ago`:k.toLocaleDateString()};h.useEffect(()=>{e&&$()},[e]),h.useEffect(()=>{e&&u==="products"&&M()},[e,j,b,P,u]),h.useEffect(()=>{y(1)},[b,P]);const $=async()=>{l(!0);try{const O=await B.getDutchieAZStoreSummary(parseInt(e,10));n(O)}catch(O){console.error("Failed to load store summary:",O)}finally{l(!1)}},M=async()=>{if(e){d(!0);try{const O=await B.getDutchieAZStoreProducts(parseInt(e,10),{search:b||void 0,stockStatus:P||void 0,limit:N,offset:(j-1)*N});s(O.products),S(O.total)}catch(O){console.error("Failed to load products:",O)}finally{d(!1)}}},C=async()=>{m(!1),g(!0);try{await B.triggerDutchieAZCrawl(parseInt(e,10)),alert("Crawl started! Refresh the page in a few minutes to see updated data.")}catch(O){console.error("Failed to trigger crawl:",O),alert("Failed to start crawl. Please try again.")}finally{g(!1)}},R=Math.ceil(w/N);if(o)return a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading store..."})]})});if(!r)return a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-600",children:"Store not found"})})});const{dispensary:q,brands:Z,categories:E,lastCrawl:D}=r;return a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("button",{onClick:()=>t("/az"),className:"flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900",children:[a.jsx(_h,{className:"w-4 h-4"}),"Back to AZ Stores"]}),a.jsxs("div",{className:"relative",children:[a.jsxs("button",{onClick:()=>m(!p),disabled:x,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed",children:[a.jsx(Xt,{className:`w-4 h-4 ${x?"animate-spin":""}`}),x?"Crawling...":"Crawl Now",!x&&a.jsx(ej,{className:"w-4 h-4"})]}),p&&!x&&a.jsx("div",{className:"absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-10",children:a.jsx("button",{onClick:C,className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-lg",children:"Start Full Crawl"})})]})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-start justify-between gap-4 mb-4",children:[a.jsxs("div",{className:"flex items-start gap-4",children:[a.jsx("div",{className:"p-3 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-8 h-8 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:q.dba_name||q.name}),q.company_name&&a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:q.company_name}),a.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:["Platform ID: ",q.platform_dispensary_id||"Not resolved"]})]})]}),a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600 bg-gray-50 px-4 py-2 rounded-lg",children:[a.jsx(xr,{className:"w-4 h-4"}),a.jsxs("div",{children:[a.jsx("span",{className:"font-medium",children:"Last Crawl:"}),a.jsx("span",{className:"ml-2",children:D!=null&&D.completed_at?new Date(D.completed_at).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):"Never"}),(D==null?void 0:D.status)&&a.jsx("span",{className:`ml-2 px-2 py-0.5 rounded text-xs ${D.status==="completed"?"bg-green-100 text-green-800":D.status==="failed"?"bg-red-100 text-red-800":"bg-yellow-100 text-yellow-800"}`,children:D.status})]})]})]}),a.jsxs("div",{className:"flex flex-wrap gap-4",children:[q.address&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(yi,{className:"w-4 h-4"}),a.jsxs("span",{children:[q.address,", ",q.city,", ",q.state," ",q.zip]})]}),q.phone&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(Ah,{className:"w-4 h-4"}),a.jsx("span",{children:q.phone})]}),q.website&&a.jsxs("a",{href:q.website,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(Jr,{className:"w-4 h-4"}),"Website"]})]})]}),a.jsxs("div",{className:"grid grid-cols-5 gap-4",children:[a.jsx("button",{onClick:()=>{f("products"),_(""),v("")},className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${u==="products"&&!P?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.totalProducts})]})]})}),a.jsx("button",{onClick:()=>{f("products"),_("in_stock"),v("")},className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${P==="in_stock"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-emerald-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-emerald-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"In Stock"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.inStockCount})]})]})}),a.jsx("button",{onClick:()=>{f("products"),_("out_of_stock"),v("")},className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${P==="out_of_stock"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-red-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-red-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Out of Stock"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.outOfStockCount})]})]})}),a.jsx("button",{onClick:()=>f("brands"),className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${u==="brands"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(Cr,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.brandCount})]})]})}),a.jsx("button",{onClick:()=>f("categories"),className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${u==="categories"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(Uc,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Categories"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.categoryCount})]})]})})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"border-b border-gray-200",children:a.jsxs("div",{className:"flex gap-4 px-6",children:[a.jsxs("button",{onClick:()=>{f("products"),_("")},className:`py-4 px-2 text-sm font-medium border-b-2 ${u==="products"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Products (",r.totalProducts,")"]}),a.jsxs("button",{onClick:()=>f("brands"),className:`py-4 px-2 text-sm font-medium border-b-2 ${u==="brands"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Brands (",r.brandCount,")"]}),a.jsxs("button",{onClick:()=>f("categories"),className:`py-4 px-2 text-sm font-medium border-b-2 ${u==="categories"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Categories (",r.categoryCount,")"]})]})}),a.jsxs("div",{className:"p-6",children:[u==="products"&&a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{className:"flex items-center gap-4 mb-4",children:[a.jsx("input",{type:"text",placeholder:"Search products by name or brand...",value:b,onChange:O=>v(O.target.value),className:"input input-bordered input-sm flex-1"}),a.jsxs("select",{value:P,onChange:O=>_(O.target.value),className:"select select-bordered select-sm",children:[a.jsx("option",{value:"",children:"All Stock"}),a.jsx("option",{value:"in_stock",children:"In Stock"}),a.jsx("option",{value:"out_of_stock",children:"Out of Stock"}),a.jsx("option",{value:"unknown",children:"Unknown"})]}),(b||P)&&a.jsx("button",{onClick:()=>{v(""),_("")},className:"btn btn-sm btn-ghost",children:"Clear"}),a.jsxs("div",{className:"text-sm text-gray-600",children:[w," products"]})]}),c?a.jsxs("div",{className:"text-center py-8",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-6 w-6 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading products..."})]}):i.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No products found"}):a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"overflow-x-auto -mx-6 px-6",children:a.jsxs("table",{className:"table table-xs table-zebra table-pin-rows w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Image"}),a.jsx("th",{children:"Product Name"}),a.jsx("th",{children:"Brand"}),a.jsx("th",{children:"Type"}),a.jsx("th",{className:"text-right",children:"Price"}),a.jsx("th",{className:"text-center",children:"THC %"}),a.jsx("th",{className:"text-center",children:"Stock"}),a.jsx("th",{className:"text-center",children:"Qty"}),a.jsx("th",{children:"Last Updated"})]})}),a.jsx("tbody",{children:i.map(O=>a.jsxs("tr",{children:[a.jsx("td",{className:"whitespace-nowrap",children:O.image_url?a.jsx("img",{src:O.image_url,alt:O.name,className:"w-12 h-12 object-cover rounded",onError:k=>k.currentTarget.style.display="none"}):"-"}),a.jsx("td",{className:"font-medium max-w-[200px]",children:a.jsx("div",{className:"line-clamp-2",title:O.name,children:O.name})}),a.jsx("td",{className:"max-w-[120px]",children:a.jsx("div",{className:"line-clamp-2",title:O.brand||"-",children:O.brand||"-"})}),a.jsxs("td",{className:"whitespace-nowrap",children:[a.jsx("span",{className:"badge badge-ghost badge-sm",children:O.type||"-"}),O.subcategory&&a.jsx("span",{className:"badge badge-ghost badge-sm ml-1",children:O.subcategory})]}),a.jsx("td",{className:"text-right font-semibold whitespace-nowrap",children:O.sale_price?a.jsxs("div",{className:"flex flex-col items-end",children:[a.jsxs("span",{className:"text-error",children:["$",O.sale_price]}),a.jsxs("span",{className:"text-gray-400 line-through text-xs",children:["$",O.regular_price]})]}):O.regular_price?`$${O.regular_price}`:"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:O.thc_percentage?a.jsxs("span",{className:"badge badge-success badge-sm",children:[O.thc_percentage,"%"]}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:O.stock_status==="in_stock"?a.jsx("span",{className:"badge badge-success badge-sm",children:"In Stock"}):O.stock_status==="out_of_stock"?a.jsx("span",{className:"badge badge-error badge-sm",children:"Out"}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Unknown"})}),a.jsx("td",{className:"text-center whitespace-nowrap",children:O.total_quantity!=null?O.total_quantity:"-"}),a.jsx("td",{className:"whitespace-nowrap text-xs text-gray-500",children:O.updated_at?T(O.updated_at):"-"})]},O.id))})]})}),R>1&&a.jsxs("div",{className:"flex justify-center items-center gap-2 mt-4",children:[a.jsx("button",{onClick:()=>y(O=>Math.max(1,O-1)),disabled:j===1,className:"btn btn-sm btn-outline",children:"Previous"}),a.jsx("div",{className:"flex gap-1",children:Array.from({length:Math.min(5,R)},(O,k)=>{let L;return R<=5||j<=3?L=k+1:j>=R-2?L=R-4+k:L=j-2+k,a.jsx("button",{onClick:()=>y(L),className:`btn btn-sm ${j===L?"btn-primary":"btn-outline"}`,children:L},L)})}),a.jsx("button",{onClick:()=>y(O=>Math.min(R,O+1)),disabled:j===R,className:"btn btn-sm btn-outline",children:"Next"})]})]})]}),u==="brands"&&a.jsx("div",{className:"space-y-4",children:Z.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No brands found"}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:Z.map(O=>a.jsxs("button",{onClick:()=>{f("products"),v(O.brand_name),_("")},className:"border border-gray-200 rounded-lg p-4 text-center hover:border-blue-300 hover:shadow-md transition-all cursor-pointer",children:[a.jsx("p",{className:"font-medium text-gray-900 line-clamp-2",children:O.brand_name}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:[O.product_count," product",O.product_count!==1?"s":""]})]},O.brand_name))})}),u==="categories"&&a.jsx("div",{className:"space-y-4",children:E.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No categories found"}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:E.map((O,k)=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4 text-center",children:[a.jsx("p",{className:"font-medium text-gray-900",children:O.type}),O.subcategory&&a.jsx("p",{className:"text-sm text-gray-600",children:O.subcategory}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:[O.product_count," product",O.product_count!==1?"s":""]})]},k))})})]})]})]})})}function N7(){const e=dt(),[t,r]=h.useState(null),[n,i]=h.useState([]),[s,o]=h.useState([]),[l,c]=h.useState([]),[d,u]=h.useState(!0),[f,p]=h.useState("overview");h.useEffect(()=>{m()},[]);const m=async()=>{u(!0);try{const[g,b,v,j]=await Promise.all([B.getDutchieAZDashboard(),B.getDutchieAZStores({limit:200}),B.getDutchieAZBrands?B.getDutchieAZBrands({limit:100}):Promise.resolve({brands:[]}),B.getDutchieAZCategories?B.getDutchieAZCategories():Promise.resolve({categories:[]})]);r(g),i(b.stores||[]),o(v.brands||[]),c(j.categories||[])}catch(g){console.error("Failed to load analytics data:",g)}finally{u(!1)}},x=g=>{if(!g)return"Never";const b=new Date(g),j=new Date().getTime()-b.getTime(),y=Math.floor(j/(1e3*60*60)),w=Math.floor(j/(1e3*60*60*24));return y<1?"Just now":y<24?`${y}h ago`:w===1?"Yesterday":w<7?`${w} days ago`:b.toLocaleDateString()};return d?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading analytics..."})]})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Wholesale & Inventory Analytics"}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"Arizona Dutchie dispensaries data overview"})]}),a.jsxs("button",{onClick:m,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),a.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4",children:[a.jsx(Ii,{title:"Dispensaries",value:(t==null?void 0:t.dispensaryCount)||0,icon:a.jsx(Ln,{className:"w-5 h-5 text-blue-600"}),color:"blue"}),a.jsx(Ii,{title:"Total Products",value:(t==null?void 0:t.productCount)||0,icon:a.jsx(Ct,{className:"w-5 h-5 text-green-600"}),color:"green"}),a.jsx(Ii,{title:"Brands",value:(t==null?void 0:t.brandCount)||0,icon:a.jsx(Cr,{className:"w-5 h-5 text-purple-600"}),color:"purple"}),a.jsx(Ii,{title:"Categories",value:(t==null?void 0:t.categoryCount)||0,icon:a.jsx(Tx,{className:"w-5 h-5 text-orange-600"}),color:"orange"}),a.jsx(Ii,{title:"Snapshots (24h)",value:(t==null?void 0:t.snapshotCount24h)||0,icon:a.jsx(zn,{className:"w-5 h-5 text-cyan-600"}),color:"cyan"}),a.jsx(Ii,{title:"Failed Jobs (24h)",value:(t==null?void 0:t.failedJobCount)||0,icon:a.jsx(Uc,{className:"w-5 h-5 text-red-600"}),color:"red"}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-600 mb-1",children:[a.jsx(xr,{className:"w-4 h-4"}),a.jsx("span",{className:"text-xs",children:"Last Crawl"})]}),a.jsx("p",{className:"text-sm font-semibold text-gray-900",children:x((t==null?void 0:t.lastCrawlTime)||null)})]})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"border-b border-gray-200",children:a.jsxs("div",{className:"flex gap-4 px-6",children:[a.jsx(Vo,{active:f==="overview",onClick:()=>p("overview"),icon:a.jsx(HA,{className:"w-4 h-4"}),label:"Overview"}),a.jsx(Vo,{active:f==="stores",onClick:()=>p("stores"),icon:a.jsx(Ln,{className:"w-4 h-4"}),label:`Stores (${n.length})`}),a.jsx(Vo,{active:f==="brands",onClick:()=>p("brands"),icon:a.jsx(Cr,{className:"w-4 h-4"}),label:`Brands (${s.length})`}),a.jsx(Vo,{active:f==="categories",onClick:()=>p("categories"),icon:a.jsx(Tx,{className:"w-4 h-4"}),label:`Categories (${l.length})`})]})}),a.jsxs("div",{className:"p-6",children:[f==="overview"&&a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Top Stores by Products"}),a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",children:n.slice(0,6).map(g=>a.jsxs("button",{onClick:()=>e(`/az/stores/${g.id}`),className:"flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors text-left",children:[a.jsxs("div",{children:[a.jsx("p",{className:"font-medium text-gray-900",children:g.dba_name||g.name}),a.jsxs("p",{className:"text-sm text-gray-600",children:[g.city,", ",g.state]}),a.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:["Last crawl: ",x(g.last_crawl_at||null)]})]}),a.jsx(Cf,{className:"w-5 h-5 text-gray-400"})]},g.id))})]}),a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Top Brands"}),a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4",children:s.slice(0,12).map(g=>a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg text-center",children:[a.jsx("p",{className:"font-medium text-gray-900 text-sm line-clamp-2",children:g.brand_name}),a.jsx("p",{className:"text-lg font-bold text-purple-600 mt-1",children:g.product_count}),a.jsx("p",{className:"text-xs text-gray-500",children:"products"})]},g.brand_name))})]}),a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Product Categories"}),a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4",children:l.slice(0,12).map((g,b)=>a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg text-center",children:[a.jsx("p",{className:"font-medium text-gray-900",children:g.type}),g.subcategory&&a.jsx("p",{className:"text-xs text-gray-600",children:g.subcategory}),a.jsx("p",{className:"text-lg font-bold text-orange-600 mt-1",children:g.product_count}),a.jsx("p",{className:"text-xs text-gray-500",children:"products"})]},b))})]})]}),f==="stores"&&a.jsx("div",{className:"space-y-4",children:a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"table table-sm w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Store Name"}),a.jsx("th",{children:"City"}),a.jsx("th",{className:"text-center",children:"Platform ID"}),a.jsx("th",{className:"text-center",children:"Last Crawl"}),a.jsx("th",{})]})}),a.jsx("tbody",{children:n.map(g=>a.jsxs("tr",{className:"hover",children:[a.jsx("td",{className:"font-medium",children:g.dba_name||g.name}),a.jsxs("td",{children:[g.city,", ",g.state]}),a.jsx("td",{className:"text-center",children:g.platform_dispensary_id?a.jsx("span",{className:"badge badge-success badge-sm",children:"Resolved"}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Pending"})}),a.jsx("td",{className:"text-center text-sm text-gray-600",children:x(g.last_crawl_at||null)}),a.jsx("td",{children:a.jsx("button",{onClick:()=>e(`/az/stores/${g.id}`),className:"btn btn-xs btn-ghost",children:"View"})})]},g.id))})]})})}),f==="brands"&&a.jsx("div",{className:"space-y-4",children:s.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No brands found. Run a crawl to populate brand data."}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4",children:s.map(g=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4 text-center hover:border-purple-300 hover:shadow-md transition-all",children:[a.jsx("p",{className:"font-medium text-gray-900 text-sm line-clamp-2 h-10",children:g.brand_name}),a.jsxs("div",{className:"mt-2 space-y-1",children:[a.jsxs("div",{className:"flex justify-between text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Products:"}),a.jsx("span",{className:"font-semibold",children:g.product_count})]}),a.jsxs("div",{className:"flex justify-between text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Stores:"}),a.jsx("span",{className:"font-semibold",children:g.dispensary_count})]})]})]},g.brand_name))})}),f==="categories"&&a.jsx("div",{className:"space-y-4",children:l.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No categories found. Run a crawl to populate category data."}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:l.map((g,b)=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4 hover:border-orange-300 hover:shadow-md transition-all",children:[a.jsx("p",{className:"font-medium text-gray-900",children:g.type}),g.subcategory&&a.jsx("p",{className:"text-sm text-gray-600",children:g.subcategory}),a.jsxs("div",{className:"mt-3 grid grid-cols-2 gap-2 text-xs",children:[a.jsxs("div",{className:"bg-gray-50 rounded p-2 text-center",children:[a.jsx("p",{className:"font-bold text-lg text-orange-600",children:g.product_count}),a.jsx("p",{className:"text-gray-500",children:"products"})]}),a.jsxs("div",{className:"bg-gray-50 rounded p-2 text-center",children:[a.jsx("p",{className:"font-bold text-lg text-blue-600",children:g.brand_count}),a.jsx("p",{className:"text-gray-500",children:"brands"})]})]}),g.avg_thc!=null&&a.jsxs("p",{className:"text-xs text-gray-500 mt-2 text-center",children:["Avg THC: ",g.avg_thc.toFixed(1),"%"]})]},b))})})]})]})]})})}function Ii({title:e,value:t,icon:r,color:n}){const i={blue:"bg-blue-50",green:"bg-green-50",purple:"bg-purple-50",orange:"bg-orange-50",cyan:"bg-cyan-50",red:"bg-red-50"};return a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:`p-2 ${i[n]||"bg-gray-50"} rounded-lg`,children:r}),a.jsxs("div",{children:[a.jsx("p",{className:"text-xs text-gray-600",children:e}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:t.toLocaleString()})]})]})})}function Vo({active:e,onClick:t,icon:r,label:n}){return a.jsxs("button",{onClick:t,className:`flex items-center gap-2 py-4 px-2 text-sm font-medium border-b-2 transition-colors ${e?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:[r,n]})}function ve({children:e}){const{isAuthenticated:t,checkAuth:r}=Ph();return h.useEffect(()=>{r()},[]),t?a.jsx(a.Fragment,{children:e}):a.jsx(H1,{to:"/login",replace:!0})}function k7(){return a.jsx(QC,{children:a.jsxs(HC,{children:[a.jsx(de,{path:"/login",element:a.jsx(DA,{})}),a.jsx(de,{path:"/",element:a.jsx(ve,{children:a.jsx(XB,{})})}),a.jsx(de,{path:"/products",element:a.jsx(ve,{children:a.jsx(JB,{})})}),a.jsx(de,{path:"/products/:id",element:a.jsx(ve,{children:a.jsx(e7,{})})}),a.jsx(de,{path:"/stores",element:a.jsx(ve,{children:a.jsx(t7,{})})}),a.jsx(de,{path:"/dispensaries",element:a.jsx(ve,{children:a.jsx(r7,{})})}),a.jsx(de,{path:"/dispensaries/:state/:city/:slug",element:a.jsx(ve,{children:a.jsx(n7,{})})}),a.jsx(de,{path:"/stores/:state/:storeName/:slug/brands",element:a.jsx(ve,{children:a.jsx(a7,{})})}),a.jsx(de,{path:"/stores/:state/:storeName/:slug/specials",element:a.jsx(ve,{children:a.jsx(s7,{})})}),a.jsx(de,{path:"/stores/:state/:storeName/:slug",element:a.jsx(ve,{children:a.jsx(i7,{})})}),a.jsx(de,{path:"/categories",element:a.jsx(ve,{children:a.jsx(o7,{})})}),a.jsx(de,{path:"/campaigns",element:a.jsx(ve,{children:a.jsx(l7,{})})}),a.jsx(de,{path:"/analytics",element:a.jsx(ve,{children:a.jsx(u7,{})})}),a.jsx(de,{path:"/settings",element:a.jsx(ve,{children:a.jsx(d7,{})})}),a.jsx(de,{path:"/changes",element:a.jsx(ve,{children:a.jsx(v7,{})})}),a.jsx(de,{path:"/proxies",element:a.jsx(ve,{children:a.jsx(p7,{})})}),a.jsx(de,{path:"/logs",element:a.jsx(ve,{children:a.jsx(m7,{})})}),a.jsx(de,{path:"/scraper-tools",element:a.jsx(ve,{children:a.jsx(y7,{})})}),a.jsx(de,{path:"/scraper-monitor",element:a.jsx(ve,{children:a.jsx(g7,{})})}),a.jsx(de,{path:"/scraper-schedule",element:a.jsx(ve,{children:a.jsx(x7,{})})}),a.jsx(de,{path:"/az-schedule",element:a.jsx(ve,{children:a.jsx(j7,{})})}),a.jsx(de,{path:"/az",element:a.jsx(ve,{children:a.jsx(w7,{})})}),a.jsx(de,{path:"/az/stores/:id",element:a.jsx(ve,{children:a.jsx(S7,{})})}),a.jsx(de,{path:"/api-permissions",element:a.jsx(ve,{children:a.jsx(b7,{})})}),a.jsx(de,{path:"/wholesale-analytics",element:a.jsx(ve,{children:a.jsx(N7,{})})}),a.jsx(de,{path:"*",element:a.jsx(H1,{to:"/",replace:!0})})]})})}Od.createRoot(document.getElementById("root")).render(a.jsx(fs.StrictMode,{children:a.jsx(k7,{})})); diff --git a/backend/public/app/index.html b/backend/public/app/index.html new file mode 100644 index 00000000..37352ff8 --- /dev/null +++ b/backend/public/app/index.html @@ -0,0 +1,14 @@ + + + + + + + Dutchie Menus Admin + + + + +
+ + diff --git a/backend/public/app/wordpress/menus-v1.2.0.zip b/backend/public/app/wordpress/menus-v1.2.0.zip new file mode 100644 index 00000000..47cecdc8 Binary files /dev/null and b/backend/public/app/wordpress/menus-v1.2.0.zip differ diff --git a/backend/public/app/wordpress/menus-v1.3.0.zip b/backend/public/app/wordpress/menus-v1.3.0.zip new file mode 100644 index 00000000..668e3d17 Binary files /dev/null and b/backend/public/app/wordpress/menus-v1.3.0.zip differ diff --git a/backend/public/downloads/crawlsy-menus-v1.4.0.zip b/backend/public/app/wordpress/menus-v1.4.0.zip similarity index 90% rename from backend/public/downloads/crawlsy-menus-v1.4.0.zip rename to backend/public/app/wordpress/menus-v1.4.0.zip index 1c2ca86e..f4afdbf0 100644 Binary files a/backend/public/downloads/crawlsy-menus-v1.4.0.zip and b/backend/public/app/wordpress/menus-v1.4.0.zip differ diff --git a/backend/public/downloads/crawlsy-menus-v1.5.0.zip b/backend/public/downloads/crawlsy-menus-v1.5.0.zip new file mode 100644 index 00000000..b8303c9f Binary files /dev/null and b/backend/public/downloads/crawlsy-menus-v1.5.0.zip differ diff --git a/backend/public/downloads/dutchie-analytics-v1.0.3.zip b/backend/public/downloads/dutchie-analytics-v1.0.3.zip new file mode 100644 index 00000000..7eea9dbf Binary files /dev/null and b/backend/public/downloads/dutchie-analytics-v1.0.3.zip differ diff --git a/backend/schema-dump.sql b/backend/schema-dump.sql new file mode 100644 index 00000000..3986500a --- /dev/null +++ b/backend/schema-dump.sql @@ -0,0 +1,4042 @@ +-- +-- PostgreSQL database dump +-- + +\restrict u76b1lsSuckyRNpZbORH9drBRaNwzQbqR7X3xYnHxUdiczMnjtBCdx8KbLlDBsP + +-- Dumped from database version 15.15 +-- Dumped by pg_dump version 15.15 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: set_requires_recrawl(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.set_requires_recrawl() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF NEW.field_name IN ('website', 'menu_url') THEN + NEW.requires_recrawl := TRUE; + END IF; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.set_requires_recrawl() OWNER TO dutchie; + +-- +-- Name: update_api_token_updated_at(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_api_token_updated_at() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_api_token_updated_at() OWNER TO dutchie; + +-- +-- Name: update_brand_scrape_jobs_updated_at(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_brand_scrape_jobs_updated_at() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_brand_scrape_jobs_updated_at() OWNER TO dutchie; + +-- +-- Name: update_sandbox_timestamp(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_sandbox_timestamp() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_sandbox_timestamp() OWNER TO dutchie; + +-- +-- Name: update_schedule_updated_at(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_schedule_updated_at() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_schedule_updated_at() OWNER TO dutchie; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: api_token_usage; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.api_token_usage ( + id integer NOT NULL, + token_id integer, + endpoint character varying(255) NOT NULL, + method character varying(10) NOT NULL, + status_code integer, + response_time_ms integer, + request_size integer, + response_size integer, + ip_address inet, + user_agent text, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.api_token_usage OWNER TO dutchie; + +-- +-- Name: api_token_usage_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.api_token_usage_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.api_token_usage_id_seq OWNER TO dutchie; + +-- +-- Name: api_token_usage_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.api_token_usage_id_seq OWNED BY public.api_token_usage.id; + + +-- +-- Name: api_tokens; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.api_tokens ( + id integer NOT NULL, + name character varying(255) NOT NULL, + token character varying(255) NOT NULL, + description text, + user_id integer, + active boolean DEFAULT true, + rate_limit integer DEFAULT 100, + allowed_endpoints text[], + expires_at timestamp without time zone, + last_used_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.api_tokens OWNER TO dutchie; + +-- +-- Name: api_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.api_tokens_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.api_tokens_id_seq OWNER TO dutchie; + +-- +-- Name: api_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.api_tokens_id_seq OWNED BY public.api_tokens.id; + + +-- +-- Name: azdhs_list; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.azdhs_list ( + id integer NOT NULL, + name character varying(255) NOT NULL, + company_name character varying(255), + slug character varying(255), + address character varying(500), + city character varying(100), + state character varying(2) DEFAULT 'AZ'::character varying, + zip character varying(10), + phone character varying(20), + email character varying(255), + status_line text, + azdhs_url text, + latitude numeric(10,8), + longitude numeric(11,8), + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + website text, + dba_name character varying(255), + google_rating numeric(2,1), + google_review_count integer +); + + +ALTER TABLE public.azdhs_list OWNER TO dutchie; + +-- +-- Name: azdhs_list_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.azdhs_list_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.azdhs_list_id_seq OWNER TO dutchie; + +-- +-- Name: azdhs_list_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.azdhs_list_id_seq OWNED BY public.azdhs_list.id; + + +-- +-- Name: batch_history; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.batch_history ( + id integer NOT NULL, + product_id integer, + thc_percentage numeric(5,2), + cbd_percentage numeric(5,2), + terpenes text[], + strain_type character varying(100), + recorded_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.batch_history OWNER TO dutchie; + +-- +-- Name: batch_history_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.batch_history_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.batch_history_id_seq OWNER TO dutchie; + +-- +-- Name: batch_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.batch_history_id_seq OWNED BY public.batch_history.id; + + +-- +-- Name: brand_history; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.brand_history ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + brand_name character varying(255) NOT NULL, + event_type character varying(20) NOT NULL, + event_at timestamp with time zone DEFAULT now(), + product_count integer, + metadata jsonb +); + + +ALTER TABLE public.brand_history OWNER TO dutchie; + +-- +-- Name: brand_history_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.brand_history_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.brand_history_id_seq OWNER TO dutchie; + +-- +-- Name: brand_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.brand_history_id_seq OWNED BY public.brand_history.id; + + +-- +-- Name: brand_scrape_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.brand_scrape_jobs ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + brand_slug text NOT NULL, + brand_name text NOT NULL, + status text DEFAULT 'pending'::text NOT NULL, + worker_id text, + started_at timestamp without time zone, + completed_at timestamp without time zone, + products_found integer DEFAULT 0, + products_saved integer DEFAULT 0, + error_message text, + retry_count integer DEFAULT 0, + created_at timestamp without time zone DEFAULT now(), + updated_at timestamp without time zone DEFAULT now() +); + + +ALTER TABLE public.brand_scrape_jobs OWNER TO dutchie; + +-- +-- Name: brand_scrape_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.brand_scrape_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.brand_scrape_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: brand_scrape_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.brand_scrape_jobs_id_seq OWNED BY public.brand_scrape_jobs.id; + + +-- +-- Name: brands; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.brands ( + id integer NOT NULL, + store_id integer NOT NULL, + name character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + first_seen_at timestamp with time zone DEFAULT now(), + last_seen_at timestamp with time zone DEFAULT now(), + dispensary_id integer +); + + +ALTER TABLE public.brands OWNER TO dutchie; + +-- +-- Name: brands_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.brands_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.brands_id_seq OWNER TO dutchie; + +-- +-- Name: brands_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.brands_id_seq OWNED BY public.brands.id; + + +-- +-- Name: campaign_products; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.campaign_products ( + id integer NOT NULL, + campaign_id integer, + product_id integer, + display_order integer DEFAULT 0, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.campaign_products OWNER TO dutchie; + +-- +-- Name: campaign_products_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.campaign_products_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.campaign_products_id_seq OWNER TO dutchie; + +-- +-- Name: campaign_products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.campaign_products_id_seq OWNED BY public.campaign_products.id; + + +-- +-- Name: campaigns; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.campaigns ( + id integer NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + description text, + display_style character varying(50) DEFAULT 'grid'::character varying, + active boolean DEFAULT true, + start_date timestamp without time zone, + end_date timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.campaigns OWNER TO dutchie; + +-- +-- Name: campaigns_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.campaigns_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.campaigns_id_seq OWNER TO dutchie; + +-- +-- Name: campaigns_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.campaigns_id_seq OWNED BY public.campaigns.id; + + +-- +-- Name: categories; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.categories ( + id integer NOT NULL, + store_id integer, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + dutchie_url text NOT NULL, + scrape_enabled boolean DEFAULT true, + last_scraped_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + parent_id integer, + display_order integer DEFAULT 0, + description text, + path character varying(500), + dispensary_id integer +); + + +ALTER TABLE public.categories OWNER TO dutchie; + +-- +-- Name: categories_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.categories_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.categories_id_seq OWNER TO dutchie; + +-- +-- Name: categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.categories_id_seq OWNED BY public.categories.id; + + +-- +-- Name: clicks; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.clicks ( + id integer NOT NULL, + product_id integer, + campaign_id integer, + ip_address character varying(45), + user_agent text, + referrer text, + clicked_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.clicks OWNER TO dutchie; + +-- +-- Name: clicks_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.clicks_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.clicks_id_seq OWNER TO dutchie; + +-- +-- Name: clicks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.clicks_id_seq OWNED BY public.clicks.id; + + +-- +-- Name: crawl_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawl_jobs ( + id integer NOT NULL, + store_id integer NOT NULL, + job_type character varying(50) DEFAULT 'full_crawl'::character varying NOT NULL, + trigger_type character varying(50) DEFAULT 'scheduled'::character varying NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + priority integer DEFAULT 0, + scheduled_at timestamp with time zone DEFAULT now() NOT NULL, + started_at timestamp with time zone, + completed_at timestamp with time zone, + products_found integer, + products_new integer, + products_updated integer, + error_message text, + worker_id character varying(100), + metadata jsonb DEFAULT '{}'::jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + orchestrator_run_id uuid, + detection_result jsonb, + in_stock_count integer, + out_of_stock_count integer, + limited_count integer, + unknown_count integer, + availability_changed_count integer, + CONSTRAINT chk_crawl_job_status CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'running'::character varying, 'completed'::character varying, 'failed'::character varying, 'cancelled'::character varying])::text[]))) +); + + +ALTER TABLE public.crawl_jobs OWNER TO dutchie; + +-- +-- Name: COLUMN crawl_jobs.orchestrator_run_id; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawl_jobs.orchestrator_run_id IS 'Groups related jobs from same orchestrator run'; + + +-- +-- Name: crawl_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawl_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawl_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: crawl_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawl_jobs_id_seq OWNED BY public.crawl_jobs.id; + + +-- +-- Name: crawler_schedule; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawler_schedule ( + id integer NOT NULL, + schedule_type character varying(50) NOT NULL, + enabled boolean DEFAULT true NOT NULL, + interval_hours integer, + run_time time without time zone, + description text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE public.crawler_schedule OWNER TO dutchie; + +-- +-- Name: dispensaries; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensaries ( + id integer NOT NULL, + azdhs_id integer, + name character varying(255) NOT NULL, + company_name character varying(255), + address character varying(500) NOT NULL, + city character varying(100) NOT NULL, + state character varying(2) NOT NULL, + zip character varying(10), + status_line character varying(100), + azdhs_url text, + latitude numeric(10,8), + longitude numeric(11,8), + dba_name character varying(255), + phone character varying(20), + email character varying(255), + website text, + google_rating numeric(2,1), + google_review_count integer, + menu_url text, + scraper_template character varying(100), + scraper_config jsonb, + last_menu_scrape timestamp without time zone, + menu_scrape_status character varying(50) DEFAULT 'pending'::character varying, + slug character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + menu_provider character varying(50), + menu_provider_confidence smallint DEFAULT 0, + crawler_mode character varying(20) DEFAULT 'production'::character varying, + crawler_status character varying(30) DEFAULT 'idle'::character varying, + last_menu_error_at timestamp with time zone, + last_error_message text, + provider_detection_data jsonb DEFAULT '{}'::jsonb, + product_provider character varying(50), + product_confidence smallint DEFAULT 0, + product_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_product_scan_at timestamp with time zone, + product_detection_data jsonb DEFAULT '{}'::jsonb, + specials_provider character varying(50), + specials_confidence smallint DEFAULT 0, + specials_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_specials_scan_at timestamp with time zone, + specials_detection_data jsonb DEFAULT '{}'::jsonb, + brand_provider character varying(50), + brand_confidence smallint DEFAULT 0, + brand_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_brand_scan_at timestamp with time zone, + brand_detection_data jsonb DEFAULT '{}'::jsonb, + metadata_provider character varying(50), + metadata_confidence smallint DEFAULT 0, + metadata_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_metadata_scan_at timestamp with time zone, + metadata_detection_data jsonb DEFAULT '{}'::jsonb, + provider_type character varying(50) DEFAULT 'unknown'::character varying, + scrape_enabled boolean DEFAULT false, + last_crawl_at timestamp with time zone, + next_crawl_at timestamp with time zone, + crawl_status character varying(50) DEFAULT 'pending'::character varying, + crawl_error text, + consecutive_failures integer DEFAULT 0, + total_crawls integer DEFAULT 0, + successful_crawls integer DEFAULT 0, + CONSTRAINT chk_crawler_mode CHECK (((crawler_mode)::text = ANY ((ARRAY['production'::character varying, 'sandbox'::character varying])::text[]))), + CONSTRAINT chk_crawler_status CHECK (((crawler_status)::text = ANY ((ARRAY['idle'::character varying, 'queued_detection'::character varying, 'queued_crawl'::character varying, 'running'::character varying, 'ok'::character varying, 'error_needs_review'::character varying])::text[]))), + CONSTRAINT chk_provider_confidence CHECK (((menu_provider_confidence >= 0) AND (menu_provider_confidence <= 100))) +); + + +ALTER TABLE public.dispensaries OWNER TO dutchie; + +-- +-- Name: COLUMN dispensaries.menu_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.menu_provider IS 'Detected menu platform: dutchie, treez, jane, weedmaps, etc.'; + + +-- +-- Name: COLUMN dispensaries.menu_provider_confidence; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.menu_provider_confidence IS 'Confidence score 0-100 for provider detection'; + + +-- +-- Name: COLUMN dispensaries.crawler_mode; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.crawler_mode IS 'production = stable templates, sandbox = learning mode'; + + +-- +-- Name: COLUMN dispensaries.crawler_status; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.crawler_status IS 'Current state in crawl pipeline'; + + +-- +-- Name: COLUMN dispensaries.provider_detection_data; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.provider_detection_data IS 'JSON blob with detection signals and history'; + + +-- +-- Name: COLUMN dispensaries.product_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.product_provider IS 'Provider for product intelligence (dutchie, treez, jane, etc.)'; + + +-- +-- Name: COLUMN dispensaries.product_crawler_mode; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.product_crawler_mode IS 'production or sandbox mode for product crawling'; + + +-- +-- Name: COLUMN dispensaries.specials_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.specials_provider IS 'Provider for specials/deals intelligence'; + + +-- +-- Name: COLUMN dispensaries.brand_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.brand_provider IS 'Provider for brand intelligence'; + + +-- +-- Name: COLUMN dispensaries.metadata_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.metadata_provider IS 'Provider for metadata/taxonomy intelligence'; + + +-- +-- Name: store_crawl_schedule; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.store_crawl_schedule ( + id integer NOT NULL, + store_id integer NOT NULL, + enabled boolean DEFAULT true NOT NULL, + interval_hours integer, + daily_special_enabled boolean DEFAULT true, + daily_special_time time without time zone, + priority integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + last_status character varying(50), + last_summary text, + last_run_at timestamp with time zone, + last_error text +); + + +ALTER TABLE public.store_crawl_schedule OWNER TO dutchie; + +-- +-- Name: COLUMN store_crawl_schedule.last_status; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.store_crawl_schedule.last_status IS 'Orchestrator result status: success, error, sandbox_only, detection_only'; + + +-- +-- Name: COLUMN store_crawl_schedule.last_summary; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.store_crawl_schedule.last_summary IS 'Human-readable summary of last orchestrator run'; + + +-- +-- Name: COLUMN store_crawl_schedule.last_run_at; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.store_crawl_schedule.last_run_at IS 'When orchestrator last ran for this store'; + + +-- +-- Name: stores; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.stores ( + id integer NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + dutchie_url text NOT NULL, + active boolean DEFAULT true, + scrape_enabled boolean DEFAULT true, + last_scraped_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + logo_url text, + timezone character varying(50) DEFAULT 'America/Phoenix'::character varying, + dispensary_id integer +); + + +ALTER TABLE public.stores OWNER TO dutchie; + +-- +-- Name: COLUMN stores.dispensary_id; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.stores.dispensary_id IS 'FK to dispensaries table (master AZDHS directory)'; + + +-- +-- Name: crawl_schedule_status; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.crawl_schedule_status AS + SELECT s.id AS store_id, + s.name AS store_name, + s.slug AS store_slug, + s.timezone, + s.active, + s.scrape_enabled, + s.last_scraped_at, + s.dispensary_id, + d.name AS dispensary_name, + d.company_name AS dispensary_company, + d.city AS dispensary_city, + d.state AS dispensary_state, + d.slug AS dispensary_slug, + d.address AS dispensary_address, + d.menu_url AS dispensary_menu_url, + d.product_provider, + d.product_confidence, + d.product_crawler_mode, + COALESCE(scs.enabled, true) AS schedule_enabled, + COALESCE(scs.interval_hours, cs_global.interval_hours, 4) AS interval_hours, + COALESCE(scs.daily_special_enabled, true) AS daily_special_enabled, + COALESCE(scs.daily_special_time, '00:01:00'::time without time zone) AS daily_special_time, + COALESCE(scs.priority, 0) AS priority, + scs.last_status, + scs.last_summary, + scs.last_run_at AS schedule_last_run, + scs.last_error, + CASE + WHEN (s.last_scraped_at IS NULL) THEN now() + ELSE ((s.last_scraped_at + ((COALESCE(scs.interval_hours, cs_global.interval_hours, 4) || ' hours'::text))::interval))::timestamp with time zone + END AS next_scheduled_run, + cj.id AS latest_job_id, + cj.status AS latest_job_status, + cj.job_type AS latest_job_type, + cj.trigger_type AS latest_job_trigger, + cj.started_at AS latest_job_started, + cj.completed_at AS latest_job_completed, + cj.products_found AS latest_products_found, + cj.products_new AS latest_products_new, + cj.products_updated AS latest_products_updated, + cj.error_message AS latest_job_error + FROM ((((public.stores s + LEFT JOIN public.dispensaries d ON ((d.id = s.dispensary_id))) + LEFT JOIN public.store_crawl_schedule scs ON ((scs.store_id = s.id))) + LEFT JOIN public.crawler_schedule cs_global ON (((cs_global.schedule_type)::text = 'global_interval'::text))) + LEFT JOIN LATERAL ( SELECT cj2.id, + cj2.store_id, + cj2.job_type, + cj2.trigger_type, + cj2.status, + cj2.priority, + cj2.scheduled_at, + cj2.started_at, + cj2.completed_at, + cj2.products_found, + cj2.products_new, + cj2.products_updated, + cj2.error_message, + cj2.worker_id, + cj2.metadata, + cj2.created_at, + cj2.updated_at, + cj2.orchestrator_run_id, + cj2.detection_result + FROM public.crawl_jobs cj2 + WHERE (cj2.store_id = s.id) + ORDER BY cj2.created_at DESC + LIMIT 1) cj ON (true)) + WHERE (s.active = true); + + +ALTER TABLE public.crawl_schedule_status OWNER TO dutchie; + +-- +-- Name: crawler_sandboxes; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawler_sandboxes ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + suspected_menu_provider character varying(50), + mode character varying(30) DEFAULT 'detection'::character varying NOT NULL, + raw_html_location text, + screenshot_location text, + analysis_json jsonb DEFAULT '{}'::jsonb, + urls_tested jsonb DEFAULT '[]'::jsonb, + menu_entry_points jsonb DEFAULT '[]'::jsonb, + detection_signals jsonb DEFAULT '{}'::jsonb, + status character varying(30) DEFAULT 'pending'::character varying NOT NULL, + confidence_score smallint DEFAULT 0, + failure_reason text, + human_review_notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + analyzed_at timestamp with time zone, + reviewed_at timestamp with time zone, + category character varying(30) DEFAULT 'product'::character varying, + template_name character varying(100), + quality_score smallint DEFAULT 0, + products_extracted integer DEFAULT 0, + fields_missing integer DEFAULT 0, + error_count integer DEFAULT 0, + CONSTRAINT chk_sandbox_mode CHECK (((mode)::text = ANY ((ARRAY['detection'::character varying, 'template_learning'::character varying, 'validation'::character varying])::text[]))), + CONSTRAINT chk_sandbox_status CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'analyzing'::character varying, 'template_ready'::character varying, 'needs_human_review'::character varying, 'moved_to_production'::character varying, 'failed'::character varying])::text[]))) +); + + +ALTER TABLE public.crawler_sandboxes OWNER TO dutchie; + +-- +-- Name: TABLE crawler_sandboxes; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON TABLE public.crawler_sandboxes IS 'Learning/testing environment for unknown menu providers'; + + +-- +-- Name: COLUMN crawler_sandboxes.category; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawler_sandboxes.category IS 'Intelligence category: product, specials, brand, metadata'; + + +-- +-- Name: COLUMN crawler_sandboxes.quality_score; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawler_sandboxes.quality_score IS 'Quality score 0-100 for sandbox run results'; + + +-- +-- Name: crawler_sandboxes_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawler_sandboxes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawler_sandboxes_id_seq OWNER TO dutchie; + +-- +-- Name: crawler_sandboxes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawler_sandboxes_id_seq OWNED BY public.crawler_sandboxes.id; + + +-- +-- Name: crawler_schedule_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawler_schedule_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawler_schedule_id_seq OWNER TO dutchie; + +-- +-- Name: crawler_schedule_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawler_schedule_id_seq OWNED BY public.crawler_schedule.id; + + +-- +-- Name: crawler_templates; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawler_templates ( + id integer NOT NULL, + provider character varying(50) NOT NULL, + name character varying(100) NOT NULL, + version integer DEFAULT 1, + is_active boolean DEFAULT true NOT NULL, + is_default_for_provider boolean DEFAULT false, + selector_config jsonb DEFAULT '{}'::jsonb NOT NULL, + navigation_config jsonb DEFAULT '{}'::jsonb, + transform_config jsonb DEFAULT '{}'::jsonb, + validation_rules jsonb DEFAULT '{}'::jsonb, + test_urls jsonb DEFAULT '[]'::jsonb, + expected_structure jsonb DEFAULT '{}'::jsonb, + dispensaries_using integer DEFAULT 0, + success_rate numeric(5,2) DEFAULT 0, + last_successful_crawl timestamp with time zone, + last_failed_crawl timestamp with time zone, + notes text, + created_by character varying(100), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + environment character varying(20) DEFAULT 'production'::character varying +); + + +ALTER TABLE public.crawler_templates OWNER TO dutchie; + +-- +-- Name: TABLE crawler_templates; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON TABLE public.crawler_templates IS 'Reusable scraping configurations per menu provider'; + + +-- +-- Name: COLUMN crawler_templates.environment; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawler_templates.environment IS 'Template environment: production or sandbox'; + + +-- +-- Name: crawler_templates_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawler_templates_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawler_templates_id_seq OWNER TO dutchie; + +-- +-- Name: crawler_templates_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawler_templates_id_seq OWNED BY public.crawler_templates.id; + + +-- +-- Name: products; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.products ( + id integer NOT NULL, + store_id integer, + category_id integer, + dutchie_product_id character varying(255), + name character varying(500) NOT NULL, + slug character varying(500) NOT NULL, + description text, + price numeric(10,2), + original_price numeric(10,2), + strain_type character varying(100), + thc_percentage numeric(10,4), + cbd_percentage numeric(10,4), + brand character varying(255), + weight character varying(100), + image_url text, + local_image_path text, + dutchie_url text NOT NULL, + in_stock boolean DEFAULT true, + is_special boolean DEFAULT false, + metadata jsonb, + first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + dispensary_id integer, + variant character varying(255), + special_ends_at timestamp without time zone, + special_text text, + special_type character varying(100), + terpenes text[], + effects text[], + flavors text[], + regular_price numeric(10,2), + sale_price numeric(10,2), + stock_quantity integer, + stock_status text, + discount_type character varying(50), + discount_value character varying(100), + availability_status character varying(20) DEFAULT 'unknown'::character varying, + availability_raw jsonb, + last_seen_in_stock_at timestamp with time zone, + last_seen_out_of_stock_at timestamp with time zone, + enterprise_product_id character varying(255), + sku character varying(100), + brand_external_id character varying(255), + brand_logo_url text, + subcategory character varying(100), + canonical_category character varying(255), + rec_price numeric(10,2), + med_price numeric(10,2), + rec_special_price numeric(10,2), + med_special_price numeric(10,2), + is_on_special boolean DEFAULT false, + special_name text, + discount_percent numeric(10,2), + special_data jsonb, + inventory_quantity integer, + inventory_available integer, + is_below_threshold boolean DEFAULT false, + status character varying(20) DEFAULT 'Active'::character varying, + cannabinoids jsonb, + weight_mg integer, + net_weight_value numeric(10,2), + net_weight_unit character varying(20), + options text[], + raw_options text[], + additional_images text[], + is_featured boolean DEFAULT false, + medical_only boolean DEFAULT false, + rec_only boolean DEFAULT false, + source_created_at timestamp with time zone, + source_updated_at timestamp with time zone, + raw_data jsonb, + external_id character varying(255) +); + + +ALTER TABLE public.products OWNER TO dutchie; + +-- +-- Name: COLUMN products.availability_status; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.availability_status IS 'Normalized status: in_stock, out_of_stock, limited, unknown'; + + +-- +-- Name: COLUMN products.availability_raw; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.availability_raw IS 'Raw availability payload from provider for debugging'; + + +-- +-- Name: COLUMN products.last_seen_in_stock_at; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.last_seen_in_stock_at IS 'Last time product was seen in stock'; + + +-- +-- Name: COLUMN products.last_seen_out_of_stock_at; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.last_seen_out_of_stock_at IS 'Last time product was seen out of stock'; + + +-- +-- Name: COLUMN products.enterprise_product_id; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.enterprise_product_id IS 'Dutchie enterpriseProductId - shared across dispensaries'; + + +-- +-- Name: COLUMN products.brand_external_id; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.brand_external_id IS 'Dutchie brand.id / brandId field'; + + +-- +-- Name: COLUMN products.canonical_category; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.canonical_category IS 'Dutchie POSMetaData.canonicalCategory - pipe-separated category path'; + + +-- +-- Name: COLUMN products.rec_special_price; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.rec_special_price IS 'Dutchie recSpecialPrices[0] - discounted recreational price'; + + +-- +-- Name: COLUMN products.special_data; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.special_data IS 'Full Dutchie specialData JSONB including all active specials'; + + +-- +-- Name: COLUMN products.cannabinoids; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.cannabinoids IS 'Full Dutchie cannabinoidsV2 array as JSONB'; + + +-- +-- Name: COLUMN products.raw_data; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.raw_data IS 'Complete Dutchie GraphQL response for this product'; + + +-- +-- Name: current_specials; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.current_specials AS + SELECT p.id, + p.store_id, + p.name, + p.brand, + p.subcategory, + p.strain_type, + p.rec_price, + p.rec_special_price, + p.discount_percent, + p.special_name, + p.image_url, + p.slug, + p.thc_percentage, + p.cbd_percentage, + s.name AS store_name, + s.slug AS store_slug + FROM (public.products p + JOIN public.stores s ON ((s.id = p.store_id))) + WHERE ((p.is_on_special = true) AND ((p.status)::text = 'Active'::text) AND (p.rec_special_price IS NOT NULL)); + + +ALTER TABLE public.current_specials OWNER TO dutchie; + +-- +-- Name: derived_brands; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.derived_brands AS + SELECT p.store_id, + p.brand AS brand_name, + p.brand_external_id, + max(p.brand_logo_url) AS brand_logo_url, + count(*) AS product_count, + count(*) FILTER (WHERE ((p.status)::text = 'Active'::text)) AS active_count, + count(*) FILTER (WHERE p.is_on_special) AS special_count, + min(p.rec_price) AS min_price, + max(p.rec_price) AS max_price, + avg(p.rec_price) AS avg_price, + array_agg(DISTINCT p.subcategory) FILTER (WHERE (p.subcategory IS NOT NULL)) AS categories, + max(p.updated_at) AS last_updated + FROM public.products p + WHERE (p.brand IS NOT NULL) + GROUP BY p.store_id, p.brand, p.brand_external_id; + + +ALTER TABLE public.derived_brands OWNER TO dutchie; + +-- +-- Name: derived_categories; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.derived_categories AS + SELECT p.store_id, + p.subcategory AS category_name, + count(*) AS product_count, + count(*) FILTER (WHERE ((p.status)::text = 'Active'::text)) AS active_count, + count(*) FILTER (WHERE p.is_on_special) AS special_count, + min(p.rec_price) AS min_price, + max(p.rec_price) AS max_price, + array_agg(DISTINCT p.brand) FILTER (WHERE (p.brand IS NOT NULL)) AS brands, + max(p.updated_at) AS last_updated + FROM public.products p + WHERE (p.subcategory IS NOT NULL) + GROUP BY p.store_id, p.subcategory; + + +ALTER TABLE public.derived_categories OWNER TO dutchie; + +-- +-- Name: dispensaries_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensaries_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensaries_id_seq OWNER TO dutchie; + +-- +-- Name: dispensaries_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensaries_id_seq OWNED BY public.dispensaries.id; + + +-- +-- Name: dispensary_brand_stats; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.dispensary_brand_stats AS + SELECT d.id AS dispensary_id, + COALESCE(d.dba_name, d.name) AS dispensary_name, + count(DISTINCT p.brand) FILTER (WHERE (p.last_seen_at >= (now() - '7 days'::interval))) AS current_brands, + count(DISTINCT p.brand) AS total_brands_ever, + ( SELECT count(DISTINCT bh.brand_name) AS count + FROM public.brand_history bh + WHERE ((bh.dispensary_id = d.id) AND ((bh.event_type)::text = 'added'::text) AND (bh.event_at >= (now() - '7 days'::interval)))) AS new_brands_7d, + ( SELECT count(DISTINCT bh.brand_name) AS count + FROM public.brand_history bh + WHERE ((bh.dispensary_id = d.id) AND ((bh.event_type)::text = 'dropped'::text) AND (bh.event_at >= (now() - '7 days'::interval)))) AS dropped_brands_7d + FROM (public.dispensaries d + LEFT JOIN public.products p ON ((p.dispensary_id = d.id))) + GROUP BY d.id, d.dba_name, d.name; + + +ALTER TABLE public.dispensary_brand_stats OWNER TO dutchie; + +-- +-- Name: dispensary_changes; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensary_changes ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + field_name character varying(100) NOT NULL, + old_value text, + new_value text, + source character varying(50) NOT NULL, + confidence_score character varying(20), + change_notes text, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + requires_recrawl boolean DEFAULT false, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + reviewed_at timestamp without time zone, + reviewed_by integer, + rejection_reason text, + CONSTRAINT dispensary_changes_status_check CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'approved'::character varying, 'rejected'::character varying])::text[]))) +); + + +ALTER TABLE public.dispensary_changes OWNER TO dutchie; + +-- +-- Name: dispensary_changes_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensary_changes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensary_changes_id_seq OWNER TO dutchie; + +-- +-- Name: dispensary_changes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensary_changes_id_seq OWNED BY public.dispensary_changes.id; + + +-- +-- Name: dispensary_crawl_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensary_crawl_jobs ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + schedule_id integer, + job_type character varying(50) DEFAULT 'orchestrator'::character varying NOT NULL, + trigger_type character varying(50) DEFAULT 'scheduled'::character varying NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + priority integer DEFAULT 0, + scheduled_at timestamp with time zone DEFAULT now(), + started_at timestamp with time zone, + completed_at timestamp with time zone, + duration_ms integer, + detection_ran boolean DEFAULT false, + crawl_ran boolean DEFAULT false, + crawl_type character varying(20), + products_found integer, + products_new integer, + products_updated integer, + detected_provider character varying(50), + detected_confidence smallint, + detected_mode character varying(20), + error_message text, + worker_id character varying(100), + run_id uuid, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + in_stock_count integer, + out_of_stock_count integer, + limited_count integer, + unknown_count integer, + availability_changed_count integer +); + + +ALTER TABLE public.dispensary_crawl_jobs OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensary_crawl_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensary_crawl_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensary_crawl_jobs_id_seq OWNED BY public.dispensary_crawl_jobs.id; + + +-- +-- Name: dispensary_crawl_schedule; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensary_crawl_schedule ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + is_active boolean DEFAULT true NOT NULL, + interval_minutes integer DEFAULT 240 NOT NULL, + priority integer DEFAULT 0 NOT NULL, + last_run_at timestamp with time zone, + next_run_at timestamp with time zone, + last_status character varying(50), + last_summary text, + last_error text, + last_duration_ms integer, + consecutive_failures integer DEFAULT 0, + total_runs integer DEFAULT 0, + successful_runs integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + + +ALTER TABLE public.dispensary_crawl_schedule OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_schedule_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensary_crawl_schedule_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensary_crawl_schedule_id_seq OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_schedule_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensary_crawl_schedule_id_seq OWNED BY public.dispensary_crawl_schedule.id; + + +-- +-- Name: dispensary_crawl_status; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.dispensary_crawl_status AS + SELECT d.id AS dispensary_id, + COALESCE(d.dba_name, d.name) AS dispensary_name, + d.city, + d.state, + d.slug, + d.website, + d.menu_url, + COALESCE(d.product_provider, d.provider_type) AS product_provider, + d.provider_type, + d.product_confidence, + d.product_crawler_mode, + d.last_product_scan_at, + COALESCE(dcs.is_active, d.scrape_enabled, false) AS schedule_active, + COALESCE(dcs.interval_minutes, 240) AS interval_minutes, + COALESCE(dcs.priority, 0) AS priority, + COALESCE(dcs.last_run_at, d.last_crawl_at) AS last_run_at, + COALESCE(dcs.next_run_at, d.next_crawl_at) AS next_run_at, + COALESCE(dcs.last_status, d.crawl_status) AS last_status, + dcs.last_summary, + COALESCE(dcs.last_error, d.crawl_error) AS last_error, + COALESCE(dcs.consecutive_failures, d.consecutive_failures, 0) AS consecutive_failures, + COALESCE(dcs.total_runs, d.total_crawls, 0) AS total_runs, + COALESCE(dcs.successful_runs, d.successful_crawls, 0) AS successful_runs, + dcj.id AS latest_job_id, + dcj.job_type AS latest_job_type, + dcj.status AS latest_job_status, + dcj.started_at AS latest_job_started, + dcj.products_found AS latest_products_found + FROM ((public.dispensaries d + LEFT JOIN public.dispensary_crawl_schedule dcs ON ((dcs.dispensary_id = d.id))) + LEFT JOIN LATERAL ( SELECT dispensary_crawl_jobs.id, + dispensary_crawl_jobs.dispensary_id, + dispensary_crawl_jobs.schedule_id, + dispensary_crawl_jobs.job_type, + dispensary_crawl_jobs.trigger_type, + dispensary_crawl_jobs.status, + dispensary_crawl_jobs.priority, + dispensary_crawl_jobs.scheduled_at, + dispensary_crawl_jobs.started_at, + dispensary_crawl_jobs.completed_at, + dispensary_crawl_jobs.duration_ms, + dispensary_crawl_jobs.detection_ran, + dispensary_crawl_jobs.crawl_ran, + dispensary_crawl_jobs.crawl_type, + dispensary_crawl_jobs.products_found, + dispensary_crawl_jobs.products_new, + dispensary_crawl_jobs.products_updated, + dispensary_crawl_jobs.detected_provider, + dispensary_crawl_jobs.detected_confidence, + dispensary_crawl_jobs.detected_mode, + dispensary_crawl_jobs.error_message, + dispensary_crawl_jobs.worker_id, + dispensary_crawl_jobs.run_id, + dispensary_crawl_jobs.created_at, + dispensary_crawl_jobs.updated_at, + dispensary_crawl_jobs.in_stock_count, + dispensary_crawl_jobs.out_of_stock_count, + dispensary_crawl_jobs.limited_count, + dispensary_crawl_jobs.unknown_count, + dispensary_crawl_jobs.availability_changed_count + FROM public.dispensary_crawl_jobs + WHERE (dispensary_crawl_jobs.dispensary_id = d.id) + ORDER BY dispensary_crawl_jobs.created_at DESC + LIMIT 1) dcj ON (true)) + ORDER BY + CASE + WHEN (d.scrape_enabled = true) THEN 0 + ELSE 1 + END, COALESCE(dcs.priority, 0) DESC, COALESCE(d.dba_name, d.name); + + +ALTER TABLE public.dispensary_crawl_status OWNER TO dutchie; + +-- +-- Name: dutchie_product_snapshots; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dutchie_product_snapshots ( + id integer NOT NULL, + dutchie_product_id integer NOT NULL, + dispensary_id integer NOT NULL, + platform_dispensary_id character varying(100) NOT NULL, + external_product_id character varying(100) NOT NULL, + pricing_type character varying(20) DEFAULT 'unknown'::character varying, + crawl_mode character varying(20) DEFAULT 'mode_a'::character varying, + status character varying(50), + featured boolean DEFAULT false, + special boolean DEFAULT false, + medical_only boolean DEFAULT false, + rec_only boolean DEFAULT false, + is_present_in_feed boolean DEFAULT true, + stock_status character varying(20) DEFAULT 'unknown'::character varying, + rec_min_price_cents integer, + rec_max_price_cents integer, + rec_min_special_price_cents integer, + med_min_price_cents integer, + med_max_price_cents integer, + med_min_special_price_cents integer, + wholesale_min_price_cents integer, + total_quantity_available integer, + total_kiosk_quantity_available integer, + manual_inventory boolean DEFAULT false, + is_below_threshold boolean DEFAULT false, + is_below_kiosk_threshold boolean DEFAULT false, + options jsonb, + raw_payload jsonb NOT NULL, + crawled_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + + +ALTER TABLE public.dutchie_product_snapshots OWNER TO dutchie; + +-- +-- Name: dutchie_product_snapshots_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dutchie_product_snapshots_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dutchie_product_snapshots_id_seq OWNER TO dutchie; + +-- +-- Name: dutchie_product_snapshots_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dutchie_product_snapshots_id_seq OWNED BY public.dutchie_product_snapshots.id; + + +-- +-- Name: dutchie_products; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dutchie_products ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + platform character varying(20) DEFAULT 'dutchie'::character varying NOT NULL, + external_product_id character varying(100) NOT NULL, + platform_dispensary_id character varying(100) NOT NULL, + c_name character varying(500), + name character varying(500) NOT NULL, + brand_name character varying(255), + brand_id character varying(100), + brand_logo_url text, + type character varying(100), + subcategory character varying(100), + strain_type character varying(50), + provider character varying(100), + thc numeric(10,4), + thc_content numeric(10,4), + cbd numeric(10,4), + cbd_content numeric(10,4), + cannabinoids_v2 jsonb, + effects jsonb, + status character varying(50), + medical_only boolean DEFAULT false, + rec_only boolean DEFAULT false, + featured boolean DEFAULT false, + coming_soon boolean DEFAULT false, + certificate_of_analysis_enabled boolean DEFAULT false, + is_below_threshold boolean DEFAULT false, + is_below_kiosk_threshold boolean DEFAULT false, + options_below_threshold boolean DEFAULT false, + options_below_kiosk_threshold boolean DEFAULT false, + stock_status character varying(20) DEFAULT 'unknown'::character varying, + total_quantity_available integer DEFAULT 0, + primary_image_url text, + images jsonb, + measurements jsonb, + weight character varying(50), + past_c_names text[], + created_at_dutchie timestamp with time zone, + updated_at_dutchie timestamp with time zone, + latest_raw_payload jsonb, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + + +ALTER TABLE public.dutchie_products OWNER TO dutchie; + +-- +-- Name: dutchie_products_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dutchie_products_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dutchie_products_id_seq OWNER TO dutchie; + +-- +-- Name: dutchie_products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dutchie_products_id_seq OWNED BY public.dutchie_products.id; + + +-- +-- Name: failed_proxies; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.failed_proxies ( + id integer NOT NULL, + host character varying(255) NOT NULL, + port integer NOT NULL, + protocol character varying(10) NOT NULL, + username character varying(255), + password character varying(255), + failure_count integer NOT NULL, + last_error text, + failed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + city character varying(100), + state character varying(100), + country character varying(100), + country_code character varying(2), + location_updated_at timestamp without time zone +); + + +ALTER TABLE public.failed_proxies OWNER TO dutchie; + +-- +-- Name: failed_proxies_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.failed_proxies_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.failed_proxies_id_seq OWNER TO dutchie; + +-- +-- Name: failed_proxies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.failed_proxies_id_seq OWNED BY public.failed_proxies.id; + + +-- +-- Name: jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.jobs ( + id integer NOT NULL, + type character varying(50) NOT NULL, + status character varying(50) DEFAULT 'pending'::character varying, + store_id integer, + progress integer DEFAULT 0, + total_items integer, + processed_items integer DEFAULT 0, + error text, + started_at timestamp without time zone, + completed_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.jobs OWNER TO dutchie; + +-- +-- Name: jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.jobs_id_seq OWNER TO dutchie; + +-- +-- Name: jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.jobs_id_seq OWNED BY public.jobs.id; + + +-- +-- Name: price_history; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.price_history ( + id integer NOT NULL, + product_id integer, + regular_price numeric(10,2), + sale_price numeric(10,2), + recorded_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.price_history OWNER TO dutchie; + +-- +-- Name: price_history_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.price_history_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.price_history_id_seq OWNER TO dutchie; + +-- +-- Name: price_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.price_history_id_seq OWNED BY public.price_history.id; + + +-- +-- Name: product_categories; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.product_categories ( + id integer NOT NULL, + product_id integer, + category_slug character varying(255) NOT NULL, + first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.product_categories OWNER TO dutchie; + +-- +-- Name: product_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.product_categories_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.product_categories_id_seq OWNER TO dutchie; + +-- +-- Name: product_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.product_categories_id_seq OWNED BY public.product_categories.id; + + +-- +-- Name: products_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.products_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.products_id_seq OWNER TO dutchie; + +-- +-- Name: products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.products_id_seq OWNED BY public.products.id; + + +-- +-- Name: proxies; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.proxies ( + id integer NOT NULL, + host character varying(255) NOT NULL, + port integer NOT NULL, + protocol character varying(10) NOT NULL, + username character varying(255), + password character varying(255), + active boolean DEFAULT true, + is_anonymous boolean DEFAULT false, + last_tested_at timestamp without time zone, + test_result character varying(50), + response_time_ms integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + failure_count integer DEFAULT 0, + city character varying(100), + state character varying(100), + country character varying(100), + country_code character varying(2), + location_updated_at timestamp without time zone +); + + +ALTER TABLE public.proxies OWNER TO dutchie; + +-- +-- Name: proxies_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.proxies_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.proxies_id_seq OWNER TO dutchie; + +-- +-- Name: proxies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.proxies_id_seq OWNED BY public.proxies.id; + + +-- +-- Name: proxy_test_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.proxy_test_jobs ( + id integer NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + total_proxies integer DEFAULT 0 NOT NULL, + tested_proxies integer DEFAULT 0 NOT NULL, + passed_proxies integer DEFAULT 0 NOT NULL, + failed_proxies integer DEFAULT 0 NOT NULL, + started_at timestamp without time zone, + completed_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.proxy_test_jobs OWNER TO dutchie; + +-- +-- Name: proxy_test_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.proxy_test_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.proxy_test_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: proxy_test_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.proxy_test_jobs_id_seq OWNED BY public.proxy_test_jobs.id; + + +-- +-- Name: sandbox_crawl_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.sandbox_crawl_jobs ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + sandbox_id integer, + job_type character varying(30) DEFAULT 'detection'::character varying NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + priority integer DEFAULT 0, + scheduled_at timestamp with time zone DEFAULT now() NOT NULL, + started_at timestamp with time zone, + completed_at timestamp with time zone, + worker_id character varying(100), + result_summary jsonb DEFAULT '{}'::jsonb, + error_message text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + category character varying(30) DEFAULT 'product'::character varying, + template_name character varying(100), + CONSTRAINT chk_sandbox_job_status CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'running'::character varying, 'completed'::character varying, 'failed'::character varying, 'cancelled'::character varying])::text[]))), + CONSTRAINT chk_sandbox_job_type CHECK (((job_type)::text = ANY ((ARRAY['detection'::character varying, 'template_test'::character varying, 'deep_crawl'::character varying])::text[]))) +); + + +ALTER TABLE public.sandbox_crawl_jobs OWNER TO dutchie; + +-- +-- Name: TABLE sandbox_crawl_jobs; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON TABLE public.sandbox_crawl_jobs IS 'Job queue for sandbox crawl operations (separate from production)'; + + +-- +-- Name: sandbox_crawl_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.sandbox_crawl_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.sandbox_crawl_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: sandbox_crawl_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.sandbox_crawl_jobs_id_seq OWNED BY public.sandbox_crawl_jobs.id; + + +-- +-- Name: settings; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.settings ( + key character varying(255) NOT NULL, + value text NOT NULL, + description text, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.settings OWNER TO dutchie; + +-- +-- Name: specials; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.specials ( + id integer NOT NULL, + store_id integer NOT NULL, + product_id integer, + name character varying(255) NOT NULL, + description text, + discount_amount numeric(10,2), + discount_percentage numeric(5,2), + special_price numeric(10,2), + original_price numeric(10,2), + valid_date date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.specials OWNER TO dutchie; + +-- +-- Name: specials_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.specials_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.specials_id_seq OWNER TO dutchie; + +-- +-- Name: specials_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.specials_id_seq OWNED BY public.specials.id; + + +-- +-- Name: store_crawl_schedule_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.store_crawl_schedule_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.store_crawl_schedule_id_seq OWNER TO dutchie; + +-- +-- Name: store_crawl_schedule_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.store_crawl_schedule_id_seq OWNED BY public.store_crawl_schedule.id; + + +-- +-- Name: stores_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.stores_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.stores_id_seq OWNER TO dutchie; + +-- +-- Name: stores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.stores_id_seq OWNED BY public.stores.id; + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + email character varying(255) NOT NULL, + password_hash character varying(255) NOT NULL, + role character varying(50) DEFAULT 'admin'::character varying, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.users OWNER TO dutchie; + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.users_id_seq OWNER TO dutchie; + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: wp_dutchie_api_permissions; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.wp_dutchie_api_permissions ( + id integer NOT NULL, + user_name character varying(255) NOT NULL, + api_key character varying(255) NOT NULL, + allowed_ips text, + allowed_domains text, + is_active smallint DEFAULT 1, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + last_used_at timestamp without time zone, + store_id integer, + store_name character varying(255) +); + + +ALTER TABLE public.wp_dutchie_api_permissions OWNER TO dutchie; + +-- +-- Name: wp_dutchie_api_permissions_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.wp_dutchie_api_permissions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.wp_dutchie_api_permissions_id_seq OWNER TO dutchie; + +-- +-- Name: wp_dutchie_api_permissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.wp_dutchie_api_permissions_id_seq OWNED BY public.wp_dutchie_api_permissions.id; + + +-- +-- Name: api_token_usage id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_token_usage ALTER COLUMN id SET DEFAULT nextval('public.api_token_usage_id_seq'::regclass); + + +-- +-- Name: api_tokens id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens ALTER COLUMN id SET DEFAULT nextval('public.api_tokens_id_seq'::regclass); + + +-- +-- Name: azdhs_list id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.azdhs_list ALTER COLUMN id SET DEFAULT nextval('public.azdhs_list_id_seq'::regclass); + + +-- +-- Name: batch_history id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.batch_history ALTER COLUMN id SET DEFAULT nextval('public.batch_history_id_seq'::regclass); + + +-- +-- Name: brand_history id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_history ALTER COLUMN id SET DEFAULT nextval('public.brand_history_id_seq'::regclass); + + +-- +-- Name: brand_scrape_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs ALTER COLUMN id SET DEFAULT nextval('public.brand_scrape_jobs_id_seq'::regclass); + + +-- +-- Name: brands id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands ALTER COLUMN id SET DEFAULT nextval('public.brands_id_seq'::regclass); + + +-- +-- Name: campaign_products id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products ALTER COLUMN id SET DEFAULT nextval('public.campaign_products_id_seq'::regclass); + + +-- +-- Name: campaigns id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaigns ALTER COLUMN id SET DEFAULT nextval('public.campaigns_id_seq'::regclass); + + +-- +-- Name: categories id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories ALTER COLUMN id SET DEFAULT nextval('public.categories_id_seq'::regclass); + + +-- +-- Name: clicks id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks ALTER COLUMN id SET DEFAULT nextval('public.clicks_id_seq'::regclass); + + +-- +-- Name: crawl_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawl_jobs ALTER COLUMN id SET DEFAULT nextval('public.crawl_jobs_id_seq'::regclass); + + +-- +-- Name: crawler_sandboxes id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_sandboxes ALTER COLUMN id SET DEFAULT nextval('public.crawler_sandboxes_id_seq'::regclass); + + +-- +-- Name: crawler_schedule id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_schedule ALTER COLUMN id SET DEFAULT nextval('public.crawler_schedule_id_seq'::regclass); + + +-- +-- Name: crawler_templates id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_templates ALTER COLUMN id SET DEFAULT nextval('public.crawler_templates_id_seq'::regclass); + + +-- +-- Name: dispensaries id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries ALTER COLUMN id SET DEFAULT nextval('public.dispensaries_id_seq'::regclass); + + +-- +-- Name: dispensary_changes id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes ALTER COLUMN id SET DEFAULT nextval('public.dispensary_changes_id_seq'::regclass); + + +-- +-- Name: dispensary_crawl_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs ALTER COLUMN id SET DEFAULT nextval('public.dispensary_crawl_jobs_id_seq'::regclass); + + +-- +-- Name: dispensary_crawl_schedule id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule ALTER COLUMN id SET DEFAULT nextval('public.dispensary_crawl_schedule_id_seq'::regclass); + + +-- +-- Name: dutchie_product_snapshots id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_product_snapshots ALTER COLUMN id SET DEFAULT nextval('public.dutchie_product_snapshots_id_seq'::regclass); + + +-- +-- Name: dutchie_products id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_products ALTER COLUMN id SET DEFAULT nextval('public.dutchie_products_id_seq'::regclass); + + +-- +-- Name: failed_proxies id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.failed_proxies ALTER COLUMN id SET DEFAULT nextval('public.failed_proxies_id_seq'::regclass); + + +-- +-- Name: jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.jobs ALTER COLUMN id SET DEFAULT nextval('public.jobs_id_seq'::regclass); + + +-- +-- Name: price_history id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.price_history ALTER COLUMN id SET DEFAULT nextval('public.price_history_id_seq'::regclass); + + +-- +-- Name: product_categories id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories ALTER COLUMN id SET DEFAULT nextval('public.product_categories_id_seq'::regclass); + + +-- +-- Name: products id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products ALTER COLUMN id SET DEFAULT nextval('public.products_id_seq'::regclass); + + +-- +-- Name: proxies id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxies ALTER COLUMN id SET DEFAULT nextval('public.proxies_id_seq'::regclass); + + +-- +-- Name: proxy_test_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxy_test_jobs ALTER COLUMN id SET DEFAULT nextval('public.proxy_test_jobs_id_seq'::regclass); + + +-- +-- Name: sandbox_crawl_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs ALTER COLUMN id SET DEFAULT nextval('public.sandbox_crawl_jobs_id_seq'::regclass); + + +-- +-- Name: specials id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials ALTER COLUMN id SET DEFAULT nextval('public.specials_id_seq'::regclass); + + +-- +-- Name: store_crawl_schedule id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule ALTER COLUMN id SET DEFAULT nextval('public.store_crawl_schedule_id_seq'::regclass); + + +-- +-- Name: stores id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores ALTER COLUMN id SET DEFAULT nextval('public.stores_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: wp_dutchie_api_permissions id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.wp_dutchie_api_permissions ALTER COLUMN id SET DEFAULT nextval('public.wp_dutchie_api_permissions_id_seq'::regclass); + + +-- +-- Name: api_token_usage api_token_usage_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_token_usage + ADD CONSTRAINT api_token_usage_pkey PRIMARY KEY (id); + + +-- +-- Name: api_tokens api_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: api_tokens api_tokens_token_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_token_key UNIQUE (token); + + +-- +-- Name: azdhs_list azdhs_list_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.azdhs_list + ADD CONSTRAINT azdhs_list_pkey PRIMARY KEY (id); + + +-- +-- Name: batch_history batch_history_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.batch_history + ADD CONSTRAINT batch_history_pkey PRIMARY KEY (id); + + +-- +-- Name: brand_history brand_history_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_history + ADD CONSTRAINT brand_history_pkey PRIMARY KEY (id); + + +-- +-- Name: brand_scrape_jobs brand_scrape_jobs_dispensary_id_brand_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs + ADD CONSTRAINT brand_scrape_jobs_dispensary_id_brand_slug_key UNIQUE (dispensary_id, brand_slug); + + +-- +-- Name: brand_scrape_jobs brand_scrape_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs + ADD CONSTRAINT brand_scrape_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: brands brands_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands + ADD CONSTRAINT brands_pkey PRIMARY KEY (id); + + +-- +-- Name: campaign_products campaign_products_campaign_id_product_id_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_campaign_id_product_id_key UNIQUE (campaign_id, product_id); + + +-- +-- Name: campaign_products campaign_products_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_pkey PRIMARY KEY (id); + + +-- +-- Name: campaigns campaigns_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaigns + ADD CONSTRAINT campaigns_pkey PRIMARY KEY (id); + + +-- +-- Name: campaigns campaigns_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaigns + ADD CONSTRAINT campaigns_slug_key UNIQUE (slug); + + +-- +-- Name: categories categories_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_pkey PRIMARY KEY (id); + + +-- +-- Name: categories categories_store_id_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_store_id_slug_key UNIQUE (store_id, slug); + + +-- +-- Name: clicks clicks_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks + ADD CONSTRAINT clicks_pkey PRIMARY KEY (id); + + +-- +-- Name: crawl_jobs crawl_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawl_jobs + ADD CONSTRAINT crawl_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: crawler_sandboxes crawler_sandboxes_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_sandboxes + ADD CONSTRAINT crawler_sandboxes_pkey PRIMARY KEY (id); + + +-- +-- Name: crawler_schedule crawler_schedule_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_schedule + ADD CONSTRAINT crawler_schedule_pkey PRIMARY KEY (id); + + +-- +-- Name: crawler_templates crawler_templates_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_templates + ADD CONSTRAINT crawler_templates_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensaries dispensaries_azdhs_id_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_azdhs_id_key UNIQUE (azdhs_id); + + +-- +-- Name: dispensaries dispensaries_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensaries dispensaries_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_slug_key UNIQUE (slug); + + +-- +-- Name: dispensary_changes dispensary_changes_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes + ADD CONSTRAINT dispensary_changes_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensary_crawl_jobs dispensary_crawl_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs + ADD CONSTRAINT dispensary_crawl_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensary_crawl_schedule dispensary_crawl_schedule_dispensary_id_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule + ADD CONSTRAINT dispensary_crawl_schedule_dispensary_id_key UNIQUE (dispensary_id); + + +-- +-- Name: dispensary_crawl_schedule dispensary_crawl_schedule_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule + ADD CONSTRAINT dispensary_crawl_schedule_pkey PRIMARY KEY (id); + + +-- +-- Name: dutchie_product_snapshots dutchie_product_snapshots_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_product_snapshots + ADD CONSTRAINT dutchie_product_snapshots_pkey PRIMARY KEY (id); + + +-- +-- Name: dutchie_products dutchie_products_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_products + ADD CONSTRAINT dutchie_products_pkey PRIMARY KEY (id); + + +-- +-- Name: failed_proxies failed_proxies_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.failed_proxies + ADD CONSTRAINT failed_proxies_pkey PRIMARY KEY (id); + + +-- +-- Name: jobs jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.jobs + ADD CONSTRAINT jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: price_history price_history_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.price_history + ADD CONSTRAINT price_history_pkey PRIMARY KEY (id); + + +-- +-- Name: product_categories product_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories + ADD CONSTRAINT product_categories_pkey PRIMARY KEY (id); + + +-- +-- Name: product_categories product_categories_product_id_category_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories + ADD CONSTRAINT product_categories_product_id_category_slug_key UNIQUE (product_id, category_slug); + + +-- +-- Name: products products_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_pkey PRIMARY KEY (id); + + +-- +-- Name: products products_store_id_slug_unique; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_store_id_slug_unique UNIQUE (store_id, slug); + + +-- +-- Name: proxies proxies_host_port_protocol_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxies + ADD CONSTRAINT proxies_host_port_protocol_key UNIQUE (host, port, protocol); + + +-- +-- Name: proxies proxies_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxies + ADD CONSTRAINT proxies_pkey PRIMARY KEY (id); + + +-- +-- Name: proxy_test_jobs proxy_test_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxy_test_jobs + ADD CONSTRAINT proxy_test_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: sandbox_crawl_jobs sandbox_crawl_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs + ADD CONSTRAINT sandbox_crawl_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.settings + ADD CONSTRAINT settings_pkey PRIMARY KEY (key); + + +-- +-- Name: specials specials_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials + ADD CONSTRAINT specials_pkey PRIMARY KEY (id); + + +-- +-- Name: store_crawl_schedule store_crawl_schedule_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule + ADD CONSTRAINT store_crawl_schedule_pkey PRIMARY KEY (id); + + +-- +-- Name: stores stores_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores + ADD CONSTRAINT stores_pkey PRIMARY KEY (id); + + +-- +-- Name: stores stores_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores + ADD CONSTRAINT stores_slug_key UNIQUE (slug); + + +-- +-- Name: dutchie_products uk_dutchie_products; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_products + ADD CONSTRAINT uk_dutchie_products UNIQUE (dispensary_id, external_product_id); + + +-- +-- Name: crawler_schedule uq_crawler_schedule_type; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_schedule + ADD CONSTRAINT uq_crawler_schedule_type UNIQUE (schedule_type); + + +-- +-- Name: store_crawl_schedule uq_store_crawl_schedule_store; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule + ADD CONSTRAINT uq_store_crawl_schedule_store UNIQUE (store_id); + + +-- +-- Name: crawler_templates uq_template_name; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_templates + ADD CONSTRAINT uq_template_name UNIQUE (provider, name, version); + + +-- +-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_email_key UNIQUE (email); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: wp_dutchie_api_permissions wp_dutchie_api_permissions_api_key_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.wp_dutchie_api_permissions + ADD CONSTRAINT wp_dutchie_api_permissions_api_key_key UNIQUE (api_key); + + +-- +-- Name: wp_dutchie_api_permissions wp_dutchie_api_permissions_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.wp_dutchie_api_permissions + ADD CONSTRAINT wp_dutchie_api_permissions_pkey PRIMARY KEY (id); + + +-- +-- Name: failed_proxies_host_port_protocol_idx; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE UNIQUE INDEX failed_proxies_host_port_protocol_idx ON public.failed_proxies USING btree (host, port, protocol); + + +-- +-- Name: idx_api_token_usage_created_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_token_usage_created_at ON public.api_token_usage USING btree (created_at); + + +-- +-- Name: idx_api_token_usage_endpoint; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_token_usage_endpoint ON public.api_token_usage USING btree (endpoint); + + +-- +-- Name: idx_api_token_usage_token_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_token_usage_token_id ON public.api_token_usage USING btree (token_id); + + +-- +-- Name: idx_api_tokens_active; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_tokens_active ON public.api_tokens USING btree (active); + + +-- +-- Name: idx_api_tokens_token; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_tokens_token ON public.api_tokens USING btree (token); + + +-- +-- Name: idx_batch_history_product; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_batch_history_product ON public.batch_history USING btree (product_id, recorded_at DESC); + + +-- +-- Name: idx_batch_history_recorded; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_batch_history_recorded ON public.batch_history USING btree (recorded_at DESC); + + +-- +-- Name: idx_brand_history_brand; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_history_brand ON public.brand_history USING btree (brand_name, event_at DESC); + + +-- +-- Name: idx_brand_history_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_history_dispensary ON public.brand_history USING btree (dispensary_id, event_at DESC); + + +-- +-- Name: idx_brand_history_event; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_history_event ON public.brand_history USING btree (event_type, event_at DESC); + + +-- +-- Name: idx_brand_jobs_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_jobs_dispensary ON public.brand_scrape_jobs USING btree (dispensary_id); + + +-- +-- Name: idx_brand_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_jobs_status ON public.brand_scrape_jobs USING btree (status); + + +-- +-- Name: idx_brand_jobs_worker; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_jobs_worker ON public.brand_scrape_jobs USING btree (worker_id) WHERE (worker_id IS NOT NULL); + + +-- +-- Name: idx_brands_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brands_dispensary ON public.brands USING btree (dispensary_id); + + +-- +-- Name: idx_brands_last_seen; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brands_last_seen ON public.brands USING btree (last_seen_at DESC); + + +-- +-- Name: idx_brands_store_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brands_store_id ON public.brands USING btree (store_id); + + +-- +-- Name: idx_brands_store_name; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE UNIQUE INDEX idx_brands_store_name ON public.brands USING btree (store_id, name); + + +-- +-- Name: idx_categories_dispensary_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_categories_dispensary_id ON public.categories USING btree (dispensary_id); + + +-- +-- Name: idx_categories_parent_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_categories_parent_id ON public.categories USING btree (parent_id); + + +-- +-- Name: idx_categories_path; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_categories_path ON public.categories USING btree (path); + + +-- +-- Name: idx_clicks_campaign_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_clicks_campaign_id ON public.clicks USING btree (campaign_id); + + +-- +-- Name: idx_clicks_clicked_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_clicks_clicked_at ON public.clicks USING btree (clicked_at); + + +-- +-- Name: idx_clicks_product_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_clicks_product_id ON public.clicks USING btree (product_id); + + +-- +-- Name: idx_crawl_jobs_pending; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_pending ON public.crawl_jobs USING btree (scheduled_at) WHERE ((status)::text = 'pending'::text); + + +-- +-- Name: idx_crawl_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_status ON public.crawl_jobs USING btree (status); + + +-- +-- Name: idx_crawl_jobs_store_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_store_status ON public.crawl_jobs USING btree (store_id, status); + + +-- +-- Name: idx_crawl_jobs_store_time; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_store_time ON public.crawl_jobs USING btree (store_id, created_at DESC); + + +-- +-- Name: idx_disp_brand_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_brand_mode ON public.dispensaries USING btree (brand_crawler_mode); + + +-- +-- Name: idx_disp_brand_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_brand_provider ON public.dispensaries USING btree (brand_provider); + + +-- +-- Name: idx_disp_metadata_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_metadata_mode ON public.dispensaries USING btree (metadata_crawler_mode); + + +-- +-- Name: idx_disp_metadata_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_metadata_provider ON public.dispensaries USING btree (metadata_provider); + + +-- +-- Name: idx_disp_product_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_product_mode ON public.dispensaries USING btree (product_crawler_mode); + + +-- +-- Name: idx_disp_product_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_product_provider ON public.dispensaries USING btree (product_provider); + + +-- +-- Name: idx_disp_specials_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_specials_mode ON public.dispensaries USING btree (specials_crawler_mode); + + +-- +-- Name: idx_disp_specials_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_specials_provider ON public.dispensaries USING btree (specials_provider); + + +-- +-- Name: idx_dispensaries_azdhs_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_azdhs_id ON public.dispensaries USING btree (azdhs_id); + + +-- +-- Name: idx_dispensaries_city; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_city ON public.dispensaries USING btree (city); + + +-- +-- Name: idx_dispensaries_crawl_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_crawl_status ON public.dispensaries USING btree (crawl_status); + + +-- +-- Name: idx_dispensaries_crawler_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_crawler_mode ON public.dispensaries USING btree (crawler_mode); + + +-- +-- Name: idx_dispensaries_crawler_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_crawler_status ON public.dispensaries USING btree (crawler_status); + + +-- +-- Name: idx_dispensaries_dutchie_production; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_dutchie_production ON public.dispensaries USING btree (id) WHERE (((menu_provider)::text = 'dutchie'::text) AND ((crawler_mode)::text = 'production'::text)); + + +-- +-- Name: idx_dispensaries_location; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_location ON public.dispensaries USING btree (latitude, longitude) WHERE ((latitude IS NOT NULL) AND (longitude IS NOT NULL)); + + +-- +-- Name: idx_dispensaries_menu_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_menu_status ON public.dispensaries USING btree (menu_scrape_status); + + +-- +-- Name: idx_dispensaries_needs_detection; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_needs_detection ON public.dispensaries USING btree (id) WHERE ((menu_provider IS NULL) OR (menu_provider_confidence < 70)); + + +-- +-- Name: idx_dispensaries_next_crawl; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_next_crawl ON public.dispensaries USING btree (next_crawl_at) WHERE (scrape_enabled = true); + + +-- +-- Name: idx_dispensaries_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_provider ON public.dispensaries USING btree (menu_provider); + + +-- +-- Name: idx_dispensaries_provider_confidence; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_provider_confidence ON public.dispensaries USING btree (menu_provider_confidence); + + +-- +-- Name: idx_dispensaries_sandbox; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_sandbox ON public.dispensaries USING btree (id) WHERE ((crawler_mode)::text = 'sandbox'::text); + + +-- +-- Name: idx_dispensaries_slug; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_slug ON public.dispensaries USING btree (slug); + + +-- +-- Name: idx_dispensaries_state; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_state ON public.dispensaries USING btree (state); + + +-- +-- Name: idx_dispensary_changes_created_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_created_at ON public.dispensary_changes USING btree (created_at DESC); + + +-- +-- Name: idx_dispensary_changes_dispensary_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_dispensary_status ON public.dispensary_changes USING btree (dispensary_id, status); + + +-- +-- Name: idx_dispensary_changes_requires_recrawl; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_requires_recrawl ON public.dispensary_changes USING btree (requires_recrawl) WHERE (requires_recrawl = true); + + +-- +-- Name: idx_dispensary_changes_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_status ON public.dispensary_changes USING btree (status); + + +-- +-- Name: idx_dispensary_crawl_jobs_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_dispensary ON public.dispensary_crawl_jobs USING btree (dispensary_id, created_at DESC); + + +-- +-- Name: idx_dispensary_crawl_jobs_pending; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_pending ON public.dispensary_crawl_jobs USING btree (priority DESC, scheduled_at) WHERE ((status)::text = 'pending'::text); + + +-- +-- Name: idx_dispensary_crawl_jobs_recent; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_recent ON public.dispensary_crawl_jobs USING btree (created_at DESC); + + +-- +-- Name: idx_dispensary_crawl_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_status ON public.dispensary_crawl_jobs USING btree (status); + + +-- +-- Name: idx_dispensary_crawl_schedule_active; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_active ON public.dispensary_crawl_schedule USING btree (is_active); + + +-- +-- Name: idx_dispensary_crawl_schedule_next_run; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_next_run ON public.dispensary_crawl_schedule USING btree (next_run_at) WHERE (is_active = true); + + +-- +-- Name: idx_dispensary_crawl_schedule_priority; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_priority ON public.dispensary_crawl_schedule USING btree (priority DESC, next_run_at); + + +-- +-- Name: idx_dispensary_crawl_schedule_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_status ON public.dispensary_crawl_schedule USING btree (last_status); + + +-- +-- Name: idx_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_jobs_status ON public.jobs USING btree (status); + + +-- +-- Name: idx_jobs_store_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_jobs_store_id ON public.jobs USING btree (store_id); + + +-- +-- Name: idx_jobs_type; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_jobs_type ON public.jobs USING btree (type); + + +-- +-- Name: idx_price_history_product; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_price_history_product ON public.price_history USING btree (product_id, recorded_at DESC); + + +-- +-- Name: idx_price_history_recorded; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_price_history_recorded ON public.price_history USING btree (recorded_at DESC); + + +-- +-- Name: idx_product_categories_product; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_product_categories_product ON public.product_categories USING btree (product_id); + + +-- +-- Name: idx_product_categories_slug; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_product_categories_slug ON public.product_categories USING btree (category_slug, last_seen_at DESC); + + +-- +-- Name: idx_products_availability_by_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_availability_by_dispensary ON public.products USING btree (dispensary_id, availability_status) WHERE (dispensary_id IS NOT NULL); + + +-- +-- Name: idx_products_availability_by_store; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_availability_by_store ON public.products USING btree (store_id, availability_status) WHERE (store_id IS NOT NULL); + + +-- +-- Name: idx_products_availability_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_availability_status ON public.products USING btree (availability_status); + + +-- +-- Name: idx_products_brand_external; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_brand_external ON public.products USING btree (brand_external_id); + + +-- +-- Name: idx_products_dispensary_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_dispensary_id ON public.products USING btree (dispensary_id); + + +-- +-- Name: idx_products_enterprise; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_enterprise ON public.products USING btree (enterprise_product_id); + + +-- +-- Name: idx_products_is_special; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_is_special ON public.products USING btree (is_on_special); + + +-- +-- Name: idx_products_sku; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_sku ON public.products USING btree (sku); + + +-- +-- Name: idx_products_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_status ON public.products USING btree (status); + + +-- +-- Name: idx_products_stock_quantity; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_stock_quantity ON public.products USING btree (stock_quantity) WHERE (stock_quantity IS NOT NULL); + + +-- +-- Name: idx_products_stock_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_stock_status ON public.products USING btree (stock_status); + + +-- +-- Name: idx_products_subcategory; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_subcategory ON public.products USING btree (subcategory); + + +-- +-- Name: idx_proxies_location; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_proxies_location ON public.proxies USING btree (country_code, state, city); + + +-- +-- Name: idx_proxy_test_jobs_created_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_proxy_test_jobs_created_at ON public.proxy_test_jobs USING btree (created_at DESC); + + +-- +-- Name: idx_proxy_test_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_proxy_test_jobs_status ON public.proxy_test_jobs USING btree (status); + + +-- +-- Name: idx_sandbox_active_per_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE UNIQUE INDEX idx_sandbox_active_per_dispensary ON public.crawler_sandboxes USING btree (dispensary_id) WHERE ((status)::text <> ALL ((ARRAY['moved_to_production'::character varying, 'failed'::character varying])::text[])); + + +-- +-- Name: idx_sandbox_category; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_category ON public.crawler_sandboxes USING btree (category); + + +-- +-- Name: idx_sandbox_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_dispensary ON public.crawler_sandboxes USING btree (dispensary_id); + + +-- +-- Name: idx_sandbox_job_category; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_category ON public.sandbox_crawl_jobs USING btree (category); + + +-- +-- Name: idx_sandbox_job_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_dispensary ON public.sandbox_crawl_jobs USING btree (dispensary_id); + + +-- +-- Name: idx_sandbox_job_pending; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_pending ON public.sandbox_crawl_jobs USING btree (scheduled_at) WHERE ((status)::text = 'pending'::text); + + +-- +-- Name: idx_sandbox_job_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_status ON public.sandbox_crawl_jobs USING btree (status); + + +-- +-- Name: idx_sandbox_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_mode ON public.crawler_sandboxes USING btree (mode); + + +-- +-- Name: idx_sandbox_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_status ON public.crawler_sandboxes USING btree (status); + + +-- +-- Name: idx_sandbox_suspected_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_suspected_provider ON public.crawler_sandboxes USING btree (suspected_menu_provider); + + +-- +-- Name: idx_sandbox_template; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_template ON public.crawler_sandboxes USING btree (template_name); + + +-- +-- Name: idx_specials_product_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_specials_product_id ON public.specials USING btree (product_id); + + +-- +-- Name: idx_specials_store_date; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_specials_store_date ON public.specials USING btree (store_id, valid_date DESC); + + +-- +-- Name: idx_stores_dispensary_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_stores_dispensary_id ON public.stores USING btree (dispensary_id); + + +-- +-- Name: idx_template_active; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_template_active ON public.crawler_templates USING btree (is_active); + + +-- +-- Name: idx_template_default; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_template_default ON public.crawler_templates USING btree (provider, is_default_for_provider) WHERE (is_default_for_provider = true); + + +-- +-- Name: idx_template_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_template_provider ON public.crawler_templates USING btree (provider); + + +-- +-- Name: idx_wp_api_permissions_store_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_wp_api_permissions_store_id ON public.wp_dutchie_api_permissions USING btree (store_id); + + +-- +-- Name: api_tokens api_tokens_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER api_tokens_updated_at BEFORE UPDATE ON public.api_tokens FOR EACH ROW EXECUTE FUNCTION public.update_api_token_updated_at(); + + +-- +-- Name: crawl_jobs trigger_crawl_jobs_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_crawl_jobs_updated_at BEFORE UPDATE ON public.crawl_jobs FOR EACH ROW EXECUTE FUNCTION public.update_schedule_updated_at(); + + +-- +-- Name: crawler_schedule trigger_crawler_schedule_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_crawler_schedule_updated_at BEFORE UPDATE ON public.crawler_schedule FOR EACH ROW EXECUTE FUNCTION public.update_schedule_updated_at(); + + +-- +-- Name: sandbox_crawl_jobs trigger_sandbox_job_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_sandbox_job_updated_at BEFORE UPDATE ON public.sandbox_crawl_jobs FOR EACH ROW EXECUTE FUNCTION public.update_sandbox_timestamp(); + + +-- +-- Name: crawler_sandboxes trigger_sandbox_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_sandbox_updated_at BEFORE UPDATE ON public.crawler_sandboxes FOR EACH ROW EXECUTE FUNCTION public.update_sandbox_timestamp(); + + +-- +-- Name: dispensary_changes trigger_set_requires_recrawl; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_set_requires_recrawl BEFORE INSERT ON public.dispensary_changes FOR EACH ROW EXECUTE FUNCTION public.set_requires_recrawl(); + + +-- +-- Name: store_crawl_schedule trigger_store_crawl_schedule_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_store_crawl_schedule_updated_at BEFORE UPDATE ON public.store_crawl_schedule FOR EACH ROW EXECUTE FUNCTION public.update_schedule_updated_at(); + + +-- +-- Name: crawler_templates trigger_template_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_template_updated_at BEFORE UPDATE ON public.crawler_templates FOR EACH ROW EXECUTE FUNCTION public.update_sandbox_timestamp(); + + +-- +-- Name: brand_scrape_jobs trigger_update_brand_scrape_jobs_timestamp; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_update_brand_scrape_jobs_timestamp BEFORE UPDATE ON public.brand_scrape_jobs FOR EACH ROW EXECUTE FUNCTION public.update_brand_scrape_jobs_updated_at(); + + +-- +-- Name: api_token_usage api_token_usage_token_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_token_usage + ADD CONSTRAINT api_token_usage_token_id_fkey FOREIGN KEY (token_id) REFERENCES public.api_tokens(id) ON DELETE CASCADE; + + +-- +-- Name: api_tokens api_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: batch_history batch_history_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.batch_history + ADD CONSTRAINT batch_history_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: brand_history brand_history_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_history + ADD CONSTRAINT brand_history_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: brand_scrape_jobs brand_scrape_jobs_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs + ADD CONSTRAINT brand_scrape_jobs_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id); + + +-- +-- Name: brands brands_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands + ADD CONSTRAINT brands_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id); + + +-- +-- Name: brands brands_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands + ADD CONSTRAINT brands_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: campaign_products campaign_products_campaign_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) ON DELETE CASCADE; + + +-- +-- Name: campaign_products campaign_products_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: categories categories_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: categories categories_parent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.categories(id) ON DELETE CASCADE; + + +-- +-- Name: categories categories_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: clicks clicks_campaign_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks + ADD CONSTRAINT clicks_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) ON DELETE SET NULL; + + +-- +-- Name: clicks clicks_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks + ADD CONSTRAINT clicks_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: crawl_jobs crawl_jobs_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawl_jobs + ADD CONSTRAINT crawl_jobs_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: crawler_sandboxes crawler_sandboxes_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_sandboxes + ADD CONSTRAINT crawler_sandboxes_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dispensaries dispensaries_azdhs_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_azdhs_id_fkey FOREIGN KEY (azdhs_id) REFERENCES public.azdhs_list(id); + + +-- +-- Name: dispensary_changes dispensary_changes_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes + ADD CONSTRAINT dispensary_changes_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dispensary_changes dispensary_changes_reviewed_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes + ADD CONSTRAINT dispensary_changes_reviewed_by_fkey FOREIGN KEY (reviewed_by) REFERENCES public.users(id); + + +-- +-- Name: dispensary_crawl_jobs dispensary_crawl_jobs_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs + ADD CONSTRAINT dispensary_crawl_jobs_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dispensary_crawl_jobs dispensary_crawl_jobs_schedule_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs + ADD CONSTRAINT dispensary_crawl_jobs_schedule_id_fkey FOREIGN KEY (schedule_id) REFERENCES public.dispensary_crawl_schedule(id) ON DELETE SET NULL; + + +-- +-- Name: dispensary_crawl_schedule dispensary_crawl_schedule_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule + ADD CONSTRAINT dispensary_crawl_schedule_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dutchie_product_snapshots dutchie_product_snapshots_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_product_snapshots + ADD CONSTRAINT dutchie_product_snapshots_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dutchie_product_snapshots dutchie_product_snapshots_dutchie_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_product_snapshots + ADD CONSTRAINT dutchie_product_snapshots_dutchie_product_id_fkey FOREIGN KEY (dutchie_product_id) REFERENCES public.dutchie_products(id) ON DELETE CASCADE; + + +-- +-- Name: dutchie_products dutchie_products_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dutchie_products + ADD CONSTRAINT dutchie_products_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: jobs jobs_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.jobs + ADD CONSTRAINT jobs_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: price_history price_history_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.price_history + ADD CONSTRAINT price_history_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_categories product_categories_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories + ADD CONSTRAINT product_categories_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: products products_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.categories(id) ON DELETE SET NULL; + + +-- +-- Name: products products_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: products products_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: sandbox_crawl_jobs sandbox_crawl_jobs_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs + ADD CONSTRAINT sandbox_crawl_jobs_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: sandbox_crawl_jobs sandbox_crawl_jobs_sandbox_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs + ADD CONSTRAINT sandbox_crawl_jobs_sandbox_id_fkey FOREIGN KEY (sandbox_id) REFERENCES public.crawler_sandboxes(id) ON DELETE SET NULL; + + +-- +-- Name: specials specials_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials + ADD CONSTRAINT specials_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: specials specials_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials + ADD CONSTRAINT specials_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: store_crawl_schedule store_crawl_schedule_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule + ADD CONSTRAINT store_crawl_schedule_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: stores stores_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores + ADD CONSTRAINT stores_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE SET NULL; + + +-- +-- Name: wp_dutchie_api_permissions wp_dutchie_api_permissions_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.wp_dutchie_api_permissions + ADD CONSTRAINT wp_dutchie_api_permissions_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict u76b1lsSuckyRNpZbORH9drBRaNwzQbqR7X3xYnHxUdiczMnjtBCdx8KbLlDBsP + diff --git a/backend/src/dutchie-az/config/dutchie.ts b/backend/src/dutchie-az/config/dutchie.ts index 2e05fd03..9f409684 100644 --- a/backend/src/dutchie-az/config/dutchie.ts +++ b/backend/src/dutchie-az/config/dutchie.ts @@ -6,6 +6,13 @@ */ export const dutchieConfig = { + // ============================================================ + // GRAPHQL ENDPOINT + // ============================================================ + + /** GraphQL endpoint - must be the api-3 graphql endpoint (NOT api-gw.dutchie.com which no longer exists) */ + graphqlEndpoint: 'https://dutchie.com/api-3/graphql', + // ============================================================ // GRAPHQL PERSISTED QUERY HASHES // ============================================================ diff --git a/backend/src/dutchie-az/index.ts b/backend/src/dutchie-az/index.ts index d4d6d03e..939483ce 100644 --- a/backend/src/dutchie-az/index.ts +++ b/backend/src/dutchie-az/index.ts @@ -37,8 +37,9 @@ export { resolveDispensaryId, fetchAllProducts, fetchAllProductsBothModes, - discoverDispensaries, discoverArizonaDispensaries, + // Alias for backward compatibility + discoverArizonaDispensaries as discoverDispensaries, } from './services/graphql-client'; // Services - Discovery diff --git a/backend/src/dutchie-az/routes/index.ts b/backend/src/dutchie-az/routes/index.ts index 388dcc15..835ca3eb 100644 --- a/backend/src/dutchie-az/routes/index.ts +++ b/backend/src/dutchie-az/routes/index.ts @@ -1376,8 +1376,6 @@ router.get('/monitor/active-jobs', async (_req: Request, res: Response) => { jrl.items_succeeded, jrl.items_failed, jrl.metadata, - jrl.worker_id, - jrl.worker_hostname, js.description as job_description, EXTRACT(EPOCH FROM (NOW() - jrl.started_at)) as duration_seconds FROM job_run_logs jrl diff --git a/backend/src/dutchie-az/services/directory-matcher.ts b/backend/src/dutchie-az/services/directory-matcher.ts new file mode 100644 index 00000000..39fc5af7 --- /dev/null +++ b/backend/src/dutchie-az/services/directory-matcher.ts @@ -0,0 +1,481 @@ +/** + * Directory-Based Store Matcher + * + * Scrapes provider directory pages (Curaleaf, Sol, etc.) to get store lists, + * then matches them to existing dispensaries by fuzzy name/city/address matching. + * + * This allows us to: + * 1. Find specific store URLs for directory-style websites + * 2. Match stores confidently by name+city + * 3. Mark non-Dutchie providers as not_crawlable until we build crawlers + */ + +import { query } from '../db/connection'; + +// ============================================================ +// TYPES +// ============================================================ + +export interface DirectoryStore { + name: string; + city: string; + state: string; + address: string | null; + storeUrl: string; +} + +export interface MatchResult { + directoryStore: DirectoryStore; + dispensaryId: number | null; + dispensaryName: string | null; + confidence: 'high' | 'medium' | 'low' | 'none'; + matchReason: string; +} + +export interface DirectoryMatchReport { + provider: string; + totalDirectoryStores: number; + highConfidenceMatches: number; + mediumConfidenceMatches: number; + lowConfidenceMatches: number; + unmatched: number; + results: MatchResult[]; +} + +// ============================================================ +// NORMALIZATION FUNCTIONS +// ============================================================ + +/** + * Normalize a string for comparison: + * - Lowercase + * - Remove common suffixes (dispensary, cannabis, etc.) + * - Remove punctuation + * - Collapse whitespace + */ +function normalizeForComparison(str: string): string { + if (!str) return ''; + + return str + .toLowerCase() + .replace(/\s+(dispensary|cannabis|marijuana|medical|recreational|shop|store|flower|wellness)(\s|$)/gi, ' ') + .replace(/[^\w\s]/g, ' ') // Remove punctuation + .replace(/\s+/g, ' ') // Collapse whitespace + .trim(); +} + +/** + * Normalize city name for comparison + */ +function normalizeCity(city: string): string { + if (!city) return ''; + + return city + .toLowerCase() + .replace(/[^\w\s]/g, '') + .trim(); +} + +/** + * Calculate similarity between two strings (0-1) + * Uses Levenshtein distance normalized by max length + */ +function stringSimilarity(a: string, b: string): number { + if (!a || !b) return 0; + if (a === b) return 1; + + const longer = a.length > b.length ? a : b; + const shorter = a.length > b.length ? b : a; + + if (longer.length === 0) return 1; + + const distance = levenshteinDistance(longer, shorter); + return (longer.length - distance) / longer.length; +} + +/** + * Levenshtein distance between two strings + */ +function levenshteinDistance(a: string, b: string): number { + const matrix: number[][] = []; + + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1 // deletion + ); + } + } + } + + return matrix[b.length][a.length]; +} + +/** + * Check if string contains another (with normalization) + */ +function containsNormalized(haystack: string, needle: string): boolean { + return normalizeForComparison(haystack).includes(normalizeForComparison(needle)); +} + +// ============================================================ +// PROVIDER DIRECTORY SCRAPERS +// ============================================================ + +/** + * Sol Flower (livewithsol.com) - Static HTML, easy to scrape + */ +export async function scrapeSolDirectory(): Promise { + console.log('[DirectoryMatcher] Scraping Sol Flower directory...'); + + try { + const response = await fetch('https://www.livewithsol.com/locations/', { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: 'text/html', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const html = await response.text(); + + // Extract store entries from HTML + // Sol's structure: Each location has name, address in specific divs + const stores: DirectoryStore[] = []; + + // Pattern to find location cards + // Format: NAME with address nearby + const locationRegex = + /]+href="(\/locations\/[^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?(\d+[^<]+(?:Ave|St|Blvd|Dr|Rd|Way)[^<]*)/gi; + + let match; + while ((match = locationRegex.exec(html)) !== null) { + const [, path, name, address] = match; + + // Extract city from common Arizona cities + let city = 'Unknown'; + const cityPatterns = [ + { pattern: /phoenix/i, city: 'Phoenix' }, + { pattern: /scottsdale/i, city: 'Scottsdale' }, + { pattern: /tempe/i, city: 'Tempe' }, + { pattern: /tucson/i, city: 'Tucson' }, + { pattern: /mesa/i, city: 'Mesa' }, + { pattern: /sun city/i, city: 'Sun City' }, + { pattern: /glendale/i, city: 'Glendale' }, + ]; + + for (const { pattern, city: cityName } of cityPatterns) { + if (pattern.test(name) || pattern.test(address)) { + city = cityName; + break; + } + } + + stores.push({ + name: name.trim(), + city, + state: 'AZ', + address: address.trim(), + storeUrl: `https://www.livewithsol.com${path}`, + }); + } + + // If regex didn't work, use known hardcoded values (fallback) + if (stores.length === 0) { + console.log('[DirectoryMatcher] Using hardcoded Sol locations'); + return [ + { name: 'Sol Flower 32nd & Shea', city: 'Phoenix', state: 'AZ', address: '3217 E Shea Blvd Suite 1 A', storeUrl: 'https://www.livewithsol.com/locations/deer-valley/' }, + { name: 'Sol Flower Scottsdale Airpark', city: 'Scottsdale', state: 'AZ', address: '14980 N 78th Way Ste 204', storeUrl: 'https://www.livewithsol.com/locations/scottsdale-airpark/' }, + { name: 'Sol Flower Sun City', city: 'Sun City', state: 'AZ', address: '13650 N 99th Ave', storeUrl: 'https://www.livewithsol.com/locations/sun-city/' }, + { name: 'Sol Flower Tempe McClintock', city: 'Tempe', state: 'AZ', address: '1322 N McClintock Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-mcclintock/' }, + { name: 'Sol Flower Tempe University', city: 'Tempe', state: 'AZ', address: '2424 W University Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-university/' }, + { name: 'Sol Flower Foothills Tucson', city: 'Tucson', state: 'AZ', address: '6026 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/foothills-tucson/' }, + { name: 'Sol Flower South Tucson', city: 'Tucson', state: 'AZ', address: '3000 W Valencia Rd Ste 210', storeUrl: 'https://www.livewithsol.com/locations/south-tucson/' }, + { name: 'Sol Flower North Tucson', city: 'Tucson', state: 'AZ', address: '4837 N 1st Ave', storeUrl: 'https://www.livewithsol.com/locations/north-tucson/' }, + { name: 'Sol Flower Casas Adobes', city: 'Tucson', state: 'AZ', address: '6437 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/casas-adobes/' }, + ]; + } + + console.log(`[DirectoryMatcher] Found ${stores.length} Sol Flower locations`); + return stores; + } catch (error: any) { + console.error('[DirectoryMatcher] Error scraping Sol directory:', error.message); + // Return hardcoded fallback + return [ + { name: 'Sol Flower 32nd & Shea', city: 'Phoenix', state: 'AZ', address: '3217 E Shea Blvd Suite 1 A', storeUrl: 'https://www.livewithsol.com/locations/deer-valley/' }, + { name: 'Sol Flower Scottsdale Airpark', city: 'Scottsdale', state: 'AZ', address: '14980 N 78th Way Ste 204', storeUrl: 'https://www.livewithsol.com/locations/scottsdale-airpark/' }, + { name: 'Sol Flower Sun City', city: 'Sun City', state: 'AZ', address: '13650 N 99th Ave', storeUrl: 'https://www.livewithsol.com/locations/sun-city/' }, + { name: 'Sol Flower Tempe McClintock', city: 'Tempe', state: 'AZ', address: '1322 N McClintock Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-mcclintock/' }, + { name: 'Sol Flower Tempe University', city: 'Tempe', state: 'AZ', address: '2424 W University Dr', storeUrl: 'https://www.livewithsol.com/locations/tempe-university/' }, + { name: 'Sol Flower Foothills Tucson', city: 'Tucson', state: 'AZ', address: '6026 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/foothills-tucson/' }, + { name: 'Sol Flower South Tucson', city: 'Tucson', state: 'AZ', address: '3000 W Valencia Rd Ste 210', storeUrl: 'https://www.livewithsol.com/locations/south-tucson/' }, + { name: 'Sol Flower North Tucson', city: 'Tucson', state: 'AZ', address: '4837 N 1st Ave', storeUrl: 'https://www.livewithsol.com/locations/north-tucson/' }, + { name: 'Sol Flower Casas Adobes', city: 'Tucson', state: 'AZ', address: '6437 N Oracle Rd', storeUrl: 'https://www.livewithsol.com/locations/casas-adobes/' }, + ]; + } +} + +/** + * Curaleaf - Has age-gate, so we need hardcoded AZ locations + * In production, this would use Playwright to bypass age-gate + */ +export async function scrapeCuraleafDirectory(): Promise { + console.log('[DirectoryMatcher] Using hardcoded Curaleaf AZ locations (age-gate blocks simple fetch)...'); + + // Hardcoded Arizona Curaleaf locations from public knowledge + // These would be scraped via Playwright in production + return [ + { name: 'Curaleaf Phoenix Camelback', city: 'Phoenix', state: 'AZ', address: '4811 E Camelback Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-phoenix-camelback' }, + { name: 'Curaleaf Phoenix Midtown', city: 'Phoenix', state: 'AZ', address: '1928 E Highland Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-phoenix-midtown' }, + { name: 'Curaleaf Glendale East', city: 'Glendale', state: 'AZ', address: '5150 W Glendale Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-glendale-east' }, + { name: 'Curaleaf Glendale West', city: 'Glendale', state: 'AZ', address: '6501 W Glendale Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-glendale-west' }, + { name: 'Curaleaf Gilbert', city: 'Gilbert', state: 'AZ', address: '1736 E Williams Field Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-gilbert' }, + { name: 'Curaleaf Mesa', city: 'Mesa', state: 'AZ', address: '1540 S Power Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-mesa' }, + { name: 'Curaleaf Tempe', city: 'Tempe', state: 'AZ', address: '1815 E Broadway Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-tempe' }, + { name: 'Curaleaf Scottsdale', city: 'Scottsdale', state: 'AZ', address: '8904 E Indian Bend Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-scottsdale' }, + { name: 'Curaleaf Tucson Prince', city: 'Tucson', state: 'AZ', address: '3955 W Prince Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-tucson-prince' }, + { name: 'Curaleaf Tucson Midvale', city: 'Tucson', state: 'AZ', address: '2936 N Midvale Park Rd', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-tucson-midvale' }, + { name: 'Curaleaf Sedona', city: 'Sedona', state: 'AZ', address: '525 AZ-179', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-sedona' }, + { name: 'Curaleaf Youngtown', city: 'Youngtown', state: 'AZ', address: '11125 W Grand Ave', storeUrl: 'https://curaleaf.com/stores/curaleaf-az-youngtown' }, + ]; +} + +// ============================================================ +// MATCHING LOGIC +// ============================================================ + +interface Dispensary { + id: number; + name: string; + city: string | null; + state: string | null; + address: string | null; + menu_type: string | null; + menu_url: string | null; + website: string | null; +} + +/** + * Match a directory store to an existing dispensary + */ +function matchStoreToDispensary(store: DirectoryStore, dispensaries: Dispensary[]): MatchResult { + const normalizedStoreName = normalizeForComparison(store.name); + const normalizedStoreCity = normalizeCity(store.city); + + let bestMatch: Dispensary | null = null; + let bestScore = 0; + let matchReason = ''; + + for (const disp of dispensaries) { + const normalizedDispName = normalizeForComparison(disp.name); + const normalizedDispCity = normalizeCity(disp.city || ''); + + let score = 0; + const reasons: string[] = []; + + // 1. Name similarity (max 50 points) + const nameSimilarity = stringSimilarity(normalizedStoreName, normalizedDispName); + score += nameSimilarity * 50; + if (nameSimilarity > 0.8) reasons.push(`name_match(${(nameSimilarity * 100).toFixed(0)}%)`); + + // 2. City match (25 points for exact, 15 for partial) + if (normalizedStoreCity && normalizedDispCity) { + if (normalizedStoreCity === normalizedDispCity) { + score += 25; + reasons.push('city_exact'); + } else if ( + normalizedStoreCity.includes(normalizedDispCity) || + normalizedDispCity.includes(normalizedStoreCity) + ) { + score += 15; + reasons.push('city_partial'); + } + } + + // 3. Address contains street name (15 points) + if (store.address && disp.address) { + const storeStreet = store.address.toLowerCase().split(/\s+/).slice(1, 4).join(' '); + const dispStreet = disp.address.toLowerCase().split(/\s+/).slice(1, 4).join(' '); + if (storeStreet && dispStreet && stringSimilarity(storeStreet, dispStreet) > 0.7) { + score += 15; + reasons.push('address_match'); + } + } + + // 4. Brand name in dispensary name (10 points) + const brandName = store.name.split(' ')[0].toLowerCase(); // e.g., "Curaleaf", "Sol" + if (disp.name.toLowerCase().includes(brandName)) { + score += 10; + reasons.push('brand_match'); + } + + if (score > bestScore) { + bestScore = score; + bestMatch = disp; + matchReason = reasons.join(', '); + } + } + + // Determine confidence level + let confidence: 'high' | 'medium' | 'low' | 'none'; + if (bestScore >= 70) { + confidence = 'high'; + } else if (bestScore >= 50) { + confidence = 'medium'; + } else if (bestScore >= 30) { + confidence = 'low'; + } else { + confidence = 'none'; + } + + return { + directoryStore: store, + dispensaryId: bestMatch?.id || null, + dispensaryName: bestMatch?.name || null, + confidence, + matchReason: matchReason || 'no_match', + }; +} + +// ============================================================ +// MAIN FUNCTIONS +// ============================================================ + +/** + * Run directory matching for a provider and update database + * Only applies high-confidence matches automatically + */ +export async function matchDirectoryToDispensaries( + provider: 'curaleaf' | 'sol', + dryRun: boolean = true +): Promise { + console.log(`[DirectoryMatcher] Running ${provider} directory matching (dryRun=${dryRun})...`); + + // Get directory stores + let directoryStores: DirectoryStore[]; + if (provider === 'curaleaf') { + directoryStores = await scrapeCuraleafDirectory(); + } else if (provider === 'sol') { + directoryStores = await scrapeSolDirectory(); + } else { + throw new Error(`Unknown provider: ${provider}`); + } + + // Get all AZ dispensaries from database + const { rows: dispensaries } = await query( + `SELECT id, name, city, state, address, menu_type, menu_url, website + FROM dispensaries + WHERE state = 'AZ'` + ); + + console.log(`[DirectoryMatcher] Matching ${directoryStores.length} directory stores against ${dispensaries.length} dispensaries`); + + // Match each directory store + const results: MatchResult[] = []; + for (const store of directoryStores) { + const match = matchStoreToDispensary(store, dispensaries); + results.push(match); + + // Only apply high-confidence matches if not dry run + if (!dryRun && match.confidence === 'high' && match.dispensaryId) { + await applyDirectoryMatch(match.dispensaryId, provider, store); + } + } + + // Count results + const report: DirectoryMatchReport = { + provider, + totalDirectoryStores: directoryStores.length, + highConfidenceMatches: results.filter((r) => r.confidence === 'high').length, + mediumConfidenceMatches: results.filter((r) => r.confidence === 'medium').length, + lowConfidenceMatches: results.filter((r) => r.confidence === 'low').length, + unmatched: results.filter((r) => r.confidence === 'none').length, + results, + }; + + console.log(`[DirectoryMatcher] ${provider} matching complete:`); + console.log(` - High confidence: ${report.highConfidenceMatches}`); + console.log(` - Medium confidence: ${report.mediumConfidenceMatches}`); + console.log(` - Low confidence: ${report.lowConfidenceMatches}`); + console.log(` - Unmatched: ${report.unmatched}`); + + return report; +} + +/** + * Apply a directory match to a dispensary + */ +async function applyDirectoryMatch( + dispensaryId: number, + provider: string, + store: DirectoryStore +): Promise { + console.log(`[DirectoryMatcher] Applying match: dispensary ${dispensaryId} -> ${store.storeUrl}`); + + await query( + ` + UPDATE dispensaries SET + menu_type = $1, + menu_url = $2, + platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object( + 'detected_provider', $1::text, + 'detection_method', 'directory_match'::text, + 'detected_at', NOW(), + 'directory_store_name', $3::text, + 'directory_store_url', $2::text, + 'directory_store_city', $4::text, + 'directory_store_address', $5::text, + 'not_crawlable', true, + 'not_crawlable_reason', $6::text + ), + updated_at = NOW() + WHERE id = $7 + `, + [ + provider, + store.storeUrl, + store.name, + store.city, + store.address, + `${provider} proprietary menu - no crawler available`, + dispensaryId, + ] + ); +} + +/** + * Preview matches without applying them + */ +export async function previewDirectoryMatches( + provider: 'curaleaf' | 'sol' +): Promise { + return matchDirectoryToDispensaries(provider, true); +} + +/** + * Apply high-confidence matches + */ +export async function applyHighConfidenceMatches( + provider: 'curaleaf' | 'sol' +): Promise { + return matchDirectoryToDispensaries(provider, false); +} diff --git a/backend/src/dutchie-az/services/discovery.ts b/backend/src/dutchie-az/services/discovery.ts index 961daf44..496fbd9f 100644 --- a/backend/src/dutchie-az/services/discovery.ts +++ b/backend/src/dutchie-az/services/discovery.ts @@ -5,7 +5,7 @@ */ import { query, getClient } from '../db/connection'; -import { discoverArizonaDispensaries, resolveDispensaryId } from './graphql-client'; +import { discoverArizonaDispensaries, resolveDispensaryId, resolveDispensaryIdWithDetails, ResolveDispensaryResult } from './graphql-client'; import { Dispensary } from '../types'; /** @@ -146,85 +146,405 @@ export async function discoverDispensaries(): Promise<{ discovered: number; erro } /** - * Resolve platform dispensary IDs for all dispensaries that don't have one + * Extract cName (slug) from a Dutchie menu_url + * Supports formats: + * - https://dutchie.com/embedded-menu/ + * - https://dutchie.com/dispensary/ */ -export async function resolvePlatformDispensaryIds(): Promise<{ resolved: number; failed: number }> { +export function extractCNameFromMenuUrl(menuUrl: string | null | undefined): string | null { + if (!menuUrl) return null; + + try { + const url = new URL(menuUrl); + const pathname = url.pathname; + + // Match /embedded-menu/ or /dispensary/ + const embeddedMatch = pathname.match(/^\/embedded-menu\/([^/?]+)/); + if (embeddedMatch) return embeddedMatch[1]; + + const dispensaryMatch = pathname.match(/^\/dispensary\/([^/?]+)/); + if (dispensaryMatch) return dispensaryMatch[1]; + + return null; + } catch { + return null; + } +} + +/** + * Resolve platform dispensary IDs for all dispensaries that don't have one + * CRITICAL: Uses cName extracted from menu_url, NOT the slug column! + * + * Uses the new resolveDispensaryIdWithDetails which: + * 1. Extracts dispensaryId from window.reactEnv in the embedded menu page (preferred) + * 2. Falls back to GraphQL if reactEnv extraction fails + * 3. Returns HTTP status so we can mark 403/404 stores as not_crawlable + */ +export async function resolvePlatformDispensaryIds(): Promise<{ resolved: number; failed: number; skipped: number; notCrawlable: number }> { console.log('[Discovery] Resolving platform dispensary IDs...'); - const { rows: dispensaries } = await query( + const { rows: dispensaries } = await query( ` - SELECT * FROM dispensaries - WHERE menu_type = 'dutchie' AND platform_dispensary_id IS NULL + SELECT id, name, slug, menu_url, menu_type, platform_dispensary_id, crawl_status + FROM dispensaries + WHERE menu_type = 'dutchie' + AND platform_dispensary_id IS NULL + AND menu_url IS NOT NULL + AND (crawl_status IS NULL OR crawl_status != 'not_crawlable') ORDER BY id ` ); let resolved = 0; let failed = 0; + let skipped = 0; + let notCrawlable = 0; for (const dispensary of dispensaries) { try { - console.log(`[Discovery] Resolving ID for: ${dispensary.name} (${dispensary.slug})`); - const platformId = await resolveDispensaryId(dispensary.slug); + // Extract cName from menu_url - this is the CORRECT way to get the Dutchie slug + const cName = extractCNameFromMenuUrl(dispensary.menu_url); - if (platformId) { + if (!cName) { + console.log(`[Discovery] Skipping ${dispensary.name}: Could not extract cName from menu_url: ${dispensary.menu_url}`); + skipped++; + continue; + } + + console.log(`[Discovery] Resolving ID for: ${dispensary.name} (cName=${cName}, menu_url=${dispensary.menu_url})`); + + // Use the new detailed resolver that extracts from reactEnv first + const result = await resolveDispensaryIdWithDetails(cName); + + if (result.dispensaryId) { + // SUCCESS: Store resolved await query( ` - UPDATE dispensaries SET platform_dispensary_id = $1, updated_at = NOW() - WHERE id = $2 + UPDATE dispensaries + SET platform_dispensary_id = $1, + platform_dispensary_id_resolved_at = NOW(), + crawl_status = 'ready', + crawl_status_reason = $2, + crawl_status_updated_at = NOW(), + last_tested_menu_url = $3, + last_http_status = $4, + updated_at = NOW() + WHERE id = $5 `, - [platformId, dispensary.id] + [ + result.dispensaryId, + `Resolved from ${result.source || 'page'}`, + dispensary.menu_url, + result.httpStatus, + dispensary.id, + ] ); resolved++; - console.log(`[Discovery] Resolved: ${dispensary.slug} -> ${platformId}`); + console.log(`[Discovery] Resolved: ${cName} -> ${result.dispensaryId} (source: ${result.source})`); + } else if (result.httpStatus === 403 || result.httpStatus === 404) { + // NOT CRAWLABLE: Store removed or not accessible + await query( + ` + UPDATE dispensaries + SET platform_dispensary_id = NULL, + crawl_status = 'not_crawlable', + crawl_status_reason = $1, + crawl_status_updated_at = NOW(), + last_tested_menu_url = $2, + last_http_status = $3, + updated_at = NOW() + WHERE id = $4 + `, + [ + result.error || `HTTP ${result.httpStatus}: Removed from Dutchie`, + dispensary.menu_url, + result.httpStatus, + dispensary.id, + ] + ); + notCrawlable++; + console.log(`[Discovery] Marked not crawlable: ${cName} (HTTP ${result.httpStatus})`); } else { + // FAILED: Could not resolve but page loaded + await query( + ` + UPDATE dispensaries + SET crawl_status = 'not_ready', + crawl_status_reason = $1, + crawl_status_updated_at = NOW(), + last_tested_menu_url = $2, + last_http_status = $3, + updated_at = NOW() + WHERE id = $4 + `, + [ + result.error || 'Could not extract dispensaryId from page', + dispensary.menu_url, + result.httpStatus, + dispensary.id, + ] + ); failed++; - console.log(`[Discovery] Could not resolve: ${dispensary.slug}`); + console.log(`[Discovery] Could not resolve: ${cName} - ${result.error}`); } // Delay between requests await new Promise((r) => setTimeout(r, 2000)); } catch (error: any) { failed++; - console.error(`[Discovery] Error resolving ${dispensary.slug}:`, error.message); + console.error(`[Discovery] Error resolving ${dispensary.name}:`, error.message); } } - console.log(`[Discovery] Completed: ${resolved} resolved, ${failed} failed`); - return { resolved, failed }; + console.log(`[Discovery] Completed: ${resolved} resolved, ${failed} failed, ${skipped} skipped, ${notCrawlable} not crawlable`); + return { resolved, failed, skipped, notCrawlable }; } /** * Get all dispensaries */ +// Explicit column list for dispensaries table (avoids SELECT * issues with schema differences) +const DISPENSARY_COLUMNS = ` + id, name, slug, city, state, zip, address, latitude, longitude, + menu_type, menu_url, platform_dispensary_id, website, + provider_detection_data, created_at, updated_at +`; + export async function getAllDispensaries(): Promise { - const { rows } = await query( - `SELECT * FROM dispensaries WHERE menu_type = 'dutchie' ORDER BY name` + const { rows } = await query( + `SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE menu_type = 'dutchie' ORDER BY name` ); - return rows; + return rows.map(mapDbRowToDispensary); +} + +/** + * Map snake_case DB row to camelCase Dispensary object + * CRITICAL: DB returns snake_case (platform_dispensary_id) but TypeScript expects camelCase (platformDispensaryId) + * This function is exported for use in other modules that query dispensaries directly. + * + * NOTE: The consolidated dispensaries table column mappings: + * - zip → postalCode + * - menu_type → menuType (keep platform as 'dutchie') + * - last_crawl_at → lastCrawledAt + * - platform_dispensary_id → platformDispensaryId + */ +export function mapDbRowToDispensary(row: any): Dispensary { + // Extract website from raw_metadata if available (field may not exist in all environments) + let rawMetadata = undefined; + if (row.raw_metadata !== undefined) { + rawMetadata = typeof row.raw_metadata === 'string' + ? JSON.parse(row.raw_metadata) + : row.raw_metadata; + } + const website = row.website || rawMetadata?.website || undefined; + + return { + id: row.id, + platform: row.platform || 'dutchie', // keep platform as-is, default to 'dutchie' + name: row.name, + slug: row.slug, + city: row.city, + state: row.state, + postalCode: row.postalCode || row.zip || row.postal_code, + latitude: row.latitude ? parseFloat(row.latitude) : undefined, + longitude: row.longitude ? parseFloat(row.longitude) : undefined, + address: row.address, + platformDispensaryId: row.platformDispensaryId || row.platform_dispensary_id, // CRITICAL mapping! + isDelivery: row.is_delivery, + isPickup: row.is_pickup, + rawMetadata: rawMetadata, + lastCrawledAt: row.lastCrawledAt || row.last_crawl_at, // use last_crawl_at + productCount: row.product_count, + createdAt: row.created_at, + updatedAt: row.updated_at, + menuType: row.menuType || row.menu_type, + menuUrl: row.menuUrl || row.menu_url, + scrapeEnabled: row.scrapeEnabled ?? row.scrape_enabled, + providerDetectionData: row.provider_detection_data, + platformDispensaryIdResolvedAt: row.platform_dispensary_id_resolved_at, + website, + }; } /** * Get dispensary by ID + * NOTE: Uses SQL aliases to map snake_case → camelCase directly */ export async function getDispensaryById(id: number): Promise { - const { rows } = await query( - `SELECT * FROM dispensaries WHERE id = $1`, + const { rows } = await query( + ` + SELECT + id, + name, + slug, + city, + state, + zip AS "postalCode", + address, + latitude, + longitude, + menu_type AS "menuType", + menu_url AS "menuUrl", + platform_dispensary_id AS "platformDispensaryId", + website, + provider_detection_data AS "providerDetectionData", + created_at, + updated_at + FROM dispensaries + WHERE id = $1 + `, [id] ); - return rows[0] || null; + if (!rows[0]) return null; + return mapDbRowToDispensary(rows[0]); } /** * Get dispensaries with platform IDs (ready for crawling) */ export async function getDispensariesWithPlatformIds(): Promise { - const { rows } = await query( + const { rows } = await query( ` - SELECT * FROM dispensaries + SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE menu_type = 'dutchie' AND platform_dispensary_id IS NOT NULL ORDER BY name ` ); - return rows; + return rows.map(mapDbRowToDispensary); +} + +/** + * Re-resolve a single dispensary's platform ID + * Clears the existing ID and re-resolves from the menu_url cName + */ +export async function reResolveDispensaryPlatformId(dispensaryId: number): Promise<{ + success: boolean; + platformId: string | null; + cName: string | null; + error?: string; +}> { + console.log(`[Discovery] Re-resolving platform ID for dispensary ${dispensaryId}...`); + + const dispensary = await getDispensaryById(dispensaryId); + if (!dispensary) { + return { success: false, platformId: null, cName: null, error: 'Dispensary not found' }; + } + + const cName = extractCNameFromMenuUrl(dispensary.menuUrl); + if (!cName) { + console.log(`[Discovery] Could not extract cName from menu_url: ${dispensary.menuUrl}`); + return { + success: false, + platformId: null, + cName: null, + error: `Could not extract cName from menu_url: ${dispensary.menuUrl}`, + }; + } + + console.log(`[Discovery] Extracted cName: ${cName} from menu_url: ${dispensary.menuUrl}`); + + try { + const platformId = await resolveDispensaryId(cName); + + if (platformId) { + await query( + ` + UPDATE dispensaries + SET platform_dispensary_id = $1, + platform_dispensary_id_resolved_at = NOW(), + updated_at = NOW() + WHERE id = $2 + `, + [platformId, dispensaryId] + ); + console.log(`[Discovery] Resolved: ${cName} -> ${platformId}`); + return { success: true, platformId, cName }; + } else { + // Clear the invalid platform ID and mark as not crawlable + await query( + ` + UPDATE dispensaries + SET platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + '{"resolution_error": "cName no longer exists on Dutchie", "not_crawlable": true}'::jsonb, + updated_at = NOW() + WHERE id = $1 + `, + [dispensaryId] + ); + console.log(`[Discovery] Could not resolve: ${cName} - marked as not crawlable`); + return { + success: false, + platformId: null, + cName, + error: `cName "${cName}" no longer exists on Dutchie`, + }; + } + } catch (error: any) { + console.error(`[Discovery] Error resolving ${cName}:`, error.message); + return { success: false, platformId: null, cName, error: error.message }; + } +} + +/** + * Update menu_url for a dispensary and re-resolve platform ID + */ +export async function updateMenuUrlAndResolve(dispensaryId: number, newMenuUrl: string): Promise<{ + success: boolean; + platformId: string | null; + cName: string | null; + error?: string; +}> { + console.log(`[Discovery] Updating menu_url for dispensary ${dispensaryId} to: ${newMenuUrl}`); + + const cName = extractCNameFromMenuUrl(newMenuUrl); + if (!cName) { + return { + success: false, + platformId: null, + cName: null, + error: `Could not extract cName from new menu_url: ${newMenuUrl}`, + }; + } + + // Update the menu_url first + await query( + ` + UPDATE dispensaries + SET menu_url = $1, + menu_type = 'dutchie', + platform_dispensary_id = NULL, + updated_at = NOW() + WHERE id = $2 + `, + [newMenuUrl, dispensaryId] + ); + + // Now resolve the platform ID with the new cName + return await reResolveDispensaryPlatformId(dispensaryId); +} + +/** + * Mark a dispensary as not crawlable (when resolution fails permanently) + */ +export async function markDispensaryNotCrawlable(dispensaryId: number, reason: string): Promise { + await query( + ` + UPDATE dispensaries + SET platform_dispensary_id = NULL, + provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || + jsonb_build_object('not_crawlable', true, 'not_crawlable_reason', $1::text, 'not_crawlable_at', NOW()::text), + updated_at = NOW() + WHERE id = $2 + `, + [reason, dispensaryId] + ); + console.log(`[Discovery] Marked dispensary ${dispensaryId} as not crawlable: ${reason}`); +} + +/** + * Get the cName for a dispensary (extracted from menu_url) + */ +export function getDispensaryCName(dispensary: Dispensary): string | null { + return extractCNameFromMenuUrl(dispensary.menuUrl); } diff --git a/backend/src/dutchie-az/services/graphql-client.ts b/backend/src/dutchie-az/services/graphql-client.ts index a169a0a2..95e45d98 100644 --- a/backend/src/dutchie-az/services/graphql-client.ts +++ b/backend/src/dutchie-az/services/graphql-client.ts @@ -28,19 +28,23 @@ puppeteer.use(StealthPlugin()); // Re-export for backward compatibility export { GRAPHQL_HASHES, ARIZONA_CENTERPOINTS }; -interface SessionCredentials { - cookies: string; // Cookie header string - userAgent: string; - browser: Browser; -} - // ============================================================ // SESSION MANAGEMENT - Get CF cookies via Puppeteer // ============================================================ +interface SessionCredentials { + cookies: string; // Cookie header string + userAgent: string; + browser: Browser; + page: Page; // Keep page reference for extracting dispensaryId + dispensaryId?: string; // Extracted from window.reactEnv if available + httpStatus?: number; // HTTP status code from navigation +} + /** * Create a session by navigating to the embedded menu page - * and extracting CF clearance cookies for server-side requests + * and extracting CF clearance cookies for server-side requests. + * Also extracts dispensaryId from window.reactEnv if available. */ async function createSession(cName: string): Promise { const browser = await puppeteer.launch({ @@ -62,12 +66,28 @@ async function createSession(cName: string): Promise { const embeddedMenuUrl = `https://dutchie.com/embedded-menu/${cName}`; console.log(`[GraphQL Client] Loading ${embeddedMenuUrl} to get CF cookies...`); + let httpStatus: number | undefined; + let dispensaryId: string | undefined; + try { - await page.goto(embeddedMenuUrl, { + const response = await page.goto(embeddedMenuUrl, { waitUntil: 'networkidle2', timeout: dutchieConfig.navigationTimeout, }); + httpStatus = response?.status(); await new Promise((r) => setTimeout(r, dutchieConfig.pageLoadDelay)); + + // Try to extract dispensaryId from window.reactEnv + try { + dispensaryId = await page.evaluate(() => { + return (window as any).reactEnv?.dispensaryId || null; + }); + if (dispensaryId) { + console.log(`[GraphQL Client] Extracted dispensaryId from reactEnv: ${dispensaryId}`); + } + } catch (evalError: any) { + console.log(`[GraphQL Client] Could not extract dispensaryId from reactEnv: ${evalError.message}`); + } } catch (error: any) { console.warn(`[GraphQL Client] Navigation warning: ${error.message}`); // Continue anyway - we may have gotten cookies @@ -77,12 +97,12 @@ async function createSession(cName: string): Promise { const cookies = await page.cookies(); const cookieString = cookies.map((c: Protocol.Network.Cookie) => `${c.name}=${c.value}`).join('; '); - console.log(`[GraphQL Client] Got ${cookies.length} cookies`); + console.log(`[GraphQL Client] Got ${cookies.length} cookies, HTTP status: ${httpStatus}`); if (cookies.length > 0) { console.log(`[GraphQL Client] Cookie names: ${cookies.map(c => c.name).join(', ')}`); } - return { cookies: cookieString, userAgent, browser }; + return { cookies: cookieString, userAgent, browser, page, dispensaryId, httpStatus }; } /** @@ -194,15 +214,64 @@ async function executeGraphQL( // ============================================================ /** - * Resolve a dispensary slug to its internal platform ID - * Uses GetAddressBasedDispensaryData query + * Resolution result with HTTP status for error handling + */ +export interface ResolveDispensaryResult { + dispensaryId: string | null; + httpStatus?: number; + error?: string; + source?: 'reactEnv' | 'graphql'; +} + +/** + * Resolve a dispensary slug to its internal platform ID. + * + * STRATEGY: + * 1. Navigate to embedded menu page and extract window.reactEnv.dispensaryId (preferred) + * 2. Fall back to GraphQL GetAddressBasedDispensaryData query if reactEnv fails + * + * Returns the dispensaryId (platform_dispensary_id) or null if not found. + * Throws if page returns 403/404 so caller can mark as not_crawlable. */ export async function resolveDispensaryId(slug: string): Promise { + const result = await resolveDispensaryIdWithDetails(slug); + return result.dispensaryId; +} + +/** + * Resolve a dispensary slug with full details (HTTP status, source, error). + * Use this when you need to know WHY resolution failed. + */ +export async function resolveDispensaryIdWithDetails(slug: string): Promise { console.log(`[GraphQL Client] Resolving dispensary ID for slug: ${slug}`); const session = await createSession(slug); try { + // Check HTTP status first - if 403/404, the store is not crawlable + if (session.httpStatus && (session.httpStatus === 403 || session.httpStatus === 404)) { + console.log(`[GraphQL Client] Page returned HTTP ${session.httpStatus} for ${slug} - not crawlable`); + return { + dispensaryId: null, + httpStatus: session.httpStatus, + error: `HTTP ${session.httpStatus}: Store removed or not accessible`, + source: 'reactEnv', + }; + } + + // PREFERRED: Use dispensaryId from window.reactEnv (extracted during createSession) + if (session.dispensaryId) { + console.log(`[GraphQL Client] Resolved ${slug} -> ${session.dispensaryId} (from reactEnv)`); + return { + dispensaryId: session.dispensaryId, + httpStatus: session.httpStatus, + source: 'reactEnv', + }; + } + + // FALLBACK: Try GraphQL query + console.log(`[GraphQL Client] reactEnv.dispensaryId not found for ${slug}, trying GraphQL...`); + const variables = { dispensaryFilter: { cNameOrID: slug, @@ -222,12 +291,20 @@ export async function resolveDispensaryId(slug: string): Promise result?.data?.getAddressBasedDispensaryData?.dispensary?.id; if (dispensaryId) { - console.log(`[GraphQL Client] Resolved ${slug} -> ${dispensaryId}`); - return dispensaryId; + console.log(`[GraphQL Client] Resolved ${slug} -> ${dispensaryId} (from GraphQL)`); + return { + dispensaryId, + httpStatus: session.httpStatus, + source: 'graphql', + }; } - console.log(`[GraphQL Client] Could not resolve ${slug}, response:`, JSON.stringify(result).slice(0, 300)); - return null; + console.log(`[GraphQL Client] Could not resolve ${slug}, GraphQL response:`, JSON.stringify(result).slice(0, 300)); + return { + dispensaryId: null, + httpStatus: session.httpStatus, + error: 'Could not extract dispensaryId from reactEnv or GraphQL', + }; } finally { await closeSession(session); } @@ -330,28 +407,21 @@ function buildFilterVariables( ): any { const isModeA = crawlMode === 'mode_a'; + // Per CLAUDE.md Rule #11: Use simple productsFilter with dispensaryId directly + // Do NOT use dispensaryFilter.cNameOrID - that's outdated const productsFilter: Record = { dispensaryId: platformDispensaryId, pricingType: pricingType, - strainTypes: [], - subcategories: [], - types: [], - useCache: false, // Get fresh data - isDefaultSort: true, - sortBy: 'popular', - sortDirection: 1, - bypassOnlineThresholds: false, - isKioskMenu: false, - removeProductsBelowOptionThresholds: true, }; - // Mode A: Only active products (UI parity) + // Mode A: Only active products (UI parity) - Status: "Active" + // Mode B: MAX COVERAGE (OOS/inactive) - omit Status or set to null if (isModeA) { productsFilter.Status = 'Active'; } + // Mode B: No Status filter = returns all products including OOS/inactive return { - includeEnterpriseSpecials: false, productsFilter, page, perPage, diff --git a/backend/src/dutchie-az/services/job-queue.ts b/backend/src/dutchie-az/services/job-queue.ts new file mode 100644 index 00000000..70ec3a23 --- /dev/null +++ b/backend/src/dutchie-az/services/job-queue.ts @@ -0,0 +1,533 @@ +/** + * Job Queue Service + * + * DB-backed job queue with claiming/locking for distributed workers. + * Ensures only one worker processes a given store at a time. + */ + +import { query, getClient } from '../db/connection'; +import { v4 as uuidv4 } from 'uuid'; +import * as os from 'os'; + +// ============================================================ +// TYPES +// ============================================================ + +export interface QueuedJob { + id: number; + jobType: string; + dispensaryId: number | null; + status: 'pending' | 'running' | 'completed' | 'failed'; + priority: number; + retryCount: number; + maxRetries: number; + claimedBy: string | null; + claimedAt: Date | null; + workerHostname: string | null; + startedAt: Date | null; + completedAt: Date | null; + errorMessage: string | null; + productsFound: number; + productsUpserted: number; + snapshotsCreated: number; + currentPage: number; + totalPages: number | null; + lastHeartbeatAt: Date | null; + metadata: Record | null; + createdAt: Date; +} + +export interface EnqueueJobOptions { + jobType: string; + dispensaryId?: number; + priority?: number; + metadata?: Record; + maxRetries?: number; +} + +export interface ClaimJobOptions { + workerId: string; + jobTypes?: string[]; + lockDurationMinutes?: number; +} + +export interface JobProgress { + productsFound?: number; + productsUpserted?: number; + snapshotsCreated?: number; + currentPage?: number; + totalPages?: number; +} + +// ============================================================ +// WORKER IDENTITY +// ============================================================ + +let _workerId: string | null = null; + +/** + * Get or create a unique worker ID for this process + * In Kubernetes, uses POD_NAME for clarity; otherwise generates a unique ID + */ +export function getWorkerId(): string { + if (!_workerId) { + // Prefer POD_NAME in K8s (set via fieldRef) + const podName = process.env.POD_NAME; + if (podName) { + _workerId = podName; + } else { + const hostname = os.hostname(); + const pid = process.pid; + const uuid = uuidv4().slice(0, 8); + _workerId = `${hostname}-${pid}-${uuid}`; + } + } + return _workerId; +} + +/** + * Get hostname for worker tracking + * In Kubernetes, uses POD_NAME; otherwise uses os.hostname() + */ +export function getWorkerHostname(): string { + return process.env.POD_NAME || os.hostname(); +} + +// ============================================================ +// JOB ENQUEUEING +// ============================================================ + +/** + * Enqueue a new job for processing + * Returns null if a pending/running job already exists for this dispensary + */ +export async function enqueueJob(options: EnqueueJobOptions): Promise { + const { + jobType, + dispensaryId, + priority = 0, + metadata, + maxRetries = 3, + } = options; + + // Check if there's already a pending/running job for this dispensary + if (dispensaryId) { + const { rows: existing } = await query( + `SELECT id FROM dispensary_crawl_jobs + WHERE dispensary_id = $1 AND status IN ('pending', 'running') + LIMIT 1`, + [dispensaryId] + ); + + if (existing.length > 0) { + console.log(`[JobQueue] Skipping enqueue - job already exists for dispensary ${dispensaryId}`); + return null; + } + } + + const { rows } = await query( + `INSERT INTO dispensary_crawl_jobs (job_type, dispensary_id, status, priority, max_retries, metadata, created_at) + VALUES ($1, $2, 'pending', $3, $4, $5, NOW()) + RETURNING id`, + [jobType, dispensaryId || null, priority, maxRetries, metadata ? JSON.stringify(metadata) : null] + ); + + const jobId = rows[0].id; + console.log(`[JobQueue] Enqueued job ${jobId} (type=${jobType}, dispensary=${dispensaryId})`); + return jobId; +} + +/** + * Bulk enqueue jobs for multiple dispensaries + * Skips dispensaries that already have pending/running jobs + */ +export async function bulkEnqueueJobs( + jobType: string, + dispensaryIds: number[], + options: { priority?: number; metadata?: Record } = {} +): Promise<{ enqueued: number; skipped: number }> { + const { priority = 0, metadata } = options; + + // Get dispensaries that already have pending/running jobs + const { rows: existing } = await query( + `SELECT DISTINCT dispensary_id FROM dispensary_crawl_jobs + WHERE dispensary_id = ANY($1) AND status IN ('pending', 'running')`, + [dispensaryIds] + ); + const existingSet = new Set(existing.map((r: any) => r.dispensary_id)); + + // Filter out dispensaries with existing jobs + const toEnqueue = dispensaryIds.filter(id => !existingSet.has(id)); + + if (toEnqueue.length === 0) { + return { enqueued: 0, skipped: dispensaryIds.length }; + } + + // Bulk insert - each row needs 4 params: job_type, dispensary_id, priority, metadata + const metadataJson = metadata ? JSON.stringify(metadata) : null; + const values = toEnqueue.map((_, i) => { + const offset = i * 4; + return `($${offset + 1}, $${offset + 2}, 'pending', $${offset + 3}, 3, $${offset + 4}, NOW())`; + }).join(', '); + + const params: any[] = []; + toEnqueue.forEach(dispensaryId => { + params.push(jobType, dispensaryId, priority, metadataJson); + }); + + await query( + `INSERT INTO dispensary_crawl_jobs (job_type, dispensary_id, status, priority, max_retries, metadata, created_at) + VALUES ${values}`, + params + ); + + console.log(`[JobQueue] Bulk enqueued ${toEnqueue.length} jobs, skipped ${existingSet.size}`); + return { enqueued: toEnqueue.length, skipped: existingSet.size }; +} + +// ============================================================ +// JOB CLAIMING (with locking) +// ============================================================ + +/** + * Claim the next available job from the queue + * Uses SELECT FOR UPDATE SKIP LOCKED to prevent double-claims + */ +export async function claimNextJob(options: ClaimJobOptions): Promise { + const { workerId, jobTypes, lockDurationMinutes = 30 } = options; + const hostname = getWorkerHostname(); + + const client = await getClient(); + + try { + await client.query('BEGIN'); + + // Build job type filter + let typeFilter = ''; + const params: any[] = [workerId, hostname, lockDurationMinutes]; + let paramIndex = 4; + + if (jobTypes && jobTypes.length > 0) { + typeFilter = `AND job_type = ANY($${paramIndex})`; + params.push(jobTypes); + paramIndex++; + } + + // Claim the next pending job using FOR UPDATE SKIP LOCKED + // This atomically selects and locks a row, skipping any already locked by other workers + const { rows } = await client.query( + `UPDATE dispensary_crawl_jobs + SET + status = 'running', + claimed_by = $1, + claimed_at = NOW(), + worker_id = $1, + worker_hostname = $2, + started_at = NOW(), + locked_until = NOW() + ($3 || ' minutes')::INTERVAL, + last_heartbeat_at = NOW(), + updated_at = NOW() + WHERE id = ( + SELECT id FROM dispensary_crawl_jobs + WHERE status = 'pending' + ${typeFilter} + ORDER BY priority DESC, created_at ASC + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) + RETURNING *`, + params + ); + + await client.query('COMMIT'); + + if (rows.length === 0) { + return null; + } + + const job = mapDbRowToJob(rows[0]); + console.log(`[JobQueue] Worker ${workerId} claimed job ${job.id} (type=${job.jobType}, dispensary=${job.dispensaryId})`); + return job; + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +// ============================================================ +// JOB PROGRESS & COMPLETION +// ============================================================ + +/** + * Update job progress (for live monitoring) + */ +export async function updateJobProgress(jobId: number, progress: JobProgress): Promise { + const updates: string[] = ['last_heartbeat_at = NOW()', 'updated_at = NOW()']; + const params: any[] = []; + let paramIndex = 1; + + if (progress.productsFound !== undefined) { + updates.push(`products_found = $${paramIndex++}`); + params.push(progress.productsFound); + } + if (progress.productsUpserted !== undefined) { + updates.push(`products_upserted = $${paramIndex++}`); + params.push(progress.productsUpserted); + } + if (progress.snapshotsCreated !== undefined) { + updates.push(`snapshots_created = $${paramIndex++}`); + params.push(progress.snapshotsCreated); + } + if (progress.currentPage !== undefined) { + updates.push(`current_page = $${paramIndex++}`); + params.push(progress.currentPage); + } + if (progress.totalPages !== undefined) { + updates.push(`total_pages = $${paramIndex++}`); + params.push(progress.totalPages); + } + + params.push(jobId); + + await query( + `UPDATE dispensary_crawl_jobs SET ${updates.join(', ')} WHERE id = $${paramIndex}`, + params + ); +} + +/** + * Send heartbeat to keep job alive (prevents timeout) + */ +export async function heartbeat(jobId: number): Promise { + await query( + `UPDATE dispensary_crawl_jobs + SET last_heartbeat_at = NOW(), locked_until = NOW() + INTERVAL '30 minutes' + WHERE id = $1 AND status = 'running'`, + [jobId] + ); +} + +/** + * Mark job as completed + */ +export async function completeJob( + jobId: number, + result: { productsFound?: number; productsUpserted?: number; snapshotsCreated?: number } +): Promise { + await query( + `UPDATE dispensary_crawl_jobs + SET + status = 'completed', + completed_at = NOW(), + products_found = COALESCE($2, products_found), + products_upserted = COALESCE($3, products_upserted), + snapshots_created = COALESCE($4, snapshots_created), + updated_at = NOW() + WHERE id = $1`, + [jobId, result.productsFound, result.productsUpserted, result.snapshotsCreated] + ); + console.log(`[JobQueue] Job ${jobId} completed`); +} + +/** + * Mark job as failed + */ +export async function failJob(jobId: number, errorMessage: string): Promise { + // Check if we should retry + const { rows } = await query( + `SELECT retry_count, max_retries FROM dispensary_crawl_jobs WHERE id = $1`, + [jobId] + ); + + if (rows.length === 0) return false; + + const { retry_count, max_retries } = rows[0]; + + if (retry_count < max_retries) { + // Re-queue for retry + await query( + `UPDATE dispensary_crawl_jobs + SET + status = 'pending', + retry_count = retry_count + 1, + claimed_by = NULL, + claimed_at = NULL, + worker_id = NULL, + worker_hostname = NULL, + started_at = NULL, + locked_until = NULL, + last_heartbeat_at = NULL, + error_message = $2, + updated_at = NOW() + WHERE id = $1`, + [jobId, errorMessage] + ); + console.log(`[JobQueue] Job ${jobId} failed, re-queued for retry (${retry_count + 1}/${max_retries})`); + return true; // Will retry + } else { + // Mark as failed permanently + await query( + `UPDATE dispensary_crawl_jobs + SET + status = 'failed', + completed_at = NOW(), + error_message = $2, + updated_at = NOW() + WHERE id = $1`, + [jobId, errorMessage] + ); + console.log(`[JobQueue] Job ${jobId} failed permanently after ${retry_count} retries`); + return false; // No more retries + } +} + +// ============================================================ +// QUEUE MONITORING +// ============================================================ + +/** + * Get queue statistics + */ +export async function getQueueStats(): Promise<{ + pending: number; + running: number; + completed1h: number; + failed1h: number; + activeWorkers: number; + avgDurationSeconds: number | null; +}> { + const { rows } = await query(`SELECT * FROM v_queue_stats`); + const stats = rows[0] || {}; + + return { + pending: parseInt(stats.pending_jobs || '0', 10), + running: parseInt(stats.running_jobs || '0', 10), + completed1h: parseInt(stats.completed_1h || '0', 10), + failed1h: parseInt(stats.failed_1h || '0', 10), + activeWorkers: parseInt(stats.active_workers || '0', 10), + avgDurationSeconds: stats.avg_duration_seconds ? parseFloat(stats.avg_duration_seconds) : null, + }; +} + +/** + * Get active workers + */ +export async function getActiveWorkers(): Promise> { + const { rows } = await query(`SELECT * FROM v_active_workers`); + + return rows.map((row: any) => ({ + workerId: row.worker_id, + hostname: row.worker_hostname, + currentJobs: parseInt(row.current_jobs || '0', 10), + totalProductsFound: parseInt(row.total_products_found || '0', 10), + totalProductsUpserted: parseInt(row.total_products_upserted || '0', 10), + totalSnapshots: parseInt(row.total_snapshots || '0', 10), + firstClaimedAt: new Date(row.first_claimed_at), + lastHeartbeat: row.last_heartbeat ? new Date(row.last_heartbeat) : null, + })); +} + +/** + * Get running jobs with worker info + */ +export async function getRunningJobs(): Promise { + const { rows } = await query( + `SELECT cj.*, d.name as dispensary_name, d.city + FROM dispensary_crawl_jobs cj + LEFT JOIN dispensaries d ON cj.dispensary_id = d.id + WHERE cj.status = 'running' + ORDER BY cj.started_at DESC` + ); + + return rows.map(mapDbRowToJob); +} + +/** + * Recover stale jobs (workers that died without completing) + */ +export async function recoverStaleJobs(staleMinutes: number = 15): Promise { + const { rowCount } = await query( + `UPDATE dispensary_crawl_jobs + SET + status = 'pending', + claimed_by = NULL, + claimed_at = NULL, + worker_id = NULL, + worker_hostname = NULL, + started_at = NULL, + locked_until = NULL, + error_message = 'Recovered from stale worker', + retry_count = retry_count + 1, + updated_at = NOW() + WHERE status = 'running' + AND last_heartbeat_at < NOW() - ($1 || ' minutes')::INTERVAL + AND retry_count < max_retries`, + [staleMinutes] + ); + + if (rowCount && rowCount > 0) { + console.log(`[JobQueue] Recovered ${rowCount} stale jobs`); + } + return rowCount || 0; +} + +/** + * Clean up old completed/failed jobs + */ +export async function cleanupOldJobs(olderThanDays: number = 7): Promise { + const { rowCount } = await query( + `DELETE FROM dispensary_crawl_jobs + WHERE status IN ('completed', 'failed') + AND completed_at < NOW() - ($1 || ' days')::INTERVAL`, + [olderThanDays] + ); + + if (rowCount && rowCount > 0) { + console.log(`[JobQueue] Cleaned up ${rowCount} old jobs`); + } + return rowCount || 0; +} + +// ============================================================ +// HELPERS +// ============================================================ + +function mapDbRowToJob(row: any): QueuedJob { + return { + id: row.id, + jobType: row.job_type, + dispensaryId: row.dispensary_id, + status: row.status, + priority: row.priority || 0, + retryCount: row.retry_count || 0, + maxRetries: row.max_retries || 3, + claimedBy: row.claimed_by, + claimedAt: row.claimed_at ? new Date(row.claimed_at) : null, + workerHostname: row.worker_hostname, + startedAt: row.started_at ? new Date(row.started_at) : null, + completedAt: row.completed_at ? new Date(row.completed_at) : null, + errorMessage: row.error_message, + productsFound: row.products_found || 0, + productsUpserted: row.products_upserted || 0, + snapshotsCreated: row.snapshots_created || 0, + currentPage: row.current_page || 0, + totalPages: row.total_pages, + lastHeartbeatAt: row.last_heartbeat_at ? new Date(row.last_heartbeat_at) : null, + metadata: row.metadata, + createdAt: new Date(row.created_at), + // Add extra fields from join if present + ...(row.dispensary_name && { dispensaryName: row.dispensary_name }), + ...(row.city && { city: row.city }), + }; +} diff --git a/backend/src/dutchie-az/services/menu-detection.ts b/backend/src/dutchie-az/services/menu-detection.ts index b9e83ab5..492f9aea 100644 --- a/backend/src/dutchie-az/services/menu-detection.ts +++ b/backend/src/dutchie-az/services/menu-detection.ts @@ -30,6 +30,7 @@ const DISPENSARY_COLUMNS = ` export type MenuProvider = | 'dutchie' | 'curaleaf' // Curaleaf proprietary platform (not crawlable via Dutchie) + | 'sol' // Sol Flower proprietary platform (not crawlable via Dutchie) | 'treez' | 'jane' | 'iheartjane' @@ -67,8 +68,8 @@ export interface BulkDetectionResult { // ============================================================ const PROVIDER_URL_PATTERNS: Array<{ provider: MenuProvider; patterns: RegExp[] }> = [ - // IMPORTANT: Curaleaf must come BEFORE dutchie to take precedence - // Curaleaf stores have their own proprietary menu system (not crawlable via Dutchie) + // IMPORTANT: Curaleaf and Sol must come BEFORE dutchie to take precedence + // These stores have their own proprietary menu systems (not crawlable via Dutchie) { provider: 'curaleaf', patterns: [ @@ -76,6 +77,13 @@ const PROVIDER_URL_PATTERNS: Array<{ provider: MenuProvider; patterns: RegExp[] /curaleaf\.com\/dispensary\//i, // e.g., https://curaleaf.com/dispensary/arizona ], }, + { + provider: 'sol', + patterns: [ + /livewithsol\.com/i, // e.g., https://www.livewithsol.com/locations/sun-city/ + /solflower\.com/i, // alternate domain if any + ], + }, { provider: 'dutchie', patterns: [ @@ -634,8 +642,12 @@ export async function detectAndResolveDispensary(dispensaryId: number): Promise< // If not dutchie, just update menu_type and return if (detectedProvider !== 'dutchie') { - // Special handling for Curaleaf - mark as not_crawlable until we have a crawler - const isCuraleaf = detectedProvider === 'curaleaf'; + // Special handling for proprietary providers - mark as not_crawlable until we have crawlers + const PROPRIETARY_PROVIDERS = ['curaleaf', 'sol'] as const; + const isProprietaryProvider = PROPRIETARY_PROVIDERS.includes(detectedProvider as any); + const notCrawlableReason = isProprietaryProvider + ? `${detectedProvider} proprietary menu - no crawler available` + : null; await query( ` @@ -648,15 +660,15 @@ export async function detectAndResolveDispensary(dispensaryId: number): Promise< 'detection_method', 'url_pattern'::text, 'detected_at', NOW(), 'not_crawlable', $3, - 'not_crawlable_reason', CASE WHEN $3 THEN 'Curaleaf proprietary menu - no crawler available'::text ELSE NULL END + 'not_crawlable_reason', $4::text ), updated_at = NOW() WHERE id = $2 `, - [detectedProvider, dispensaryId, isCuraleaf] + [detectedProvider, dispensaryId, isProprietaryProvider, notCrawlableReason] ); result.success = true; - console.log(`[MenuDetection] ${dispensary.name}: Updated menu_type to ${detectedProvider}${isCuraleaf ? ' (not crawlable)' : ''}`); + console.log(`[MenuDetection] ${dispensary.name}: Updated menu_type to ${detectedProvider}${isProprietaryProvider ? ' (not crawlable)' : ''}`); return result; } diff --git a/backend/src/dutchie-az/services/worker.ts b/backend/src/dutchie-az/services/worker.ts new file mode 100644 index 00000000..aa3706a7 --- /dev/null +++ b/backend/src/dutchie-az/services/worker.ts @@ -0,0 +1,489 @@ +/** + * Worker Service + * + * Polls the job queue and processes crawl jobs. + * Each worker instance runs independently, claiming jobs atomically. + */ + +import { + claimNextJob, + completeJob, + failJob, + updateJobProgress, + heartbeat, + getWorkerId, + getWorkerHostname, + recoverStaleJobs, + QueuedJob, +} from './job-queue'; +import { crawlDispensaryProducts } from './product-crawler'; +import { mapDbRowToDispensary } from './discovery'; +import { query } from '../db/connection'; + +// Explicit column list for dispensaries table (avoids SELECT * issues with schema differences) +// NOTE: failed_at is included for worker compatibility checks +const DISPENSARY_COLUMNS = ` + id, name, slug, city, state, zip, address, latitude, longitude, + menu_type, menu_url, platform_dispensary_id, website, + provider_detection_data, created_at, updated_at, failed_at +`; + +// ============================================================ +// WORKER CONFIG +// ============================================================ + +const POLL_INTERVAL_MS = 5000; // Check for jobs every 5 seconds +const HEARTBEAT_INTERVAL_MS = 60000; // Send heartbeat every 60 seconds +const STALE_CHECK_INTERVAL_MS = 300000; // Check for stale jobs every 5 minutes +const SHUTDOWN_GRACE_PERIOD_MS = 30000; // Wait 30s for job to complete on shutdown + +// ============================================================ +// WORKER STATE +// ============================================================ + +let isRunning = false; +let currentJob: QueuedJob | null = null; +let pollTimer: NodeJS.Timeout | null = null; +let heartbeatTimer: NodeJS.Timeout | null = null; +let staleCheckTimer: NodeJS.Timeout | null = null; +let shutdownPromise: Promise | null = null; + +// ============================================================ +// WORKER LIFECYCLE +// ============================================================ + +/** + * Start the worker + */ +export async function startWorker(): Promise { + if (isRunning) { + console.log('[Worker] Already running'); + return; + } + + const workerId = getWorkerId(); + const hostname = getWorkerHostname(); + + console.log(`[Worker] Starting worker ${workerId} on ${hostname}`); + isRunning = true; + + // Set up graceful shutdown + setupShutdownHandlers(); + + // Start polling for jobs + pollTimer = setInterval(pollForJobs, POLL_INTERVAL_MS); + + // Start stale job recovery (only one worker should do this, but it's idempotent) + staleCheckTimer = setInterval(async () => { + try { + await recoverStaleJobs(15); + } catch (error) { + console.error('[Worker] Error recovering stale jobs:', error); + } + }, STALE_CHECK_INTERVAL_MS); + + // Immediately poll for a job + await pollForJobs(); + + console.log(`[Worker] Worker ${workerId} started, polling every ${POLL_INTERVAL_MS}ms`); +} + +/** + * Stop the worker gracefully + */ +export async function stopWorker(): Promise { + if (!isRunning) return; + + console.log('[Worker] Stopping worker...'); + isRunning = false; + + // Clear timers + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } + if (staleCheckTimer) { + clearInterval(staleCheckTimer); + staleCheckTimer = null; + } + + // Wait for current job to complete + if (currentJob) { + console.log(`[Worker] Waiting for job ${currentJob.id} to complete...`); + const startWait = Date.now(); + + while (currentJob && Date.now() - startWait < SHUTDOWN_GRACE_PERIOD_MS) { + await new Promise(r => setTimeout(r, 1000)); + } + + if (currentJob) { + console.log(`[Worker] Job ${currentJob.id} did not complete in time, marking for retry`); + await failJob(currentJob.id, 'Worker shutdown'); + } + } + + console.log('[Worker] Worker stopped'); +} + +/** + * Get worker status + */ +export function getWorkerStatus(): { + isRunning: boolean; + workerId: string; + hostname: string; + currentJob: QueuedJob | null; +} { + return { + isRunning, + workerId: getWorkerId(), + hostname: getWorkerHostname(), + currentJob, + }; +} + +// ============================================================ +// JOB PROCESSING +// ============================================================ + +/** + * Poll for and process the next available job + */ +async function pollForJobs(): Promise { + if (!isRunning || currentJob) { + return; // Already processing a job + } + + try { + const workerId = getWorkerId(); + + // Try to claim a job + const job = await claimNextJob({ + workerId, + jobTypes: ['dutchie_product_crawl', 'menu_detection', 'menu_detection_single'], + lockDurationMinutes: 30, + }); + + if (!job) { + return; // No jobs available + } + + currentJob = job; + console.log(`[Worker] Processing job ${job.id} (type=${job.jobType}, dispensary=${job.dispensaryId})`); + + // Start heartbeat for this job + heartbeatTimer = setInterval(async () => { + if (currentJob) { + try { + await heartbeat(currentJob.id); + } catch (error) { + console.error('[Worker] Heartbeat error:', error); + } + } + }, HEARTBEAT_INTERVAL_MS); + + // Process the job + await processJob(job); + + } catch (error: any) { + console.error('[Worker] Error polling for jobs:', error); + + if (currentJob) { + try { + await failJob(currentJob.id, error.message); + } catch (failError) { + console.error('[Worker] Error failing job:', failError); + } + } + } finally { + // Clear heartbeat timer + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } + currentJob = null; + } +} + +/** + * Process a single job + */ +async function processJob(job: QueuedJob): Promise { + try { + switch (job.jobType) { + case 'dutchie_product_crawl': + await processProductCrawlJob(job); + break; + + case 'menu_detection': + await processMenuDetectionJob(job); + break; + + case 'menu_detection_single': + await processSingleDetectionJob(job); + break; + + default: + throw new Error(`Unknown job type: ${job.jobType}`); + } + } catch (error: any) { + console.error(`[Worker] Job ${job.id} failed:`, error); + await failJob(job.id, error.message); + } +} + +// Maximum consecutive failures before flagging a dispensary +const MAX_CONSECUTIVE_FAILURES = 3; + +/** + * Record a successful crawl - resets failure counter + */ +async function recordCrawlSuccess(dispensaryId: number): Promise { + await query( + `UPDATE dispensaries + SET consecutive_failures = 0, + last_crawl_at = NOW(), + updated_at = NOW() + WHERE id = $1`, + [dispensaryId] + ); +} + +/** + * Record a crawl failure - increments counter and may flag dispensary + * Returns true if dispensary was flagged as failed + */ +async function recordCrawlFailure(dispensaryId: number, errorMessage: string): Promise { + // Increment failure counter + const { rows } = await query( + `UPDATE dispensaries + SET consecutive_failures = consecutive_failures + 1, + last_failure_at = NOW(), + last_failure_reason = $2, + updated_at = NOW() + WHERE id = $1 + RETURNING consecutive_failures`, + [dispensaryId, errorMessage] + ); + + const failures = rows[0]?.consecutive_failures || 0; + + // If we've hit the threshold, flag the dispensary as failed + if (failures >= MAX_CONSECUTIVE_FAILURES) { + await query( + `UPDATE dispensaries + SET failed_at = NOW(), + menu_type = NULL, + platform_dispensary_id = NULL, + failure_notes = $2, + updated_at = NOW() + WHERE id = $1`, + [dispensaryId, `Auto-flagged after ${failures} consecutive failures. Last error: ${errorMessage}`] + ); + console.log(`[Worker] Dispensary ${dispensaryId} flagged as FAILED after ${failures} consecutive failures`); + return true; + } + + console.log(`[Worker] Dispensary ${dispensaryId} failure recorded (${failures}/${MAX_CONSECUTIVE_FAILURES})`); + return false; +} + +/** + * Process a product crawl job for a single dispensary + */ +async function processProductCrawlJob(job: QueuedJob): Promise { + if (!job.dispensaryId) { + throw new Error('Product crawl job requires dispensary_id'); + } + + // Get dispensary details + const { rows } = await query( + `SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE id = $1`, + [job.dispensaryId] + ); + + if (rows.length === 0) { + throw new Error(`Dispensary ${job.dispensaryId} not found`); + } + + const dispensary = mapDbRowToDispensary(rows[0]); + + // Check if dispensary is already flagged as failed + if (rows[0].failed_at) { + console.log(`[Worker] Skipping dispensary ${job.dispensaryId} - already flagged as failed`); + await completeJob(job.id, { productsFound: 0, productsUpserted: 0 }); + return; + } + + if (!dispensary.platformDispensaryId) { + // Record failure and potentially flag + await recordCrawlFailure(job.dispensaryId, 'Missing platform_dispensary_id'); + throw new Error(`Dispensary ${job.dispensaryId} has no platform_dispensary_id`); + } + + // Get crawl options from job metadata + const pricingType = job.metadata?.pricingType || 'rec'; + const useBothModes = job.metadata?.useBothModes !== false; + + try { + // Crawl the dispensary + const result = await crawlDispensaryProducts(dispensary, pricingType, { + useBothModes, + onProgress: async (progress) => { + // Update progress for live monitoring + await updateJobProgress(job.id, { + productsFound: progress.productsFound, + productsUpserted: progress.productsUpserted, + snapshotsCreated: progress.snapshotsCreated, + currentPage: progress.currentPage, + totalPages: progress.totalPages, + }); + }, + }); + + if (result.success) { + // Success! Reset failure counter + await recordCrawlSuccess(job.dispensaryId); + await completeJob(job.id, { + productsFound: result.productsFetched, + productsUpserted: result.productsUpserted, + snapshotsCreated: result.snapshotsCreated, + }); + } else { + // Crawl returned failure - record it + const wasFlagged = await recordCrawlFailure(job.dispensaryId, result.errorMessage || 'Crawl failed'); + if (wasFlagged) { + // Don't throw - the dispensary is now flagged, job is "complete" + await completeJob(job.id, { productsFound: 0, productsUpserted: 0 }); + } else { + throw new Error(result.errorMessage || 'Crawl failed'); + } + } + } catch (error: any) { + // Record the failure + const wasFlagged = await recordCrawlFailure(job.dispensaryId, error.message); + if (wasFlagged) { + // Dispensary is now flagged - complete the job rather than fail it + await completeJob(job.id, { productsFound: 0, productsUpserted: 0 }); + } else { + throw error; + } + } +} + +/** + * Process a menu detection job (bulk) + */ +async function processMenuDetectionJob(job: QueuedJob): Promise { + const { executeMenuDetectionJob } = await import('./menu-detection'); + + const config = job.metadata || {}; + const result = await executeMenuDetectionJob(config); + + if (result.status === 'error') { + throw new Error(result.errorMessage || 'Menu detection failed'); + } + + await completeJob(job.id, { + productsFound: result.itemsProcessed, + productsUpserted: result.itemsSucceeded, + }); +} + +/** + * Process a single dispensary menu detection job + * This is the parallelizable version - each worker can detect one dispensary at a time + */ +async function processSingleDetectionJob(job: QueuedJob): Promise { + if (!job.dispensaryId) { + throw new Error('Single detection job requires dispensary_id'); + } + + const { detectAndResolveDispensary } = await import('./menu-detection'); + + // Get dispensary details + const { rows } = await query( + `SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE id = $1`, + [job.dispensaryId] + ); + + if (rows.length === 0) { + throw new Error(`Dispensary ${job.dispensaryId} not found`); + } + + const dispensary = rows[0]; + + // Skip if already detected or failed + if (dispensary.failed_at) { + console.log(`[Worker] Skipping dispensary ${job.dispensaryId} - already flagged as failed`); + await completeJob(job.id, { productsFound: 0, productsUpserted: 0 }); + return; + } + + if (dispensary.menu_type && dispensary.menu_type !== 'unknown') { + console.log(`[Worker] Skipping dispensary ${job.dispensaryId} - already detected as ${dispensary.menu_type}`); + await completeJob(job.id, { productsFound: 0, productsUpserted: 1 }); + return; + } + + console.log(`[Worker] Detecting menu for dispensary ${job.dispensaryId} (${dispensary.name})...`); + + try { + const result = await detectAndResolveDispensary(job.dispensaryId); + + if (result.success) { + console.log(`[Worker] Dispensary ${job.dispensaryId}: detected ${result.detectedProvider}, platformId=${result.platformDispensaryId || 'none'}`); + await completeJob(job.id, { + productsFound: 1, + productsUpserted: result.platformDispensaryId ? 1 : 0, + }); + } else { + // Detection failed - record failure + await recordCrawlFailure(job.dispensaryId, result.error || 'Detection failed'); + throw new Error(result.error || 'Detection failed'); + } + } catch (error: any) { + // Record the failure + const wasFlagged = await recordCrawlFailure(job.dispensaryId, error.message); + if (wasFlagged) { + // Dispensary is now flagged - complete the job rather than fail it + await completeJob(job.id, { productsFound: 0, productsUpserted: 0 }); + } else { + throw error; + } + } +} + +// ============================================================ +// SHUTDOWN HANDLING +// ============================================================ + +function setupShutdownHandlers(): void { + const shutdown = async (signal: string) => { + if (shutdownPromise) return shutdownPromise; + + console.log(`\n[Worker] Received ${signal}, shutting down...`); + shutdownPromise = stopWorker(); + await shutdownPromise; + process.exit(0); + }; + + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('SIGINT', () => shutdown('SIGINT')); +} + +// ============================================================ +// STANDALONE WORKER ENTRY POINT +// ============================================================ + +if (require.main === module) { + // Run as standalone worker + startWorker().catch((error) => { + console.error('[Worker] Fatal error:', error); + process.exit(1); + }); +} diff --git a/backend/src/dutchie-az/types/index.ts b/backend/src/dutchie-az/types/index.ts index 9e135af4..5c455aac 100644 --- a/backend/src/dutchie-az/types/index.ts +++ b/backend/src/dutchie-az/types/index.ts @@ -365,6 +365,12 @@ export interface Dispensary { productCount?: number; createdAt: Date; updatedAt: Date; + menuType?: string; + menuUrl?: string; + scrapeEnabled?: boolean; + providerDetectionData?: any; + platformDispensaryIdResolvedAt?: Date; + website?: string; // The dispensary's own website (from raw_metadata or direct column) } /** diff --git a/backend/src/index.ts b/backend/src/index.ts index b2c742e4..140021d6 100755 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -58,7 +58,7 @@ import scheduleRoutes from './routes/schedule'; import crawlerSandboxRoutes from './routes/crawler-sandbox'; import versionRoutes from './routes/version'; import publicApiRoutes from './routes/public-api'; -import { dutchieAZRouter } from './dutchie-az'; +import { dutchieAZRouter, startScheduler as startDutchieAZScheduler, initializeDefaultSchedules } from './dutchie-az'; import { trackApiUsage, checkRateLimit } from './middleware/apiTokenTracker'; import { startCrawlScheduler } from './services/crawl-scheduler'; import { validateWordPressPermissions } from './middleware/wordpressPermissions'; @@ -113,6 +113,11 @@ async function startServer() { startCrawlScheduler(); logger.info('system', 'Crawl scheduler started'); + // Start the Dutchie AZ scheduler (enqueues jobs for workers) + await initializeDefaultSchedules(); + startDutchieAZScheduler(); + logger.info('system', 'Dutchie AZ scheduler started'); + app.listen(PORT, () => { logger.info('system', `Server running on port ${PORT}`); console.log(`🚀 Server running on port ${PORT}`); diff --git a/backend/src/routes/api-permissions.ts b/backend/src/routes/api-permissions.ts index 11aadcde..9784c943 100644 --- a/backend/src/routes/api-permissions.ts +++ b/backend/src/routes/api-permissions.ts @@ -67,22 +67,24 @@ router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => { // Create new API permission router.post('/', requireRole('superadmin', 'admin'), async (req, res) => { try { - const { user_name, allowed_ips, allowed_domains, dispensary_id } = req.body; + // Support both store_id (existing) and dispensary_id (for compatibility) + const { user_name, allowed_ips, allowed_domains, store_id, dispensary_id } = req.body; + const storeIdToUse = store_id || dispensary_id; if (!user_name) { return res.status(400).json({ error: 'User name is required' }); } - if (!dispensary_id) { - return res.status(400).json({ error: 'Dispensary is required' }); + if (!storeIdToUse) { + return res.status(400).json({ error: 'Store/Dispensary is required' }); } // Get dispensary name for display - const dispensaryResult = await pool.query('SELECT name FROM dispensaries WHERE id = $1', [dispensary_id]); + const dispensaryResult = await pool.query('SELECT name FROM dispensaries WHERE id = $1', [storeIdToUse]); if (dispensaryResult.rows.length === 0) { - return res.status(400).json({ error: 'Invalid dispensary ID' }); + return res.status(400).json({ error: 'Invalid store/dispensary ID' }); } - const dispensaryName = dispensaryResult.rows[0].name; + const storeName = dispensaryResult.rows[0].name; const apiKey = generateApiKey(); @@ -93,8 +95,8 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => { allowed_ips, allowed_domains, is_active, - dispensary_id, - dispensary_name + store_id, + store_name ) VALUES ($1, $2, $3, $4, 1, $5, $6) RETURNING * @@ -103,8 +105,8 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => { apiKey, allowed_ips || null, allowed_domains || null, - dispensary_id, - dispensaryName + storeIdToUse, + storeName ]); res.status(201).json({ diff --git a/backend/src/routes/dashboard.ts b/backend/src/routes/dashboard.ts index ab6eddc5..42d4ce71 100755 --- a/backend/src/routes/dashboard.ts +++ b/backend/src/routes/dashboard.ts @@ -1,74 +1,76 @@ import { Router } from 'express'; import { authMiddleware } from '../auth/middleware'; -import { pool } from '../db/migrate'; +import { query as azQuery } from '../dutchie-az/db/connection'; const router = Router(); router.use(authMiddleware); -// Get dashboard stats - consolidated DB (all tables in one DB now) +// Get dashboard stats - uses consolidated dutchie-az DB router.get('/stats', async (req, res) => { try { - // Dispensary stats - const dispensariesResult = await pool.query(` + // Store stats from dispensaries table in consolidated DB + const dispensariesResult = await azQuery(` SELECT COUNT(*) as total, - COUNT(*) FILTER (WHERE scrape_enabled = true) as active, + COUNT(*) FILTER (WHERE menu_type IS NOT NULL AND menu_type != 'unknown') as active, + COUNT(*) FILTER (WHERE platform_dispensary_id IS NOT NULL) as with_platform_id, COUNT(*) FILTER (WHERE menu_url IS NOT NULL) as with_menu_url, - COUNT(*) FILTER (WHERE provider_type IS NOT NULL AND provider_type != 'unknown') as detected, - MIN(last_crawl_at) as oldest_crawl, - MAX(last_crawl_at) as latest_crawl + MIN(last_crawled_at) as oldest_crawl, + MAX(last_crawled_at) as latest_crawl FROM dispensaries `); // Product stats from dutchie_products table - const productsResult = await pool.query(` + const productsResult = await azQuery(` SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock, COUNT(*) FILTER (WHERE primary_image_url IS NOT NULL) as with_images, - COUNT(DISTINCT brand_name) as unique_brands, - COUNT(DISTINCT dispensary_id) as stores_with_products + COUNT(DISTINCT brand_name) FILTER (WHERE brand_name IS NOT NULL AND brand_name != '') as unique_brands, + COUNT(DISTINCT dispensary_id) as dispensaries_with_products FROM dutchie_products `); - // Campaign stats - const campaignsResult = await pool.query(` - SELECT - COUNT(*) as total, - COUNT(*) FILTER (WHERE active = true) as active - FROM campaigns - `); - - // Recent clicks (last 24 hours) - const clicksResult = await pool.query(` - SELECT COUNT(*) as clicks_24h - FROM clicks - WHERE clicked_at >= NOW() - INTERVAL '24 hours' + // Brand stats from dutchie_products + const brandResult = await azQuery(` + SELECT COUNT(DISTINCT brand_name) as total + FROM dutchie_products + WHERE brand_name IS NOT NULL AND brand_name != '' `); // Recent products added (last 24 hours) - const recentProductsResult = await pool.query(` + const recentProductsResult = await azQuery(` SELECT COUNT(*) as new_products_24h FROM dutchie_products WHERE created_at >= NOW() - INTERVAL '24 hours' `); - // Proxy stats - const proxiesResult = await pool.query(` - SELECT - COUNT(*) as total, - COUNT(*) FILTER (WHERE active = true) as active, - COUNT(*) FILTER (WHERE is_anonymous = true) as anonymous - FROM proxies - `); + // Combine results + const storeStats = dispensariesResult.rows[0]; + const productStats = productsResult.rows[0]; res.json({ - stores: dispensariesResult.rows[0], // Keep 'stores' key for frontend compat - products: productsResult.rows[0], - campaigns: campaignsResult.rows[0], - clicks: clicksResult.rows[0], - recent: recentProductsResult.rows[0], - proxies: proxiesResult.rows[0] + stores: { + total: parseInt(storeStats.total) || 0, + active: parseInt(storeStats.active) || 0, + with_menu_url: parseInt(storeStats.with_menu_url) || 0, + with_platform_id: parseInt(storeStats.with_platform_id) || 0, + oldest_crawl: storeStats.oldest_crawl, + latest_crawl: storeStats.latest_crawl + }, + products: { + total: parseInt(productStats.total) || 0, + in_stock: parseInt(productStats.in_stock) || 0, + with_images: parseInt(productStats.with_images) || 0, + unique_brands: parseInt(productStats.unique_brands) || 0, + dispensaries_with_products: parseInt(productStats.dispensaries_with_products) || 0 + }, + brands: { + total: parseInt(brandResult.rows[0].total) || 0 + }, + campaigns: { total: 0, active: 0 }, // Legacy - no longer used + clicks: { clicks_24h: 0 }, // Legacy - no longer used + recent: recentProductsResult.rows[0] }); } catch (error) { console.error('Error fetching dashboard stats:', error); @@ -76,35 +78,32 @@ router.get('/stats', async (req, res) => { } }); -// Get recent activity - from consolidated DB +// Get recent activity - from consolidated dutchie-az DB router.get('/activity', async (req, res) => { try { const { limit = 20 } = req.query; - // Recent crawls from dispensaries - const scrapesResult = await pool.query(` + // Recent crawls from dispensaries (with product counts from dutchie_products) + const scrapesResult = await azQuery(` SELECT - COALESCE(d.dba_name, d.name) as name, - d.last_crawl_at as last_scraped_at, - (SELECT COUNT(*) FROM dutchie_products p WHERE p.dispensary_id = d.id) as product_count + d.name, + d.last_crawled_at as last_scraped_at, + d.product_count FROM dispensaries d - WHERE d.last_crawl_at IS NOT NULL - ORDER BY d.last_crawl_at DESC + WHERE d.last_crawled_at IS NOT NULL + ORDER BY d.last_crawled_at DESC LIMIT $1 `, [limit]); - // Recent products from AZ pipeline - const productsResult = await pool.query(` + // Recent products from dutchie_products + const productsResult = await azQuery(` SELECT p.name, - COALESCE( - (SELECT (options->0->>'price')::numeric - FROM dutchie_product_snapshots s - WHERE s.dutchie_product_id = p.id - ORDER BY crawled_at DESC LIMIT 1), - 0 - ) as price, - COALESCE(d.dba_name, d.name) as store_name, + 0 as price, + p.brand_name as brand, + p.thc as thc_percentage, + p.cbd as cbd_percentage, + d.name as store_name, p.created_at as first_seen_at FROM dutchie_products p JOIN dispensaries d ON p.dispensary_id = d.id diff --git a/backend/src/routes/public-api.ts b/backend/src/routes/public-api.ts index 344406eb..979d43fa 100644 --- a/backend/src/routes/public-api.ts +++ b/backend/src/routes/public-api.ts @@ -23,8 +23,8 @@ interface ApiKeyPermission { allowed_ips: string | null; allowed_domains: string | null; is_active: number; - dispensary_id: number; - dispensary_name: string; + store_id: number; + store_name: string; dutchie_az_store_id?: number; } @@ -126,7 +126,7 @@ async function validatePublicApiKey( } try { - // Query WordPress permissions table with dispensary info + // Query WordPress permissions table with store info const result = await pool.query(` SELECT p.id, @@ -135,8 +135,8 @@ async function validatePublicApiKey( p.allowed_ips, p.allowed_domains, p.is_active, - p.dispensary_id, - p.dispensary_name + p.store_id, + p.store_name FROM wp_dutchie_api_permissions p WHERE p.api_key = $1 AND p.is_active = 1 `, [apiKey]); @@ -181,8 +181,8 @@ async function validatePublicApiKey( } } - // Resolve the dutchie_az store for this dispensary - // Match by dispensary name (from main DB) to dutchie_az.dispensaries.name + // Resolve the dutchie_az store for this store + // Match by store name (from main DB) to dutchie_az.dispensaries.name const storeResult = await dutchieAzQuery<{ id: number }>(` SELECT id FROM dispensaries WHERE LOWER(TRIM(name)) = LOWER(TRIM($1)) @@ -192,7 +192,7 @@ async function validatePublicApiKey( CASE WHEN LOWER(TRIM(name)) = LOWER(TRIM($1)) THEN 0 ELSE 1 END, id LIMIT 1 - `, [permission.dispensary_name]); + `, [permission.store_name]); if (storeResult.rows.length > 0) { permission.dutchie_az_store_id = storeResult.rows[0].id; @@ -243,8 +243,8 @@ router.get('/products', async (req: PublicApiRequest, res: Response) => { if (!permission.dutchie_az_store_id) { return res.status(503).json({ error: 'No menu data available', - message: `Menu data for ${permission.dispensary_name} is not yet available. The dispensary may not be set up in the new data pipeline.`, - dispensary_name: permission.dispensary_name + message: `Menu data for ${permission.store_name} is not yet available. The dispensary may not be set up in the new data pipeline.`, + dispensary_name: permission.store_name }); } @@ -375,7 +375,7 @@ router.get('/products', async (req: PublicApiRequest, res: Response) => { res.json({ success: true, - dispensary: permission.dispensary_name, + dispensary: permission.store_name, products: transformedProducts, pagination: { total: parseInt(countRows[0]?.total || '0', 10), @@ -405,7 +405,7 @@ router.get('/products/:id', async (req: PublicApiRequest, res: Response) => { if (!permission.dutchie_az_store_id) { return res.status(503).json({ error: 'No menu data available', - message: `Menu data for ${permission.dispensary_name} is not yet available.` + message: `Menu data for ${permission.store_name} is not yet available.` }); } @@ -493,7 +493,7 @@ router.get('/categories', async (req: PublicApiRequest, res: Response) => { if (!permission.dutchie_az_store_id) { return res.status(503).json({ error: 'No menu data available', - message: `Menu data for ${permission.dispensary_name} is not yet available.` + message: `Menu data for ${permission.store_name} is not yet available.` }); } @@ -511,7 +511,7 @@ router.get('/categories', async (req: PublicApiRequest, res: Response) => { res.json({ success: true, - dispensary: permission.dispensary_name, + dispensary: permission.store_name, categories }); } catch (error: any) { @@ -534,7 +534,7 @@ router.get('/brands', async (req: PublicApiRequest, res: Response) => { if (!permission.dutchie_az_store_id) { return res.status(503).json({ error: 'No menu data available', - message: `Menu data for ${permission.dispensary_name} is not yet available.` + message: `Menu data for ${permission.store_name} is not yet available.` }); } @@ -551,7 +551,7 @@ router.get('/brands', async (req: PublicApiRequest, res: Response) => { res.json({ success: true, - dispensary: permission.dispensary_name, + dispensary: permission.store_name, brands }); } catch (error: any) { @@ -574,7 +574,7 @@ router.get('/specials', async (req: PublicApiRequest, res: Response) => { if (!permission.dutchie_az_store_id) { return res.status(503).json({ error: 'No menu data available', - message: `Menu data for ${permission.dispensary_name} is not yet available.` + message: `Menu data for ${permission.store_name} is not yet available.` }); } @@ -647,7 +647,7 @@ router.get('/specials', async (req: PublicApiRequest, res: Response) => { res.json({ success: true, - dispensary: permission.dispensary_name, + dispensary: permission.store_name, specials: transformedProducts, pagination: { total: parseInt(countRows[0]?.total || '0', 10), @@ -676,7 +676,7 @@ router.get('/menu', async (req: PublicApiRequest, res: Response) => { if (!permission.dutchie_az_store_id) { return res.status(503).json({ error: 'No menu data available', - message: `Menu data for ${permission.dispensary_name} is not yet available.` + message: `Menu data for ${permission.store_name} is not yet available.` }); } @@ -723,7 +723,7 @@ router.get('/menu', async (req: PublicApiRequest, res: Response) => { res.json({ success: true, - dispensary: permission.dispensary_name, + dispensary: permission.store_name, menu: { total_products: parseInt(summary.total_products || '0', 10), in_stock_count: parseInt(summary.in_stock_count || '0', 10), diff --git a/backend/src/routes/schedule.ts b/backend/src/routes/schedule.ts index ec8f56ee..6b414924 100644 --- a/backend/src/routes/schedule.ts +++ b/backend/src/routes/schedule.ts @@ -971,10 +971,11 @@ router.delete('/dispensaries/:id/schedule', requireRole('superadmin', 'admin'), DELETE FROM dispensary_crawl_schedule WHERE dispensary_id = $1 RETURNING id `, [dispensaryId]); + const deleted = (result.rowCount ?? 0) > 0; res.json({ success: true, - deleted: result.rowCount > 0, - message: result.rowCount > 0 ? 'Schedule deleted' : 'No schedule to delete' + deleted, + message: deleted ? 'Schedule deleted' : 'No schedule to delete' }); } catch (error: any) { console.error('Error deleting schedule:', error); diff --git a/backend/test-graphql-curl.sh b/backend/test-graphql-curl.sh new file mode 100644 index 00000000..61657724 --- /dev/null +++ b/backend/test-graphql-curl.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Test Dutchie GraphQL API + +ENDPOINT="https://api-gw.dutchie.com/embedded-graphql" +BODY='{"operationName":"FilteredProducts","variables":{"includeEnterpriseSpecials":false,"productsFilter":{"dispensaryId":"AZ-Deeply-Rooted","pricingType":"rec","useCache":false},"page":0,"perPage":10},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0"}}}' + +echo "Testing Dutchie GraphQL API..." +echo "Endpoint: $ENDPOINT" + +curl -s "$ENDPOINT" \ + -X POST \ + -H "accept: application/json" \ + -H "origin: https://dutchie.com" \ + -H "referer: https://dutchie.com/embedded-menu/AZ-Deeply-Rooted" \ + -H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \ + -H "apollographql-client-name: Marketplace (production)" \ + -H "content-type: application/json" \ + -d "$BODY" | head -c 3000 + +echo "" diff --git a/backend/test-llm-scraper-deeply-rooted.ts b/backend/test-llm-scraper-deeply-rooted.ts new file mode 100644 index 00000000..49e165f0 --- /dev/null +++ b/backend/test-llm-scraper-deeply-rooted.ts @@ -0,0 +1,78 @@ +import { chromium } from 'playwright'; +import { z } from 'zod'; +import { openai } from '../llm-scraper/node_modules/@ai-sdk/openai'; +import LLMScraper from '../llm-scraper/dist/index.js'; + +async function main() { + if (!process.env.OPENAI_API_KEY) { + throw new Error('Set OPENAI_API_KEY before running this test.'); + } + + const model = process.env.OPENAI_MODEL || 'gpt-4o-mini'; + const targetUrl = 'https://azdeeplyrooted.com/menu'; + + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); + + try { + console.log(`Opening ${targetUrl}...`); + await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); + + const iframeHandle = await page.waitForSelector( + 'iframe[srcdoc*="dutchie"], iframe[id^="iframe-"]', + { timeout: 30000 } + ); + const frame = await iframeHandle.contentFrame(); + if (!frame) throw new Error('Could not access Dutchie iframe content.'); + + await frame.waitForLoadState('domcontentloaded', { timeout: 30000 }); + await frame + .waitForSelector('[data-testid*="product"], [class*="product-card"]', { timeout: 60000 }) + .catch(() => undefined); + await page.waitForTimeout(2000); + + const schema = z.object({ + products: z + .array( + z.object({ + name: z.string(), + brand: z.string().optional(), + price: z.number().optional(), + category: z.string().optional(), + size: z.string().optional(), + url: z.string().url().optional(), + }) + ) + .min(1) + .max(40) + .describe('Products visible in the embedded Dutchie menu (limit to first page)'), + }); + + const scraper = new LLMScraper(openai(model)); + const { data } = await scraper.run(page, schema, { + format: 'custom', + formatFunction: async (currentPage) => { + const iframe = + (await currentPage.$('iframe[srcdoc*=\"dutchie\"]')) || + (await currentPage.$('iframe[id^=\"iframe-\"]')); + const innerFrame = await iframe?.contentFrame(); + return innerFrame ? innerFrame.content() : currentPage.content(); + }, + prompt: + 'Extract the cannabis menu items currently visible in the embedded Dutchie menu. ' + + 'Return name, brand, numeric price (no currency symbol), category/size if present, ' + + 'and product URL if available. Skip navigation or filter labels.', + mode: 'json', + }); + + console.log(`Scraped ${data.products.length} products from ${targetUrl}`); + console.log(JSON.stringify(data.products.slice(0, 10), null, 2)); + } finally { + await browser.close(); + } +} + +main().catch((error) => { + console.error('❌ LLM scraper test failed:', error); + process.exit(1); +}); diff --git a/backups/backup_20251201_084328.sql b/backups/backup_20251201_084328.sql new file mode 100644 index 00000000..e69de29b diff --git a/backups/backup_20251201_084336.sql b/backups/backup_20251201_084336.sql new file mode 100644 index 00000000..06f5603c --- /dev/null +++ b/backups/backup_20251201_084336.sql @@ -0,0 +1,7283 @@ +-- +-- PostgreSQL database dump +-- + +\restrict F5QdqipUuR85d4CLaQukm3wJGewHHNKubAwSHT2cYXx4to2xUDcizlCKa2H5aOs + +-- Dumped from database version 15.15 +-- Dumped by pg_dump version 15.15 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: set_requires_recrawl(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.set_requires_recrawl() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF NEW.field_name IN ('website', 'menu_url') THEN + NEW.requires_recrawl := TRUE; + END IF; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.set_requires_recrawl() OWNER TO dutchie; + +-- +-- Name: update_api_token_updated_at(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_api_token_updated_at() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_api_token_updated_at() OWNER TO dutchie; + +-- +-- Name: update_brand_scrape_jobs_updated_at(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_brand_scrape_jobs_updated_at() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_brand_scrape_jobs_updated_at() OWNER TO dutchie; + +-- +-- Name: update_sandbox_timestamp(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_sandbox_timestamp() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_sandbox_timestamp() OWNER TO dutchie; + +-- +-- Name: update_schedule_updated_at(); Type: FUNCTION; Schema: public; Owner: dutchie +-- + +CREATE FUNCTION public.update_schedule_updated_at() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION public.update_schedule_updated_at() OWNER TO dutchie; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: api_token_usage; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.api_token_usage ( + id integer NOT NULL, + token_id integer, + endpoint character varying(255) NOT NULL, + method character varying(10) NOT NULL, + status_code integer, + response_time_ms integer, + request_size integer, + response_size integer, + ip_address inet, + user_agent text, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.api_token_usage OWNER TO dutchie; + +-- +-- Name: api_token_usage_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.api_token_usage_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.api_token_usage_id_seq OWNER TO dutchie; + +-- +-- Name: api_token_usage_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.api_token_usage_id_seq OWNED BY public.api_token_usage.id; + + +-- +-- Name: api_tokens; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.api_tokens ( + id integer NOT NULL, + name character varying(255) NOT NULL, + token character varying(255) NOT NULL, + description text, + user_id integer, + active boolean DEFAULT true, + rate_limit integer DEFAULT 100, + allowed_endpoints text[], + expires_at timestamp without time zone, + last_used_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.api_tokens OWNER TO dutchie; + +-- +-- Name: api_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.api_tokens_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.api_tokens_id_seq OWNER TO dutchie; + +-- +-- Name: api_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.api_tokens_id_seq OWNED BY public.api_tokens.id; + + +-- +-- Name: azdhs_list; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.azdhs_list ( + id integer NOT NULL, + name character varying(255) NOT NULL, + company_name character varying(255), + slug character varying(255), + address character varying(500), + city character varying(100), + state character varying(2) DEFAULT 'AZ'::character varying, + zip character varying(10), + phone character varying(20), + email character varying(255), + status_line text, + azdhs_url text, + latitude numeric(10,8), + longitude numeric(11,8), + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + website text, + dba_name character varying(255), + google_rating numeric(2,1), + google_review_count integer +); + + +ALTER TABLE public.azdhs_list OWNER TO dutchie; + +-- +-- Name: azdhs_list_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.azdhs_list_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.azdhs_list_id_seq OWNER TO dutchie; + +-- +-- Name: azdhs_list_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.azdhs_list_id_seq OWNED BY public.azdhs_list.id; + + +-- +-- Name: batch_history; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.batch_history ( + id integer NOT NULL, + product_id integer, + thc_percentage numeric(5,2), + cbd_percentage numeric(5,2), + terpenes text[], + strain_type character varying(100), + recorded_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.batch_history OWNER TO dutchie; + +-- +-- Name: batch_history_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.batch_history_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.batch_history_id_seq OWNER TO dutchie; + +-- +-- Name: batch_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.batch_history_id_seq OWNED BY public.batch_history.id; + + +-- +-- Name: brand_history; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.brand_history ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + brand_name character varying(255) NOT NULL, + event_type character varying(20) NOT NULL, + event_at timestamp with time zone DEFAULT now(), + product_count integer, + metadata jsonb +); + + +ALTER TABLE public.brand_history OWNER TO dutchie; + +-- +-- Name: brand_history_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.brand_history_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.brand_history_id_seq OWNER TO dutchie; + +-- +-- Name: brand_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.brand_history_id_seq OWNED BY public.brand_history.id; + + +-- +-- Name: brand_scrape_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.brand_scrape_jobs ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + brand_slug text NOT NULL, + brand_name text NOT NULL, + status text DEFAULT 'pending'::text NOT NULL, + worker_id text, + started_at timestamp without time zone, + completed_at timestamp without time zone, + products_found integer DEFAULT 0, + products_saved integer DEFAULT 0, + error_message text, + retry_count integer DEFAULT 0, + created_at timestamp without time zone DEFAULT now(), + updated_at timestamp without time zone DEFAULT now() +); + + +ALTER TABLE public.brand_scrape_jobs OWNER TO dutchie; + +-- +-- Name: brand_scrape_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.brand_scrape_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.brand_scrape_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: brand_scrape_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.brand_scrape_jobs_id_seq OWNED BY public.brand_scrape_jobs.id; + + +-- +-- Name: brands; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.brands ( + id integer NOT NULL, + store_id integer NOT NULL, + name character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + first_seen_at timestamp with time zone DEFAULT now(), + last_seen_at timestamp with time zone DEFAULT now(), + dispensary_id integer +); + + +ALTER TABLE public.brands OWNER TO dutchie; + +-- +-- Name: brands_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.brands_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.brands_id_seq OWNER TO dutchie; + +-- +-- Name: brands_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.brands_id_seq OWNED BY public.brands.id; + + +-- +-- Name: campaign_products; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.campaign_products ( + id integer NOT NULL, + campaign_id integer, + product_id integer, + display_order integer DEFAULT 0, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.campaign_products OWNER TO dutchie; + +-- +-- Name: campaign_products_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.campaign_products_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.campaign_products_id_seq OWNER TO dutchie; + +-- +-- Name: campaign_products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.campaign_products_id_seq OWNED BY public.campaign_products.id; + + +-- +-- Name: campaigns; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.campaigns ( + id integer NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + description text, + display_style character varying(50) DEFAULT 'grid'::character varying, + active boolean DEFAULT true, + start_date timestamp without time zone, + end_date timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.campaigns OWNER TO dutchie; + +-- +-- Name: campaigns_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.campaigns_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.campaigns_id_seq OWNER TO dutchie; + +-- +-- Name: campaigns_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.campaigns_id_seq OWNED BY public.campaigns.id; + + +-- +-- Name: categories; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.categories ( + id integer NOT NULL, + store_id integer, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + dutchie_url text NOT NULL, + scrape_enabled boolean DEFAULT true, + last_scraped_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + parent_id integer, + display_order integer DEFAULT 0, + description text, + path character varying(500), + dispensary_id integer +); + + +ALTER TABLE public.categories OWNER TO dutchie; + +-- +-- Name: categories_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.categories_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.categories_id_seq OWNER TO dutchie; + +-- +-- Name: categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.categories_id_seq OWNED BY public.categories.id; + + +-- +-- Name: clicks; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.clicks ( + id integer NOT NULL, + product_id integer, + campaign_id integer, + ip_address character varying(45), + user_agent text, + referrer text, + clicked_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.clicks OWNER TO dutchie; + +-- +-- Name: clicks_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.clicks_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.clicks_id_seq OWNER TO dutchie; + +-- +-- Name: clicks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.clicks_id_seq OWNED BY public.clicks.id; + + +-- +-- Name: crawl_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawl_jobs ( + id integer NOT NULL, + store_id integer NOT NULL, + job_type character varying(50) DEFAULT 'full_crawl'::character varying NOT NULL, + trigger_type character varying(50) DEFAULT 'scheduled'::character varying NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + priority integer DEFAULT 0, + scheduled_at timestamp with time zone DEFAULT now() NOT NULL, + started_at timestamp with time zone, + completed_at timestamp with time zone, + products_found integer, + products_new integer, + products_updated integer, + error_message text, + worker_id character varying(100), + metadata jsonb DEFAULT '{}'::jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + orchestrator_run_id uuid, + detection_result jsonb, + in_stock_count integer, + out_of_stock_count integer, + limited_count integer, + unknown_count integer, + availability_changed_count integer, + CONSTRAINT chk_crawl_job_status CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'running'::character varying, 'completed'::character varying, 'failed'::character varying, 'cancelled'::character varying])::text[]))) +); + + +ALTER TABLE public.crawl_jobs OWNER TO dutchie; + +-- +-- Name: COLUMN crawl_jobs.orchestrator_run_id; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawl_jobs.orchestrator_run_id IS 'Groups related jobs from same orchestrator run'; + + +-- +-- Name: crawl_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawl_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawl_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: crawl_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawl_jobs_id_seq OWNED BY public.crawl_jobs.id; + + +-- +-- Name: crawler_schedule; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawler_schedule ( + id integer NOT NULL, + schedule_type character varying(50) NOT NULL, + enabled boolean DEFAULT true NOT NULL, + interval_hours integer, + run_time time without time zone, + description text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE public.crawler_schedule OWNER TO dutchie; + +-- +-- Name: dispensaries; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensaries ( + id integer NOT NULL, + azdhs_id integer, + name character varying(255) NOT NULL, + company_name character varying(255), + address character varying(500) NOT NULL, + city character varying(100) NOT NULL, + state character varying(2) NOT NULL, + zip character varying(10), + status_line character varying(100), + azdhs_url text, + latitude numeric(10,8), + longitude numeric(11,8), + dba_name character varying(255), + phone character varying(20), + email character varying(255), + website text, + google_rating numeric(2,1), + google_review_count integer, + menu_url text, + scraper_template character varying(100), + scraper_config jsonb, + last_menu_scrape timestamp without time zone, + menu_scrape_status character varying(50) DEFAULT 'pending'::character varying, + slug character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + menu_provider character varying(50), + menu_provider_confidence smallint DEFAULT 0, + crawler_mode character varying(20) DEFAULT 'production'::character varying, + crawler_status character varying(30) DEFAULT 'idle'::character varying, + last_menu_error_at timestamp with time zone, + last_error_message text, + provider_detection_data jsonb DEFAULT '{}'::jsonb, + product_provider character varying(50), + product_confidence smallint DEFAULT 0, + product_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_product_scan_at timestamp with time zone, + product_detection_data jsonb DEFAULT '{}'::jsonb, + specials_provider character varying(50), + specials_confidence smallint DEFAULT 0, + specials_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_specials_scan_at timestamp with time zone, + specials_detection_data jsonb DEFAULT '{}'::jsonb, + brand_provider character varying(50), + brand_confidence smallint DEFAULT 0, + brand_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_brand_scan_at timestamp with time zone, + brand_detection_data jsonb DEFAULT '{}'::jsonb, + metadata_provider character varying(50), + metadata_confidence smallint DEFAULT 0, + metadata_crawler_mode character varying(20) DEFAULT 'sandbox'::character varying, + last_metadata_scan_at timestamp with time zone, + metadata_detection_data jsonb DEFAULT '{}'::jsonb, + provider_type character varying(50) DEFAULT 'unknown'::character varying, + scrape_enabled boolean DEFAULT false, + last_crawl_at timestamp with time zone, + next_crawl_at timestamp with time zone, + crawl_status character varying(50) DEFAULT 'pending'::character varying, + crawl_error text, + consecutive_failures integer DEFAULT 0, + total_crawls integer DEFAULT 0, + successful_crawls integer DEFAULT 0, + CONSTRAINT chk_crawler_mode CHECK (((crawler_mode)::text = ANY ((ARRAY['production'::character varying, 'sandbox'::character varying])::text[]))), + CONSTRAINT chk_crawler_status CHECK (((crawler_status)::text = ANY ((ARRAY['idle'::character varying, 'queued_detection'::character varying, 'queued_crawl'::character varying, 'running'::character varying, 'ok'::character varying, 'error_needs_review'::character varying])::text[]))), + CONSTRAINT chk_provider_confidence CHECK (((menu_provider_confidence >= 0) AND (menu_provider_confidence <= 100))) +); + + +ALTER TABLE public.dispensaries OWNER TO dutchie; + +-- +-- Name: COLUMN dispensaries.menu_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.menu_provider IS 'Detected menu platform: dutchie, treez, jane, weedmaps, etc.'; + + +-- +-- Name: COLUMN dispensaries.menu_provider_confidence; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.menu_provider_confidence IS 'Confidence score 0-100 for provider detection'; + + +-- +-- Name: COLUMN dispensaries.crawler_mode; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.crawler_mode IS 'production = stable templates, sandbox = learning mode'; + + +-- +-- Name: COLUMN dispensaries.crawler_status; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.crawler_status IS 'Current state in crawl pipeline'; + + +-- +-- Name: COLUMN dispensaries.provider_detection_data; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.provider_detection_data IS 'JSON blob with detection signals and history'; + + +-- +-- Name: COLUMN dispensaries.product_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.product_provider IS 'Provider for product intelligence (dutchie, treez, jane, etc.)'; + + +-- +-- Name: COLUMN dispensaries.product_crawler_mode; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.product_crawler_mode IS 'production or sandbox mode for product crawling'; + + +-- +-- Name: COLUMN dispensaries.specials_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.specials_provider IS 'Provider for specials/deals intelligence'; + + +-- +-- Name: COLUMN dispensaries.brand_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.brand_provider IS 'Provider for brand intelligence'; + + +-- +-- Name: COLUMN dispensaries.metadata_provider; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.dispensaries.metadata_provider IS 'Provider for metadata/taxonomy intelligence'; + + +-- +-- Name: store_crawl_schedule; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.store_crawl_schedule ( + id integer NOT NULL, + store_id integer NOT NULL, + enabled boolean DEFAULT true NOT NULL, + interval_hours integer, + daily_special_enabled boolean DEFAULT true, + daily_special_time time without time zone, + priority integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + last_status character varying(50), + last_summary text, + last_run_at timestamp with time zone, + last_error text +); + + +ALTER TABLE public.store_crawl_schedule OWNER TO dutchie; + +-- +-- Name: COLUMN store_crawl_schedule.last_status; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.store_crawl_schedule.last_status IS 'Orchestrator result status: success, error, sandbox_only, detection_only'; + + +-- +-- Name: COLUMN store_crawl_schedule.last_summary; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.store_crawl_schedule.last_summary IS 'Human-readable summary of last orchestrator run'; + + +-- +-- Name: COLUMN store_crawl_schedule.last_run_at; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.store_crawl_schedule.last_run_at IS 'When orchestrator last ran for this store'; + + +-- +-- Name: stores; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.stores ( + id integer NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + dutchie_url text NOT NULL, + active boolean DEFAULT true, + scrape_enabled boolean DEFAULT true, + last_scraped_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + logo_url text, + timezone character varying(50) DEFAULT 'America/Phoenix'::character varying, + dispensary_id integer +); + + +ALTER TABLE public.stores OWNER TO dutchie; + +-- +-- Name: COLUMN stores.dispensary_id; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.stores.dispensary_id IS 'FK to dispensaries table (master AZDHS directory)'; + + +-- +-- Name: crawl_schedule_status; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.crawl_schedule_status AS + SELECT s.id AS store_id, + s.name AS store_name, + s.slug AS store_slug, + s.timezone, + s.active, + s.scrape_enabled, + s.last_scraped_at, + s.dispensary_id, + d.name AS dispensary_name, + d.company_name AS dispensary_company, + d.city AS dispensary_city, + d.state AS dispensary_state, + d.slug AS dispensary_slug, + d.address AS dispensary_address, + d.menu_url AS dispensary_menu_url, + d.product_provider, + d.product_confidence, + d.product_crawler_mode, + COALESCE(scs.enabled, true) AS schedule_enabled, + COALESCE(scs.interval_hours, cs_global.interval_hours, 4) AS interval_hours, + COALESCE(scs.daily_special_enabled, true) AS daily_special_enabled, + COALESCE(scs.daily_special_time, '00:01:00'::time without time zone) AS daily_special_time, + COALESCE(scs.priority, 0) AS priority, + scs.last_status, + scs.last_summary, + scs.last_run_at AS schedule_last_run, + scs.last_error, + CASE + WHEN (s.last_scraped_at IS NULL) THEN now() + ELSE ((s.last_scraped_at + ((COALESCE(scs.interval_hours, cs_global.interval_hours, 4) || ' hours'::text))::interval))::timestamp with time zone + END AS next_scheduled_run, + cj.id AS latest_job_id, + cj.status AS latest_job_status, + cj.job_type AS latest_job_type, + cj.trigger_type AS latest_job_trigger, + cj.started_at AS latest_job_started, + cj.completed_at AS latest_job_completed, + cj.products_found AS latest_products_found, + cj.products_new AS latest_products_new, + cj.products_updated AS latest_products_updated, + cj.error_message AS latest_job_error + FROM ((((public.stores s + LEFT JOIN public.dispensaries d ON ((d.id = s.dispensary_id))) + LEFT JOIN public.store_crawl_schedule scs ON ((scs.store_id = s.id))) + LEFT JOIN public.crawler_schedule cs_global ON (((cs_global.schedule_type)::text = 'global_interval'::text))) + LEFT JOIN LATERAL ( SELECT cj2.id, + cj2.store_id, + cj2.job_type, + cj2.trigger_type, + cj2.status, + cj2.priority, + cj2.scheduled_at, + cj2.started_at, + cj2.completed_at, + cj2.products_found, + cj2.products_new, + cj2.products_updated, + cj2.error_message, + cj2.worker_id, + cj2.metadata, + cj2.created_at, + cj2.updated_at, + cj2.orchestrator_run_id, + cj2.detection_result + FROM public.crawl_jobs cj2 + WHERE (cj2.store_id = s.id) + ORDER BY cj2.created_at DESC + LIMIT 1) cj ON (true)) + WHERE (s.active = true); + + +ALTER TABLE public.crawl_schedule_status OWNER TO dutchie; + +-- +-- Name: crawler_sandboxes; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawler_sandboxes ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + suspected_menu_provider character varying(50), + mode character varying(30) DEFAULT 'detection'::character varying NOT NULL, + raw_html_location text, + screenshot_location text, + analysis_json jsonb DEFAULT '{}'::jsonb, + urls_tested jsonb DEFAULT '[]'::jsonb, + menu_entry_points jsonb DEFAULT '[]'::jsonb, + detection_signals jsonb DEFAULT '{}'::jsonb, + status character varying(30) DEFAULT 'pending'::character varying NOT NULL, + confidence_score smallint DEFAULT 0, + failure_reason text, + human_review_notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + analyzed_at timestamp with time zone, + reviewed_at timestamp with time zone, + category character varying(30) DEFAULT 'product'::character varying, + template_name character varying(100), + quality_score smallint DEFAULT 0, + products_extracted integer DEFAULT 0, + fields_missing integer DEFAULT 0, + error_count integer DEFAULT 0, + CONSTRAINT chk_sandbox_mode CHECK (((mode)::text = ANY ((ARRAY['detection'::character varying, 'template_learning'::character varying, 'validation'::character varying])::text[]))), + CONSTRAINT chk_sandbox_status CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'analyzing'::character varying, 'template_ready'::character varying, 'needs_human_review'::character varying, 'moved_to_production'::character varying, 'failed'::character varying])::text[]))) +); + + +ALTER TABLE public.crawler_sandboxes OWNER TO dutchie; + +-- +-- Name: TABLE crawler_sandboxes; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON TABLE public.crawler_sandboxes IS 'Learning/testing environment for unknown menu providers'; + + +-- +-- Name: COLUMN crawler_sandboxes.category; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawler_sandboxes.category IS 'Intelligence category: product, specials, brand, metadata'; + + +-- +-- Name: COLUMN crawler_sandboxes.quality_score; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawler_sandboxes.quality_score IS 'Quality score 0-100 for sandbox run results'; + + +-- +-- Name: crawler_sandboxes_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawler_sandboxes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawler_sandboxes_id_seq OWNER TO dutchie; + +-- +-- Name: crawler_sandboxes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawler_sandboxes_id_seq OWNED BY public.crawler_sandboxes.id; + + +-- +-- Name: crawler_schedule_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawler_schedule_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawler_schedule_id_seq OWNER TO dutchie; + +-- +-- Name: crawler_schedule_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawler_schedule_id_seq OWNED BY public.crawler_schedule.id; + + +-- +-- Name: crawler_templates; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.crawler_templates ( + id integer NOT NULL, + provider character varying(50) NOT NULL, + name character varying(100) NOT NULL, + version integer DEFAULT 1, + is_active boolean DEFAULT true NOT NULL, + is_default_for_provider boolean DEFAULT false, + selector_config jsonb DEFAULT '{}'::jsonb NOT NULL, + navigation_config jsonb DEFAULT '{}'::jsonb, + transform_config jsonb DEFAULT '{}'::jsonb, + validation_rules jsonb DEFAULT '{}'::jsonb, + test_urls jsonb DEFAULT '[]'::jsonb, + expected_structure jsonb DEFAULT '{}'::jsonb, + dispensaries_using integer DEFAULT 0, + success_rate numeric(5,2) DEFAULT 0, + last_successful_crawl timestamp with time zone, + last_failed_crawl timestamp with time zone, + notes text, + created_by character varying(100), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + environment character varying(20) DEFAULT 'production'::character varying +); + + +ALTER TABLE public.crawler_templates OWNER TO dutchie; + +-- +-- Name: TABLE crawler_templates; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON TABLE public.crawler_templates IS 'Reusable scraping configurations per menu provider'; + + +-- +-- Name: COLUMN crawler_templates.environment; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.crawler_templates.environment IS 'Template environment: production or sandbox'; + + +-- +-- Name: crawler_templates_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.crawler_templates_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.crawler_templates_id_seq OWNER TO dutchie; + +-- +-- Name: crawler_templates_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.crawler_templates_id_seq OWNED BY public.crawler_templates.id; + + +-- +-- Name: dispensaries_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensaries_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensaries_id_seq OWNER TO dutchie; + +-- +-- Name: dispensaries_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensaries_id_seq OWNED BY public.dispensaries.id; + + +-- +-- Name: products; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.products ( + id integer NOT NULL, + store_id integer, + category_id integer, + dutchie_product_id character varying(255), + name character varying(500) NOT NULL, + slug character varying(500) NOT NULL, + description text, + price numeric(10,2), + original_price numeric(10,2), + strain_type character varying(100), + thc_percentage numeric(5,2), + cbd_percentage numeric(5,2), + brand character varying(255), + weight character varying(100), + image_url text, + local_image_path text, + dutchie_url text NOT NULL, + in_stock boolean DEFAULT true, + is_special boolean DEFAULT false, + metadata jsonb, + first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + dispensary_id integer, + variant character varying(255), + special_ends_at timestamp without time zone, + special_text text, + special_type character varying(100), + terpenes text[], + effects text[], + flavors text[], + regular_price numeric(10,2), + sale_price numeric(10,2), + stock_quantity integer, + stock_status text, + discount_type character varying(50), + discount_value character varying(100), + availability_status character varying(20) DEFAULT 'unknown'::character varying, + availability_raw jsonb, + last_seen_in_stock_at timestamp with time zone, + last_seen_out_of_stock_at timestamp with time zone +); + + +ALTER TABLE public.products OWNER TO dutchie; + +-- +-- Name: COLUMN products.availability_status; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.availability_status IS 'Normalized status: in_stock, out_of_stock, limited, unknown'; + + +-- +-- Name: COLUMN products.availability_raw; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.availability_raw IS 'Raw availability payload from provider for debugging'; + + +-- +-- Name: COLUMN products.last_seen_in_stock_at; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.last_seen_in_stock_at IS 'Last time product was seen in stock'; + + +-- +-- Name: COLUMN products.last_seen_out_of_stock_at; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON COLUMN public.products.last_seen_out_of_stock_at IS 'Last time product was seen out of stock'; + + +-- +-- Name: dispensary_brand_stats; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.dispensary_brand_stats AS + SELECT d.id AS dispensary_id, + COALESCE(d.dba_name, d.name) AS dispensary_name, + count(DISTINCT p.brand) FILTER (WHERE (p.last_seen_at >= (now() - '7 days'::interval))) AS current_brands, + count(DISTINCT p.brand) AS total_brands_ever, + ( SELECT count(DISTINCT bh.brand_name) AS count + FROM public.brand_history bh + WHERE ((bh.dispensary_id = d.id) AND ((bh.event_type)::text = 'added'::text) AND (bh.event_at >= (now() - '7 days'::interval)))) AS new_brands_7d, + ( SELECT count(DISTINCT bh.brand_name) AS count + FROM public.brand_history bh + WHERE ((bh.dispensary_id = d.id) AND ((bh.event_type)::text = 'dropped'::text) AND (bh.event_at >= (now() - '7 days'::interval)))) AS dropped_brands_7d + FROM (public.dispensaries d + LEFT JOIN public.products p ON ((p.dispensary_id = d.id))) + GROUP BY d.id, d.dba_name, d.name; + + +ALTER TABLE public.dispensary_brand_stats OWNER TO dutchie; + +-- +-- Name: dispensary_changes; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensary_changes ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + field_name character varying(100) NOT NULL, + old_value text, + new_value text, + source character varying(50) NOT NULL, + confidence_score character varying(20), + change_notes text, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + requires_recrawl boolean DEFAULT false, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + reviewed_at timestamp without time zone, + reviewed_by integer, + rejection_reason text, + CONSTRAINT dispensary_changes_status_check CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'approved'::character varying, 'rejected'::character varying])::text[]))) +); + + +ALTER TABLE public.dispensary_changes OWNER TO dutchie; + +-- +-- Name: dispensary_changes_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensary_changes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensary_changes_id_seq OWNER TO dutchie; + +-- +-- Name: dispensary_changes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensary_changes_id_seq OWNED BY public.dispensary_changes.id; + + +-- +-- Name: dispensary_crawl_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensary_crawl_jobs ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + schedule_id integer, + job_type character varying(50) DEFAULT 'orchestrator'::character varying NOT NULL, + trigger_type character varying(50) DEFAULT 'scheduled'::character varying NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + priority integer DEFAULT 0, + scheduled_at timestamp with time zone DEFAULT now(), + started_at timestamp with time zone, + completed_at timestamp with time zone, + duration_ms integer, + detection_ran boolean DEFAULT false, + crawl_ran boolean DEFAULT false, + crawl_type character varying(20), + products_found integer, + products_new integer, + products_updated integer, + detected_provider character varying(50), + detected_confidence smallint, + detected_mode character varying(20), + error_message text, + worker_id character varying(100), + run_id uuid, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + in_stock_count integer, + out_of_stock_count integer, + limited_count integer, + unknown_count integer, + availability_changed_count integer +); + + +ALTER TABLE public.dispensary_crawl_jobs OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensary_crawl_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensary_crawl_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensary_crawl_jobs_id_seq OWNED BY public.dispensary_crawl_jobs.id; + + +-- +-- Name: dispensary_crawl_schedule; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.dispensary_crawl_schedule ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + is_active boolean DEFAULT true NOT NULL, + interval_minutes integer DEFAULT 240 NOT NULL, + priority integer DEFAULT 0 NOT NULL, + last_run_at timestamp with time zone, + next_run_at timestamp with time zone, + last_status character varying(50), + last_summary text, + last_error text, + last_duration_ms integer, + consecutive_failures integer DEFAULT 0, + total_runs integer DEFAULT 0, + successful_runs integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + + +ALTER TABLE public.dispensary_crawl_schedule OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_schedule_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.dispensary_crawl_schedule_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.dispensary_crawl_schedule_id_seq OWNER TO dutchie; + +-- +-- Name: dispensary_crawl_schedule_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.dispensary_crawl_schedule_id_seq OWNED BY public.dispensary_crawl_schedule.id; + + +-- +-- Name: dispensary_crawl_status; Type: VIEW; Schema: public; Owner: dutchie +-- + +CREATE VIEW public.dispensary_crawl_status AS + SELECT d.id AS dispensary_id, + COALESCE(d.dba_name, d.name) AS dispensary_name, + d.city, + d.state, + d.slug, + d.website, + d.menu_url, + COALESCE(d.product_provider, d.provider_type) AS product_provider, + d.provider_type, + d.product_confidence, + d.product_crawler_mode, + d.last_product_scan_at, + COALESCE(dcs.is_active, d.scrape_enabled, false) AS schedule_active, + COALESCE(dcs.interval_minutes, 240) AS interval_minutes, + COALESCE(dcs.priority, 0) AS priority, + COALESCE(dcs.last_run_at, d.last_crawl_at) AS last_run_at, + COALESCE(dcs.next_run_at, d.next_crawl_at) AS next_run_at, + COALESCE(dcs.last_status, d.crawl_status) AS last_status, + dcs.last_summary, + COALESCE(dcs.last_error, d.crawl_error) AS last_error, + COALESCE(dcs.consecutive_failures, d.consecutive_failures, 0) AS consecutive_failures, + COALESCE(dcs.total_runs, d.total_crawls, 0) AS total_runs, + COALESCE(dcs.successful_runs, d.successful_crawls, 0) AS successful_runs, + dcj.id AS latest_job_id, + dcj.job_type AS latest_job_type, + dcj.status AS latest_job_status, + dcj.started_at AS latest_job_started, + dcj.products_found AS latest_products_found + FROM ((public.dispensaries d + LEFT JOIN public.dispensary_crawl_schedule dcs ON ((dcs.dispensary_id = d.id))) + LEFT JOIN LATERAL ( SELECT dispensary_crawl_jobs.id, + dispensary_crawl_jobs.dispensary_id, + dispensary_crawl_jobs.schedule_id, + dispensary_crawl_jobs.job_type, + dispensary_crawl_jobs.trigger_type, + dispensary_crawl_jobs.status, + dispensary_crawl_jobs.priority, + dispensary_crawl_jobs.scheduled_at, + dispensary_crawl_jobs.started_at, + dispensary_crawl_jobs.completed_at, + dispensary_crawl_jobs.duration_ms, + dispensary_crawl_jobs.detection_ran, + dispensary_crawl_jobs.crawl_ran, + dispensary_crawl_jobs.crawl_type, + dispensary_crawl_jobs.products_found, + dispensary_crawl_jobs.products_new, + dispensary_crawl_jobs.products_updated, + dispensary_crawl_jobs.detected_provider, + dispensary_crawl_jobs.detected_confidence, + dispensary_crawl_jobs.detected_mode, + dispensary_crawl_jobs.error_message, + dispensary_crawl_jobs.worker_id, + dispensary_crawl_jobs.run_id, + dispensary_crawl_jobs.created_at, + dispensary_crawl_jobs.updated_at, + dispensary_crawl_jobs.in_stock_count, + dispensary_crawl_jobs.out_of_stock_count, + dispensary_crawl_jobs.limited_count, + dispensary_crawl_jobs.unknown_count, + dispensary_crawl_jobs.availability_changed_count + FROM public.dispensary_crawl_jobs + WHERE (dispensary_crawl_jobs.dispensary_id = d.id) + ORDER BY dispensary_crawl_jobs.created_at DESC + LIMIT 1) dcj ON (true)) + ORDER BY + CASE + WHEN (d.scrape_enabled = true) THEN 0 + ELSE 1 + END, COALESCE(dcs.priority, 0) DESC, COALESCE(d.dba_name, d.name); + + +ALTER TABLE public.dispensary_crawl_status OWNER TO dutchie; + +-- +-- Name: failed_proxies; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.failed_proxies ( + id integer NOT NULL, + host character varying(255) NOT NULL, + port integer NOT NULL, + protocol character varying(10) NOT NULL, + username character varying(255), + password character varying(255), + failure_count integer NOT NULL, + last_error text, + failed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + city character varying(100), + state character varying(100), + country character varying(100), + country_code character varying(2), + location_updated_at timestamp without time zone +); + + +ALTER TABLE public.failed_proxies OWNER TO dutchie; + +-- +-- Name: failed_proxies_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.failed_proxies_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.failed_proxies_id_seq OWNER TO dutchie; + +-- +-- Name: failed_proxies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.failed_proxies_id_seq OWNED BY public.failed_proxies.id; + + +-- +-- Name: jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.jobs ( + id integer NOT NULL, + type character varying(50) NOT NULL, + status character varying(50) DEFAULT 'pending'::character varying, + store_id integer, + progress integer DEFAULT 0, + total_items integer, + processed_items integer DEFAULT 0, + error text, + started_at timestamp without time zone, + completed_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.jobs OWNER TO dutchie; + +-- +-- Name: jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.jobs_id_seq OWNER TO dutchie; + +-- +-- Name: jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.jobs_id_seq OWNED BY public.jobs.id; + + +-- +-- Name: price_history; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.price_history ( + id integer NOT NULL, + product_id integer, + regular_price numeric(10,2), + sale_price numeric(10,2), + recorded_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.price_history OWNER TO dutchie; + +-- +-- Name: price_history_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.price_history_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.price_history_id_seq OWNER TO dutchie; + +-- +-- Name: price_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.price_history_id_seq OWNED BY public.price_history.id; + + +-- +-- Name: product_categories; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.product_categories ( + id integer NOT NULL, + product_id integer, + category_slug character varying(255) NOT NULL, + first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.product_categories OWNER TO dutchie; + +-- +-- Name: product_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.product_categories_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.product_categories_id_seq OWNER TO dutchie; + +-- +-- Name: product_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.product_categories_id_seq OWNED BY public.product_categories.id; + + +-- +-- Name: products_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.products_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.products_id_seq OWNER TO dutchie; + +-- +-- Name: products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.products_id_seq OWNED BY public.products.id; + + +-- +-- Name: proxies; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.proxies ( + id integer NOT NULL, + host character varying(255) NOT NULL, + port integer NOT NULL, + protocol character varying(10) NOT NULL, + username character varying(255), + password character varying(255), + active boolean DEFAULT true, + is_anonymous boolean DEFAULT false, + last_tested_at timestamp without time zone, + test_result character varying(50), + response_time_ms integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + failure_count integer DEFAULT 0, + city character varying(100), + state character varying(100), + country character varying(100), + country_code character varying(2), + location_updated_at timestamp without time zone +); + + +ALTER TABLE public.proxies OWNER TO dutchie; + +-- +-- Name: proxies_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.proxies_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.proxies_id_seq OWNER TO dutchie; + +-- +-- Name: proxies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.proxies_id_seq OWNED BY public.proxies.id; + + +-- +-- Name: proxy_test_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.proxy_test_jobs ( + id integer NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + total_proxies integer DEFAULT 0 NOT NULL, + tested_proxies integer DEFAULT 0 NOT NULL, + passed_proxies integer DEFAULT 0 NOT NULL, + failed_proxies integer DEFAULT 0 NOT NULL, + started_at timestamp without time zone, + completed_at timestamp without time zone, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.proxy_test_jobs OWNER TO dutchie; + +-- +-- Name: proxy_test_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.proxy_test_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.proxy_test_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: proxy_test_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.proxy_test_jobs_id_seq OWNED BY public.proxy_test_jobs.id; + + +-- +-- Name: sandbox_crawl_jobs; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.sandbox_crawl_jobs ( + id integer NOT NULL, + dispensary_id integer NOT NULL, + sandbox_id integer, + job_type character varying(30) DEFAULT 'detection'::character varying NOT NULL, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + priority integer DEFAULT 0, + scheduled_at timestamp with time zone DEFAULT now() NOT NULL, + started_at timestamp with time zone, + completed_at timestamp with time zone, + worker_id character varying(100), + result_summary jsonb DEFAULT '{}'::jsonb, + error_message text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + category character varying(30) DEFAULT 'product'::character varying, + template_name character varying(100), + CONSTRAINT chk_sandbox_job_status CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'running'::character varying, 'completed'::character varying, 'failed'::character varying, 'cancelled'::character varying])::text[]))), + CONSTRAINT chk_sandbox_job_type CHECK (((job_type)::text = ANY ((ARRAY['detection'::character varying, 'template_test'::character varying, 'deep_crawl'::character varying])::text[]))) +); + + +ALTER TABLE public.sandbox_crawl_jobs OWNER TO dutchie; + +-- +-- Name: TABLE sandbox_crawl_jobs; Type: COMMENT; Schema: public; Owner: dutchie +-- + +COMMENT ON TABLE public.sandbox_crawl_jobs IS 'Job queue for sandbox crawl operations (separate from production)'; + + +-- +-- Name: sandbox_crawl_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.sandbox_crawl_jobs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.sandbox_crawl_jobs_id_seq OWNER TO dutchie; + +-- +-- Name: sandbox_crawl_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.sandbox_crawl_jobs_id_seq OWNED BY public.sandbox_crawl_jobs.id; + + +-- +-- Name: settings; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.settings ( + key character varying(255) NOT NULL, + value text NOT NULL, + description text, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.settings OWNER TO dutchie; + +-- +-- Name: specials; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.specials ( + id integer NOT NULL, + store_id integer NOT NULL, + product_id integer, + name character varying(255) NOT NULL, + description text, + discount_amount numeric(10,2), + discount_percentage numeric(5,2), + special_price numeric(10,2), + original_price numeric(10,2), + valid_date date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.specials OWNER TO dutchie; + +-- +-- Name: specials_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.specials_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.specials_id_seq OWNER TO dutchie; + +-- +-- Name: specials_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.specials_id_seq OWNED BY public.specials.id; + + +-- +-- Name: store_crawl_schedule_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.store_crawl_schedule_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.store_crawl_schedule_id_seq OWNER TO dutchie; + +-- +-- Name: store_crawl_schedule_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.store_crawl_schedule_id_seq OWNED BY public.store_crawl_schedule.id; + + +-- +-- Name: stores_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.stores_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.stores_id_seq OWNER TO dutchie; + +-- +-- Name: stores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.stores_id_seq OWNED BY public.stores.id; + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + email character varying(255) NOT NULL, + password_hash character varying(255) NOT NULL, + role character varying(50) DEFAULT 'admin'::character varying, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.users OWNER TO dutchie; + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.users_id_seq OWNER TO dutchie; + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: wp_dutchie_api_permissions; Type: TABLE; Schema: public; Owner: dutchie +-- + +CREATE TABLE public.wp_dutchie_api_permissions ( + id integer NOT NULL, + user_name character varying(255) NOT NULL, + api_key character varying(255) NOT NULL, + allowed_ips text, + allowed_domains text, + is_active smallint DEFAULT 1, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + last_used_at timestamp without time zone +); + + +ALTER TABLE public.wp_dutchie_api_permissions OWNER TO dutchie; + +-- +-- Name: wp_dutchie_api_permissions_id_seq; Type: SEQUENCE; Schema: public; Owner: dutchie +-- + +CREATE SEQUENCE public.wp_dutchie_api_permissions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.wp_dutchie_api_permissions_id_seq OWNER TO dutchie; + +-- +-- Name: wp_dutchie_api_permissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dutchie +-- + +ALTER SEQUENCE public.wp_dutchie_api_permissions_id_seq OWNED BY public.wp_dutchie_api_permissions.id; + + +-- +-- Name: api_token_usage id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_token_usage ALTER COLUMN id SET DEFAULT nextval('public.api_token_usage_id_seq'::regclass); + + +-- +-- Name: api_tokens id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens ALTER COLUMN id SET DEFAULT nextval('public.api_tokens_id_seq'::regclass); + + +-- +-- Name: azdhs_list id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.azdhs_list ALTER COLUMN id SET DEFAULT nextval('public.azdhs_list_id_seq'::regclass); + + +-- +-- Name: batch_history id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.batch_history ALTER COLUMN id SET DEFAULT nextval('public.batch_history_id_seq'::regclass); + + +-- +-- Name: brand_history id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_history ALTER COLUMN id SET DEFAULT nextval('public.brand_history_id_seq'::regclass); + + +-- +-- Name: brand_scrape_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs ALTER COLUMN id SET DEFAULT nextval('public.brand_scrape_jobs_id_seq'::regclass); + + +-- +-- Name: brands id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands ALTER COLUMN id SET DEFAULT nextval('public.brands_id_seq'::regclass); + + +-- +-- Name: campaign_products id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products ALTER COLUMN id SET DEFAULT nextval('public.campaign_products_id_seq'::regclass); + + +-- +-- Name: campaigns id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaigns ALTER COLUMN id SET DEFAULT nextval('public.campaigns_id_seq'::regclass); + + +-- +-- Name: categories id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories ALTER COLUMN id SET DEFAULT nextval('public.categories_id_seq'::regclass); + + +-- +-- Name: clicks id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks ALTER COLUMN id SET DEFAULT nextval('public.clicks_id_seq'::regclass); + + +-- +-- Name: crawl_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawl_jobs ALTER COLUMN id SET DEFAULT nextval('public.crawl_jobs_id_seq'::regclass); + + +-- +-- Name: crawler_sandboxes id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_sandboxes ALTER COLUMN id SET DEFAULT nextval('public.crawler_sandboxes_id_seq'::regclass); + + +-- +-- Name: crawler_schedule id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_schedule ALTER COLUMN id SET DEFAULT nextval('public.crawler_schedule_id_seq'::regclass); + + +-- +-- Name: crawler_templates id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_templates ALTER COLUMN id SET DEFAULT nextval('public.crawler_templates_id_seq'::regclass); + + +-- +-- Name: dispensaries id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries ALTER COLUMN id SET DEFAULT nextval('public.dispensaries_id_seq'::regclass); + + +-- +-- Name: dispensary_changes id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes ALTER COLUMN id SET DEFAULT nextval('public.dispensary_changes_id_seq'::regclass); + + +-- +-- Name: dispensary_crawl_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs ALTER COLUMN id SET DEFAULT nextval('public.dispensary_crawl_jobs_id_seq'::regclass); + + +-- +-- Name: dispensary_crawl_schedule id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule ALTER COLUMN id SET DEFAULT nextval('public.dispensary_crawl_schedule_id_seq'::regclass); + + +-- +-- Name: failed_proxies id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.failed_proxies ALTER COLUMN id SET DEFAULT nextval('public.failed_proxies_id_seq'::regclass); + + +-- +-- Name: jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.jobs ALTER COLUMN id SET DEFAULT nextval('public.jobs_id_seq'::regclass); + + +-- +-- Name: price_history id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.price_history ALTER COLUMN id SET DEFAULT nextval('public.price_history_id_seq'::regclass); + + +-- +-- Name: product_categories id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories ALTER COLUMN id SET DEFAULT nextval('public.product_categories_id_seq'::regclass); + + +-- +-- Name: products id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products ALTER COLUMN id SET DEFAULT nextval('public.products_id_seq'::regclass); + + +-- +-- Name: proxies id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxies ALTER COLUMN id SET DEFAULT nextval('public.proxies_id_seq'::regclass); + + +-- +-- Name: proxy_test_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxy_test_jobs ALTER COLUMN id SET DEFAULT nextval('public.proxy_test_jobs_id_seq'::regclass); + + +-- +-- Name: sandbox_crawl_jobs id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs ALTER COLUMN id SET DEFAULT nextval('public.sandbox_crawl_jobs_id_seq'::regclass); + + +-- +-- Name: specials id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials ALTER COLUMN id SET DEFAULT nextval('public.specials_id_seq'::regclass); + + +-- +-- Name: store_crawl_schedule id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule ALTER COLUMN id SET DEFAULT nextval('public.store_crawl_schedule_id_seq'::regclass); + + +-- +-- Name: stores id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores ALTER COLUMN id SET DEFAULT nextval('public.stores_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: wp_dutchie_api_permissions id; Type: DEFAULT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.wp_dutchie_api_permissions ALTER COLUMN id SET DEFAULT nextval('public.wp_dutchie_api_permissions_id_seq'::regclass); + + +-- +-- Data for Name: api_token_usage; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.api_token_usage (id, token_id, endpoint, method, status_code, response_time_ms, request_size, response_size, ip_address, user_agent, created_at) FROM stdin; +\. + + +-- +-- Data for Name: api_tokens; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.api_tokens (id, name, token, description, user_id, active, rate_limit, allowed_endpoints, expires_at, last_used_at, created_at, updated_at) FROM stdin; +\. + + +-- +-- Data for Name: azdhs_list; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.azdhs_list (id, name, company_name, slug, address, city, state, zip, phone, email, status_line, azdhs_url, latitude, longitude, created_at, updated_at, website, dba_name, google_rating, google_review_count) FROM stdin; +9 AGI Management LLC AGI Management LLC agi-management-llc 1035 W Main St Quartzsite AZ 85346 4802342343 \N Operating · Marijuana Establishment · 480-234-2343 https://azcarecheck.azdhs.gov/s/?name=AGI%20Management%20LLC \N \N 2025-11-17 07:29:34.204841 2025-11-17 07:29:34.204841 \N \N \N \N +10 All Greens Inc All Greens Inc all-greens-inc 10032 W Bell Rd Ste 100 Sun City AZ 85351 6232140801 \N Operating · Marijuana Facility · (623) 214-0801 https://azcarecheck.azdhs.gov/s/?name=All%20Greens%20Inc \N \N 2025-11-17 07:29:34.206514 2025-11-17 07:29:34.206514 \N \N \N \N +14 Arizona Cannabis Society Inc Arizona Cannabis Society Inc arizona-cannabis-society-inc 8376 N El Mirage Rd Bldg 2 Ste 2 El Mirage AZ 85335 8882492927 \N Operating · Marijuana Facility · (888) 249-2927 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Cannabis%20Society%20Inc \N \N 2025-11-17 07:29:34.212386 2025-11-17 07:29:34.212386 \N \N \N \N +16 Nature's Medicines Arizona Natural Pain Solutions Inc. nature-s-medicines 701 East Dunlap Avenue, Suite 9 Phoenix AZ 85020 6029033769 \N Operating · Marijuana Facility · (602) 903-3769 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N 2025-11-17 07:29:34.214929 2025-11-17 07:29:34.214929 \N \N \N \N +17 Arizona Natures Wellness Arizona Natures Wellness arizona-natures-wellness 1610 West State Route 89a Sedona AZ 86336 9282023512 \N Operating · Marijuana Facility · 928-202-3512 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Natures%20Wellness \N \N 2025-11-17 07:29:34.216267 2025-11-17 07:29:34.216267 \N \N \N \N +20 Arizona Wellness Center Safford Arizona Wellness Center Safford LLC arizona-wellness-center-safford 1362 W Thatcher Blvd Safford AZ 85546 6235216899 \N Operating · Marijuana Dispensary · 623-521-6899 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Wellness%20Center%20Safford \N \N 2025-11-17 07:29:34.220168 2025-11-17 07:29:34.220168 \N \N \N \N +23 AZ Flower Power LLC AZ Flower Power LLC az-flower-power-llc 11343 East Apache Trail Apache Junction AZ 85120 9173753900 \N Operating · Marijuana Establishment · (917) 375-3900 https://azcarecheck.azdhs.gov/s/?name=AZ%20Flower%20Power%20LLC \N \N 2025-11-17 07:29:34.224267 2025-11-17 07:29:34.224267 \N \N \N \N +24 AZC1 AZCL1 azc1 4695 N Oracle Rd Ste 117 Tucson AZ 85705 5202933315 \N Operating · Marijuana Facility · 520-293-3315 https://azcarecheck.azdhs.gov/s/?name=AZC1 \N \N 2025-11-17 07:29:34.225631 2025-11-17 07:29:34.225631 \N \N \N \N +26 Greenleef Medical Bailey Management LLC greenleef-medical 253 Chase Creek St Clifton AZ 85533 4806523622 \N Operating · Marijuana Facility · 480-652-3622 https://azcarecheck.azdhs.gov/s/?name=Greenleef%20Medical \N \N 2025-11-17 07:29:34.228419 2025-11-17 07:29:34.228419 \N \N \N \N +3 D2 Dispensary 46 Wellness Llc d2-dispensary 7139 E 22nd St Tucson AZ 85710 5202143232 \N Operating · Marijuana Facility · (520) 214-3232 https://azcarecheck.azdhs.gov/s/?name=D2%20Dispensary \N \N 2025-11-17 07:29:34.194058 2025-11-17 14:46:35.922403 http://d2dispensary.com/ D2 Dispensary - Cannabis Destination + Drive Thru 4.8 5706 +5 Ponderosa Dispensary ABACA Ponderosa, LLC ponderosa-dispensary 21035 N Cave Creek Rd Ste 3 & 4 Phoenix AZ 85024 4802131402 \N Operating · Marijuana Facility · (480) 213-1402 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N 2025-11-17 07:29:34.197818 2025-11-17 14:46:35.924465 https://www.pondyaz.com/locations Ponderosa Dispensary Phoenix 4.7 2561 +8 Trulieve of Phoenix Alhambra Ad, Llc trulieve-of-phoenix-alhambra 2630 W Indian School Rd Phoenix AZ 85017 7703300831 \N Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Phoenix%20Alhambra \N \N 2025-11-17 07:29:34.203168 2025-11-17 14:46:35.927615 https://www.trulieve.com/dispensaries/arizona/phoenix-alhambra?utm_source=gmb&utm_medium=organic&utm_campaign=alhambra Trulieve Phoenix Dispensary Alhambra 4.5 2917 +13 Apollo Labs Apollo Labs apollo-labs 17301 North Perimeter Drive, suite 100 Scottsdale AZ 85255 9173401566 \N Operating · Marijuana Laboratory · (917) 340-1566 https://azcarecheck.azdhs.gov/s/?name=Apollo%20Labs \N \N 2025-11-17 07:29:34.211076 2025-11-17 14:46:35.930492 http://www.apollolabscorp.com/ Apollo Labs 5.0 7 +15 Arizona Golden Leaf Wellness, Llc Arizona Golden Leaf Wellness, Llc arizona-golden-leaf-wellness-llc 5390 W Ina Rd Marana AZ 85743 5206209123 \N Operating · Marijuana Facility · (520) 620-9123 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Golden%20Leaf%20Wellness%2C%20Llc \N \N 2025-11-17 07:29:34.213681 2025-11-17 14:46:35.9318 https://naturemedaz.com/ NatureMed 4.8 3791 +18 Arizona Organix Arizona Organix arizona-organix 5303 W Glendale Ave Glendale AZ 85301 6239372752 \N Operating · Marijuana Facility · (623) 937-2752 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Organix \N \N 2025-11-17 07:29:34.217534 2025-11-17 14:46:35.933113 https://www.arizonaorganix.org/ Arizona Organix Dispensary 4.2 2983 +19 Nirvana Center Arizona Tree Equity 2 nirvana-center 2209 South 6th Avenue Tucson AZ 85713 9286422250 \N Operating · Marijuana Establishment · (928) 642-2250 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center \N \N 2025-11-17 07:29:34.21883 2025-11-17 14:46:35.934491 https://nirvanacannabis.com/ Nirvana Cannabis - Tucson 4.7 2156 +22 TruMed Dispensary Az Compassionate Care Inc trumed-dispensary 1613 N 40th St Phoenix AZ 85008 6022751279 \N Operating · Marijuana Facility · (602) 275-1279 https://azcarecheck.azdhs.gov/s/?name=TruMed%20Dispensary \N \N 2025-11-17 07:29:34.223002 2025-11-17 14:46:35.935792 https://trumedaz.com/ TruMed Dispensary 4.5 1807 +28 Sticky Saguaro Border Health, Inc sticky-saguaro 12338 East Riggs Road Chandler AZ 85249 6026449188 \N Operating · Marijuana Facility · (602) 644-9188 https://azcarecheck.azdhs.gov/s/?name=Sticky%20Saguaro \N \N 2025-11-17 07:29:34.231093 2025-11-17 14:46:35.939598 https://stickysaguaro.com/ Sticky Saguaro 4.6 1832 +1 SWC Prescott 203 Organix, Llc swc-prescott 123 E Merritt St Prescott AZ 86301 3128195061 \N Operating · Marijuana Facility · (312) 819-5061 https://azcarecheck.azdhs.gov/s/?name=SWC%20Prescott \N \N 2025-11-17 07:29:34.188807 2025-11-17 14:46:36.085286 https://zenleafdispensaries.com/locations/prescott/?utm_source=google&utm_medium=gbp-order&utm_campaign=az-prescott SWC Prescott by Zen Leaf 4.7 2312 +4 Ponderosa Dispensary 480 License Holdings, LLC ponderosa-dispensary 25 East Blacklidge Drive Tucson AZ 85705 4802010000 \N Operating · Marijuana Establishment · 480-201-0000 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N 2025-11-17 07:29:34.195827 2025-11-17 14:46:36.086614 https://www.pondyaz.com/locations Ponderosa Dispensary Tucson 4.8 520 +11 All Rebel Rockers Inc All Rebel Rockers Inc all-rebel-rockers-inc 4730 S 48th St Phoenix AZ 85040 6028075005 \N Operating · Marijuana Facility · 602-807-5005 https://azcarecheck.azdhs.gov/s/?name=All%20Rebel%20Rockers%20Inc \N \N 2025-11-17 07:29:34.208023 2025-11-17 14:46:36.089103 https://curaleaf.com/stores/curaleaf-dispensary-48th-street?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary 48th Street 4.6 1381 +21 Key Cannabis Arizona Wellness Collective 3, Inc key-cannabis 1911 W Broadway Rd 23 Mesa AZ 85202 4809124444 \N Operating · Marijuana Facility · (480) 912-4444 https://azcarecheck.azdhs.gov/s/?name=Key%20Cannabis \N \N 2025-11-17 07:29:34.221636 2025-11-17 14:46:36.090264 https://keycannabis.com/shop/mesa-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=mesa Key Cannabis Dispensary Mesa 4.2 680 +34 The Phoenix Cannabis Research Group Inc the-phoenix 9897 W McDowell Rd #720 Tolleson AZ 85353 4804200377 \N Operating · Marijuana Facility · (480) 420-0377 https://azcarecheck.azdhs.gov/s/?name=The%20Phoenix \N \N 2025-11-17 07:29:34.239335 2025-11-17 07:29:34.239335 \N \N \N \N +36 Catalina Hills Botanical Care Catalina Hills Botanical Care Inc catalina-hills-botanical-care 2918 N Central Ave Phoenix AZ 85012 6024661087 \N Operating · Marijuana Facility · 602-466-1087 https://azcarecheck.azdhs.gov/s/?name=Catalina%20Hills%20Botanical%20Care \N \N 2025-11-17 07:29:34.24216 2025-11-17 07:29:34.24216 \N \N \N \N +44 Green Pharms Desertview Wellness & Healing Solutions, LLC green-pharms 600 South 80th Avenue, 100 Tolleson AZ 85353 9285226337 \N Operating · Marijuana Facility · (928) 522-6337 https://azcarecheck.azdhs.gov/s/?name=Green%20Pharms \N \N 2025-11-17 07:29:34.253374 2025-11-17 07:29:34.253374 \N \N \N \N +32 SC Labs C4 Laboratories sc-labs 7650 East Evans Rd Unit A, UNIT A Scottsdale AZ 85260 4802196460 \N Operating · Marijuana Laboratory · (480) 219-6460 https://azcarecheck.azdhs.gov/s/?name=SC%20Labs \N \N 2025-11-17 07:29:34.23644 2025-11-17 14:46:35.943268 http://www.sclabs.com/ SC Labs | Arizona (Formerly C4 Laboratories) 4.9 10 +33 Releaf Cactus Bloom Facilities Management LLC releaf 436 Naugle Ave Patagonia AZ 85624 5209829212 \N Operating · Marijuana Establishment · 520-982-9212 https://azcarecheck.azdhs.gov/s/?name=Releaf \N \N 2025-11-17 07:29:34.237948 2025-11-17 14:46:35.945281 https://dbloomtucson.com/releaf-85624/ Releaf 85624 4.8 221 +38 Trulieve Of Sierra Vista Cochise County Wellness, LLC trulieve-of-sierra-vista 1633 S Highway 92, Ste 7 Sierra Vista AZ 85635 4806771755 \N Operating · Marijuana Facility · 480-677-1755 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Of%20Sierra%20Vista \N \N 2025-11-17 07:29:34.244842 2025-11-17 14:46:35.948265 https://www.trulieve.com/dispensaries/arizona/sierra-vista?utm_source=gmb&utm_medium=organic&utm_campaign=sierra-vista Trulieve Sierra Vista Dispensary 4.4 488 +39 Botanica Copper State Herbal Center Inc botanica 6205 N Travel Center Dr Tucson AZ 85741 5203950230 \N Operating · Marijuana Facility · (520) 395-0230 https://azcarecheck.azdhs.gov/s/?name=Botanica \N \N 2025-11-17 07:29:34.246153 2025-11-17 14:46:35.949751 https://botanica.us/ Botanica 4.6 940 +43 JARS Cannabis Desert Medical Campus Inc jars-cannabis 10040 N. Metro Parkway W Phoenix AZ 85051 6028708700 \N Operating · Marijuana Facility · 602-870-8700 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.251953 2025-11-17 14:46:35.952537 https://jarscannabis.com/ JARS Cannabis Phoenix Metrocenter 4.8 11971 +45 Devine Desert Healing Inc Devine Desert Healing Inc devine-desert-healing-inc 17201 N 19th Ave Phoenix AZ 85023 6023884400 \N Operating · Marijuana Facility · 602-388-4400 https://azcarecheck.azdhs.gov/s/?name=Devine%20Desert%20Healing%20Inc \N \N 2025-11-17 07:29:34.254693 2025-11-17 14:46:35.953696 https://curaleaf.com/stores/curaleaf-dispensary-bell?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Bell 4.5 2873 +46 JARS Cannabis Dreem Green Inc jars-cannabis 2412 East University Drive Phoenix AZ 85034 6026756999 \N Operating · Marijuana Facility · (602) 675-6999 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.255882 2025-11-17 14:46:35.954809 https://jarscannabis.com/ JARS Cannabis Phoenix Airport 4.9 10901 +48 Earth's Healing Inc Earth's Healing Inc earth-s-healing-inc 2075 E Benson Hwy Tucson AZ 85714 5203735779 \N Operating · Marijuana Facility · (520) 373-5779 https://azcarecheck.azdhs.gov/s/?name=Earth's%20Healing%20Inc \N \N 2025-11-17 07:29:34.258296 2025-11-17 14:46:35.956163 https://earthshealing.org/ Earth's Healing South 4.8 7608 +50 Mint Cannabis Eba Holdings Inc. mint-cannabis 8729 E Manzanita Dr Scottsdale AZ 85258 4807496468 \N Operating · Marijuana Facility · (480) 749-6468 https://azcarecheck.azdhs.gov/s/?name=Mint%20Cannabis \N \N 2025-11-17 07:29:34.260718 2025-11-17 14:46:35.960115 https://mintdeals.com/scottsdale-az/ Mint Cannabis - Scottsdale 4.4 763 +52 Zanzibar FJM Group LLC zanzibar 60 W Main St Quartzsite AZ 85346 5209072181 \N Operating · Marijuana Establishment · (520) 907-2181 https://azcarecheck.azdhs.gov/s/?name=Zanzibar \N \N 2025-11-17 07:29:34.26319 2025-11-17 14:46:35.962131 https://dutchie.com/dispensary/Zanzibar-Cannabis-Dispensary/products/flower Zanzibar dispensary 4.0 71 +53 The Downtown Dispensary Forever 46 Llc the-downtown-dispensary 221 E 6th St, Suite 105 Tucson AZ 85705 5208380492 \N Operating · Marijuana Facility · (520) 838-0492 https://azcarecheck.azdhs.gov/s/?name=The%20Downtown%20Dispensary \N \N 2025-11-17 07:29:34.264361 2025-11-17 14:46:35.964105 http://d2dispensary.com/ D2 Dispensary - Downtown Cannabis Gallery 4.8 5290 +56 Full Spectrum Lab, LLC Full Spectrum Lab, LLC full-spectrum-lab-llc 3865 E 34th St, Ste 109 Tucson AZ 85713 5208380695 \N Operating · Marijuana Laboratory · (520) 838-0695 https://azcarecheck.azdhs.gov/s/?name=Full%20Spectrum%20Lab%2C%20LLC \N \N 2025-11-17 07:29:34.267909 2025-11-17 14:46:35.968038 https://fullspectrumlab.com/ Full Spectrum Lab, LLC 4.5 2 +29 HANA MEDS Broken Arrow Herbal Center Inc hana-meds 1732 W Commerce Point Pl Sahuarita AZ 85614 5202898030 \N Operating · Marijuana Facility · (520) 289-8030 https://azcarecheck.azdhs.gov/s/?name=HANA%20MEDS \N \N 2025-11-17 07:29:34.232383 2025-11-17 14:46:36.093234 https://hanadispensaries.com/location/green-valley-az/?utm_source=gmb&utm_medium=organic Hana Dispensary Green Valley 4.6 1087 +37 HANA MEDS Cjk Inc hana-meds 3411 E Corona Ave, 100 Phoenix AZ 85040 \N Operating · Marijuana Facility · 602 491-0420 https://azcarecheck.azdhs.gov/s/?name=HANA%20MEDS \N \N 2025-11-17 07:29:34.243615 2025-11-17 14:46:36.106619 http://www.hanadispensaries.com/?utm_source=gmb&utm_medium=organic Hana Dispensary Phoenix 4.7 1129 +40 Sol Flower CSI Solutions, Inc. sol-flower 14980 N 78th Way Scottsdale AZ 85260 4804203300 \N Operating · Marijuana Facility · 480-420-3300 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N 2025-11-17 07:29:34.247538 2025-11-17 14:46:36.107685 https://www.livewithsol.com/scottsdale-airpark-menu-recreational?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing Sol Flower Dispensary Scottsdale Airpark 4.6 692 +42 Deeply Rooted Boutique Cannabis Company Desert Boyz deeply-rooted-boutique-cannabis-company 11725 NW Grand Ave El Mirage AZ 85335 4807080296 \N Operating · Marijuana Establishment · (480) 708-0296 https://azcarecheck.azdhs.gov/s/?name=Deeply%20Rooted%20Boutique%20Cannabis%20Company \N \N 2025-11-17 07:29:34.250572 2025-11-17 14:46:36.108903 http://azdeeplyrooted.com/ Deeply Rooted Boutique Cannabis Company Dispensary 4.8 568 +51 Encore Labs Arizona Encore Labs Arizona encore-labs-arizona 16624 North 90th Street, #101 Scottsdale AZ 85260 6266533414 \N Operating · Marijuana Laboratory · (626) 653-3414 https://azcarecheck.azdhs.gov/s/?name=Encore%20Labs%20Arizona \N \N 2025-11-17 07:29:34.262008 2025-11-17 14:46:36.111293 https://www.encorelabs.com/ Encore Labs AZ 5.0 2 +54 Zen Leaf Phoenix (Cave Creek Rd) Fort Consulting, Llc zen-leaf-phoenix-cave-creek-rd- 12401 N Cave Creek Rd Phoenix AZ 85022 3128195061 \N Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Phoenix%20(Cave%20Creek%20Rd) \N \N 2025-11-17 07:29:34.265564 2025-11-17 14:46:36.112689 https://zenleafdispensaries.com/locations/phoenix-n-cave-creek/?utm_campaign=az-phoenix-cave-creek&utm_medium=gbp&utm_source=google Zen Leaf Dispensary Phoenix (Cave Creek Rd.) 4.6 2720 +62 Nature's Medicines Green Hills Patient Center Inc nature-s-medicines 16913 East Enterprise Drive, 201, 202, 203 Fountain Hills AZ 85268 9285374888 \N Operating · Marijuana Facility · (928) 537-4888 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N 2025-11-17 07:29:34.275351 2025-11-17 07:29:34.275351 \N \N \N \N +66 Greenmed, Inc Greenmed, Inc greenmed-inc 6464 E Tanque Verde Rd Tucson AZ 85715 5208862484 \N Operating · Marijuana Facility · (520) 886-2484 https://azcarecheck.azdhs.gov/s/?name=Greenmed%2C%20Inc \N \N 2025-11-17 07:29:34.280178 2025-11-17 07:29:34.280178 \N \N \N \N +76 Higher than High I LLC Higher than High I LLC higher-than-high-i-llc 1302 West Industrial Drive Coolidge AZ 85128 4808613649 \N Operating · Marijuana Establishment · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Higher%20than%20High%20I%20LLC \N \N 2025-11-17 07:29:34.293213 2025-11-17 07:29:34.293213 \N \N \N \N +81 Juicy Joint I LLC Juicy Joint I LLC juicy-joint-i-llc 3550 North Lane, #110 Bullhead City AZ 86442 9283246062 \N Operating · Marijuana Establishment · (928) 324-6062 https://azcarecheck.azdhs.gov/s/?name=Juicy%20Joint%20I%20LLC \N \N 2025-11-17 07:29:34.299568 2025-11-17 07:29:34.299568 \N \N \N \N +63 Sunday Goods Green Lightning, LLC sunday-goods 723 N Scottsdale Rd Tempe AZ 85281 \N Operating · Marijuana Establishment · (480)-219-1300 https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods \N \N 2025-11-17 07:29:34.27645 2025-11-17 14:46:35.974481 https://sundaygoods.com/location/dispensary-tempe-az/?utm_source=google&utm_medium=gbp&utm_campaign=tempe_gbp Sunday Goods Tempe 4.1 685 +64 Southern Arizona Integrated Therapies (Tucson SAINTS) Green Medicine southern-arizona-integrated-therapies-tucson-saints- 112 S Kolb Rd Tucson AZ 85710 5208861003 \N Operating · Marijuana Facility · (520) 886-1003 https://azcarecheck.azdhs.gov/s/?name=Southern%20Arizona%20Integrated%20Therapies%20(Tucson%20SAINTS) \N \N 2025-11-17 07:29:34.277716 2025-11-17 14:46:35.976009 https://www.tucsonsaints.com/ SAINTS Dispensary 4.8 1704 +68 Grunge Free LLC Grunge Free LLC grunge-free-llc 700 North Pinal Parkway Avenue Florence AZ 85132 9173753900 \N Operating · Marijuana Establishment · (917)375-3900 https://azcarecheck.azdhs.gov/s/?name=Grunge%20Free%20LLC \N \N 2025-11-17 07:29:34.282797 2025-11-17 14:46:35.97865 https://nirvanacannabis.com/ Nirvana Cannabis - Florence 4.8 782 +69 Ponderosa Dispensary H4L Ponderosa, LLC ponderosa-dispensary 7343 S 89th Pl Mesa AZ 85212 4802131402 \N Operating · Marijuana Facility · (480) 213-1402 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N 2025-11-17 07:29:34.284075 2025-11-17 14:46:35.980161 https://www.pondyaz.com/locations Ponderosa Dispensary Queen Creek 4.6 1709 +70 Healing Healthcare 3 Inc Healing Healthcare 3 Inc healing-healthcare-3-inc 1040 E Camelback Rd, Ste A Phoenix AZ 85014 6023543094 \N Operating · Marijuana Facility · 602-354-3094 https://azcarecheck.azdhs.gov/s/?name=Healing%20Healthcare%203%20Inc \N \N 2025-11-17 07:29:34.285217 2025-11-17 14:46:35.981877 https://curaleaf.com/stores/curaleaf-dispensary-camelback?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Camelback 4.5 2853 +73 Trulieve of Chandler Dispensary High Desert Healing Llc trulieve-of-chandler-dispensary 13433 E. Chandler Blvd. Suite A Chandler AZ 85225 9548172370 \N Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Chandler%20Dispensary \N \N 2025-11-17 07:29:34.288909 2025-11-17 14:46:35.984941 https://www.trulieve.com/dispensaries/arizona/chandler?utm_source=gmb&utm_medium=organic&utm_campaign=chandler Trulieve Chandler Dispensary 4.0 1134 +75 Ponderosa Dispensary High Mountain Health, Llc ponderosa-dispensary 1250 S Plaza Way Ste A Flagstaff AZ 86001 9287745467 \N Operating · Marijuana Facility · (928) 774-5467 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N 2025-11-17 07:29:34.291559 2025-11-17 14:46:35.986592 https://www.pondyaz.com/locations Ponderosa Dispensary Flagstaff 4.5 1340 +78 Jamestown Center Jamestown Center jamestown-center 4104 E 32nd St Yuma AZ 85365 9283441735 \N Operating · Marijuana Facility · (928) 344-1735 https://azcarecheck.azdhs.gov/s/?name=Jamestown%20Center \N \N 2025-11-17 07:29:34.295954 2025-11-17 14:46:35.990549 http://yumadispensary.com/ Yuma Dispensary 4.4 1187 +79 BEST Dispensary Jamestown Center best-dispensary 1962 N. Higley Rd Mesa AZ 85205 6232642378 \N Operating · Marijuana Facility · 623-264-2378 https://azcarecheck.azdhs.gov/s/?name=BEST%20Dispensary \N \N 2025-11-17 07:29:34.297245 2025-11-17 14:46:35.992113 http://www.bestdispensary.com/ BEST Dispensary 4.6 482 +82 K Group Partners K Group Partners Llc k-group-partners 11200 W Michigan Ave Ste 5 Youngtown AZ 85363 6234445977 \N Operating · Marijuana Facility · 623-444-5977 https://azcarecheck.azdhs.gov/s/?name=K%20Group%20Partners \N \N 2025-11-17 07:29:34.300795 2025-11-17 14:46:35.994205 https://curaleaf.com/stores/curaleaf-dispensary-youngtown?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Youngtown 4.7 2421 +83 Sol Flower Kannaboost Technology Inc sol-flower 2424 W University Dr, Ste. 101 & 119 Tempe AZ 85281 4806442071 \N Operating · Marijuana Facility · 480-644-2071 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N 2025-11-17 07:29:34.302196 2025-11-17 14:46:35.995733 https://www.livewithsol.com/locations/tempe-university/?utm_source=gmb&utm_medium=organic Sol Flower Dispensary University 4.6 1149 +58 The Mint Dispensary G.T.L. Llc the-mint-dispensary 2444 W Northern Ave Phoenix AZ 85021 4807496468 \N Operating · Marijuana Facility · (480) 749-6468 https://azcarecheck.azdhs.gov/s/?name=The%20Mint%20Dispensary \N \N 2025-11-17 07:29:34.270388 2025-11-17 14:46:36.116094 https://mintdeals.com/phoenix-az/ Mint Cannabis - Northern Ave 4.6 1233 +61 Trulieve of Peoria Dispensary Green Desert Patient Center Of Peoria trulieve-of-peoria-dispensary 9275 W Peoria Ave, Ste 104 Peoria AZ 85345 8505597734 \N Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Peoria%20Dispensary \N \N 2025-11-17 07:29:34.274105 2025-11-17 14:46:36.11788 https://www.trulieve.com/dispensaries/arizona/peoria?utm_source=gmb&utm_medium=organic&utm_campaign=peoria Trulieve Peoria Dispensary 4.7 2931 +71 Consume Cannabis Health Center Navajo, Inc consume-cannabis 1350 N Penrod Rd Show Low AZ 85901 5208083111 \N Operating · Marijuana Facility · (520)808-3111 https://azcarecheck.azdhs.gov/s/?name=Consume%20Cannabis \N \N 2025-11-17 07:29:34.286332 2025-11-17 14:46:36.120593 https://www.consumecannabis.com/dispensaries/show-low Consume Cannabis - Show Low 4.4 1111 +74 Trulieve of Avondale Dispensary High Desert Healing Llc trulieve-of-avondale-dispensary 3828 S Vermeersch Rd Avondale AZ 85323 9548172370 \N Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Avondale%20Dispensary \N \N 2025-11-17 07:29:34.290248 2025-11-17 14:46:36.121718 https://www.trulieve.com/dispensaries/arizona/avondale?utm_source=gmb&utm_medium=organic&utm_campaign=avondale Trulieve Avondale Dispensary 4.3 1046 +84 Kaycha AZ LLC Kaycha AZ LLC kaycha-az-llc 1231 W Warner Rd, Ste 105 Tempe AZ 85284 7703657752 \N Operating · Marijuana Laboratory · (770) 365-7752 https://azcarecheck.azdhs.gov/s/?name=Kaycha%20AZ%20LLC \N \N 2025-11-17 07:29:34.303535 2025-11-17 14:46:36.123949 https://www.kaychalabs.com/ Kaycha Labs - Arizona 5.0 0 +86 Trulieve of Phoenix Dispensary Kwerles Inc trulieve-of-phoenix-dispensary 2017 W. Peoria Avenue, Suite A Phoenix AZ 85029 8505080261 \N Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Phoenix%20Dispensary \N \N 2025-11-17 07:29:34.306006 2025-11-17 07:29:34.306006 \N \N \N \N +89 JARS Cannabis Legacy & Co., Inc. jars-cannabis 3001 North 24th Street, A Phoenix AZ 85016 6239369333 \N Operating · Marijuana Facility · (623) 936-9333 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.309775 2025-11-17 07:29:34.309775 \N \N \N \N +92 MCCSE214, LLC MCCSE214, LLC mccse214-llc 1975 E Northern Ave Kingman AZ 86409 9282636348 \N Operating · Marijuana Establishment · 928-263-6348 https://azcarecheck.azdhs.gov/s/?name=MCCSE214%2C%20LLC \N \N 2025-11-17 07:29:34.313692 2025-11-17 07:29:34.313692 \N \N \N \N +93 MCCSE240, LLC MCCSE240, LLC mccse240-llc 12555 NW Grand Ave, B El Mirage AZ 85335 6023515450 \N Operating · Marijuana Establishment · 602-351-5450 https://azcarecheck.azdhs.gov/s/?name=MCCSE240%2C%20LLC \N \N 2025-11-17 07:29:34.314872 2025-11-17 07:29:34.314872 \N \N \N \N +94 MCCSE29, LLC MCCSE29, LLC mccse29-llc 12323 W Camelback Rd Litchfield Park AZ 85340 6029033665 \N Operating · Marijuana Establishment · (602) 903-3665 https://azcarecheck.azdhs.gov/s/?name=MCCSE29%2C%20LLC \N \N 2025-11-17 07:29:34.316182 2025-11-17 07:29:34.316182 \N \N \N \N +100 MK Associates LLC MK Associates LLC mk-associates-llc 3270 AZ-82 Sonoita AZ 85637 7039152159 \N Operating · Marijuana Establishment · (703) 915-2159 https://azcarecheck.azdhs.gov/s/?name=MK%20Associates%20LLC \N \N 2025-11-17 07:29:34.323969 2025-11-17 07:29:34.323969 \N \N \N \N +88 JARS Cannabis Lawrence Health Services LLC jars-cannabis 2250 Highway 60, Suite M Globe AZ 85501 9287932550 \N Operating · Marijuana Establishment · 928-793-2550 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.308448 2025-11-17 14:46:35.998634 https://jarscannabis.com/ JARS Cannabis Globe 4.8 959 +91 Mint Cannabis M&T Retail Facility 1, LLC mint-cannabis 1211 North 75th Avenue Phoenix AZ 85043 4807496468 \N Operating · Marijuana Facility · 480-749-6468 https://azcarecheck.azdhs.gov/s/?name=Mint%20Cannabis \N \N 2025-11-17 07:29:34.312376 2025-11-17 14:46:36.000146 https://mintdeals.com/75-ave-phx/ Mint Cannabis - 75th Ave 4.7 1463 +95 MCCSE82, LLC MCCSE82, LLC mccse82-llc 46639 North Black Canyon Highway, 1-2 New River AZ 85087 9282995145 \N Operating · Marijuana Establishment · 928-299-5145 https://azcarecheck.azdhs.gov/s/?name=MCCSE82%2C%20LLC \N \N 2025-11-17 07:29:34.31746 2025-11-17 14:46:36.001778 https://jarscannabis.com/ JARS Cannabis New River 4.9 3180 +98 Trulieve of Casa Grande Medical Pain Relief Inc trulieve-of-casa-grande 1860 E Salk Dr Ste B-1 Casa Grande AZ 85122 8505080261 \N Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Casa%20Grande \N \N 2025-11-17 07:29:34.321338 2025-11-17 14:46:36.004751 https://www.trulieve.com/dispensaries/arizona/casa-grande?utm_source=gmb&utm_medium=organic&utm_campaign=casa-grande Trulieve Casa Grande Dispensary 4.1 1817 +99 Desert Bloom Releaf Center Medmar Tanque Verde Llc desert-bloom-releaf-center 8060 E 22nd St Ste 108 Tucson AZ 85710 5208861760 \N Operating · Marijuana Facility · 520-886-1760 https://azcarecheck.azdhs.gov/s/?name=Desert%20Bloom%20Releaf%20Center \N \N 2025-11-17 07:29:34.322492 2025-11-17 14:46:36.007154 http://www.dbloomtucson.com/ Desert Bloom Re-Leaf Center 4.2 1634 +102 JARS Cannabis Mohave Cannabis Club 1, LLC jars-cannabis 4236 E Juanita Ave Mesa AZ 85206 4804200064 \N Operating · Marijuana Facility · 480-420-0064 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.327562 2025-11-17 14:46:36.010459 https://jarscannabis.com/ JARS Cannabis Mesa 4.9 7637 +104 JARS Cannabis Mohave Cannabis Club 3, LLC jars-cannabis 20224 N 27th Ave, Ste 103 Phoenix AZ 85027 6232335133 \N Operating · Marijuana Facility · 623-233-5133 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.33016 2025-11-17 14:46:36.013202 https://jarscannabis.com/ JARS Cannabis North Phoenix 4.8 4327 +105 JARS Cannabis Mohave Cannabis Club 4, LLC jars-cannabis 8028 E State Route 69 Prescott Valley AZ 86314 4809394002 \N Operating · Marijuana Facility · 480-939-4002 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.331515 2025-11-17 14:46:36.015183 https://jarscannabis.com/ JARS Cannabis Prescott Valley 4.9 1838 +108 Green Farmacy Natural Relief Clinic Inc green-farmacy 4456 E Thomas Road Phoenix AZ 85018 5206868708 \N Operating · Marijuana Facility · (520) 686-8708 https://azcarecheck.azdhs.gov/s/?name=Green%20Farmacy \N \N 2025-11-17 07:29:34.335709 2025-11-17 14:46:36.018482 https://yilo.com/?utm_source=gmb&utm_medium=organic YiLo Superstore (Arcadia) - SKY HARBOR 4.6 586 +109 YiLo Superstore Natural Relief Clinic Inc yilo-superstore 2841 W Thunderbird Rd Phoenix AZ 85032 6025392828 \N Operating · Marijuana Facility · (602) 539-2828 https://azcarecheck.azdhs.gov/s/?name=YiLo%20Superstore \N \N 2025-11-17 07:29:34.337045 2025-11-17 14:46:36.020365 https://yilo.com/?utm_source=gmb&utm_medium=organic YiLo Superstore (Phoenix) 4.3 880 +111 Trulieve of Baseline Dispensary Nature Med Inc trulieve-of-baseline-dispensary 1821 W Baseline Rd Guadalupe AZ 85283 8505080261 \N Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Baseline%20Dispensary \N \N 2025-11-17 07:29:34.339705 2025-11-17 14:46:36.022096 https://www.trulieve.com/dispensaries/arizona/guadalupe?utm_source=gmb&utm_medium=organic&utm_campaign=baseline Trulieve Baseline Dispensary 4.4 1297 +85 Kind Meds Inc Kind Meds Inc kind-meds-inc 2152 S Vineyard St Ste 120 Mesa AZ 85210 4806869302 \N Operating · Marijuana Facility · (480) 686-9302 https://azcarecheck.azdhs.gov/s/?name=Kind%20Meds%20Inc \N \N 2025-11-17 07:29:34.304738 2025-11-17 14:46:36.125029 http://kindmedsaz.com/ Kind Meds 3.8 260 +90 Cookies Life Changers Investments LLC cookies 2715 South Hardy Drive Tempe AZ 85282 4804527275 \N Operating · Marijuana Establishment · (480) 452-7275 https://azcarecheck.azdhs.gov/s/?name=Cookies \N \N 2025-11-17 07:29:34.310958 2025-11-17 14:46:36.126103 https://tempe.cookies.co/?utm_source=gmb&utm_medium=organic Cookies Cannabis Dispensary Tempe 4.7 5626 +96 MCD-SE Venture 25, LLC MCD-SE Venture 25, LLC mcd-se-venture-25-llc 15235 North Dysart Road, 111 D El Mirage AZ 85335 6029313663 \N Operating · Marijuana Establishment · 602-931-3663 https://azcarecheck.azdhs.gov/s/?name=MCD-SE%20Venture%2025%2C%20LLC \N \N 2025-11-17 07:29:34.318677 2025-11-17 14:46:36.127232 https://mintdeals.com/az-el-mirage/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps Mint Cannabis Dispensary - EL MIRAGE 4.7 249 +106 Trulieve of Roosevelt Row Mohave Valley Consulting, Llc trulieve-of-roosevelt-row 1007 N 7th St Phoenix AZ 85006 7703300831 \N Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Roosevelt%20Row \N \N 2025-11-17 07:29:34.333047 2025-11-17 14:46:36.129558 https://www.trulieve.com/dispensaries/arizona/phoenix-roosevelt?utm_source=gmb&utm_medium=organic&utm_campaign=phoenix-roosevelt Trulieve Phoenix Dispensary Roosevelt 4.4 670 +116 Non Profit Patient Center Inc Non Profit Patient Center Inc non-profit-patient-center-inc 2960 West Grand Avenue, Bldg. A and B Phoenix AZ 85017 4808613649 \N Operating · Marijuana Facility · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Non%20Profit%20Patient%20Center%20Inc \N \N 2025-11-17 07:29:34.347144 2025-11-17 07:29:34.347144 \N \N \N \N +121 Trulieve of Tucson Grant Dispensary Patient Care Center 301, Inc. trulieve-of-tucson-grant-dispensary 2734 E Grant Rd Tucson AZ 85716 8505597734 \N Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson%20Grant%20Dispensary \N \N 2025-11-17 07:29:34.35395 2025-11-17 07:29:34.35395 \N \N \N \N +136 Sol Flower S Flower SE 1, Inc sol-flower 6026 North Oracle Road Tucson AZ 85704 6028287204 \N Operating · Marijuana Establishment · 602-828-7204 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N 2025-11-17 07:29:34.375152 2025-11-17 07:29:34.375152 \N \N \N \N +117 Ocotillo Vista, Inc. Ocotillo Vista, Inc. ocotillo-vista-inc- 2330 North 75th Avenue Phoenix AZ 85035 6027867988 \N Operating · Marijuana Facility · 602-786-7988 https://azcarecheck.azdhs.gov/s/?name=Ocotillo%20Vista%2C%20Inc. \N \N 2025-11-17 07:29:34.348551 2025-11-17 14:46:36.026998 https://nirvanacannabis.com/ Nirvana Cannabis - 75th Ave (West Phoenix) 4.7 5296 +118 Organica Patient Group Inc Organica Patient Group Inc organica-patient-group-inc 1720 E. Deer Valley Rd., Suite 101 Phoenix AZ 85204 6029104152 \N Operating · Marijuana Facility · 602-910-4152 https://azcarecheck.azdhs.gov/s/?name=Organica%20Patient%20Group%20Inc \N \N 2025-11-17 07:29:34.34979 2025-11-17 14:46:36.028619 https://herbalwellnesscenter.com/ Herbal Wellness Center North 4.6 936 +122 JARS Cannabis Payson Dreams LLC jars-cannabis 108 N Tonto St Payson AZ 85541 2487557633 \N Operating · Marijuana Dispensary · 248-755-7633 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.355308 2025-11-17 14:46:36.031469 https://jarscannabis.com/ JARS Cannabis Payson 4.9 3259 +123 Zen Leaf Phoenix (Dunlap Ave.) Perpetual Healthcare, LLC zen-leaf-phoenix-dunlap-ave- 4244 W Dunlap Rd Ste 1 Phoenix AZ 85051 3128195061 \N Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Phoenix%20(Dunlap%20Ave.) \N \N 2025-11-17 07:29:34.35661 2025-11-17 14:46:36.032755 https://zenleafdispensaries.com/locations/phoenix-w-dunlap/?utm_source=google&utm_medium=gbp&utm_campaign=az-phoenix-dunlap Zen Leaf Dispensary Phoenix (Dunlap Ave.) 4.7 3082 +126 Pinal County Wellness Center Pinal County Wellness Center pinal-county-wellness-center 8970 N 91st Ave Peoria AZ 85345 6232331010 \N Operating · Marijuana Facility · 623-233-1010 https://azcarecheck.azdhs.gov/s/?name=Pinal%20County%20Wellness%20Center \N \N 2025-11-17 07:29:34.361056 2025-11-17 14:46:36.03564 https://curaleaf.com/stores/curaleaf-dispensary-peoria?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Peoria 4.6 1866 +127 JARS Cannabis Piper's Shop LLC jars-cannabis 1809 W Thatcher Blvd Safford AZ 85546 9284241313 \N Operating · Marijuana Establishment · 928-424-1313 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.362314 2025-11-17 14:46:36.037655 https://jarscannabis.com/ JARS Cannabis Safford 4.9 725 +129 Ponderosa Dispensary Ponderosa Botanical Care Inc ponderosa-dispensary 318 South Bracken Lane Chandler AZ 85224 6238773934 \N Operating · Marijuana Facility · (623) 877-3934 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N 2025-11-17 07:29:34.365097 2025-11-17 14:46:36.039342 https://www.pondyaz.com/locations Ponderosa Dispensary Chandler 4.7 839 +132 The Prime Leaf Rainbow Collective Inc the-prime-leaf 4220 E Speedway Blvd Tucson AZ 85712 5202072753 \N Operating · Marijuana Facility · (520) 207-2753 https://azcarecheck.azdhs.gov/s/?name=The%20Prime%20Leaf \N \N 2025-11-17 07:29:34.369215 2025-11-17 14:46:36.042145 http://www.theprimeleaf.com/ The Prime Leaf 4.5 1305 +133 Noble Herb Rch Wellness Center noble-herb 522 E Route 66 Flagstaff AZ 86001 9283517775 \N Operating · Marijuana Facility · (928) 351-7775 https://azcarecheck.azdhs.gov/s/?name=Noble%20Herb \N \N 2025-11-17 07:29:34.370801 2025-11-17 14:46:36.043663 http://www.nobleherbaz.com/ Noble Herb Flagstaff Dispensary 4.4 1774 +134 Arizona Natural Concepts Rjk Ventures, Inc. arizona-natural-concepts 1039 East Carefree Highway Phoenix AZ 85085 6022245999 \N Operating · Marijuana Facility · (602) 224-5999 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Natural%20Concepts \N \N 2025-11-17 07:29:34.372342 2025-11-17 14:46:36.04537 https://ancdispensary.com/ Arizona Natural Concepts Marijuana Dispensary 4.7 717 +137 Sol Flower S Flower SE 2, Inc. sol-flower 3000 West Valencia Road, Suite 210 Tucson AZ 85746 6028287204 \N Operating · Marijuana Establishment · (602) 828-7204 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N 2025-11-17 07:29:34.376613 2025-11-17 14:46:36.046812 https://www.livewithsol.com/locations/south-tucson/?utm_source=gmb&utm_medium=organic Sol Flower Dispensary South Tucson 4.7 1914 +139 S Flower SE 4, Inc. S Flower SE 4, Inc. s-flower-se-4-inc- 6437 North Oracle Road Tucson AZ 85704 4807202943 \N Operating · Marijuana Establishment · (480) 720-2943 https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20SE%204%2C%20Inc. \N \N 2025-11-17 07:29:34.37911 2025-11-17 14:46:36.05058 https://www.livewithsol.com/locations/casas-adobes/ Sol Flower Dispensary Casas Adobes 4.8 869 +113 The Flower Shop Az Nature's Healing Center Inc the-flower-shop-az 3155 E Mcdowell Rd Ste 2 Phoenix AZ 85008 4805005054 \N Operating · Marijuana Facility · (480) 500-5054 https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az \N \N 2025-11-17 07:29:34.342757 2025-11-17 14:46:36.131775 https://theflowershopusa.com/phoenix?utm_source=google-business&utm_medium=organic The Flower Shop - Phoenix 4.4 1211 +120 Zen Leaf Arcadia Patient Alternative Relief Center, LLC zen-leaf-arcadia 2710 E Indian School Rd Phoenix AZ 85016 3128195061 \N Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Arcadia \N \N 2025-11-17 07:29:34.352388 2025-11-17 14:46:36.133962 https://zenleafdispensaries.com/locations/phoenix-arcadia/?utm_campaign=az-phoenix-arcadia&utm_medium=gbp&utm_source=google Zen Leaf Dispensary Arcadia 4.7 2099 +128 Pleasant Plants I LLC Pleasant Plants I LLC pleasant-plants-i-llc 6676 West Bell Road Glendale AZ 85308 5207277754 \N Operating · Marijuana Establishment · (520) 727-7754 https://azcarecheck.azdhs.gov/s/?name=Pleasant%20Plants%20I%20LLC \N \N 2025-11-17 07:29:34.363719 2025-11-17 14:46:36.136243 https://storycannabis.com/dispensary-locations/arizona/glendale-dispensary/?utm_source=google&utm_medium=Link&utm_campaign=bell_glandale&utm_id=googlelisting Story Cannabis Dispensary Bell Glendale 4.4 1137 +131 Trulieve of Tucson Menlo Park Dispensary Purplemed Inc trulieve-of-tucson-menlo-park-dispensary 1010 S Fwy Ste 130 Tucson AZ 85745 8505597734 \N Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson%20Menlo%20Park%20Dispensary \N \N 2025-11-17 07:29:34.367701 2025-11-17 14:46:36.13751 https://www.trulieve.com/dispensaries/arizona/tucson-menlo-park?utm_source=gmb&utm_medium=organic&utm_campaign=tucson-menlo Trulieve Tucson Dispensary Menlo Park 4.2 645 +142 Serenity Smoke, LLC Serenity Smoke, LLC serenity-smoke-llc 19 West Main Street Springerville AZ 85938 \N Not Operating · Marijuana Establishment https://azcarecheck.azdhs.gov/s/?name=Serenity%20Smoke%2C%20LLC \N \N 2025-11-17 07:29:34.38354 2025-11-17 07:29:34.38354 \N \N \N \N +147 Sky Analytical Laboratories Sky Analytical Laboratories sky-analytical-laboratories 1122 East Washington Street Phoenix AZ 85034 6232624330 \N Operating · Marijuana Laboratory · (623) 262-4330 https://azcarecheck.azdhs.gov/s/?name=Sky%20Analytical%20Laboratories \N \N 2025-11-17 07:29:34.390287 2025-11-17 07:29:34.390287 \N \N \N \N +161 The Green Halo Llc The Green Halo Llc the-green-halo-llc 3906 North Oracle Road Tucson AZ 85705 5206642251 \N Operating · Marijuana Facility · (520) 664-2251 https://azcarecheck.azdhs.gov/s/?name=The%20Green%20Halo%20Llc \N \N 2025-11-17 07:29:34.409513 2025-11-17 07:29:34.409513 \N \N \N \N +159 Trulieve of Mesa South Dispensary The Giving Tree Wellness Center Of Mesa Inc trulieve-of-mesa-south-dispensary 938 E Juanita Ave Mesa AZ 85204 7703300831 \N Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Mesa%20South%20Dispensary \N \N 2025-11-17 07:29:34.406884 2025-11-17 14:34:09.940116 https://www.trulieve.com/dispensaries/arizona/phoenix-alhambra?utm_source=gmb&utm_medium=organic&utm_campaign=alhambra Trulieve Phoenix Dispensary Alhambra 4.5 2917 +166 Zen Leaf Mesa The Medicine Room Llc zen-leaf-mesa 550 W Mckellips Rd Bldg 1 Mesa AZ 85201 3128195061 \N Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Mesa \N \N 2025-11-17 07:29:34.416852 2025-11-17 14:46:36.083731 https://zenleafdispensaries.com/locations/mesa/?utm_campaign=az-mesa&utm_medium=gbp&utm_source=google Zen Leaf Mesa 4.8 2602 +140 Cannabist Salubrious Wellness Clinic Inc cannabist 520 S Price Rd, Ste 1 & 2 Tempe AZ 85281 3128195061 \N Operating · Marijuana Facility · (312) 819-5061 https://azcarecheck.azdhs.gov/s/?name=Cannabist \N \N 2025-11-17 07:29:34.380609 2025-11-17 14:46:36.140885 https://zenleafdispensaries.com/locations/tempe/ Cannabist Tempe by Zen Leaf 4.6 1655 +155 Trulieve of Tempe Dispensary Svaccha, Llc trulieve-of-tempe-dispensary 710 W Elliot Rd, Ste 102 Tempe AZ 85284 9548172370 \N Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tempe%20Dispensary \N \N 2025-11-17 07:29:34.401217 2025-11-17 14:46:36.069922 https://www.trulieve.com/dispensaries/arizona/tempe?utm_source=gmb&utm_medium=organic&utm_campaign=tempe Trulieve Tempe Dispensary 4.2 790 +156 Swallowtail 3, LLC Swallowtail 3, LLC swallowtail-3-llc 5210 South Priest Dr, A, Suite A Guadalupe AZ 85283 4807496468 \N Operating · Marijuana Establishment · 480-749-6468 https://azcarecheck.azdhs.gov/s/?name=Swallowtail%203%2C%20LLC \N \N 2025-11-17 07:29:34.402654 2025-11-17 14:46:36.07201 https://mintdeals.com/tempe-az/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps Mint Cannabis - Tempe 4.7 7231 +164 Sunday Goods The Health Center Of Cochise Inc sunday-goods 1616 E. Glendale Ave. Phoenix AZ 85020 5208083111 \N Operating · Marijuana Facility · 520-808-3111 https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods \N \N 2025-11-17 07:29:34.413925 2025-11-17 14:46:36.079974 https://sundaygoods.com/location/dispensary-phoenix-az/?utm_source=google&utm_medium=gbp&utm_campaign=phoenix_gbp Sunday Goods North Central Phoenix 4.4 652 +141 Sea Of Green Llc Sea Of Green Llc sea-of-green-llc 6844 East Parkway Norte Mesa AZ 85212 4803255000 \N Operating · Marijuana Facility · (480) 325-5000 https://azcarecheck.azdhs.gov/s/?name=Sea%20Of%20Green%20Llc \N \N 2025-11-17 07:29:34.382053 2025-11-17 14:46:36.052356 http://trubliss.com/ truBLISS | Medical & Recreational Marijuana Dispensary 4.8 4762 +144 Trulieve of Cottonwood Dispensary Sherri Dunn, Llc trulieve-of-cottonwood-dispensary 2400 E Sr 89a Cottonwood AZ 86326 9548172370 \N Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Cottonwood%20Dispensary \N \N 2025-11-17 07:29:34.386228 2025-11-17 14:46:36.054628 https://www.trulieve.com/dispensaries/arizona/cottonwood?utm_source=gmb&utm_medium=organic&utm_campaign=cottonwood Trulieve Cottonwood Dispensary 4.4 1201 +160 Giving Tree Dispensary The Giving Tree Wellness Center Of North Phoenix Inc. giving-tree-dispensary 701 West Union Hills Drive Phoenix AZ 85027 6232429080 \N Operating · Marijuana Facility · (623) 242-9080 https://azcarecheck.azdhs.gov/s/?name=Giving%20Tree%20Dispensary \N \N 2025-11-17 07:29:34.408192 2025-11-17 14:46:36.076412 https://dutchie.com/stores/Nirvana-North-Phoenix Giving Tree Dispensary 4.6 1932 +146 Nature's Medicines Sixth Street Enterprises Inc nature-s-medicines 2439 W Mcdowell Rd Phoenix AZ 85009 4804203145 \N Operating · Marijuana Facility · 480-420-3145 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N 2025-11-17 07:29:34.388878 2025-11-17 14:46:36.056856 https://storycannabis.com/shop/arizona/phoenix-mcdowell-dispensary/rec-menu/?utm_source=google&utm_medium=listing&utm_campaign=mcdowell&utm_term=click&utm_content=website Story Cannabis Dispensary McDowell 4.6 6641 +149 The Clifton Bakery LLC Sonoran Flower LLC the-clifton-bakery-llc 700 S. CORONADO BLVD CLIFTON AZ 85533 5202417777 \N Operating · Marijuana Establishment · 520-241-7777 https://azcarecheck.azdhs.gov/s/?name=The%20Clifton%20Bakery%20LLC \N \N 2025-11-17 07:29:34.393045 2025-11-17 14:46:36.061345 http://thecliftonbakery.com/ Clifton Bakery 4.8 93 +151 Nirvana Center Dispensaries SSW Ventures, LLC nirvana-center-dispensaries 702 East Buckeye Road Phoenix AZ 85034 6027867988 \N Operating · Marijuana Facility · (602) 786-7988 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Dispensaries \N \N 2025-11-17 07:29:34.3958 2025-11-17 14:46:36.063655 https://nirvanacannabis.com/ Nirvana Cannabis - 7th St (Downtown Phoenix) 4.9 3212 +152 Steep Hill Arizona Laboratory Steep Hill Arizona Laboratory steep-hill-arizona-laboratory 14620 North Cave Creek Road, 3 Phoenix AZ 85022 6029207808 \N Operating · Marijuana Laboratory · 602-920-7808 https://azcarecheck.azdhs.gov/s/?name=Steep%20Hill%20Arizona%20Laboratory \N \N 2025-11-17 07:29:34.397108 2025-11-17 14:46:36.065762 http://www.steephillarizona.com/ Steep Hill Arizona Laboratory \N \N +162 Green Pharms The Healing Center Farmacy Llc green-pharms 7235 E Hampton Ave Unit 115 Mesa AZ 85209 4804106704 \N Operating · Marijuana Facility · (480) 410-6704 https://azcarecheck.azdhs.gov/s/?name=Green%20Pharms \N \N 2025-11-17 07:29:34.41092 2025-11-17 14:46:36.078089 http://greenpharms.com/ GreenPharms Dispensary Mesa 4.6 3982 +143 Trulieve Maricopa Dispensary Sherri Dunn, Llc trulieve-maricopa-dispensary 44405 W. Honeycutt Avenue maricopa AZ 85139 9548172370 \N Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Maricopa%20Dispensary \N \N 2025-11-17 07:29:34.384872 2025-11-17 14:46:36.14239 https://www.trulieve.com/dispensaries/arizona/maricopa?utm_source=gmb&utm_medium=organic&utm_campaign=maricopa Trulieve Maricopa Dispensary 3.2 203 +150 Ponderosa Dispensary Soothing Ponderosa, LLC ponderosa-dispensary 5550 E Mcdowell Rd, Ste 103 Mesa AZ 85215 4802131402 \N Operating · Marijuana Facility · (480) 213-1402 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N 2025-11-17 07:29:34.394407 2025-11-17 14:46:36.145817 https://www.pondyaz.com/locations Ponderosa Dispensary Mesa 4.7 1940 +170 Total Health & Wellness Inc Total Health & Wellness Inc total-health-wellness-inc 3830 North 7th Street Phoenix AZ 85014 9286422250 \N Operating · Marijuana Facility · (623) 295-1788 https://azcarecheck.azdhs.gov/s/?name=Total%20Health%20%26%20Wellness%20Inc \N \N 2025-11-17 07:29:34.422141 2025-11-17 14:34:09.946522 https://nirvanacannabis.com/ Nirvana Cannabis - Tucson 4.7 2156 +182 Zonacare Zonacare zonacare 4415 East Monroe Street Phoenix AZ 85034 6023965757 \N Operating · Marijuana Facility · 602-396-5757 https://azcarecheck.azdhs.gov/s/?name=Zonacare \N \N 2025-11-17 07:29:34.438386 2025-11-17 14:46:36.155148 https://curaleaf.com/stores/curaleaf-dispensary-phoenix-airport?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Phoenix Airport 4.4 2705 +176 Medusa Farms Verde Dispensary Inc medusa-farms 3490 N Bank St Kingman AZ 86409 3128195061 \N Operating · Marijuana Facility · (928) 421-0020 https://azcarecheck.azdhs.gov/s/?name=Medusa%20Farms \N \N 2025-11-17 07:29:34.430181 2025-11-17 14:34:09.948839 https://zenleafdispensaries.com/locations/chandler/?utm_campaign=az-chandler&utm_medium=gbp&utm_source=google Zen Leaf Dispensary Chandler 4.8 3044 +179 Wickenburg Alternative Medicine Llc Wickenburg Alternative Medicine Llc wickenburg-alternative-medicine-llc 12620 N Cave Creek Road, Ste 1 Phoenix AZ 85022 6026449188 \N Operating · Marijuana Facility · (623) 478-2233 https://azcarecheck.azdhs.gov/s/?name=Wickenburg%20Alternative%20Medicine%20Llc \N \N 2025-11-17 07:29:34.434192 2025-11-17 14:34:09.949935 https://stickysaguaro.com/ Sticky Saguaro 4.6 1832 +163 Health For Life Crismon The Healing Center Wellness Center Llc health-for-life-crismon 9949 E Apache Trail Mesa AZ 85207 4804001170 \N Operating · Marijuana Facility · (480) 400-1170 https://azcarecheck.azdhs.gov/s/?name=Health%20For%20Life%20Crismon \N \N 2025-11-17 07:29:34.412379 2025-11-17 14:46:36.150573 https://healthforlifeaz.com/crismon/?utm_source=google&utm_medium=organic&utm_campaign=gbp-crimson Health for Life - Crismon - Medical and Recreational Cannabis Dispensary 4.5 1993 +168 Total Accountability Systems I Inc Total Accountability Systems I Inc total-accountability-systems-i-inc 6287 E Copper Hill Dr. Ste A Prescott Valley AZ 86314 \N Operating · Marijuana Facility · (928) 3505870 https://azcarecheck.azdhs.gov/s/?name=Total%20Accountability%20Systems%20I%20Inc \N \N 2025-11-17 07:29:34.419645 2025-11-17 14:46:36.096267 https://nirvanacannabis.com/ Nirvana Cannabis - Prescott Valley 4.7 4098 +180 Woodstock 1 Woodstock 1 woodstock-1 1629 N 195 Ave, 101 Buckeye AZ 85396 6029801505 \N Operating · Marijuana Establishment · 602-980-1505 https://azcarecheck.azdhs.gov/s/?name=Woodstock%201 \N \N 2025-11-17 07:29:34.435609 2025-11-17 14:46:36.104008 https://www.waddellslonghorn.com/ Waddell's Longhorn 4.2 1114 +181 JARS Cannabis Yuma County Dispensary LLC jars-cannabis 3345 E County 15th St Somerton AZ 85350 9289198667 \N Operating · Marijuana Establishment · 928-919-8667 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.436973 2025-11-17 14:46:36.105442 https://jarscannabis.com/ JARS Cannabis Yuma 4.9 2154 +175 Flor Verde Dispensary Verde Americano, LLC flor-verde-dispensary 1115 Circulo Mercado Rio Rico AZ 85648 \N Operating · Marijuana Dispensary · 602 689-3559 https://azcarecheck.azdhs.gov/s/?name=Flor%20Verde%20Dispensary \N \N 2025-11-17 07:29:34.428758 2025-11-17 07:29:34.428758 \N Green Med Wellness \N \N +153 Superior Organics Superior Organics superior-organics 211 S 57th Dr Phoenix AZ 85043 6029269100 \N Operating · Marijuana Facility · (602) 926-9100 https://azcarecheck.azdhs.gov/s/?name=Superior%20Organics \N \N 2025-11-17 07:29:34.398418 2025-11-17 14:46:36.067744 https://thesuperiordispensary.com/ The Superior Dispensary 4.5 959 +158 Fire. Dispensary The Desert Valley Pharmacy Inc fire-dispensary 2825 W Thomas Rd Phoenix AZ 85017 4808613649 \N Operating · Marijuana Facility · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Fire.%20Dispensary \N \N 2025-11-17 07:29:34.405532 2025-11-17 14:46:36.074341 https://natureswonderaz.com/phoenix-dispensary-menu-recreational Nature's Wonder Phoenix Dispensary 4.8 2122 +167 Total Accountability Patient Care Total Accountability Patient Care total-accountability-patient-care 1525 N. Park Ave Tucson AZ 85719 5205868710 \N Operating · Marijuana Facility · (520) 586-8710 https://azcarecheck.azdhs.gov/s/?name=Total%20Accountability%20Patient%20Care \N \N 2025-11-17 07:29:34.418267 2025-11-17 14:46:36.151757 https://theprimeleaf.com/ The Prime Leaf 4.6 669 +169 Oasis Total Health & Wellness Inc oasis 17006 S Weber Dr Chandler AZ 85226 4806267333 \N Operating · Marijuana Facility · (480) 626-7333 https://azcarecheck.azdhs.gov/s/?name=Oasis \N \N 2025-11-17 07:29:34.420881 2025-11-17 14:46:36.152967 https://storycannabis.com/dispensary-locations/arizona/north-chandler-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=north_chandler&utm_term=click&utm_content=website Story Cannabis Dispensary North Chandler 4.3 853 +173 Valley Of The Sun Medical Dispensary, Inc. Valley Of The Sun Medical Dispensary, Inc. valley-of-the-sun-medical-dispensary-inc- 16200 W Eddie Albert Way Goodyear AZ 85338 6239323859 \N Operating · Marijuana Facility · (623) 932-3859 https://azcarecheck.azdhs.gov/s/?name=Valley%20Of%20The%20Sun%20Medical%20Dispensary%2C%20Inc. \N \N 2025-11-17 07:29:34.426103 2025-11-17 14:46:36.15404 http://votsmd.com/?utm_source=local&utm_medium=organic&utm_campaign=gmb Valley of the Sun Dispensary 4.0 598 +174 Zen Leaf Gilbert Vending Logistics Llc zen-leaf-gilbert 5409 S Power Rd Mesa AZ 85212 3128195061 \N Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Gilbert \N \N 2025-11-17 07:29:34.427427 2025-11-17 14:46:36.099926 https://zenleafdispensaries.com/locations/gilbert/?utm_source=google&utm_medium=gbp&utm_campaign=az-gilbert Zen Leaf Dispensary Gilbert 4.8 2791 +171 Uncle Harry Inc Uncle Harry Inc uncle-harry-inc 17036 North Cave Creek Rd Phoenix AZ 85032 8188229888 \N Operating · Marijuana Facility · (818) 822-9888 https://azcarecheck.azdhs.gov/s/?name=Uncle%20Harry%20Inc \N \N 2025-11-17 07:29:34.423429 2025-11-17 14:46:36.097461 https://mintdeals.com/phoenix-az/ Mint Cannabis - Phoenix 4.7 2658 +172 The Good Dispensary Valley Healing Group Inc the-good-dispensary 1842 W Broadway Rd Mesa AZ 85202 4809008042 \N Operating · Marijuana Facility · (480) 900-8042 https://azcarecheck.azdhs.gov/s/?name=The%20Good%20Dispensary \N \N 2025-11-17 07:29:34.424693 2025-11-17 14:46:36.098583 https://thegooddispensary.com/ The GOOD Dispensary 4.8 5884 +177 White Mountain Health Center Inc White Mountain Health Center Inc white-mountain-health-center-inc 9420 W Bell Rd Ste 108 Sun City AZ 85351 6233744141 \N Operating · Marijuana Facility · (623) 374-4141 https://azcarecheck.azdhs.gov/s/?name=White%20Mountain%20Health%20Center%20Inc \N \N 2025-11-17 07:29:34.431509 2025-11-17 14:46:36.101233 https://whitemountainhealthcenter.com/ White Mountain Health Center 4.7 1664 +178 Whoa Qc Inc Whoa Qc Inc whoa-qc-inc 5558 W. Bell Rd. Glendale AZ 85308 6025350999 \N Operating · Marijuana Facility · 602-535-0999 https://azcarecheck.azdhs.gov/s/?name=Whoa%20Qc%20Inc \N \N 2025-11-17 07:29:34.432825 2025-11-17 14:46:36.102624 https://curaleaf.com/stores/curaleaf-az-glendale-east?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Glendale East 4.8 3990 +2 The Mint Dispensary 4245 Investments Llc the-mint-dispensary 330 E Southern Ave #37 Mesa AZ 85210 4806641470 \N Operating · Marijuana Facility · (480) 664-1470 https://azcarecheck.azdhs.gov/s/?name=The%20Mint%20Dispensary \N \N 2025-11-17 07:29:34.191912 2025-11-17 14:46:35.917041 https://mintdeals.com/mesa-az/ Mint Cannabis - Mesa 4.7 5993 +6 Trulieve of Tatum Abedon Saiz Llc trulieve-of-tatum 16635 N tatum blvd, 110 Phoenix AZ 85032 7703300831 \N Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tatum \N \N 2025-11-17 07:29:34.199358 2025-11-17 14:46:35.926015 https://www.trulieve.com/dispensaries/arizona/phoenix-tatum?utm_source=gmb&utm_medium=organic&utm_campaign=phoenix-tatum Trulieve Phoenix Dispensary Tatum 4.4 194 +12 Apache County Dispensary LLC Apache County Dispensary LLC apache-county-dispensary-llc 900 East Main Street Springerville AZ 85938 6209215967 \N Operating · Marijuana Establishment · 620-921-5967 https://azcarecheck.azdhs.gov/s/?name=Apache%20County%20Dispensary%20LLC \N \N 2025-11-17 07:29:34.209719 2025-11-17 14:46:35.929078 https://keycannabis.com/shop/springerville-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=springerville Key Cannabis Dispensary Springerville 4.7 126 +25 Zen Leaf Chandler AZGM 3, LLC zen-leaf-chandler 7200 W Chandler Blvd Ste 7 Chandler AZ 85226 3128195061 \N Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Chandler \N \N 2025-11-17 07:29:34.226944 2025-11-17 14:46:35.937725 https://zenleafdispensaries.com/locations/chandler/?utm_campaign=az-chandler&utm_medium=gbp&utm_source=google Zen Leaf Dispensary Chandler 4.8 3044 +30 The Flower Shop Az Buds & Roses, Inc the-flower-shop-az 5205 E University Dr Mesa AZ 85205 4805005054 \N Operating · Marijuana Facility · (480) 500-5054 https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az \N \N 2025-11-17 07:29:34.233765 2025-11-17 14:46:35.941442 https://theflowershopusa.com/mesa?utm_source=google-business&utm_medium=organic The Flower Shop - Mesa 4.4 2604 +35 Sunday Goods Cardinal Square, Inc sunday-goods 13150 W Bell Rd Surprise AZ 85378 5208083111 \N Operating · Marijuana Facility · 520-808-3111 https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods \N \N 2025-11-17 07:29:34.240675 2025-11-17 14:46:35.946795 https://sundaygoods.com/location/sunday-goods-surprise-az-cannabis-dispensary/?utm_source=google&utm_medium=gbp&utm_campaign=surprise_gbp Sunday Goods Surprise 4.4 13 +41 Curious Cultivators I LLC Curious Cultivators I LLC curious-cultivators-i-llc 200 London Bridge Road, 100 Lake Havasu City AZ 86403 3106944397 \N Operating · Marijuana Establishment · (310) 694-4397 https://azcarecheck.azdhs.gov/s/?name=Curious%20Cultivators%20I%20LLC \N \N 2025-11-17 07:29:34.248815 2025-11-17 14:46:35.951009 https://storycannabis.com/dispensary-locations/arizona/havasu-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=lake_havasu&utm_term=click&utm_content=website Story Cannabis Dispensary Lake Havasu 4.7 403 +49 East Valley Patient Wellness Group Inc East Valley Patient Wellness Group Inc east-valley-patient-wellness-group-inc 13650 N 99th Ave Sun City AZ 85351 6232468080 \N Operating · Marijuana Facility · (623) 246-8080 https://azcarecheck.azdhs.gov/s/?name=East%20Valley%20Patient%20Wellness%20Group%20Inc \N \N 2025-11-17 07:29:34.259555 2025-11-17 14:46:35.957836 https://www.livewithsol.com/sun-city-dispensary?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing Sol Flower Dispensary Sun City 4.6 1115 +55 Trulieve of Tucson Fort Mountain Consulting, Llc trulieve-of-tucson 4659 E 22nd St Tucson AZ 85711 7703300831 \N Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson \N \N 2025-11-17 07:29:34.266746 2025-11-17 14:46:35.965944 https://www.trulieve.com/dispensaries/arizona/tucson-toumey-park?utm_source=gmb&utm_medium=organic&utm_campaign=tucson-swan Trulieve Tucson Dispensary 4.6 1169 +59 JARS Cannabis Gila Dreams X, LLC jars-cannabis 100 East State Highway 260 Payson AZ 85541 9284742420 \N Operating · Marijuana Establishment · 928-474-2420 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.271592 2025-11-17 14:46:35.970726 https://jarscannabis.com/ JARS Cannabis Payson 4.9 3259 +60 Earth's Healing North Globe Farmacy Inc earth-s-healing-north 78 W River Rd Tucson AZ 85704 5209096612 \N Operating · Marijuana Facility · (520) 909-6612 https://azcarecheck.azdhs.gov/s/?name=Earth's%20Healing%20North \N \N 2025-11-17 07:29:34.272932 2025-11-17 14:46:35.972738 https://earthshealing.org/ Earth's Healing North 4.8 7149 +65 Trulieve Bisbee Dispensary Green Sky Patient Center Of Scottsdale North Inc trulieve-bisbee-dispensary 1191 S Naco Hwy Bisbee AZ 85603 8505597734 \N Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Bisbee%20Dispensary \N \N 2025-11-17 07:29:34.279003 2025-11-17 14:46:35.977396 https://www.trulieve.com/dispensaries/arizona/?utm_source=gmb&utm_medium=organic&utm_campaign=bisbee Trulieve Bisbee Dispensary 3.5 13 +72 Herbal Wellness Center Inc Herbal Wellness Center Inc herbal-wellness-center-inc 4126 W Indian School Rd Phoenix AZ 85019 6029104152 \N Operating · Marijuana Facility · 602-910-4152 https://azcarecheck.azdhs.gov/s/?name=Herbal%20Wellness%20Center%20Inc \N \N 2025-11-17 07:29:34.287415 2025-11-17 14:46:35.983321 https://herbalwellnesscenter.com/ Herbal Wellness Center West 4.3 2364 +77 Holistic Patient Wellness Group Holistic Patient Wellness Group holistic-patient-wellness-group 1322 N Mcclintock Dr Tempe AZ 85281 4807956363 \N Operating · Marijuana Facility · (480) 795-6363 https://azcarecheck.azdhs.gov/s/?name=Holistic%20Patient%20Wellness%20Group \N \N 2025-11-17 07:29:34.294687 2025-11-17 14:46:35.988741 https://www.livewithsol.com/tempe-mcclintock-dispensary?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing Sol Flower Dispensary McClintock 4.7 2789 +87 L1 Management, Llc L1 Management, Llc l1-management-llc 1525 N. Granite Reef Rd. Scottsdale AZ 85257 6026168167 \N Operating · Marijuana Laboratory · (602) 616-8167 https://azcarecheck.azdhs.gov/s/?name=L1%20Management%2C%20Llc \N \N 2025-11-17 07:29:34.307196 2025-11-17 14:46:35.997091 https://levelonelabs.com/ Level One Labs 5.0 7 +97 MCD-SE Venture 26, LLC MCD-SE Venture 26, LLC mcd-se-venture-26-llc 15235 North Dysart Road, 11C El Mirage AZ 85335 6029313663 \N Operating · Marijuana Establishment · 602-931-3663 https://azcarecheck.azdhs.gov/s/?name=MCD-SE%20Venture%2026%2C%20LLC \N \N 2025-11-17 07:29:34.320101 2025-11-17 14:46:36.003255 https://mintdeals.com/az-el-mirage/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps Mint Cannabis Dispensary - EL MIRAGE 4.7 249 +103 JARS Cannabis Mohave Cannabis Club 2, LLC jars-cannabis 20340 N Lake Pleasant Rd. Ste 107 Peoria AZ 85382 6232461065 \N Operating · Marijuana Facility · 623-246-1065 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N 2025-11-17 07:29:34.328893 2025-11-17 14:46:36.011974 https://jarscannabis.com/ JARS Cannabis Peoria 4.8 2462 +107 Natural Herbal Remedies Inc Natural Herbal Remedies Inc natural-herbal-remedies-inc 3333 S Central Ave Phoenix AZ 85040 4807390366 \N Operating · Marijuana Facility · 480-739-0366 https://azcarecheck.azdhs.gov/s/?name=Natural%20Herbal%20Remedies%20Inc \N \N 2025-11-17 07:29:34.334407 2025-11-17 14:46:36.016863 https://curaleaf.com/stores/curaleaf-dispensary-central?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Central Phoenix 4.6 2577 +112 The Flower Shop Az Nature's Healing Center Inc the-flower-shop-az 10827 S 51st St, Ste 104 Phoenix AZ 85044 4805005054 \N Operating · Marijuana Facility · (480) 500-5054 https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az \N \N 2025-11-17 07:29:34.341296 2025-11-17 14:46:36.023538 https://theflowershopusa.com/ahwatukee?utm_source=google-business&utm_medium=organic The Flower Shop - Ahwatukee 4.4 1291 +114 Nature's Wonder Inc Nature's Wonder Inc nature-s-wonder-inc 260 W Apache Trail Dr Apache Junction AZ 85120 4808613649 \N Operating · Marijuana Facility · (480) 861-3649 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Wonder%20Inc \N \N 2025-11-17 07:29:34.344384 2025-11-17 14:46:36.025171 https://natureswonderaz.com/apache-junction-dispensary-menu-recreational Nature's Wonder Apache Junction Dispensary 4.7 1891 +119 Trulieve of Glendale Pahana, Inc. trulieve-of-glendale 13631 N 59th Ave, Ste B110 Glendale AZ 85304 8505080261 \N Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Glendale \N \N 2025-11-17 07:29:34.351103 2025-11-17 14:46:36.03016 https://www.trulieve.com/dispensaries/arizona/glendale?utm_source=gmb&utm_medium=organic&utm_campaign=glendale Trulieve Glendale Dispensary 4.2 599 +125 Phytotherapeutics Of Tucson Phytotherapeutics Of Tucson phytotherapeutics-of-tucson 2175 N 83rd Ave Phoenix AZ 85035 6232445349 \N Operating · Marijuana Facility · 623-244-5349 https://azcarecheck.azdhs.gov/s/?name=Phytotherapeutics%20Of%20Tucson \N \N 2025-11-17 07:29:34.35978 2025-11-17 14:46:36.033997 https://curaleaf.com/stores/curaleaf-dispensary-pavilions?utm_source=google&utm_medium=organic&utm_campaign=gmb-menu Curaleaf Dispensary Pavilions 4.5 2239 +130 Pp Wellness Center Pp Wellness Center pp-wellness-center 8160 W Union Hills Dr Ste A106 Glendale AZ 85308 6233851310 \N Operating · Marijuana Facility · (623) 385-1310 https://azcarecheck.azdhs.gov/s/?name=Pp%20Wellness%20Center \N \N 2025-11-17 07:29:34.366362 2025-11-17 14:46:36.04065 https://curaleaf.com/stores/curaleaf-dispensary-glendale?utm_source=google&utm_medium=organic&utm_campaign=gmb-menu Curaleaf Dispensary Glendale 4.6 2166 +138 S Flower SE 3 Inc. S Flower SE 3 Inc. s-flower-se-3-inc- 4837 North 1st Avenue, Ste 102 Tucson AZ 85718 6028287204 \N Operating · Marijuana Establishment · 602-828-7204 https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20SE%203%20Inc. \N \N 2025-11-17 07:29:34.377846 2025-11-17 14:46:36.0486 https://www.livewithsol.com/locations/north-tucson/?utm_source=gmb&utm_medium=organic Sol Flower Dispensary North Tucson 4.9 2080 +148 Smithers CTS Arizona LLC Smithers CTS Arizona LLC smithers-cts-arizona-llc 734 W Highland Avenue, 2nd floor Phoenix AZ 85013 9546967791 \N Operating · Marijuana Laboratory · (954) 696-7791 https://azcarecheck.azdhs.gov/s/?name=Smithers%20CTS%20Arizona%20LLC \N \N 2025-11-17 07:29:34.391643 2025-11-17 14:46:36.059222 https://www.smithers.com/industries/cannabis-testing/contact-us/arizona-cannabis-testing-services Smithers Cannabis Testing Services Arizona 5.0 3 +165 The Kind Relief Inc The Kind Relief Inc the-kind-relief-inc 18423 E San Tan Blvd Ste #1 Queen Creek AZ 85142 4805509121 \N Operating · Marijuana Facility · 480-550-9121 https://azcarecheck.azdhs.gov/s/?name=The%20Kind%20Relief%20Inc \N \N 2025-11-17 07:29:34.415378 2025-11-17 14:46:36.081927 https://curaleaf.com/stores/curaleaf-az-queen-creek?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Queen Creek 4.7 4110 +7 Absolute Health Care Inc Absolute Health Care Inc absolute-health-care-inc 175 S Hamilton Pl Bldg 4 Ste 110 Gilbert AZ 85233 4803610078 \N Operating · Marijuana Facility · 480-361-0078 https://azcarecheck.azdhs.gov/s/?name=Absolute%20Health%20Care%20Inc \N \N 2025-11-17 07:29:34.200939 2025-11-17 14:46:36.087793 https://curaleaf.com/stores/curaleaf-dispensary-gilbert?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Gilbert 4.6 3423 +27 Blue Palo Verde 1, LLC Blue Palo Verde 1, LLC blue-palo-verde-1-llc 7710 South Wilmot Road, Suite 100 Tucson AZ 85756 5868556649 \N Operating · Marijuana Establishment · 586-855-6649 https://azcarecheck.azdhs.gov/s/?name=Blue%20Palo%20Verde%201%2C%20LLC \N \N 2025-11-17 07:29:34.229802 2025-11-17 14:46:36.091726 https://thegreenhalo.com/ Halo Cannabis 4.4 1580 +31 Trulieve of Scottsdale Dispensary Byers Dispensary Inc trulieve-of-scottsdale-dispensary 15190 N Hayden Rd Scottsdale AZ 85260 8505080261 \N Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Scottsdale%20Dispensary \N \N 2025-11-17 07:29:34.235227 2025-11-17 14:46:36.09487 https://www.trulieve.com/dispensaries/arizona/scottsdale?utm_source=gmb&utm_medium=organic&utm_campaign=scottsdale Trulieve Scottsdale Dispensary 4.3 819 +47 Nature's Wonder DYNAMIC TRIO HOLDINGS LLC nature-s-wonder 6812 East Cave Creek Road, 2, 2A and 3 Cave Creek AZ 85331 4808613649 \N Operating · Marijuana Establishment · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Wonder \N \N 2025-11-17 07:29:34.257086 2025-11-17 14:46:36.109985 https://natureswonderaz.com/cave-creek-dispensary-menu-recreational Nature's Wonder Cave Creek Dispensary 4.5 453 +57 Farm Fresh Fwa Inc farm-fresh 790 N Lake Havasu Ave #4 Lake Havasu City AZ 86404 9287336339 \N Operating · Marijuana Facility · (928) 733-6339 https://azcarecheck.azdhs.gov/s/?name=Farm%20Fresh \N \N 2025-11-17 07:29:34.269021 2025-11-17 14:46:36.114094 http://farmfreshdispensary.com/ Farm Fresh Medical/Recreational Marijuana Dispensary 4.6 642 +67 Marigold Dispensary Greens Goddess Products, Inc marigold-dispensary 2601 W. Dunlap Avenue, 21 Phoenix AZ 85017 6029004557 \N Operating · Marijuana Facility · (602) 900-4557 https://azcarecheck.azdhs.gov/s/?name=Marigold%20Dispensary \N \N 2025-11-17 07:29:34.281424 2025-11-17 14:46:36.11941 https://keycannabis.com/shop/phoenix-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=phoenix Key Cannabis Dispensary Phoenix 4.7 2664 +80 Joint Junkies I LLC Joint Junkies I LLC joint-junkies-i-llc 26427 S Arizona Ave Chandler AZ 85248 9286385831 \N Operating · Marijuana Establishment · (928) 638-5831 https://azcarecheck.azdhs.gov/s/?name=Joint%20Junkies%20I%20LLC \N \N 2025-11-17 07:29:34.298382 2025-11-17 14:46:36.122862 https://storycannabis.com/dispensary-locations/arizona/south-chandler-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=south_chandler&utm_term=click&utm_content=website Story Cannabis Dispensary South Chandler 4.4 1227 +101 Nirvana Center Phoenix Mmj Apothecary nirvana-center-phoenix 9240 West Northern Avenue, Ste. 103b Peoria AZ 85345 9286848880 \N Operating · Marijuana Facility · (928) 684-8880 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Phoenix \N \N 2025-11-17 07:29:34.326102 2025-11-17 14:46:36.128403 https://www.pondyaz.com/locations Ponderosa Dispensary Glendale 4.5 1057 +110 Natural Remedy Patient Center Natural Remedy Patient Center natural-remedy-patient-center 16277 N Greenway Hayden Loop, 1st Floor Scottsdale AZ 85260 6028420020 \N Operating · Marijuana Facility · 602-842-0020 https://azcarecheck.azdhs.gov/s/?name=Natural%20Remedy%20Patient%20Center \N \N 2025-11-17 07:29:34.338426 2025-11-17 14:46:36.13068 https://curaleaf.com/stores/curaleaf-dispensary-scottsdale?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu Curaleaf Dispensary Scottsdale 4.6 849 +115 Nirvana Center Dispensaries Nirvana Enterprises AZ, LLC nirvana-center-dispensaries 2 North 35th Avenue Phoenix AZ 85009 4803786917 \N Operating · Marijuana Facility · (480) 378-6917 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Dispensaries \N \N 2025-11-17 07:29:34.345817 2025-11-17 14:46:36.132882 https://www.backpackboyz.com/content/arizona Backpack Boyz - Phoenix 4.8 6915 +124 Phoenix Relief Center Inc Phoenix Relief Center Inc phoenix-relief-center-inc 6330 S 35th Ave, Ste 104 Phoenix AZ 85041 6022763401 \N Operating · Marijuana Facility · (602) 276-3401 https://azcarecheck.azdhs.gov/s/?name=Phoenix%20Relief%20Center%20Inc \N \N 2025-11-17 07:29:34.358314 2025-11-17 14:46:36.135072 https://sundaygoods.com/location/dispensary-laveen-phoenix-az/?utm_source=google&utm_medium=gbp&utm_campaign=prc_sundaygoods PRC by Sunday Goods 4.6 1691 +135 S Flower N Phoenix, Inc. S Flower N Phoenix, Inc. s-flower-n-phoenix-inc- 3217 E Shea Blvd, Suite 1 A Phoenix AZ 85028 6235820436 \N Operating · Marijuana Facility · (623) 582-0436 https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20N%20Phoenix%2C%20Inc. \N \N 2025-11-17 07:29:34.373746 2025-11-17 14:46:36.139089 https://www.livewithsol.com/deer-valley-dispensary/?utm_source=gmb&utm_medium=organic Sol Flower Dispensary 32nd & Shea 4.7 570 +145 Nature's Medicines Sixth Street Enterprises Inc nature-s-medicines 6840 West Grand Ave Glendale AZ 85301 6233018478 \N Operating · Marijuana Facility · (623) 301-8478 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N 2025-11-17 07:29:34.387578 2025-11-17 14:46:36.143867 https://storycannabis.com/dispensary-locations/arizona/glendale-grand-ave-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=grand_glendale&utm_term=click&utm_content=website Story Cannabis Dispensary Grand Glendale 4.5 3046 +154 Trulieve of Apache Junction Dispensary Svaccha, Llc trulieve-of-apache-junction-dispensary 1985 W Apache Trail Ste 4 Apache Junction AZ 85120 9548172370 \N Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Apache%20Junction%20Dispensary \N \N 2025-11-17 07:29:34.399827 2025-11-17 14:46:36.147668 https://www.trulieve.com/dispensaries/arizona/apache-junction?utm_source=gmb&utm_medium=organic&utm_campaign=apache-junction Trulieve Apache Junction Dispensary 4.3 408 +157 Trulieve of Mesa North Dispensary Sweet 5, Llc trulieve-of-mesa-north-dispensary 1150 W McLellan Rd Mesa AZ 85201 7703300831 \N Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Mesa%20North%20Dispensary \N \N 2025-11-17 07:29:34.40414 2025-11-17 14:46:36.149211 https://www.trulieve.com/dispensaries/arizona/mesa-north?utm_source=gmb&utm_medium=organic&utm_campaign=north-mesa Trulieve Mesa Dispensary North 4.3 600 +\. + + +-- +-- Data for Name: batch_history; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.batch_history (id, product_id, thc_percentage, cbd_percentage, terpenes, strain_type, recorded_at) FROM stdin; +150 2067 29.07 \N {} \N 2025-11-18 03:16:27.275158 +\. + + +-- +-- Data for Name: brand_history; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.brand_history (id, dispensary_id, brand_name, event_type, event_at, product_count, metadata) FROM stdin; +\. + + +-- +-- Data for Name: brand_scrape_jobs; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.brand_scrape_jobs (id, dispensary_id, brand_slug, brand_name, status, worker_id, started_at, completed_at, products_found, products_saved, error_message, retry_count, created_at, updated_at) FROM stdin; +75 112 stiiizy STIIIZY completed W5 2025-11-18 04:25:53.901713 2025-11-18 04:26:27.684424 4 4 \N 0 2025-11-18 03:46:29.518009 2025-11-18 04:26:27.684424 +1 112 the-essence (the) Essence completed W1 2025-11-18 04:14:02.106137 2025-11-18 04:14:38.241988 1 1 \N 0 2025-11-18 03:46:29.400943 2025-11-18 04:14:38.241988 +2 112 abundant-organics Abundant Organics completed W1 2025-11-18 04:14:40.246589 2025-11-18 04:15:14.096187 7 7 \N 0 2025-11-18 03:46:29.407714 2025-11-18 04:15:14.096187 +17 112 connected-cannabis Connected Cannabis completed W1 2025-11-18 04:18:14.898101 2025-11-18 04:18:48.522999 8 8 \N 0 2025-11-18 03:46:29.431821 2025-11-18 04:18:48.522999 +81 112 tipsy-turtle Tipsy Turtle completed W1 2025-11-18 04:26:35.855262 2025-11-18 04:27:09.479535 7 7 \N 0 2025-11-18 03:46:29.52645 2025-11-18 04:27:09.479535 +26 112 drip drip completed W2 2025-11-18 04:19:29.576761 2025-11-18 04:20:03.384747 3 3 \N 0 2025-11-18 03:46:29.444823 2025-11-18 04:20:03.384747 +43 112 i-o-extracts I.O. Extracts completed W3 2025-11-18 04:22:08.184067 2025-11-18 04:22:41.805 17 17 \N 0 2025-11-18 03:46:29.469954 2025-11-18 04:22:41.805 +44 112 jeeter Jeeter completed W4 2025-11-18 04:22:17.03744 2025-11-18 04:22:50.704615 10 10 \N 0 2025-11-18 03:46:29.471216 2025-11-18 04:22:50.704615 +45 112 keef Keef completed W5 2025-11-18 04:22:17.156205 2025-11-18 04:22:53.001298 10 10 \N 0 2025-11-18 03:46:29.472538 2025-11-18 04:22:53.001298 +33 112 green-dot-labs Green Dot Labs completed W1 2025-11-18 04:20:37.632642 2025-11-18 04:21:11.222859 2 2 \N 0 2025-11-18 03:46:29.45554 2025-11-18 04:21:11.222859 +46 112 leafers Leafers completed W1 2025-11-18 04:22:24.511673 2025-11-18 04:22:59.117183 6 6 \N 0 2025-11-18 03:46:29.474076 2025-11-18 04:22:59.117183 +34 112 grow-sciences Grow Sciences completed W2 2025-11-18 04:20:40.99174 2025-11-18 04:21:14.590476 11 11 \N 0 2025-11-18 03:46:29.457168 2025-11-18 04:21:14.590476 +50 112 mac-pharms Mac Pharms completed W5 2025-11-18 04:22:55.006174 2025-11-18 04:23:28.81776 7 7 \N 0 2025-11-18 03:46:29.48044 2025-11-18 04:23:28.81776 +35 112 gron Grön completed W3 2025-11-18 04:20:56.908166 2025-11-18 04:21:30.513036 13 13 \N 0 2025-11-18 03:46:29.45854 2025-11-18 04:21:30.513036 +83 112 tropics Tropics completed W3 2025-11-18 04:26:53.735901 2025-11-18 04:27:27.540672 6 6 \N 0 2025-11-18 03:46:29.529227 2025-11-18 04:27:27.540672 +84 112 tru-infusion TRU Infusion completed W4 2025-11-18 04:27:04.778625 2025-11-18 04:27:38.527908 10 10 \N 0 2025-11-18 03:46:29.530524 2025-11-18 04:27:38.527908 +9 112 bud-bros Bud Bros completed W3 2025-11-18 04:16:45.499636 2025-11-18 04:17:19.737935 16 16 \N 0 2025-11-18 03:46:29.420128 2025-11-18 04:50:06.399597 +10 112 cake Cake completed W1 2025-11-18 04:17:03.501248 2025-11-18 04:17:37.189179 4 4 \N 0 2025-11-18 03:46:29.421484 2025-11-18 04:50:06.406623 +59 112 on-the-rocks On The Rocks completed W4 2025-11-18 04:24:03.984374 2025-11-18 04:24:38.937062 1 1 \N 0 2025-11-18 03:46:29.494641 2025-11-18 04:24:38.937062 +21 112 deeply-rooted Deeply Rooted completed W1 2025-11-18 04:18:50.527787 2025-11-18 04:19:24.226809 44 44 \N 0 2025-11-18 03:46:29.437421 2025-11-18 04:50:06.423098 +22 112 dermafreeze Dermafreeze completed W2 2025-11-18 04:18:53.979487 2025-11-18 04:19:27.570781 4 4 \N 0 2025-11-18 03:46:29.438779 2025-11-18 04:50:06.426968 +36 112 halo-cannabis Halo Cannabis completed W4 2025-11-18 04:21:05.601341 2025-11-18 04:21:39.181784 1 1 \N 0 2025-11-18 03:46:29.459922 2025-11-18 04:50:06.439475 +37 112 halo-infusions Halo Infusions completed W1 2025-11-18 04:21:13.228672 2025-11-18 04:21:46.854829 11 11 \N 0 2025-11-18 03:46:29.4613 2025-11-18 04:50:06.442628 +38 112 hash-factory Hash Factory completed W2 2025-11-18 04:21:16.595583 2025-11-18 04:21:50.14148 1 1 \N 0 2025-11-18 03:46:29.462899 2025-11-18 04:50:06.446172 +76 112 sublime-brands Sublime Brands completed W1 2025-11-18 04:26:00.274607 2025-11-18 04:26:33.851524 1 1 \N 0 2025-11-18 03:46:29.519329 2025-11-18 04:26:33.851524 +78 112 the-pharm The Pharm completed W3 2025-11-18 04:26:18.019906 2025-11-18 04:26:51.731201 14 14 \N 0 2025-11-18 03:46:29.522272 2025-11-18 04:26:51.731201 +39 112 high-rollin-cannabis High Rollin Cannabis completed W3 2025-11-18 04:21:32.518319 2025-11-18 04:22:06.179344 1 1 \N 0 2025-11-18 03:46:29.464302 2025-11-18 04:50:06.449065 +40 112 high-west-farms High West Farms completed W4 2025-11-18 04:21:41.185847 2025-11-18 04:22:15.034666 3 3 \N 0 2025-11-18 03:46:29.465743 2025-11-18 04:50:06.451755 +41 112 high-mart HighMart completed W1 2025-11-18 04:21:48.85662 2025-11-18 04:22:22.507822 3 3 \N 0 2025-11-18 03:46:29.467267 2025-11-18 04:50:06.454262 +42 112 hot-rod Hot Rod completed W2 2025-11-18 04:21:52.146224 2025-11-18 04:22:25.653911 1 1 \N 0 2025-11-18 03:46:29.468594 2025-11-18 04:50:06.456627 +55 112 mr-honey Mr. Honey completed W5 2025-11-18 04:23:30.822956 2025-11-18 04:24:04.73329 13 13 \N 0 2025-11-18 03:46:29.48862 2025-11-18 04:50:06.465377 +69 112 simply-twisted Simply Twisted completed W4 2025-11-18 04:25:17.698928 2025-11-18 04:25:51.428771 3 3 \N 0 2025-11-18 03:46:29.509947 2025-11-18 04:50:06.476178 +77 112 the-healing-alchemist The Healing Alchemist completed W2 2025-11-18 04:26:03.144608 2025-11-18 04:26:36.898874 5 5 \N 0 2025-11-18 03:46:29.520696 2025-11-18 04:50:06.484154 +79 112 the-strain-source-tss The Strain Source (TSS) completed W4 2025-11-18 04:26:29.162408 2025-11-18 04:27:02.774913 3 3 \N 0 2025-11-18 03:46:29.52359 2025-11-18 04:50:06.486865 +80 112 thunder-bud Thunder Bud completed W5 2025-11-18 04:26:29.688678 2025-11-18 04:27:08.36237 14 14 \N 0 2025-11-18 03:46:29.525052 2025-11-18 04:50:06.489634 +82 112 trip Trip completed W2 2025-11-18 04:26:38.903365 2025-11-18 04:27:12.524496 1 1 \N 0 2025-11-18 03:46:29.527735 2025-11-18 04:50:06.492848 +85 112 varz Varz completed W5 2025-11-18 04:27:10.366717 2025-11-18 04:27:44.054841 21 21 \N 0 2025-11-18 03:46:29.531797 2025-11-18 04:50:06.495717 +65 112 select Select completed TEST1 2025-11-18 05:07:06.144562 2025-11-18 05:08:07.878845 8 8 \N 0 2025-11-18 03:46:29.504128 2025-11-18 05:08:07.878845 +87 112 wana Wana completed W2 2025-11-18 04:27:14.530332 2025-11-18 04:27:48.260624 13 13 \N 0 2025-11-18 03:46:29.534424 2025-11-18 04:27:48.260624 +88 112 wizard-trees Wizard Trees completed W3 2025-11-18 04:27:29.545206 2025-11-18 04:28:03.171652 2 2 \N 0 2025-11-18 03:46:29.535818 2025-11-18 04:28:03.171652 +89 112 wyld Wyld completed W4 2025-11-18 04:27:40.532172 2025-11-18 04:28:14.318239 11 11 \N 0 2025-11-18 03:46:29.537379 2025-11-18 04:28:14.318239 +3 112 achieve Achieve completed W1 2025-11-18 04:15:16.100727 2025-11-18 04:15:50.018595 6 6 \N 0 2025-11-18 03:46:29.40974 2025-11-18 04:50:06.377863 +5 112 aloha-tymemachine Aloha Tymemachine completed W2 2025-11-18 04:15:53.510641 2025-11-18 04:16:28.938863 4 4 \N 0 2025-11-18 03:46:29.413497 2025-11-18 04:50:06.387697 +6 112 asylum Asylum completed W3 2025-11-18 04:16:02.297201 2025-11-18 04:16:43.494498 1 1 \N 0 2025-11-18 03:46:29.415252 2025-11-18 04:50:06.391493 +7 112 barrio-cannabis-co Barrio Cannabis Co. completed W1 2025-11-18 04:16:27.851087 2025-11-18 04:17:01.495857 4 4 \N 0 2025-11-18 03:46:29.417128 2025-11-18 04:50:06.395143 +47 112 legends Legends completed W2 2025-11-18 04:22:27.65773 2025-11-18 04:23:01.364898 13 13 \N 0 2025-11-18 03:46:29.475681 2025-11-18 04:23:01.364898 +18 112 cq CQ completed W2 2025-11-18 04:18:18.305125 2025-11-18 04:18:51.97498 2 2 \N 0 2025-11-18 03:46:29.43325 2025-11-18 04:50:06.403531 +13 112 cannabish Cannabish completed W1 2025-11-18 04:17:39.194499 2025-11-18 04:18:12.894267 1 1 \N 0 2025-11-18 03:46:29.425801 2025-11-18 04:50:06.409656 +14 112 chill-pill Chill Pill completed W2 2025-11-18 04:17:42.419255 2025-11-18 04:18:16.301691 9 9 \N 0 2025-11-18 03:46:29.427353 2025-11-18 04:50:06.412696 +16 112 collective Collective completed W4 2025-11-18 04:18:04.185338 2025-11-18 04:18:40.773166 1 1 \N 0 2025-11-18 03:46:29.430327 2025-11-18 04:50:06.416303 +19 112 cure-injoy Cure Injoy completed W3 2025-11-18 04:18:33.525542 2025-11-18 04:19:07.18283 5 5 \N 0 2025-11-18 03:46:29.434551 2025-11-18 04:50:06.420072 +25 112 dr-zodiak Dr. Zodiak completed W1 2025-11-18 04:19:26.230645 2025-11-18 04:19:59.954247 13 13 \N 0 2025-11-18 03:46:29.443433 2025-11-18 04:50:06.429962 +51 112 mad-terp-labs Mad Terp Labs completed W1 2025-11-18 04:23:01.121561 2025-11-18 04:23:34.807109 8 8 \N 0 2025-11-18 03:46:29.482128 2025-11-18 04:23:34.807109 +27 112 drip-oils Drip Oils completed W3 2025-11-18 04:19:45.7334 2025-11-18 04:20:19.280126 12 12 \N 0 2025-11-18 03:46:29.446683 2025-11-18 04:50:06.433428 +28 112 easy-tiger Easy Tiger completed W4 2025-11-18 04:19:54.234551 2025-11-18 04:20:27.91401 6 6 \N 0 2025-11-18 03:46:29.448163 2025-11-18 04:50:06.436656 +52 112 made Made completed W2 2025-11-18 04:23:03.369148 2025-11-18 04:23:37.310313 4 4 \N 0 2025-11-18 03:46:29.483767 2025-11-18 04:23:37.310313 +48 112 lost-dutchmen Lost Dutchmen completed W3 2025-11-18 04:22:43.809388 2025-11-18 04:23:17.51929 1 1 \N 0 2025-11-18 03:46:29.477471 2025-11-18 04:50:06.459299 +49 112 lunch-box Lunch Box completed W4 2025-11-18 04:22:52.709372 2025-11-18 04:23:26.319126 4 4 \N 0 2025-11-18 03:46:29.478813 2025-11-18 04:50:06.462491 +53 112 mellow-vibes Mellow Vibes completed W3 2025-11-18 04:23:19.524863 2025-11-18 04:23:53.273245 3 3 \N 0 2025-11-18 03:46:29.48541 2025-11-18 04:23:53.273245 +56 112 nwd NWD completed W1 2025-11-18 04:23:36.812723 2025-11-18 04:24:10.496013 3 3 \N 0 2025-11-18 03:46:29.490104 2025-11-18 04:50:06.468121 +57 112 ogeez OGEEZ completed W2 2025-11-18 04:23:39.316216 2025-11-18 04:24:12.890776 4 4 \N 0 2025-11-18 03:46:29.491536 2025-11-18 04:50:06.470781 +66 112 session Session completed W1 2025-11-18 04:24:48.544775 2025-11-18 04:25:22.2373 5 5 \N 0 2025-11-18 03:46:29.505877 2025-11-18 04:50:06.473563 +54 112 mfused Mfused completed W4 2025-11-18 04:23:28.324351 2025-11-18 04:24:01.97912 15 15 \N 0 2025-11-18 03:46:29.487151 2025-11-18 04:24:01.97912 +73 112 space-rocks Space Rocks completed W3 2025-11-18 04:25:42.393785 2025-11-18 04:26:16.01797 10 10 \N 0 2025-11-18 03:46:29.515386 2025-11-18 04:50:06.478659 +74 112 sticky-saguaro Sticky Saguaro completed W4 2025-11-18 04:25:53.433793 2025-11-18 04:26:27.156824 20 20 \N 0 2025-11-18 03:46:29.516621 2025-11-18 04:50:06.481094 +86 112 vortex Vortex completed W1 2025-11-18 04:27:11.483805 2025-11-18 04:27:45.215893 10 10 \N 0 2025-11-18 03:46:29.533102 2025-11-18 04:50:06.498191 +90 112 yam-yams Yam Yams completed W5 2025-11-18 04:27:46.059689 2025-11-18 04:28:19.682756 1 1 \N 0 2025-11-18 03:46:29.538981 2025-11-18 04:50:06.500969 +58 112 o-geez OGeez! completed W3 2025-11-18 04:23:55.277124 2025-11-18 04:24:28.896768 8 8 \N 0 2025-11-18 03:46:29.493192 2025-11-18 04:24:28.896768 +60 112 papas-herb Papa's Herb completed W5 2025-11-18 04:24:06.736295 2025-11-18 04:24:40.490321 6 6 \N 0 2025-11-18 03:46:29.49605 2025-11-18 04:24:40.490321 +61 112 preferred-gardens Preferred Gardens completed W1 2025-11-18 04:24:12.500248 2025-11-18 04:24:46.542611 2 2 \N 0 2025-11-18 03:46:29.497801 2025-11-18 04:24:46.542611 +4 112 alien-labs Alien Labs completed W1 2025-11-18 04:15:52.021777 2025-11-18 04:16:25.846475 6 6 \N 0 2025-11-18 03:46:29.411772 2025-11-18 04:16:25.846475 +62 112 sauce Sauce completed W2 2025-11-18 04:24:14.895373 2025-11-18 04:24:49.109178 9 9 \N 0 2025-11-18 03:46:29.499535 2025-11-18 04:24:49.109178 +8 112 breeze-canna BREEZE Canna completed W2 2025-11-18 04:16:30.943221 2025-11-18 04:17:04.635912 3 3 \N 0 2025-11-18 03:46:29.418686 2025-11-18 04:17:04.635912 +63 112 savvy Savvy completed W3 2025-11-18 04:24:30.900272 2025-11-18 04:25:04.622319 8 8 \N 0 2025-11-18 03:46:29.501109 2025-11-18 04:25:04.622319 +64 112 seed-junky Seed Junky completed W4 2025-11-18 04:24:40.940826 2025-11-18 04:25:15.69781 1 1 \N 0 2025-11-18 03:46:29.502671 2025-11-18 04:25:15.69781 +11 112 camino Camino completed W2 2025-11-18 04:17:06.640977 2025-11-18 04:17:40.414565 1 1 \N 0 2025-11-18 03:46:29.422899 2025-11-18 04:17:40.414565 +12 112 canamo-concentrates Canamo Concentrates completed W3 2025-11-18 04:17:21.742658 2025-11-18 04:17:55.631825 31 31 \N 0 2025-11-18 03:46:29.424246 2025-11-18 04:17:55.631825 +67 112 session-cannabis-co Session Cannabis Co. completed W2 2025-11-18 04:24:51.113402 2025-11-18 04:25:25.34323 2 2 \N 0 2025-11-18 03:46:29.507272 2025-11-18 04:25:25.34323 +15 112 clout-king Clout King completed W3 2025-11-18 04:17:57.636499 2025-11-18 04:18:31.52228 1 1 \N 0 2025-11-18 03:46:29.428835 2025-11-18 04:18:31.52228 +68 112 shango Shango completed W3 2025-11-18 04:25:06.626401 2025-11-18 04:25:40.388671 3 3 \N 0 2025-11-18 03:46:29.508713 2025-11-18 04:25:40.388671 +20 112 daze-off Daze Off completed W4 2025-11-18 04:18:42.776754 2025-11-18 04:19:16.498791 1 1 \N 0 2025-11-18 03:46:29.435841 2025-11-18 04:19:16.498791 +70 112 sip Sip completed W5 2025-11-18 04:25:18.193275 2025-11-18 04:25:51.897438 9 9 \N 0 2025-11-18 03:46:29.511219 2025-11-18 04:25:51.897438 +23 112 dime-industries Dime Industries completed W3 2025-11-18 04:19:09.187751 2025-11-18 04:19:43.728726 25 25 \N 0 2025-11-18 03:46:29.440203 2025-11-18 04:19:43.728726 +71 112 smokiez-edibles Smokiez Edibles completed W1 2025-11-18 04:25:24.242738 2025-11-18 04:25:58.269987 6 6 \N 0 2025-11-18 03:46:29.512617 2025-11-18 04:25:58.269987 +24 112 doja DOJA completed W4 2025-11-18 04:19:18.502823 2025-11-18 04:19:52.230335 2 2 \N 0 2025-11-18 03:46:29.441824 2025-11-18 04:19:52.230335 +72 112 sonoran-roots Sonoran Roots completed W2 2025-11-18 04:25:27.347882 2025-11-18 04:26:01.140807 1 1 \N 0 2025-11-18 03:46:29.514073 2025-11-18 04:26:01.140807 +29 112 elevate Elevate completed W1 2025-11-18 04:20:01.958315 2025-11-18 04:20:35.62971 8 8 \N 0 2025-11-18 03:46:29.449467 2025-11-18 04:20:35.62971 +30 112 feel-sublime Feel Sublime completed W2 2025-11-18 04:20:05.388822 2025-11-18 04:20:38.987228 4 4 \N 0 2025-11-18 03:46:29.451095 2025-11-18 04:20:38.987228 +31 112 gelato Gelato completed W3 2025-11-18 04:20:21.285185 2025-11-18 04:20:54.904637 2 2 \N 0 2025-11-18 03:46:29.452784 2025-11-18 04:20:54.904637 +32 112 goldsmith-extracts Goldsmith Extracts completed W4 2025-11-18 04:20:29.918115 2025-11-18 04:21:03.596776 28 28 \N 0 2025-11-18 03:46:29.45424 2025-11-18 04:21:03.596776 +\. + + +-- +-- Data for Name: brands; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.brands (id, store_id, name, created_at, updated_at, first_seen_at, last_seen_at, dispensary_id) FROM stdin; +\. + + +-- +-- Data for Name: campaign_products; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.campaign_products (id, campaign_id, product_id, display_order, created_at) FROM stdin; +\. + + +-- +-- Data for Name: campaigns; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.campaigns (id, name, slug, description, display_style, active, start_date, end_date, created_at, updated_at) FROM stdin; +1 Featured Products featured Default featured products campaign grid t \N \N 2025-11-14 19:28:15.688953 2025-11-14 19:28:15.688953 +\. + + +-- +-- Data for Name: categories; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.categories (id, store_id, name, slug, dutchie_url, scrape_enabled, last_scraped_at, created_at, parent_id, display_order, description, path, dispensary_id) FROM stdin; +2 1 Brands brands https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/brands t 2025-12-01 08:12:20.041137 2025-11-14 19:28:15.686922 \N 0 \N brands \N +61 1 Accessories accessories https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/products/accessories t 2025-12-01 08:28:07.137032 2025-11-15 04:55:57.700625 1 0 \N products/accessories \N +1 1 Shop shop https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/shop t 2025-11-30 04:04:39.792483 2025-11-14 19:28:15.684518 \N 0 \N shop \N +84 18 Shop shop https://curaleaf.com/stores/curaleaf-az-48th-street t \N 2025-11-17 02:05:36.421613 \N 0 \N shop \N +86 18 Pre-Rolls pre-rolls https://curaleaf.com/stores/curaleaf-az-48th-street?category=pre-rolls t \N 2025-11-17 02:05:36.421613 84 0 \N shop/pre-rolls \N +87 18 Vaporizers vaporizers https://curaleaf.com/stores/curaleaf-az-48th-street?category=vaporizers t \N 2025-11-17 02:05:36.421613 84 0 \N shop/vaporizers \N +88 18 Concentrates concentrates https://curaleaf.com/stores/curaleaf-az-48th-street?category=concentrates t \N 2025-11-17 02:05:36.421613 84 0 \N shop/concentrates \N +89 18 Edibles edibles https://curaleaf.com/stores/curaleaf-az-48th-street?category=edibles t \N 2025-11-17 02:05:36.421613 84 0 \N shop/edibles \N +90 18 Tinctures tinctures https://curaleaf.com/stores/curaleaf-az-48th-street?category=tinctures t \N 2025-11-17 02:05:36.421613 84 0 \N shop/tinctures \N +91 18 Topicals topicals https://curaleaf.com/stores/curaleaf-az-48th-street?category=topicals t \N 2025-11-17 02:05:36.421613 84 0 \N shop/topicals \N +92 18 Capsules capsules https://curaleaf.com/stores/curaleaf-az-48th-street?category=capsules t \N 2025-11-17 02:05:36.421613 84 0 \N shop/capsules \N +93 18 Accessories accessories https://curaleaf.com/stores/curaleaf-az-48th-street?category=accessories t \N 2025-11-17 02:05:36.421613 84 0 \N shop/accessories \N +58 1 Concentrates concentrates https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/products/concentrates t 2025-12-01 07:18:29.854479 2025-11-15 04:55:57.700625 1 0 \N products/concentrates \N +85 18 Flower flower https://curaleaf.com/stores/curaleaf-az-48th-street?category=flower t 2025-11-17 02:17:25.932088 2025-11-17 02:05:36.421613 84 0 \N shop/flower \N +59 1 Edibles edibles https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/products/edibles t 2025-12-01 07:30:09.762582 2025-11-15 04:55:57.700625 1 0 \N products/edibles \N +56 1 Pre-Rolls pre-rolls https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/products/pre-rolls t 2025-12-01 07:30:12.322984 2025-11-15 04:55:57.700625 1 0 \N products/pre-rolls \N +55 1 Flower flower https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/products/flower t 2025-12-01 07:30:18.941642 2025-11-15 04:55:57.700625 1 0 \N products/flower \N +60 1 Topicals topicals https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/products/topicals t 2025-12-01 07:31:48.897011 2025-11-15 04:55:57.700625 1 0 \N products/topicals \N +3 1 Specials specials https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/specials t 2025-12-01 07:35:46.914608 2025-11-14 19:28:15.687999 \N 0 \N specials \N +57 1 Vaporizers vaporizers https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/products/vaporizers t 2025-12-01 07:41:48.770689 2025-11-15 04:55:57.700625 1 0 \N products/vaporizers \N +\. + + +-- +-- Data for Name: clicks; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.clicks (id, product_id, campaign_id, ip_address, user_agent, referrer, clicked_at) FROM stdin; +\. + + +-- +-- Data for Name: crawl_jobs; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.crawl_jobs (id, store_id, job_type, trigger_type, status, priority, scheduled_at, started_at, completed_at, products_found, products_new, products_updated, error_message, worker_id, metadata, created_at, updated_at, orchestrator_run_id, detection_result, in_stock_count, out_of_stock_count, limited_count, unknown_count, availability_changed_count) FROM stdin; +\. + + +-- +-- Data for Name: crawler_sandboxes; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.crawler_sandboxes (id, dispensary_id, suspected_menu_provider, mode, raw_html_location, screenshot_location, analysis_json, urls_tested, menu_entry_points, detection_signals, status, confidence_score, failure_reason, human_review_notes, created_at, updated_at, analyzed_at, reviewed_at, category, template_name, quality_score, products_extracted, fields_missing, error_count) FROM stdin; +\. + + +-- +-- Data for Name: crawler_schedule; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.crawler_schedule (id, schedule_type, enabled, interval_hours, run_time, description, created_at, updated_at) FROM stdin; +1 global_interval t 4 \N Crawl all stores every N hours 2025-11-30 15:13:49.378973+00 2025-11-30 15:13:49.378973+00 +2 daily_special t \N 00:01:00 Daily specials run at store local midnight 2025-11-30 15:13:49.378973+00 2025-11-30 15:13:49.378973+00 +\. + + +-- +-- Data for Name: crawler_templates; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.crawler_templates (id, provider, name, version, is_active, is_default_for_provider, selector_config, navigation_config, transform_config, validation_rules, test_urls, expected_structure, dispensaries_using, success_rate, last_successful_crawl, last_failed_crawl, notes, created_by, created_at, updated_at, environment) FROM stdin; +2 treez treez_products_v0 1 f f {"type": "api_based", "notes": "Treez API-based scraper - unreliable, sandbox only", "uses_puppeteer": true, "requires_api_key": false, "product_container": "products"} {"entry_paths": ["/menu", "/shop"], "wait_strategy": "networkidle2", "requires_javascript": true} {} {} [] {} 0 0.00 \N \N Treez sandbox template - v0 implementation, needs quality improvement \N 2025-11-30 16:00:01.953487+00 2025-11-30 16:00:01.953487+00 sandbox +1 dutchie dutchie_standard 1 t t {"type": "api_based", "notes": "Dutchie uses GraphQL API, scraped via puppeteer interception", "uses_puppeteer": true, "graphql_endpoint": "/graphql", "product_container": "data.menu.products"} {"age_gate": {"type": "auto_detected", "handled_by_stealth": true}, "entry_paths": ["/menu", "/order", "/embedded-menu", "/products"], "wait_strategy": "networkidle2", "requires_javascript": true} {} {} [] {} 0 0.00 \N \N Default Dutchie template - uses existing scraper-v2 pipeline \N 2025-11-30 15:49:07.150919+00 2025-11-30 16:00:01.953487+00 production +\. + + +-- +-- Data for Name: dispensaries; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.dispensaries (id, azdhs_id, name, company_name, address, city, state, zip, status_line, azdhs_url, latitude, longitude, dba_name, phone, email, website, google_rating, google_review_count, menu_url, scraper_template, scraper_config, last_menu_scrape, menu_scrape_status, slug, created_at, updated_at, menu_provider, menu_provider_confidence, crawler_mode, crawler_status, last_menu_error_at, last_error_message, provider_detection_data, product_provider, product_confidence, product_crawler_mode, last_product_scan_at, product_detection_data, specials_provider, specials_confidence, specials_crawler_mode, last_specials_scan_at, specials_detection_data, brand_provider, brand_confidence, brand_crawler_mode, last_brand_scan_at, brand_detection_data, metadata_provider, metadata_confidence, metadata_crawler_mode, last_metadata_scan_at, metadata_detection_data, provider_type, scrape_enabled, last_crawl_at, next_crawl_at, crawl_status, crawl_error, consecutive_failures, total_crawls, successful_crawls) FROM stdin; +80 10 All Greens Inc All Greens Inc 10032 W Bell Rd Ste 100 Sun City AZ 85351 Operating · Marijuana Facility · (623) 214-0801 https://azcarecheck.azdhs.gov/s/?name=All%20Greens%20Inc \N \N \N 6232140801 \N \N \N \N \N \N \N \N pending all-greens-inc 2025-11-17 07:29:34.206514 2025-11-17 07:29:34.206514 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +84 14 Arizona Cannabis Society Inc Arizona Cannabis Society Inc 8376 N El Mirage Rd Bldg 2 Ste 2 El Mirage AZ 85335 Operating · Marijuana Facility · (888) 249-2927 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Cannabis%20Society%20Inc \N \N \N 8882492927 \N \N \N \N \N \N \N \N pending arizona-cannabis-society-inc 2025-11-17 07:29:34.212386 2025-11-17 07:29:34.212386 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +86 16 Nature's Medicines Arizona Natural Pain Solutions Inc. 701 East Dunlap Avenue, Suite 9 Phoenix AZ 85020 Operating · Marijuana Facility · (602) 903-3769 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N \N 6029033769 \N \N \N \N \N \N \N \N pending nature-s-medicines 2025-11-17 07:29:34.214929 2025-11-17 07:29:34.214929 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +88 18 Arizona Organix Arizona Organix 5303 W Glendale Ave Glendale AZ 85301 Operating · Marijuana Facility · (623) 937-2752 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Organix \N \N Arizona Organix Dispensary 6239372752 \N https://www.arizonaorganix.org/ 4.2 2983 \N \N \N \N pending arizona-organix 2025-11-17 07:29:34.217534 2025-11-17 14:46:35.933113 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +89 19 Nirvana Center Arizona Tree Equity 2 2209 South 6th Avenue Tucson AZ 85713 Operating · Marijuana Establishment · (928) 642-2250 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center \N \N Nirvana Cannabis - Tucson 9286422250 \N https://nirvanacannabis.com/ 4.7 2156 \N \N \N \N pending nirvana-center 2025-11-17 07:29:34.21883 2025-11-17 14:46:35.934491 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +90 20 Arizona Wellness Center Safford Arizona Wellness Center Safford LLC 1362 W Thatcher Blvd Safford AZ 85546 Operating · Marijuana Dispensary · 623-521-6899 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Wellness%20Center%20Safford \N \N \N 6235216899 \N \N \N \N \N \N \N \N pending arizona-wellness-center-safford 2025-11-17 07:29:34.220168 2025-11-17 07:29:34.220168 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +91 21 Key Cannabis Arizona Wellness Collective 3, Inc 1911 W Broadway Rd 23 Mesa AZ 85202 Operating · Marijuana Facility · (480) 912-4444 https://azcarecheck.azdhs.gov/s/?name=Key%20Cannabis \N \N Key Cannabis Dispensary Mesa 4809124444 \N https://keycannabis.com/shop/mesa-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=mesa 4.2 680 \N \N \N \N pending key-cannabis 2025-11-17 07:29:34.221636 2025-11-17 14:46:36.090264 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +87 17 Arizona Natures Wellness Arizona Natures Wellness 1610 West State Route 89a Sedona AZ 86336 Operating · Marijuana Facility · 928-202-3512 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Natures%20Wellness \N \N \N 9282023512 \N \N \N \N https://dutchie.com/dispensary/curaleaf-dispensary-sedona \N \N \N pending arizona-natures-wellness 2025-11-17 07:29:34.216267 2025-11-17 07:29:34.216267 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +72 2 The Mint Dispensary 4245 Investments Llc 330 E Southern Ave #37 Mesa AZ 85210 Operating · Marijuana Facility · (480) 664-1470 https://azcarecheck.azdhs.gov/s/?name=The%20Mint%20Dispensary \N \N Mint Cannabis - Mesa 4806641470 \N https://mintdeals.com/mesa-az/ 4.7 5993 \N \N \N \N pending the-mint-dispensary 2025-11-17 07:29:34.191912 2025-12-01 15:16:43.841461 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +73 3 D2 Dispensary 46 Wellness Llc 7139 E 22nd St Tucson AZ 85710 Operating · Marijuana Facility · (520) 214-3232 https://azcarecheck.azdhs.gov/s/?name=D2%20Dispensary \N \N D2 Dispensary - Cannabis Destination + Drive Thru 5202143232 \N http://d2dispensary.com/ 4.8 5706 \N \N \N \N pending d2-dispensary 2025-11-17 07:29:34.194058 2025-12-01 15:16:43.847073 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +74 4 Ponderosa Dispensary 480 License Holdings, LLC 25 East Blacklidge Drive Tucson AZ 85705 Operating · Marijuana Establishment · 480-201-0000 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N Ponderosa Dispensary Tucson 4802010000 \N https://www.pondyaz.com/locations 4.8 520 \N \N \N \N pending ponderosa-dispensary 2025-11-17 07:29:34.195827 2025-12-01 15:16:43.85022 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +75 5 Ponderosa Dispensary ABACA Ponderosa, LLC 21035 N Cave Creek Rd Ste 3 & 4 Phoenix AZ 85024 Operating · Marijuana Facility · (480) 213-1402 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N Ponderosa Dispensary Phoenix 4802131402 \N https://www.pondyaz.com/locations 4.7 2561 \N \N \N \N pending ponderosa-dispensary-phoenix 2025-11-17 07:29:34.197818 2025-12-01 15:16:43.854255 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +76 6 Trulieve of Tatum Abedon Saiz Llc 16635 N tatum blvd, 110 Phoenix AZ 85032 Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tatum \N \N Trulieve Phoenix Dispensary Tatum 7703300831 \N https://www.trulieve.com/dispensaries/arizona/phoenix-tatum?utm_source=gmb&utm_medium=organic&utm_campaign=phoenix-tatum 4.4 194 \N \N \N \N pending trulieve-of-tatum 2025-11-17 07:29:34.199358 2025-12-01 15:16:43.857326 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +82 12 Apache County Dispensary LLC Apache County Dispensary LLC 900 East Main Street Springerville AZ 85938 Operating · Marijuana Establishment · 620-921-5967 https://azcarecheck.azdhs.gov/s/?name=Apache%20County%20Dispensary%20LLC \N \N Key Cannabis Dispensary Springerville 6209215967 \N https://keycannabis.com/shop/springerville-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=springerville 4.7 126 \N \N \N \N pending apache-county-dispensary-llc 2025-11-17 07:29:34.209719 2025-12-01 15:16:43.86392 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +83 13 Apollo Labs Apollo Labs 17301 North Perimeter Drive, suite 100 Scottsdale AZ 85255 Operating · Marijuana Laboratory · (917) 340-1566 https://azcarecheck.azdhs.gov/s/?name=Apollo%20Labs \N \N Apollo Labs 9173401566 \N http://www.apollolabscorp.com/ 5.0 7 \N \N \N \N pending apollo-labs 2025-11-17 07:29:34.211076 2025-12-01 15:16:43.866507 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +85 15 Arizona Golden Leaf Wellness, Llc Arizona Golden Leaf Wellness, Llc 5390 W Ina Rd Marana AZ 85743 Operating · Marijuana Facility · (520) 620-9123 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Golden%20Leaf%20Wellness%2C%20Llc \N \N NatureMed 5206209123 \N https://naturemedaz.com/ 4.8 3791 \N \N \N \N pending arizona-golden-leaf-wellness-llc 2025-11-17 07:29:34.213681 2025-12-01 15:16:43.869247 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +71 1 SWC Prescott 203 Organix, Llc 123 E Merritt St Prescott AZ 86301 Operating · Marijuana Facility · (312) 819-5061 https://azcarecheck.azdhs.gov/s/?name=SWC%20Prescott \N \N SWC Prescott by Zen Leaf 3128195061 \N https://zenleafdispensaries.com/locations/prescott/?utm_source=google&utm_medium=gbp-order&utm_campaign=az-prescott 4.7 2312 \N \N \N \N pending swc-prescott 2025-11-17 07:29:34.188807 2025-12-01 15:16:43.872486 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +92 22 TruMed Dispensary Az Compassionate Care Inc 1613 N 40th St Phoenix AZ 85008 Operating · Marijuana Facility · (602) 275-1279 https://azcarecheck.azdhs.gov/s/?name=TruMed%20Dispensary \N \N TruMed Dispensary 6022751279 \N https://trumedaz.com/ 4.5 1807 \N \N \N \N pending trumed-dispensary 2025-11-17 07:29:34.223002 2025-11-17 14:46:35.935792 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +93 23 AZ Flower Power LLC AZ Flower Power LLC 11343 East Apache Trail Apache Junction AZ 85120 Operating · Marijuana Establishment · (917) 375-3900 https://azcarecheck.azdhs.gov/s/?name=AZ%20Flower%20Power%20LLC \N \N \N 9173753900 \N \N \N \N \N \N \N \N pending az-flower-power-llc 2025-11-17 07:29:34.224267 2025-11-17 07:29:34.224267 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +95 25 Zen Leaf Chandler AZGM 3, LLC 7200 W Chandler Blvd Ste 7 Chandler AZ 85226 Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Chandler \N \N Zen Leaf Dispensary Chandler 3128195061 \N https://zenleafdispensaries.com/locations/chandler/?utm_campaign=az-chandler&utm_medium=gbp&utm_source=google 4.8 3044 \N \N \N \N pending zen-leaf-chandler 2025-11-17 07:29:34.226944 2025-11-17 14:46:35.937725 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +96 26 Greenleef Medical Bailey Management LLC 253 Chase Creek St Clifton AZ 85533 Operating · Marijuana Facility · 480-652-3622 https://azcarecheck.azdhs.gov/s/?name=Greenleef%20Medical \N \N \N 4806523622 \N \N \N \N \N \N \N \N pending greenleef-medical 2025-11-17 07:29:34.228419 2025-11-17 07:29:34.228419 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +97 27 Blue Palo Verde 1, LLC Blue Palo Verde 1, LLC 7710 South Wilmot Road, Suite 100 Tucson AZ 85756 Operating · Marijuana Establishment · 586-855-6649 https://azcarecheck.azdhs.gov/s/?name=Blue%20Palo%20Verde%201%2C%20LLC \N \N Halo Cannabis 5868556649 \N https://thegreenhalo.com/ 4.4 1580 \N \N \N \N pending blue-palo-verde-1-llc 2025-11-17 07:29:34.229802 2025-11-17 14:46:36.091726 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +98 28 Sticky Saguaro Border Health, Inc 12338 East Riggs Road Chandler AZ 85249 Operating · Marijuana Facility · (602) 644-9188 https://azcarecheck.azdhs.gov/s/?name=Sticky%20Saguaro \N \N Sticky Saguaro 6026449188 \N https://stickysaguaro.com/ 4.6 1832 \N \N \N \N pending sticky-saguaro 2025-11-17 07:29:34.231093 2025-11-17 14:46:35.939598 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +99 29 HANA MEDS Broken Arrow Herbal Center Inc 1732 W Commerce Point Pl Sahuarita AZ 85614 Operating · Marijuana Facility · (520) 289-8030 https://azcarecheck.azdhs.gov/s/?name=HANA%20MEDS \N \N Hana Dispensary Green Valley 5202898030 \N https://hanadispensaries.com/location/green-valley-az/?utm_source=gmb&utm_medium=organic 4.6 1087 \N \N \N \N pending hana-meds 2025-11-17 07:29:34.232383 2025-11-17 14:46:36.093234 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +100 30 The Flower Shop Az Buds & Roses, Inc 5205 E University Dr Mesa AZ 85205 Operating · Marijuana Facility · (480) 500-5054 https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az \N \N The Flower Shop - Mesa 4805005054 \N https://theflowershopusa.com/mesa?utm_source=google-business&utm_medium=organic 4.4 2604 \N \N \N \N pending the-flower-shop-az 2025-11-17 07:29:34.233765 2025-11-17 14:46:35.941442 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +101 31 Trulieve of Scottsdale Dispensary Byers Dispensary Inc 15190 N Hayden Rd Scottsdale AZ 85260 Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Scottsdale%20Dispensary \N \N Trulieve Scottsdale Dispensary 8505080261 \N https://www.trulieve.com/dispensaries/arizona/scottsdale?utm_source=gmb&utm_medium=organic&utm_campaign=scottsdale 4.3 819 \N \N \N \N pending trulieve-of-scottsdale-dispensary 2025-11-17 07:29:34.235227 2025-11-17 14:46:36.09487 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +102 32 SC Labs C4 Laboratories 7650 East Evans Rd Unit A, UNIT A Scottsdale AZ 85260 Operating · Marijuana Laboratory · (480) 219-6460 https://azcarecheck.azdhs.gov/s/?name=SC%20Labs \N \N SC Labs | Arizona (Formerly C4 Laboratories) 4802196460 \N http://www.sclabs.com/ 4.9 10 \N \N \N \N pending sc-labs 2025-11-17 07:29:34.23644 2025-11-17 14:46:35.943268 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +103 33 Releaf Cactus Bloom Facilities Management LLC 436 Naugle Ave Patagonia AZ 85624 Operating · Marijuana Establishment · 520-982-9212 https://azcarecheck.azdhs.gov/s/?name=Releaf \N \N Releaf 85624 5209829212 \N https://dbloomtucson.com/releaf-85624/ 4.8 221 \N \N \N \N pending releaf 2025-11-17 07:29:34.237948 2025-11-17 14:46:35.945281 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +104 34 The Phoenix Cannabis Research Group Inc 9897 W McDowell Rd #720 Tolleson AZ 85353 Operating · Marijuana Facility · (480) 420-0377 https://azcarecheck.azdhs.gov/s/?name=The%20Phoenix \N \N \N 4804200377 \N \N \N \N \N \N \N \N pending the-phoenix 2025-11-17 07:29:34.239335 2025-11-17 07:29:34.239335 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +105 35 Sunday Goods Cardinal Square, Inc 13150 W Bell Rd Surprise AZ 85378 Operating · Marijuana Facility · 520-808-3111 https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods \N \N Sunday Goods Surprise 5208083111 \N https://sundaygoods.com/location/sunday-goods-surprise-az-cannabis-dispensary/?utm_source=google&utm_medium=gbp&utm_campaign=surprise_gbp 4.4 13 \N \N \N \N pending sunday-goods 2025-11-17 07:29:34.240675 2025-11-17 14:46:35.946795 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +107 37 HANA MEDS Cjk Inc 3411 E Corona Ave, 100 Phoenix AZ 85040 Operating · Marijuana Facility · 602 491-0420 https://azcarecheck.azdhs.gov/s/?name=HANA%20MEDS \N \N Hana Dispensary Phoenix \N http://www.hanadispensaries.com/?utm_source=gmb&utm_medium=organic 4.7 1129 \N \N \N \N pending hana-meds-phoenix 2025-11-17 07:29:34.243615 2025-11-17 14:46:36.106619 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +108 38 Trulieve Of Sierra Vista Cochise County Wellness, LLC 1633 S Highway 92, Ste 7 Sierra Vista AZ 85635 Operating · Marijuana Facility · 480-677-1755 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Of%20Sierra%20Vista \N \N Trulieve Sierra Vista Dispensary 4806771755 \N https://www.trulieve.com/dispensaries/arizona/sierra-vista?utm_source=gmb&utm_medium=organic&utm_campaign=sierra-vista 4.4 488 \N \N \N \N pending trulieve-of-sierra-vista 2025-11-17 07:29:34.244842 2025-11-17 14:46:35.948265 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +109 39 Botanica Copper State Herbal Center Inc 6205 N Travel Center Dr Tucson AZ 85741 Operating · Marijuana Facility · (520) 395-0230 https://azcarecheck.azdhs.gov/s/?name=Botanica \N \N Botanica 5203950230 \N https://botanica.us/ 4.6 940 \N \N \N \N pending botanica 2025-11-17 07:29:34.246153 2025-11-17 14:46:35.949751 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +110 40 Sol Flower CSI Solutions, Inc. 14980 N 78th Way Scottsdale AZ 85260 Operating · Marijuana Facility · 480-420-3300 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N Sol Flower Dispensary Scottsdale Airpark 4804203300 \N https://www.livewithsol.com/scottsdale-airpark-menu-recreational?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing 4.6 692 \N \N \N \N pending sol-flower 2025-11-17 07:29:34.247538 2025-11-17 14:46:36.107685 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +111 41 Curious Cultivators I LLC Curious Cultivators I LLC 200 London Bridge Road, 100 Lake Havasu City AZ 86403 Operating · Marijuana Establishment · (310) 694-4397 https://azcarecheck.azdhs.gov/s/?name=Curious%20Cultivators%20I%20LLC \N \N Story Cannabis Dispensary Lake Havasu 3106944397 \N https://storycannabis.com/dispensary-locations/arizona/havasu-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=lake_havasu&utm_term=click&utm_content=website 4.7 403 \N \N \N \N pending curious-cultivators-i-llc 2025-11-17 07:29:34.248815 2025-11-17 14:46:35.951009 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +106 36 Catalina Hills Botanical Care Catalina Hills Botanical Care Inc 2918 N Central Ave Phoenix AZ 85012 Operating · Marijuana Facility · 602-466-1087 https://azcarecheck.azdhs.gov/s/?name=Catalina%20Hills%20Botanical%20Care \N \N \N 6024661087 \N \N \N \N https://dutchie.com/dispensary/curaleaf-dispensary-midtown \N \N \N pending catalina-hills-botanical-care 2025-11-17 07:29:34.24216 2025-11-17 07:29:34.24216 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +94 24 AZC1 AZCL1 4695 N Oracle Rd Ste 117 Tucson AZ 85705 Operating · Marijuana Facility · 520-293-3315 https://azcarecheck.azdhs.gov/s/?name=AZC1 \N \N \N 5202933315 \N \N \N \N https://dutchie.com/dispensary/curaleaf-tucson \N \N \N pending azc1 2025-11-17 07:29:34.225631 2025-11-17 07:29:34.225631 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +113 43 JARS Cannabis Desert Medical Campus Inc 10040 N. Metro Parkway W Phoenix AZ 85051 Operating · Marijuana Facility · 602-870-8700 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Phoenix Metrocenter 6028708700 \N https://jarscannabis.com/ 4.8 11971 \N \N \N \N pending jars-cannabis 2025-11-17 07:29:34.251953 2025-11-17 14:46:35.952537 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +114 44 Green Pharms Desertview Wellness & Healing Solutions, LLC 600 South 80th Avenue, 100 Tolleson AZ 85353 Operating · Marijuana Facility · (928) 522-6337 https://azcarecheck.azdhs.gov/s/?name=Green%20Pharms \N \N \N 9285226337 \N \N \N \N \N \N \N \N pending green-pharms 2025-11-17 07:29:34.253374 2025-11-17 07:29:34.253374 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +116 46 JARS Cannabis Dreem Green Inc 2412 East University Drive Phoenix AZ 85034 Operating · Marijuana Facility · (602) 675-6999 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Phoenix Airport 6026756999 \N https://jarscannabis.com/ 4.9 10901 \N \N \N \N pending jars-cannabis-phoenix-2 2025-11-17 07:29:34.255882 2025-11-17 14:46:35.954809 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +117 47 Nature's Wonder DYNAMIC TRIO HOLDINGS LLC 6812 East Cave Creek Road, 2, 2A and 3 Cave Creek AZ 85331 Operating · Marijuana Establishment · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Wonder \N \N Nature's Wonder Cave Creek Dispensary 4808613649 \N https://natureswonderaz.com/cave-creek-dispensary-menu-recreational 4.5 453 \N \N \N \N pending nature-s-wonder 2025-11-17 07:29:34.257086 2025-11-17 14:46:36.109985 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +118 48 Earth's Healing Inc Earth's Healing Inc 2075 E Benson Hwy Tucson AZ 85714 Operating · Marijuana Facility · (520) 373-5779 https://azcarecheck.azdhs.gov/s/?name=Earth's%20Healing%20Inc \N \N Earth's Healing South 5203735779 \N https://earthshealing.org/ 4.8 7608 \N \N \N \N pending earth-s-healing-inc 2025-11-17 07:29:34.258296 2025-11-17 14:46:35.956163 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +119 49 East Valley Patient Wellness Group Inc East Valley Patient Wellness Group Inc 13650 N 99th Ave Sun City AZ 85351 Operating · Marijuana Facility · (623) 246-8080 https://azcarecheck.azdhs.gov/s/?name=East%20Valley%20Patient%20Wellness%20Group%20Inc \N \N Sol Flower Dispensary Sun City 6232468080 \N https://www.livewithsol.com/sun-city-dispensary?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing 4.6 1115 \N \N \N \N pending east-valley-patient-wellness-group-inc 2025-11-17 07:29:34.259555 2025-11-17 14:46:35.957836 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +120 50 Mint Cannabis Eba Holdings Inc. 8729 E Manzanita Dr Scottsdale AZ 85258 Operating · Marijuana Facility · (480) 749-6468 https://azcarecheck.azdhs.gov/s/?name=Mint%20Cannabis \N \N Mint Cannabis - Scottsdale 4807496468 \N https://mintdeals.com/scottsdale-az/ 4.4 763 \N \N \N \N pending mint-cannabis 2025-11-17 07:29:34.260718 2025-11-17 14:46:35.960115 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +121 51 Encore Labs Arizona Encore Labs Arizona 16624 North 90th Street, #101 Scottsdale AZ 85260 Operating · Marijuana Laboratory · (626) 653-3414 https://azcarecheck.azdhs.gov/s/?name=Encore%20Labs%20Arizona \N \N Encore Labs AZ 6266533414 \N https://www.encorelabs.com/ 5.0 2 \N \N \N \N pending encore-labs-arizona 2025-11-17 07:29:34.262008 2025-11-17 14:46:36.111293 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +122 52 Zanzibar FJM Group LLC 60 W Main St Quartzsite AZ 85346 Operating · Marijuana Establishment · (520) 907-2181 https://azcarecheck.azdhs.gov/s/?name=Zanzibar \N \N Zanzibar dispensary 5209072181 \N https://dutchie.com/dispensary/Zanzibar-Cannabis-Dispensary/products/flower 4.0 71 \N \N \N \N pending zanzibar 2025-11-17 07:29:34.26319 2025-11-17 14:46:35.962131 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +123 53 The Downtown Dispensary Forever 46 Llc 221 E 6th St, Suite 105 Tucson AZ 85705 Operating · Marijuana Facility · (520) 838-0492 https://azcarecheck.azdhs.gov/s/?name=The%20Downtown%20Dispensary \N \N D2 Dispensary - Downtown Cannabis Gallery 5208380492 \N http://d2dispensary.com/ 4.8 5290 \N \N \N \N pending the-downtown-dispensary 2025-11-17 07:29:34.264361 2025-11-17 14:46:35.964105 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +124 54 Zen Leaf Phoenix (Cave Creek Rd) Fort Consulting, Llc 12401 N Cave Creek Rd Phoenix AZ 85022 Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Phoenix%20(Cave%20Creek%20Rd) \N \N Zen Leaf Dispensary Phoenix (Cave Creek Rd.) 3128195061 \N https://zenleafdispensaries.com/locations/phoenix-n-cave-creek/?utm_campaign=az-phoenix-cave-creek&utm_medium=gbp&utm_source=google 4.6 2720 \N \N \N \N pending zen-leaf-phoenix-cave-creek-rd- 2025-11-17 07:29:34.265564 2025-11-17 14:46:36.112689 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +125 55 Trulieve of Tucson Fort Mountain Consulting, Llc 4659 E 22nd St Tucson AZ 85711 Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson \N \N Trulieve Tucson Dispensary 7703300831 \N https://www.trulieve.com/dispensaries/arizona/tucson-toumey-park?utm_source=gmb&utm_medium=organic&utm_campaign=tucson-swan 4.6 1169 \N \N \N \N pending trulieve-of-tucson 2025-11-17 07:29:34.266746 2025-11-17 14:46:35.965944 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +126 56 Full Spectrum Lab, LLC Full Spectrum Lab, LLC 3865 E 34th St, Ste 109 Tucson AZ 85713 Operating · Marijuana Laboratory · (520) 838-0695 https://azcarecheck.azdhs.gov/s/?name=Full%20Spectrum%20Lab%2C%20LLC \N \N Full Spectrum Lab, LLC 5208380695 \N https://fullspectrumlab.com/ 4.5 2 \N \N \N \N pending full-spectrum-lab-llc 2025-11-17 07:29:34.267909 2025-11-17 14:46:35.968038 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +127 57 Farm Fresh Fwa Inc 790 N Lake Havasu Ave #4 Lake Havasu City AZ 86404 Operating · Marijuana Facility · (928) 733-6339 https://azcarecheck.azdhs.gov/s/?name=Farm%20Fresh \N \N Farm Fresh Medical/Recreational Marijuana Dispensary 9287336339 \N http://farmfreshdispensary.com/ 4.6 642 \N \N \N \N pending farm-fresh 2025-11-17 07:29:34.269021 2025-11-17 14:46:36.114094 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +128 58 The Mint Dispensary G.T.L. Llc 2444 W Northern Ave Phoenix AZ 85021 Operating · Marijuana Facility · (480) 749-6468 https://azcarecheck.azdhs.gov/s/?name=The%20Mint%20Dispensary \N \N Mint Cannabis - Northern Ave 4807496468 \N https://mintdeals.com/phoenix-az/ 4.6 1233 \N \N \N \N pending the-mint-dispensary-phoenix 2025-11-17 07:29:34.270388 2025-11-17 14:46:36.116094 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +129 59 JARS Cannabis Gila Dreams X, LLC 100 East State Highway 260 Payson AZ 85541 Operating · Marijuana Establishment · 928-474-2420 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Payson 9284742420 \N https://jarscannabis.com/ 4.9 3259 \N \N \N \N pending jars-cannabis-payson-1 2025-11-17 07:29:34.271592 2025-11-17 14:46:35.970726 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +130 60 Earth's Healing North Globe Farmacy Inc 78 W River Rd Tucson AZ 85704 Operating · Marijuana Facility · (520) 909-6612 https://azcarecheck.azdhs.gov/s/?name=Earth's%20Healing%20North \N \N Earth's Healing North 5209096612 \N https://earthshealing.org/ 4.8 7149 \N \N \N \N pending earth-s-healing-north 2025-11-17 07:29:34.272932 2025-11-17 14:46:35.972738 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +131 61 Trulieve of Peoria Dispensary Green Desert Patient Center Of Peoria 9275 W Peoria Ave, Ste 104 Peoria AZ 85345 Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Peoria%20Dispensary \N \N Trulieve Peoria Dispensary 8505597734 \N https://www.trulieve.com/dispensaries/arizona/peoria?utm_source=gmb&utm_medium=organic&utm_campaign=peoria 4.7 2931 \N \N \N \N pending trulieve-of-peoria-dispensary 2025-11-17 07:29:34.274105 2025-11-17 14:46:36.11788 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +132 62 Nature's Medicines Green Hills Patient Center Inc 16913 East Enterprise Drive, 201, 202, 203 Fountain Hills AZ 85268 Operating · Marijuana Facility · (928) 537-4888 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N \N 9285374888 \N \N \N \N \N \N \N \N pending nature-s-medicines-fountain-hills 2025-11-17 07:29:34.275351 2025-11-17 07:29:34.275351 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +231 161 The Green Halo Llc The Green Halo Llc 3906 North Oracle Road Tucson AZ 85705 Operating · Marijuana Facility · (520) 664-2251 https://azcarecheck.azdhs.gov/s/?name=The%20Green%20Halo%20Llc \N \N \N 5206642251 \N \N \N \N \N \N \N \N pending the-green-halo-llc 2025-11-17 07:29:34.409513 2025-11-17 07:29:34.409513 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +133 63 Sunday Goods Green Lightning, LLC 723 N Scottsdale Rd Tempe AZ 85281 Operating · Marijuana Establishment · (480)-219-1300 https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods \N \N Sunday Goods Tempe \N https://sundaygoods.com/location/dispensary-tempe-az/?utm_source=google&utm_medium=gbp&utm_campaign=tempe_gbp 4.1 685 \N \N \N \N pending sunday-goods-tempe 2025-11-17 07:29:34.27645 2025-11-17 14:46:35.974481 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +134 64 Southern Arizona Integrated Therapies (Tucson SAINTS) Green Medicine 112 S Kolb Rd Tucson AZ 85710 Operating · Marijuana Facility · (520) 886-1003 https://azcarecheck.azdhs.gov/s/?name=Southern%20Arizona%20Integrated%20Therapies%20(Tucson%20SAINTS) \N \N SAINTS Dispensary 5208861003 \N https://www.tucsonsaints.com/ 4.8 1704 \N \N \N \N pending southern-arizona-integrated-therapies-tucson-saints- 2025-11-17 07:29:34.277716 2025-11-17 14:46:35.976009 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +135 65 Trulieve Bisbee Dispensary Green Sky Patient Center Of Scottsdale North Inc 1191 S Naco Hwy Bisbee AZ 85603 Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Bisbee%20Dispensary \N \N Trulieve Bisbee Dispensary 8505597734 \N https://www.trulieve.com/dispensaries/arizona/?utm_source=gmb&utm_medium=organic&utm_campaign=bisbee 3.5 13 \N \N \N \N pending trulieve-bisbee-dispensary 2025-11-17 07:29:34.279003 2025-11-17 14:46:35.977396 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +136 66 Greenmed, Inc Greenmed, Inc 6464 E Tanque Verde Rd Tucson AZ 85715 Operating · Marijuana Facility · (520) 886-2484 https://azcarecheck.azdhs.gov/s/?name=Greenmed%2C%20Inc \N \N \N 5208862484 \N \N \N \N \N \N \N \N pending greenmed-inc 2025-11-17 07:29:34.280178 2025-11-17 07:29:34.280178 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +137 67 Marigold Dispensary Greens Goddess Products, Inc 2601 W. Dunlap Avenue, 21 Phoenix AZ 85017 Operating · Marijuana Facility · (602) 900-4557 https://azcarecheck.azdhs.gov/s/?name=Marigold%20Dispensary \N \N Key Cannabis Dispensary Phoenix 6029004557 \N https://keycannabis.com/shop/phoenix-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=phoenix 4.7 2664 \N \N \N \N pending marigold-dispensary 2025-11-17 07:29:34.281424 2025-11-17 14:46:36.11941 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +138 68 Grunge Free LLC Grunge Free LLC 700 North Pinal Parkway Avenue Florence AZ 85132 Operating · Marijuana Establishment · (917)375-3900 https://azcarecheck.azdhs.gov/s/?name=Grunge%20Free%20LLC \N \N Nirvana Cannabis - Florence 9173753900 \N https://nirvanacannabis.com/ 4.8 782 \N \N \N \N pending grunge-free-llc 2025-11-17 07:29:34.282797 2025-11-17 14:46:35.97865 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +139 69 Ponderosa Dispensary H4L Ponderosa, LLC 7343 S 89th Pl Mesa AZ 85212 Operating · Marijuana Facility · (480) 213-1402 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N Ponderosa Dispensary Queen Creek 4802131402 \N https://www.pondyaz.com/locations 4.6 1709 \N \N \N \N pending ponderosa-dispensary-mesa-1 2025-11-17 07:29:34.284075 2025-11-17 14:46:35.980161 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +141 71 Consume Cannabis Health Center Navajo, Inc 1350 N Penrod Rd Show Low AZ 85901 Operating · Marijuana Facility · (520)808-3111 https://azcarecheck.azdhs.gov/s/?name=Consume%20Cannabis \N \N Consume Cannabis - Show Low 5208083111 \N https://www.consumecannabis.com/dispensaries/show-low 4.4 1111 \N \N \N \N pending consume-cannabis 2025-11-17 07:29:34.286332 2025-11-17 14:46:36.120593 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +142 72 Herbal Wellness Center Inc Herbal Wellness Center Inc 4126 W Indian School Rd Phoenix AZ 85019 Operating · Marijuana Facility · 602-910-4152 https://azcarecheck.azdhs.gov/s/?name=Herbal%20Wellness%20Center%20Inc \N \N Herbal Wellness Center West 6029104152 \N https://herbalwellnesscenter.com/ 4.3 2364 \N \N \N \N pending herbal-wellness-center-inc 2025-11-17 07:29:34.287415 2025-11-17 14:46:35.983321 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +143 73 Trulieve of Chandler Dispensary High Desert Healing Llc 13433 E. Chandler Blvd. Suite A Chandler AZ 85225 Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Chandler%20Dispensary \N \N Trulieve Chandler Dispensary 9548172370 \N https://www.trulieve.com/dispensaries/arizona/chandler?utm_source=gmb&utm_medium=organic&utm_campaign=chandler 4.0 1134 \N \N \N \N pending trulieve-of-chandler-dispensary 2025-11-17 07:29:34.288909 2025-11-17 14:46:35.984941 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +144 74 Trulieve of Avondale Dispensary High Desert Healing Llc 3828 S Vermeersch Rd Avondale AZ 85323 Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Avondale%20Dispensary \N \N Trulieve Avondale Dispensary 9548172370 \N https://www.trulieve.com/dispensaries/arizona/avondale?utm_source=gmb&utm_medium=organic&utm_campaign=avondale 4.3 1046 \N \N \N \N pending trulieve-of-avondale-dispensary 2025-11-17 07:29:34.290248 2025-11-17 14:46:36.121718 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +145 75 Ponderosa Dispensary High Mountain Health, Llc 1250 S Plaza Way Ste A Flagstaff AZ 86001 Operating · Marijuana Facility · (928) 774-5467 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N Ponderosa Dispensary Flagstaff 9287745467 \N https://www.pondyaz.com/locations 4.5 1340 \N \N \N \N pending ponderosa-dispensary-flagstaff 2025-11-17 07:29:34.291559 2025-11-17 14:46:35.986592 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +146 76 Higher than High I LLC Higher than High I LLC 1302 West Industrial Drive Coolidge AZ 85128 Operating · Marijuana Establishment · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Higher%20than%20High%20I%20LLC \N \N \N 4808613649 \N \N \N \N \N \N \N \N pending higher-than-high-i-llc 2025-11-17 07:29:34.293213 2025-11-17 07:29:34.293213 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +147 77 Holistic Patient Wellness Group Holistic Patient Wellness Group 1322 N Mcclintock Dr Tempe AZ 85281 Operating · Marijuana Facility · (480) 795-6363 https://azcarecheck.azdhs.gov/s/?name=Holistic%20Patient%20Wellness%20Group \N \N Sol Flower Dispensary McClintock 4807956363 \N https://www.livewithsol.com/tempe-mcclintock-dispensary?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing 4.7 2789 \N \N \N \N pending holistic-patient-wellness-group 2025-11-17 07:29:34.294687 2025-11-17 14:46:35.988741 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +148 78 Jamestown Center Jamestown Center 4104 E 32nd St Yuma AZ 85365 Operating · Marijuana Facility · (928) 344-1735 https://azcarecheck.azdhs.gov/s/?name=Jamestown%20Center \N \N Yuma Dispensary 9283441735 \N http://yumadispensary.com/ 4.4 1187 \N \N \N \N pending jamestown-center 2025-11-17 07:29:34.295954 2025-11-17 14:46:35.990549 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +150 80 Joint Junkies I LLC Joint Junkies I LLC 26427 S Arizona Ave Chandler AZ 85248 Operating · Marijuana Establishment · (928) 638-5831 https://azcarecheck.azdhs.gov/s/?name=Joint%20Junkies%20I%20LLC \N \N Story Cannabis Dispensary South Chandler 9286385831 \N https://storycannabis.com/dispensary-locations/arizona/south-chandler-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=south_chandler&utm_term=click&utm_content=website 4.4 1227 \N \N \N \N pending joint-junkies-i-llc 2025-11-17 07:29:34.298382 2025-11-17 14:46:36.122862 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +151 81 Juicy Joint I LLC Juicy Joint I LLC 3550 North Lane, #110 Bullhead City AZ 86442 Operating · Marijuana Establishment · (928) 324-6062 https://azcarecheck.azdhs.gov/s/?name=Juicy%20Joint%20I%20LLC \N \N \N 9283246062 \N \N \N \N \N \N \N \N pending juicy-joint-i-llc 2025-11-17 07:29:34.299568 2025-11-17 07:29:34.299568 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +152 82 K Group Partners K Group Partners Llc 11200 W Michigan Ave Ste 5 Youngtown AZ 85363 Operating · Marijuana Facility · 623-444-5977 https://azcarecheck.azdhs.gov/s/?name=K%20Group%20Partners \N \N Curaleaf Dispensary Youngtown 6234445977 \N https://curaleaf.com/stores/curaleaf-dispensary-youngtown?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.7 2421 https://dutchie.com/dispensary/curaleaf-youngtown \N \N \N pending k-group-partners 2025-11-17 07:29:34.300795 2025-11-17 14:46:35.994205 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +154 84 Kaycha AZ LLC Kaycha AZ LLC 1231 W Warner Rd, Ste 105 Tempe AZ 85284 Operating · Marijuana Laboratory · (770) 365-7752 https://azcarecheck.azdhs.gov/s/?name=Kaycha%20AZ%20LLC \N \N Kaycha Labs - Arizona 7703657752 \N https://www.kaychalabs.com/ 5.0 0 \N \N \N \N pending kaycha-az-llc 2025-11-17 07:29:34.303535 2025-11-17 14:46:36.123949 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +155 85 Kind Meds Inc Kind Meds Inc 2152 S Vineyard St Ste 120 Mesa AZ 85210 Operating · Marijuana Facility · (480) 686-9302 https://azcarecheck.azdhs.gov/s/?name=Kind%20Meds%20Inc \N \N Kind Meds 4806869302 \N http://kindmedsaz.com/ 3.8 260 \N \N \N \N pending kind-meds-inc 2025-11-17 07:29:34.304738 2025-11-17 14:46:36.125029 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +156 86 Trulieve of Phoenix Dispensary Kwerles Inc 2017 W. Peoria Avenue, Suite A Phoenix AZ 85029 Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Phoenix%20Dispensary \N \N \N 8505080261 \N \N \N \N \N \N \N \N pending trulieve-of-phoenix-dispensary 2025-11-17 07:29:34.306006 2025-11-17 07:29:34.306006 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +157 87 L1 Management, Llc L1 Management, Llc 1525 N. Granite Reef Rd. Scottsdale AZ 85257 Operating · Marijuana Laboratory · (602) 616-8167 https://azcarecheck.azdhs.gov/s/?name=L1%20Management%2C%20Llc \N \N Level One Labs 6026168167 \N https://levelonelabs.com/ 5.0 7 \N \N \N \N pending l1-management-llc 2025-11-17 07:29:34.307196 2025-11-17 14:46:35.997091 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +158 88 JARS Cannabis Lawrence Health Services LLC 2250 Highway 60, Suite M Globe AZ 85501 Operating · Marijuana Establishment · 928-793-2550 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Globe 9287932550 \N https://jarscannabis.com/ 4.8 959 \N \N \N \N pending jars-cannabis-globe 2025-11-17 07:29:34.308448 2025-11-17 14:46:35.998634 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +159 89 JARS Cannabis Legacy & Co., Inc. 3001 North 24th Street, A Phoenix AZ 85016 Operating · Marijuana Facility · (623) 936-9333 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N \N 6239369333 \N \N \N \N \N \N \N \N pending jars-cannabis-phoenix-3 2025-11-17 07:29:34.309775 2025-11-17 07:29:34.309775 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +160 90 Cookies Life Changers Investments LLC 2715 South Hardy Drive Tempe AZ 85282 Operating · Marijuana Establishment · (480) 452-7275 https://azcarecheck.azdhs.gov/s/?name=Cookies \N \N Cookies Cannabis Dispensary Tempe 4804527275 \N https://tempe.cookies.co/?utm_source=gmb&utm_medium=organic 4.7 5626 \N \N \N \N pending cookies 2025-11-17 07:29:34.310958 2025-11-17 14:46:36.126103 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +161 91 Mint Cannabis M&T Retail Facility 1, LLC 1211 North 75th Avenue Phoenix AZ 85043 Operating · Marijuana Facility · 480-749-6468 https://azcarecheck.azdhs.gov/s/?name=Mint%20Cannabis \N \N Mint Cannabis - 75th Ave 4807496468 \N https://mintdeals.com/75-ave-phx/ 4.7 1463 \N \N \N \N pending mint-cannabis-phoenix 2025-11-17 07:29:34.312376 2025-11-17 14:46:36.000146 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +162 92 MCCSE214, LLC MCCSE214, LLC 1975 E Northern Ave Kingman AZ 86409 Operating · Marijuana Establishment · 928-263-6348 https://azcarecheck.azdhs.gov/s/?name=MCCSE214%2C%20LLC \N \N \N 9282636348 \N \N \N \N \N \N \N \N pending mccse214-llc 2025-11-17 07:29:34.313692 2025-11-17 07:29:34.313692 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +163 93 MCCSE240, LLC MCCSE240, LLC 12555 NW Grand Ave, B El Mirage AZ 85335 Operating · Marijuana Establishment · 602-351-5450 https://azcarecheck.azdhs.gov/s/?name=MCCSE240%2C%20LLC \N \N \N 6023515450 \N \N \N \N \N \N \N \N pending mccse240-llc 2025-11-17 07:29:34.314872 2025-11-17 07:29:34.314872 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +164 94 MCCSE29, LLC MCCSE29, LLC 12323 W Camelback Rd Litchfield Park AZ 85340 Operating · Marijuana Establishment · (602) 903-3665 https://azcarecheck.azdhs.gov/s/?name=MCCSE29%2C%20LLC \N \N \N 6029033665 \N \N \N \N \N \N \N \N pending mccse29-llc 2025-11-17 07:29:34.316182 2025-11-17 07:29:34.316182 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +165 95 MCCSE82, LLC MCCSE82, LLC 46639 North Black Canyon Highway, 1-2 New River AZ 85087 Operating · Marijuana Establishment · 928-299-5145 https://azcarecheck.azdhs.gov/s/?name=MCCSE82%2C%20LLC \N \N JARS Cannabis New River 9282995145 \N https://jarscannabis.com/ 4.9 3180 \N \N \N \N pending mccse82-llc 2025-11-17 07:29:34.31746 2025-11-17 14:46:36.001778 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +166 96 MCD-SE Venture 25, LLC MCD-SE Venture 25, LLC 15235 North Dysart Road, 111 D El Mirage AZ 85335 Operating · Marijuana Establishment · 602-931-3663 https://azcarecheck.azdhs.gov/s/?name=MCD-SE%20Venture%2025%2C%20LLC \N \N Mint Cannabis Dispensary - EL MIRAGE 6029313663 \N https://mintdeals.com/az-el-mirage/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps 4.7 249 \N \N \N \N pending mcd-se-venture-25-llc 2025-11-17 07:29:34.318677 2025-11-17 14:46:36.127232 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +167 97 MCD-SE Venture 26, LLC MCD-SE Venture 26, LLC 15235 North Dysart Road, 11C El Mirage AZ 85335 Operating · Marijuana Establishment · 602-931-3663 https://azcarecheck.azdhs.gov/s/?name=MCD-SE%20Venture%2026%2C%20LLC \N \N Mint Cannabis Dispensary - EL MIRAGE 6029313663 \N https://mintdeals.com/az-el-mirage/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps 4.7 249 \N \N \N \N pending mcd-se-venture-26-llc 2025-11-17 07:29:34.320101 2025-11-17 14:46:36.003255 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +168 98 Trulieve of Casa Grande Medical Pain Relief Inc 1860 E Salk Dr Ste B-1 Casa Grande AZ 85122 Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Casa%20Grande \N \N Trulieve Casa Grande Dispensary 8505080261 \N https://www.trulieve.com/dispensaries/arizona/casa-grande?utm_source=gmb&utm_medium=organic&utm_campaign=casa-grande 4.1 1817 \N \N \N \N pending trulieve-of-casa-grande 2025-11-17 07:29:34.321338 2025-11-17 14:46:36.004751 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +169 99 Desert Bloom Releaf Center Medmar Tanque Verde Llc 8060 E 22nd St Ste 108 Tucson AZ 85710 Operating · Marijuana Facility · 520-886-1760 https://azcarecheck.azdhs.gov/s/?name=Desert%20Bloom%20Releaf%20Center \N \N Desert Bloom Re-Leaf Center 5208861760 \N http://www.dbloomtucson.com/ 4.2 1634 \N \N \N \N pending desert-bloom-releaf-center 2025-11-17 07:29:34.322492 2025-11-17 14:46:36.007154 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +170 100 MK Associates LLC MK Associates LLC 3270 AZ-82 Sonoita AZ 85637 Operating · Marijuana Establishment · (703) 915-2159 https://azcarecheck.azdhs.gov/s/?name=MK%20Associates%20LLC \N \N \N 7039152159 \N \N \N \N \N \N \N \N pending mk-associates-llc 2025-11-17 07:29:34.323969 2025-11-17 07:29:34.323969 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +171 101 Nirvana Center Phoenix Mmj Apothecary 9240 West Northern Avenue, Ste. 103b Peoria AZ 85345 Operating · Marijuana Facility · (928) 684-8880 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Phoenix \N \N Ponderosa Dispensary Glendale 9286848880 \N https://www.pondyaz.com/locations 4.5 1057 \N \N \N \N pending nirvana-center-phoenix 2025-11-17 07:29:34.326102 2025-11-17 14:46:36.128403 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +172 102 JARS Cannabis Mohave Cannabis Club 1, LLC 4236 E Juanita Ave Mesa AZ 85206 Operating · Marijuana Facility · 480-420-0064 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Mesa 4804200064 \N https://jarscannabis.com/ 4.9 7637 \N \N \N \N pending jars-cannabis-mesa 2025-11-17 07:29:34.327562 2025-11-17 14:46:36.010459 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +173 103 JARS Cannabis Mohave Cannabis Club 2, LLC 20340 N Lake Pleasant Rd. Ste 107 Peoria AZ 85382 Operating · Marijuana Facility · 623-246-1065 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Peoria 6232461065 \N https://jarscannabis.com/ 4.8 2462 \N \N \N \N pending jars-cannabis-peoria 2025-11-17 07:29:34.328893 2025-11-17 14:46:36.011974 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +174 104 JARS Cannabis Mohave Cannabis Club 3, LLC 20224 N 27th Ave, Ste 103 Phoenix AZ 85027 Operating · Marijuana Facility · 623-233-5133 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis North Phoenix 6232335133 \N https://jarscannabis.com/ 4.8 4327 \N \N \N \N pending jars-cannabis-phoenix-4 2025-11-17 07:29:34.33016 2025-11-17 14:46:36.013202 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +175 105 JARS Cannabis Mohave Cannabis Club 4, LLC 8028 E State Route 69 Prescott Valley AZ 86314 Operating · Marijuana Facility · 480-939-4002 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Prescott Valley 4809394002 \N https://jarscannabis.com/ 4.9 1838 \N \N \N \N pending jars-cannabis-prescott-valley 2025-11-17 07:29:34.331515 2025-11-17 14:46:36.015183 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +176 106 Trulieve of Roosevelt Row Mohave Valley Consulting, Llc 1007 N 7th St Phoenix AZ 85006 Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Roosevelt%20Row \N \N Trulieve Phoenix Dispensary Roosevelt 7703300831 \N https://www.trulieve.com/dispensaries/arizona/phoenix-roosevelt?utm_source=gmb&utm_medium=organic&utm_campaign=phoenix-roosevelt 4.4 670 \N \N \N \N pending trulieve-of-roosevelt-row 2025-11-17 07:29:34.333047 2025-11-17 14:46:36.129558 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +178 108 Green Farmacy Natural Relief Clinic Inc 4456 E Thomas Road Phoenix AZ 85018 Operating · Marijuana Facility · (520) 686-8708 https://azcarecheck.azdhs.gov/s/?name=Green%20Farmacy \N \N YiLo Superstore (Arcadia) - SKY HARBOR 5206868708 \N https://yilo.com/?utm_source=gmb&utm_medium=organic 4.6 586 \N \N \N \N pending green-farmacy 2025-11-17 07:29:34.335709 2025-11-17 14:46:36.018482 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +179 109 YiLo Superstore Natural Relief Clinic Inc 2841 W Thunderbird Rd Phoenix AZ 85032 Operating · Marijuana Facility · (602) 539-2828 https://azcarecheck.azdhs.gov/s/?name=YiLo%20Superstore \N \N YiLo Superstore (Phoenix) 6025392828 \N https://yilo.com/?utm_source=gmb&utm_medium=organic 4.3 880 \N \N \N \N pending yilo-superstore 2025-11-17 07:29:34.337045 2025-11-17 14:46:36.020365 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +181 111 Trulieve of Baseline Dispensary Nature Med Inc 1821 W Baseline Rd Guadalupe AZ 85283 Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Baseline%20Dispensary \N \N Trulieve Baseline Dispensary 8505080261 \N https://www.trulieve.com/dispensaries/arizona/guadalupe?utm_source=gmb&utm_medium=organic&utm_campaign=baseline 4.4 1297 \N \N \N \N pending trulieve-of-baseline-dispensary 2025-11-17 07:29:34.339705 2025-11-17 14:46:36.022096 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +182 112 The Flower Shop Az Nature's Healing Center Inc 10827 S 51st St, Ste 104 Phoenix AZ 85044 Operating · Marijuana Facility · (480) 500-5054 https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az \N \N The Flower Shop - Ahwatukee 4805005054 \N https://theflowershopusa.com/ahwatukee?utm_source=google-business&utm_medium=organic 4.4 1291 \N \N \N \N pending the-flower-shop-az-phoenix-1 2025-11-17 07:29:34.341296 2025-11-17 14:46:36.023538 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +183 113 The Flower Shop Az Nature's Healing Center Inc 3155 E Mcdowell Rd Ste 2 Phoenix AZ 85008 Operating · Marijuana Facility · (480) 500-5054 https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az \N \N The Flower Shop - Phoenix 4805005054 \N https://theflowershopusa.com/phoenix?utm_source=google-business&utm_medium=organic 4.4 1211 \N \N \N \N pending the-flower-shop-az-phoenix-2 2025-11-17 07:29:34.342757 2025-11-17 14:46:36.131775 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +184 114 Nature's Wonder Inc Nature's Wonder Inc 260 W Apache Trail Dr Apache Junction AZ 85120 Operating · Marijuana Facility · (480) 861-3649 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Wonder%20Inc \N \N Nature's Wonder Apache Junction Dispensary 4808613649 \N https://natureswonderaz.com/apache-junction-dispensary-menu-recreational 4.7 1891 \N \N \N \N pending nature-s-wonder-inc 2025-11-17 07:29:34.344384 2025-11-17 14:46:36.025171 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +185 115 Nirvana Center Dispensaries Nirvana Enterprises AZ, LLC 2 North 35th Avenue Phoenix AZ 85009 Operating · Marijuana Facility · (480) 378-6917 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Dispensaries \N \N Backpack Boyz - Phoenix 4803786917 \N https://www.backpackboyz.com/content/arizona 4.8 6915 \N \N \N \N pending nirvana-center-dispensaries 2025-11-17 07:29:34.345817 2025-11-17 14:46:36.132882 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +186 116 Non Profit Patient Center Inc Non Profit Patient Center Inc 2960 West Grand Avenue, Bldg. A and B Phoenix AZ 85017 Operating · Marijuana Facility · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Non%20Profit%20Patient%20Center%20Inc \N \N \N 4808613649 \N \N \N \N \N \N \N \N pending non-profit-patient-center-inc 2025-11-17 07:29:34.347144 2025-11-17 07:29:34.347144 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +187 117 Ocotillo Vista, Inc. Ocotillo Vista, Inc. 2330 North 75th Avenue Phoenix AZ 85035 Operating · Marijuana Facility · 602-786-7988 https://azcarecheck.azdhs.gov/s/?name=Ocotillo%20Vista%2C%20Inc. \N \N Nirvana Cannabis - 75th Ave (West Phoenix) 6027867988 \N https://nirvanacannabis.com/ 4.7 5296 \N \N \N \N pending ocotillo-vista-inc- 2025-11-17 07:29:34.348551 2025-11-17 14:46:36.026998 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +188 118 Organica Patient Group Inc Organica Patient Group Inc 1720 E. Deer Valley Rd., Suite 101 Phoenix AZ 85204 Operating · Marijuana Facility · 602-910-4152 https://azcarecheck.azdhs.gov/s/?name=Organica%20Patient%20Group%20Inc \N \N Herbal Wellness Center North 6029104152 \N https://herbalwellnesscenter.com/ 4.6 936 \N \N \N \N pending organica-patient-group-inc 2025-11-17 07:29:34.34979 2025-11-17 14:46:36.028619 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +189 119 Trulieve of Glendale Pahana, Inc. 13631 N 59th Ave, Ste B110 Glendale AZ 85304 Operating · Marijuana Facility · 850-508-0261 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Glendale \N \N Trulieve Glendale Dispensary 8505080261 \N https://www.trulieve.com/dispensaries/arizona/glendale?utm_source=gmb&utm_medium=organic&utm_campaign=glendale 4.2 599 \N \N \N \N pending trulieve-of-glendale 2025-11-17 07:29:34.351103 2025-11-17 14:46:36.03016 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +190 120 Zen Leaf Arcadia Patient Alternative Relief Center, LLC 2710 E Indian School Rd Phoenix AZ 85016 Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Arcadia \N \N Zen Leaf Dispensary Arcadia 3128195061 \N https://zenleafdispensaries.com/locations/phoenix-arcadia/?utm_campaign=az-phoenix-arcadia&utm_medium=gbp&utm_source=google 4.7 2099 \N \N \N \N pending zen-leaf-arcadia 2025-11-17 07:29:34.352388 2025-11-17 14:46:36.133962 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +191 121 Trulieve of Tucson Grant Dispensary Patient Care Center 301, Inc. 2734 E Grant Rd Tucson AZ 85716 Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson%20Grant%20Dispensary \N \N \N 8505597734 \N \N \N \N \N \N \N \N pending trulieve-of-tucson-grant-dispensary 2025-11-17 07:29:34.35395 2025-11-17 07:29:34.35395 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +192 122 JARS Cannabis Payson Dreams LLC 108 N Tonto St Payson AZ 85541 Operating · Marijuana Dispensary · 248-755-7633 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Payson 2487557633 \N https://jarscannabis.com/ 4.9 3259 \N \N \N \N pending jars-cannabis-payson-2 2025-11-17 07:29:34.355308 2025-11-17 14:46:36.031469 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +193 123 Zen Leaf Phoenix (Dunlap Ave.) Perpetual Healthcare, LLC 4244 W Dunlap Rd Ste 1 Phoenix AZ 85051 Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Phoenix%20(Dunlap%20Ave.) \N \N Zen Leaf Dispensary Phoenix (Dunlap Ave.) 3128195061 \N https://zenleafdispensaries.com/locations/phoenix-w-dunlap/?utm_source=google&utm_medium=gbp&utm_campaign=az-phoenix-dunlap 4.7 3082 \N \N \N \N pending zen-leaf-phoenix-dunlap-ave- 2025-11-17 07:29:34.35661 2025-11-17 14:46:36.032755 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +232 162 Green Pharms The Healing Center Farmacy Llc 7235 E Hampton Ave Unit 115 Mesa AZ 85209 Operating · Marijuana Facility · (480) 410-6704 https://azcarecheck.azdhs.gov/s/?name=Green%20Pharms \N \N GreenPharms Dispensary Mesa 4804106704 \N http://greenpharms.com/ 4.6 3982 \N \N \N \N pending green-pharms-mesa 2025-11-17 07:29:34.41092 2025-11-17 14:46:36.078089 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +194 124 Phoenix Relief Center Inc Phoenix Relief Center Inc 6330 S 35th Ave, Ste 104 Phoenix AZ 85041 Operating · Marijuana Facility · (602) 276-3401 https://azcarecheck.azdhs.gov/s/?name=Phoenix%20Relief%20Center%20Inc \N \N PRC by Sunday Goods 6022763401 \N https://sundaygoods.com/location/dispensary-laveen-phoenix-az/?utm_source=google&utm_medium=gbp&utm_campaign=prc_sundaygoods 4.6 1691 \N \N \N \N pending phoenix-relief-center-inc 2025-11-17 07:29:34.358314 2025-11-17 14:46:36.135072 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +197 127 JARS Cannabis Piper's Shop LLC 1809 W Thatcher Blvd Safford AZ 85546 Operating · Marijuana Establishment · 928-424-1313 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Safford 9284241313 \N https://jarscannabis.com/ 4.9 725 \N \N \N \N pending jars-cannabis-safford 2025-11-17 07:29:34.362314 2025-11-17 14:46:36.037655 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +198 128 Pleasant Plants I LLC Pleasant Plants I LLC 6676 West Bell Road Glendale AZ 85308 Operating · Marijuana Establishment · (520) 727-7754 https://azcarecheck.azdhs.gov/s/?name=Pleasant%20Plants%20I%20LLC \N \N Story Cannabis Dispensary Bell Glendale 5207277754 \N https://storycannabis.com/dispensary-locations/arizona/glendale-dispensary/?utm_source=google&utm_medium=Link&utm_campaign=bell_glandale&utm_id=googlelisting 4.4 1137 \N \N \N \N pending pleasant-plants-i-llc 2025-11-17 07:29:34.363719 2025-11-17 14:46:36.136243 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +199 129 Ponderosa Dispensary Ponderosa Botanical Care Inc 318 South Bracken Lane Chandler AZ 85224 Operating · Marijuana Facility · (623) 877-3934 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N Ponderosa Dispensary Chandler 6238773934 \N https://www.pondyaz.com/locations 4.7 839 \N \N \N \N pending ponderosa-dispensary-chandler 2025-11-17 07:29:34.365097 2025-11-17 14:46:36.039342 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +201 131 Trulieve of Tucson Menlo Park Dispensary Purplemed Inc 1010 S Fwy Ste 130 Tucson AZ 85745 Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson%20Menlo%20Park%20Dispensary \N \N Trulieve Tucson Dispensary Menlo Park 8505597734 \N https://www.trulieve.com/dispensaries/arizona/tucson-menlo-park?utm_source=gmb&utm_medium=organic&utm_campaign=tucson-menlo 4.2 645 \N \N \N \N pending trulieve-of-tucson-menlo-park-dispensary 2025-11-17 07:29:34.367701 2025-11-17 14:46:36.13751 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +202 132 The Prime Leaf Rainbow Collective Inc 4220 E Speedway Blvd Tucson AZ 85712 Operating · Marijuana Facility · (520) 207-2753 https://azcarecheck.azdhs.gov/s/?name=The%20Prime%20Leaf \N \N The Prime Leaf 5202072753 \N http://www.theprimeleaf.com/ 4.5 1305 \N \N \N \N pending the-prime-leaf 2025-11-17 07:29:34.369215 2025-11-17 14:46:36.042145 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +203 133 Noble Herb Rch Wellness Center 522 E Route 66 Flagstaff AZ 86001 Operating · Marijuana Facility · (928) 351-7775 https://azcarecheck.azdhs.gov/s/?name=Noble%20Herb \N \N Noble Herb Flagstaff Dispensary 9283517775 \N http://www.nobleherbaz.com/ 4.4 1774 \N \N \N \N pending noble-herb 2025-11-17 07:29:34.370801 2025-11-17 14:46:36.043663 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +204 134 Arizona Natural Concepts Rjk Ventures, Inc. 1039 East Carefree Highway Phoenix AZ 85085 Operating · Marijuana Facility · (602) 224-5999 https://azcarecheck.azdhs.gov/s/?name=Arizona%20Natural%20Concepts \N \N Arizona Natural Concepts Marijuana Dispensary 6022245999 \N https://ancdispensary.com/ 4.7 717 \N \N \N \N pending arizona-natural-concepts 2025-11-17 07:29:34.372342 2025-11-17 14:46:36.04537 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +208 138 S Flower SE 3 Inc. S Flower SE 3 Inc. 4837 North 1st Avenue, Ste 102 Tucson AZ 85718 Operating · Marijuana Establishment · 602-828-7204 https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20SE%203%20Inc. \N \N Sol Flower Dispensary North Tucson 6028287204 \N https://www.livewithsol.com/locations/north-tucson/?utm_source=gmb&utm_medium=organic 4.9 2080 \N \N \N \N pending s-flower-se-3-inc- 2025-11-17 07:29:34.377846 2025-11-17 14:46:36.0486 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +209 139 S Flower SE 4, Inc. S Flower SE 4, Inc. 6437 North Oracle Road Tucson AZ 85704 Operating · Marijuana Establishment · (480) 720-2943 https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20SE%204%2C%20Inc. \N \N Sol Flower Dispensary Casas Adobes 4807202943 \N https://www.livewithsol.com/locations/casas-adobes/ 4.8 869 \N \N \N \N pending s-flower-se-4-inc- 2025-11-17 07:29:34.37911 2025-11-17 14:46:36.05058 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +210 140 Cannabist Salubrious Wellness Clinic Inc 520 S Price Rd, Ste 1 & 2 Tempe AZ 85281 Operating · Marijuana Facility · (312) 819-5061 https://azcarecheck.azdhs.gov/s/?name=Cannabist \N \N Cannabist Tempe by Zen Leaf 3128195061 \N https://zenleafdispensaries.com/locations/tempe/ 4.6 1655 \N \N \N \N pending cannabist 2025-11-17 07:29:34.380609 2025-11-17 14:46:36.140885 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +211 141 Sea Of Green Llc Sea Of Green Llc 6844 East Parkway Norte Mesa AZ 85212 Operating · Marijuana Facility · (480) 325-5000 https://azcarecheck.azdhs.gov/s/?name=Sea%20Of%20Green%20Llc \N \N truBLISS | Medical & Recreational Marijuana Dispensary 4803255000 \N http://trubliss.com/ 4.8 4762 \N \N \N \N pending sea-of-green-llc 2025-11-17 07:29:34.382053 2025-11-17 14:46:36.052356 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +212 142 Serenity Smoke, LLC Serenity Smoke, LLC 19 West Main Street Springerville AZ 85938 Not Operating · Marijuana Establishment https://azcarecheck.azdhs.gov/s/?name=Serenity%20Smoke%2C%20LLC \N \N \N \N \N \N \N \N \N \N \N pending serenity-smoke-llc 2025-11-17 07:29:34.38354 2025-11-17 07:29:34.38354 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +196 126 Pinal County Wellness Center Pinal County Wellness Center 8970 N 91st Ave Peoria AZ 85345 Operating · Marijuana Facility · 623-233-1010 https://azcarecheck.azdhs.gov/s/?name=Pinal%20County%20Wellness%20Center \N \N Curaleaf Dispensary Peoria 6232331010 \N https://curaleaf.com/stores/curaleaf-dispensary-peoria?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.6 1866 https://dutchie.com/dispensary/curaleaf-dispensary-peoria \N \N \N pending pinal-county-wellness-center 2025-11-17 07:29:34.361056 2025-11-17 14:46:36.03564 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +252 182 Zonacare Zonacare 4415 East Monroe Street Phoenix AZ 85034 Operating · Marijuana Facility · 602-396-5757 https://azcarecheck.azdhs.gov/s/?name=Zonacare \N \N Curaleaf Dispensary Phoenix Airport 6023965757 \N https://curaleaf.com/stores/curaleaf-dispensary-phoenix-airport?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.4 2705 https://dutchie.com/dispensary/curaleaf-phoenix \N \N \N pending zonacare 2025-11-17 07:29:34.438386 2025-11-17 14:46:36.155148 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +206 136 Sol Flower S Flower SE 1, Inc 6026 North Oracle Road Tucson AZ 85704 Operating · Marijuana Establishment · 602-828-7204 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N \N 6028287204 \N \N \N \N https://dutchie.com/dispensary/sol-flower-dispensary-north-tucson \N \N \N pending sol-flower-tucson-1 2025-11-17 07:29:34.375152 2025-11-17 07:29:34.375152 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +205 135 S Flower N Phoenix, Inc. S Flower N Phoenix, Inc. 3217 E Shea Blvd, Suite 1 A Phoenix AZ 85028 Operating · Marijuana Facility · (623) 582-0436 https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20N%20Phoenix%2C%20Inc. \N \N Sol Flower Dispensary 32nd & Shea 6235820436 \N https://www.livewithsol.com/deer-valley-dispensary/?utm_source=gmb&utm_medium=organic 4.7 570 https://dutchie.com/dispensary/sol-flower-dispensary-deer-valley \N \N \N pending s-flower-n-phoenix-inc- 2025-11-17 07:29:34.373746 2025-11-17 14:46:36.139089 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +213 143 Trulieve Maricopa Dispensary Sherri Dunn, Llc 44405 W. Honeycutt Avenue maricopa AZ 85139 Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Maricopa%20Dispensary \N \N Trulieve Maricopa Dispensary 9548172370 \N https://www.trulieve.com/dispensaries/arizona/maricopa?utm_source=gmb&utm_medium=organic&utm_campaign=maricopa 3.2 203 \N \N \N \N pending trulieve-maricopa-dispensary 2025-11-17 07:29:34.384872 2025-11-17 14:46:36.14239 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +214 144 Trulieve of Cottonwood Dispensary Sherri Dunn, Llc 2400 E Sr 89a Cottonwood AZ 86326 Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Cottonwood%20Dispensary \N \N Trulieve Cottonwood Dispensary 9548172370 \N https://www.trulieve.com/dispensaries/arizona/cottonwood?utm_source=gmb&utm_medium=organic&utm_campaign=cottonwood 4.4 1201 \N \N \N \N pending trulieve-of-cottonwood-dispensary 2025-11-17 07:29:34.386228 2025-11-17 14:46:36.054628 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +215 145 Nature's Medicines Sixth Street Enterprises Inc 6840 West Grand Ave Glendale AZ 85301 Operating · Marijuana Facility · (623) 301-8478 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N Story Cannabis Dispensary Grand Glendale 6233018478 \N https://storycannabis.com/dispensary-locations/arizona/glendale-grand-ave-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=grand_glendale&utm_term=click&utm_content=website 4.5 3046 \N \N \N \N pending nature-s-medicines-glendale 2025-11-17 07:29:34.387578 2025-11-17 14:46:36.143867 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +216 146 Nature's Medicines Sixth Street Enterprises Inc 2439 W Mcdowell Rd Phoenix AZ 85009 Operating · Marijuana Facility · 480-420-3145 https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines \N \N Story Cannabis Dispensary McDowell 4804203145 \N https://storycannabis.com/shop/arizona/phoenix-mcdowell-dispensary/rec-menu/?utm_source=google&utm_medium=listing&utm_campaign=mcdowell&utm_term=click&utm_content=website 4.6 6641 \N \N \N \N pending nature-s-medicines-phoenix-2 2025-11-17 07:29:34.388878 2025-11-17 14:46:36.056856 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +217 147 Sky Analytical Laboratories Sky Analytical Laboratories 1122 East Washington Street Phoenix AZ 85034 Operating · Marijuana Laboratory · (623) 262-4330 https://azcarecheck.azdhs.gov/s/?name=Sky%20Analytical%20Laboratories \N \N \N 6232624330 \N \N \N \N \N \N \N \N pending sky-analytical-laboratories 2025-11-17 07:29:34.390287 2025-11-17 07:29:34.390287 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +218 148 Smithers CTS Arizona LLC Smithers CTS Arizona LLC 734 W Highland Avenue, 2nd floor Phoenix AZ 85013 Operating · Marijuana Laboratory · (954) 696-7791 https://azcarecheck.azdhs.gov/s/?name=Smithers%20CTS%20Arizona%20LLC \N \N Smithers Cannabis Testing Services Arizona 9546967791 \N https://www.smithers.com/industries/cannabis-testing/contact-us/arizona-cannabis-testing-services 5.0 3 \N \N \N \N pending smithers-cts-arizona-llc 2025-11-17 07:29:34.391643 2025-11-17 14:46:36.059222 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +219 149 The Clifton Bakery LLC Sonoran Flower LLC 700 S. CORONADO BLVD CLIFTON AZ 85533 Operating · Marijuana Establishment · 520-241-7777 https://azcarecheck.azdhs.gov/s/?name=The%20Clifton%20Bakery%20LLC \N \N Clifton Bakery 5202417777 \N http://thecliftonbakery.com/ 4.8 93 \N \N \N \N pending the-clifton-bakery-llc 2025-11-17 07:29:34.393045 2025-11-17 14:46:36.061345 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +220 150 Ponderosa Dispensary Soothing Ponderosa, LLC 5550 E Mcdowell Rd, Ste 103 Mesa AZ 85215 Operating · Marijuana Facility · (480) 213-1402 https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary \N \N Ponderosa Dispensary Mesa 4802131402 \N https://www.pondyaz.com/locations 4.7 1940 \N \N \N \N pending ponderosa-dispensary-mesa-2 2025-11-17 07:29:34.394407 2025-11-17 14:46:36.145817 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +221 151 Nirvana Center Dispensaries SSW Ventures, LLC 702 East Buckeye Road Phoenix AZ 85034 Operating · Marijuana Facility · (602) 786-7988 https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Dispensaries \N \N Nirvana Cannabis - 7th St (Downtown Phoenix) 6027867988 \N https://nirvanacannabis.com/ 4.9 3212 \N \N \N \N pending nirvana-center-dispensaries-phoenix-2 2025-11-17 07:29:34.3958 2025-11-17 14:46:36.063655 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +222 152 Steep Hill Arizona Laboratory Steep Hill Arizona Laboratory 14620 North Cave Creek Road, 3 Phoenix AZ 85022 Operating · Marijuana Laboratory · 602-920-7808 https://azcarecheck.azdhs.gov/s/?name=Steep%20Hill%20Arizona%20Laboratory \N \N Steep Hill Arizona Laboratory 6029207808 \N http://www.steephillarizona.com/ \N \N \N \N \N \N pending steep-hill-arizona-laboratory 2025-11-17 07:29:34.397108 2025-11-17 14:46:36.065762 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +223 153 Superior Organics Superior Organics 211 S 57th Dr Phoenix AZ 85043 Operating · Marijuana Facility · (602) 926-9100 https://azcarecheck.azdhs.gov/s/?name=Superior%20Organics \N \N The Superior Dispensary 6029269100 \N https://thesuperiordispensary.com/ 4.5 959 \N \N \N \N pending superior-organics 2025-11-17 07:29:34.398418 2025-11-17 14:46:36.067744 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +224 154 Trulieve of Apache Junction Dispensary Svaccha, Llc 1985 W Apache Trail Ste 4 Apache Junction AZ 85120 Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Apache%20Junction%20Dispensary \N \N Trulieve Apache Junction Dispensary 9548172370 \N https://www.trulieve.com/dispensaries/arizona/apache-junction?utm_source=gmb&utm_medium=organic&utm_campaign=apache-junction 4.3 408 \N \N \N \N pending trulieve-of-apache-junction-dispensary 2025-11-17 07:29:34.399827 2025-11-17 14:46:36.147668 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +225 155 Trulieve of Tempe Dispensary Svaccha, Llc 710 W Elliot Rd, Ste 102 Tempe AZ 85284 Operating · Marijuana Facility · 954-817-2370 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tempe%20Dispensary \N \N Trulieve Tempe Dispensary 9548172370 \N https://www.trulieve.com/dispensaries/arizona/tempe?utm_source=gmb&utm_medium=organic&utm_campaign=tempe 4.2 790 \N \N \N \N pending trulieve-of-tempe-dispensary 2025-11-17 07:29:34.401217 2025-11-17 14:46:36.069922 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +226 156 Swallowtail 3, LLC Swallowtail 3, LLC 5210 South Priest Dr, A, Suite A Guadalupe AZ 85283 Operating · Marijuana Establishment · 480-749-6468 https://azcarecheck.azdhs.gov/s/?name=Swallowtail%203%2C%20LLC \N \N Mint Cannabis - Tempe 4807496468 \N https://mintdeals.com/tempe-az/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps 4.7 7231 \N \N \N \N pending swallowtail-3-llc 2025-11-17 07:29:34.402654 2025-11-17 14:46:36.07201 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +227 157 Trulieve of Mesa North Dispensary Sweet 5, Llc 1150 W McLellan Rd Mesa AZ 85201 Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Mesa%20North%20Dispensary \N \N Trulieve Mesa Dispensary North 7703300831 \N https://www.trulieve.com/dispensaries/arizona/mesa-north?utm_source=gmb&utm_medium=organic&utm_campaign=north-mesa 4.3 600 \N \N \N \N pending trulieve-of-mesa-north-dispensary 2025-11-17 07:29:34.40414 2025-11-17 14:46:36.149211 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +228 158 Fire. Dispensary The Desert Valley Pharmacy Inc 2825 W Thomas Rd Phoenix AZ 85017 Operating · Marijuana Facility · 480-861-3649 https://azcarecheck.azdhs.gov/s/?name=Fire.%20Dispensary \N \N Nature's Wonder Phoenix Dispensary 4808613649 \N https://natureswonderaz.com/phoenix-dispensary-menu-recreational 4.8 2122 \N \N \N \N pending fire-dispensary 2025-11-17 07:29:34.405532 2025-11-17 14:46:36.074341 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +229 159 Trulieve of Mesa South Dispensary The Giving Tree Wellness Center Of Mesa Inc 938 E Juanita Ave Mesa AZ 85204 Operating · Marijuana Facility · 850-559-7734 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Mesa%20South%20Dispensary \N \N Trulieve Phoenix Dispensary Alhambra 7703300831 \N https://www.trulieve.com/dispensaries/arizona/phoenix-alhambra?utm_source=gmb&utm_medium=organic&utm_campaign=alhambra 4.5 2917 \N \N \N \N pending trulieve-of-mesa-south-dispensary 2025-11-17 07:29:34.406884 2025-11-17 14:34:09.940116 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +230 160 Giving Tree Dispensary The Giving Tree Wellness Center Of North Phoenix Inc. 701 West Union Hills Drive Phoenix AZ 85027 Operating · Marijuana Facility · (623) 242-9080 https://azcarecheck.azdhs.gov/s/?name=Giving%20Tree%20Dispensary \N \N Giving Tree Dispensary 6232429080 \N https://dutchie.com/stores/Nirvana-North-Phoenix 4.6 1932 \N \N \N \N pending giving-tree-dispensary 2025-11-17 07:29:34.408192 2025-11-17 14:46:36.076412 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +233 163 Health For Life Crismon The Healing Center Wellness Center Llc 9949 E Apache Trail Mesa AZ 85207 Operating · Marijuana Facility · (480) 400-1170 https://azcarecheck.azdhs.gov/s/?name=Health%20For%20Life%20Crismon \N \N Health for Life - Crismon - Medical and Recreational Cannabis Dispensary 4804001170 \N https://healthforlifeaz.com/crismon/?utm_source=google&utm_medium=organic&utm_campaign=gbp-crimson 4.5 1993 \N \N \N \N pending health-for-life-crismon 2025-11-17 07:29:34.412379 2025-11-17 14:46:36.150573 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +234 164 Sunday Goods The Health Center Of Cochise Inc 1616 E. Glendale Ave. Phoenix AZ 85020 Operating · Marijuana Facility · 520-808-3111 https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods \N \N Sunday Goods North Central Phoenix 5208083111 \N https://sundaygoods.com/location/dispensary-phoenix-az/?utm_source=google&utm_medium=gbp&utm_campaign=phoenix_gbp 4.4 652 \N \N \N \N pending sunday-goods-phoenix 2025-11-17 07:29:34.413925 2025-11-17 14:46:36.079974 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +236 166 Zen Leaf Mesa The Medicine Room Llc 550 W Mckellips Rd Bldg 1 Mesa AZ 85201 Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Mesa \N \N Zen Leaf Mesa 3128195061 \N https://zenleafdispensaries.com/locations/mesa/?utm_campaign=az-mesa&utm_medium=gbp&utm_source=google 4.8 2602 \N \N \N \N pending zen-leaf-mesa 2025-11-17 07:29:34.416852 2025-11-17 14:46:36.083731 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +237 167 Total Accountability Patient Care Total Accountability Patient Care 1525 N. Park Ave Tucson AZ 85719 Operating · Marijuana Facility · (520) 586-8710 https://azcarecheck.azdhs.gov/s/?name=Total%20Accountability%20Patient%20Care \N \N The Prime Leaf 5205868710 \N https://theprimeleaf.com/ 4.6 669 \N \N \N \N pending total-accountability-patient-care 2025-11-17 07:29:34.418267 2025-11-17 14:46:36.151757 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +238 168 Total Accountability Systems I Inc Total Accountability Systems I Inc 6287 E Copper Hill Dr. Ste A Prescott Valley AZ 86314 Operating · Marijuana Facility · (928) 3505870 https://azcarecheck.azdhs.gov/s/?name=Total%20Accountability%20Systems%20I%20Inc \N \N Nirvana Cannabis - Prescott Valley \N https://nirvanacannabis.com/ 4.7 4098 \N \N \N \N pending total-accountability-systems-i-inc 2025-11-17 07:29:34.419645 2025-11-17 14:46:36.096267 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +239 169 Oasis Total Health & Wellness Inc 17006 S Weber Dr Chandler AZ 85226 Operating · Marijuana Facility · (480) 626-7333 https://azcarecheck.azdhs.gov/s/?name=Oasis \N \N Story Cannabis Dispensary North Chandler 4806267333 \N https://storycannabis.com/dispensary-locations/arizona/north-chandler-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=north_chandler&utm_term=click&utm_content=website 4.3 853 \N \N \N \N pending oasis 2025-11-17 07:29:34.420881 2025-11-17 14:46:36.152967 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +240 170 Total Health & Wellness Inc Total Health & Wellness Inc 3830 North 7th Street Phoenix AZ 85014 Operating · Marijuana Facility · (623) 295-1788 https://azcarecheck.azdhs.gov/s/?name=Total%20Health%20%26%20Wellness%20Inc \N \N Nirvana Cannabis - Tucson 9286422250 \N https://nirvanacannabis.com/ 4.7 2156 \N \N \N \N pending total-health-wellness-inc 2025-11-17 07:29:34.422141 2025-11-17 14:34:09.946522 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +241 171 Uncle Harry Inc Uncle Harry Inc 17036 North Cave Creek Rd Phoenix AZ 85032 Operating · Marijuana Facility · (818) 822-9888 https://azcarecheck.azdhs.gov/s/?name=Uncle%20Harry%20Inc \N \N Mint Cannabis - Phoenix 8188229888 \N https://mintdeals.com/phoenix-az/ 4.7 2658 \N \N \N \N pending uncle-harry-inc 2025-11-17 07:29:34.423429 2025-11-17 14:46:36.097461 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +242 172 The Good Dispensary Valley Healing Group Inc 1842 W Broadway Rd Mesa AZ 85202 Operating · Marijuana Facility · (480) 900-8042 https://azcarecheck.azdhs.gov/s/?name=The%20Good%20Dispensary \N \N The GOOD Dispensary 4809008042 \N https://thegooddispensary.com/ 4.8 5884 \N \N \N \N pending the-good-dispensary 2025-11-17 07:29:34.424693 2025-11-17 14:46:36.098583 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +243 173 Valley Of The Sun Medical Dispensary, Inc. Valley Of The Sun Medical Dispensary, Inc. 16200 W Eddie Albert Way Goodyear AZ 85338 Operating · Marijuana Facility · (623) 932-3859 https://azcarecheck.azdhs.gov/s/?name=Valley%20Of%20The%20Sun%20Medical%20Dispensary%2C%20Inc. \N \N Valley of the Sun Dispensary 6239323859 \N http://votsmd.com/?utm_source=local&utm_medium=organic&utm_campaign=gmb 4.0 598 \N \N \N \N pending valley-of-the-sun-medical-dispensary-inc- 2025-11-17 07:29:34.426103 2025-11-17 14:46:36.15404 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +244 174 Zen Leaf Gilbert Vending Logistics Llc 5409 S Power Rd Mesa AZ 85212 Operating · Marijuana Facility · 312-819-5061 https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Gilbert \N \N Zen Leaf Dispensary Gilbert 3128195061 \N https://zenleafdispensaries.com/locations/gilbert/?utm_source=google&utm_medium=gbp&utm_campaign=az-gilbert 4.8 2791 \N \N \N \N pending zen-leaf-gilbert 2025-11-17 07:29:34.427427 2025-11-17 14:46:36.099926 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +245 175 Flor Verde Dispensary Verde Americano, LLC 1115 Circulo Mercado Rio Rico AZ 85648 Operating · Marijuana Dispensary · 602 689-3559 https://azcarecheck.azdhs.gov/s/?name=Flor%20Verde%20Dispensary \N \N Green Med Wellness \N \N \N \N \N \N \N \N pending flor-verde-dispensary 2025-11-17 07:29:34.428758 2025-11-17 07:29:34.428758 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +246 176 Medusa Farms Verde Dispensary Inc 3490 N Bank St Kingman AZ 86409 Operating · Marijuana Facility · (928) 421-0020 https://azcarecheck.azdhs.gov/s/?name=Medusa%20Farms \N \N Zen Leaf Dispensary Chandler 3128195061 \N https://zenleafdispensaries.com/locations/chandler/?utm_campaign=az-chandler&utm_medium=gbp&utm_source=google 4.8 3044 \N \N \N \N pending medusa-farms 2025-11-17 07:29:34.430181 2025-11-17 14:34:09.948839 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +247 177 White Mountain Health Center Inc White Mountain Health Center Inc 9420 W Bell Rd Ste 108 Sun City AZ 85351 Operating · Marijuana Facility · (623) 374-4141 https://azcarecheck.azdhs.gov/s/?name=White%20Mountain%20Health%20Center%20Inc \N \N White Mountain Health Center 6233744141 \N https://whitemountainhealthcenter.com/ 4.7 1664 \N \N \N \N pending white-mountain-health-center-inc 2025-11-17 07:29:34.431509 2025-11-17 14:46:36.101233 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +249 179 Wickenburg Alternative Medicine Llc Wickenburg Alternative Medicine Llc 12620 N Cave Creek Road, Ste 1 Phoenix AZ 85022 Operating · Marijuana Facility · (623) 478-2233 https://azcarecheck.azdhs.gov/s/?name=Wickenburg%20Alternative%20Medicine%20Llc \N \N Sticky Saguaro 6026449188 \N https://stickysaguaro.com/ 4.6 1832 \N \N \N \N pending wickenburg-alternative-medicine-llc 2025-11-17 07:29:34.434192 2025-11-17 14:34:09.949935 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +250 180 Woodstock 1 Woodstock 1 1629 N 195 Ave, 101 Buckeye AZ 85396 Operating · Marijuana Establishment · 602-980-1505 https://azcarecheck.azdhs.gov/s/?name=Woodstock%201 \N \N Waddell's Longhorn 6029801505 \N https://www.waddellslonghorn.com/ 4.2 1114 \N \N \N \N pending woodstock-1 2025-11-17 07:29:34.435609 2025-11-17 14:46:36.104008 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +251 181 JARS Cannabis Yuma County Dispensary LLC 3345 E County 15th St Somerton AZ 85350 Operating · Marijuana Establishment · 928-919-8667 https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis \N \N JARS Cannabis Yuma 9289198667 \N https://jarscannabis.com/ 4.9 2154 \N \N \N \N pending jars-cannabis-somerton 2025-11-17 07:29:34.436973 2025-11-17 14:46:36.105442 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +79 9 AGI Management LLC AGI Management LLC 1035 W Main St Quartzsite AZ 85346 Operating · Marijuana Establishment · 480-234-2343 https://azcarecheck.azdhs.gov/s/?name=AGI%20Management%20LLC \N \N \N 4802342343 \N https://example-dispensary.com \N \N \N \N \N \N pending agi-management-llc 2025-11-17 07:29:34.204841 2025-11-17 17:32:14.006972 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +149 79 BEST Dispensary Jamestown Center 1962 N. Higley Rd Mesa AZ 85205 Operating · Marijuana Facility · 623-264-2378 https://azcarecheck.azdhs.gov/s/?name=BEST%20Dispensary \N \N BEST Dispensary 6232642378 \N http://www.bestdispensary.com/ 4.6 482 https://best.treez.io/onlinemenu/?customerType=ADULT treez \N \N pending best-dispensary 2025-11-17 07:29:34.297245 2025-11-18 08:01:45.11724 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +195 125 Phytotherapeutics Of Tucson Phytotherapeutics Of Tucson 2175 N 83rd Ave Phoenix AZ 85035 Operating · Marijuana Facility · 623-244-5349 https://azcarecheck.azdhs.gov/s/?name=Phytotherapeutics%20Of%20Tucson \N \N Curaleaf Dispensary Pavilions 6232445349 \N https://curaleaf.com/stores/curaleaf-dispensary-pavilions?utm_source=google&utm_medium=organic&utm_campaign=gmb-menu 4.5 2239 https://dutchie.com/dispensary/curaleaf-83rd-ave \N \N \N pending phytotherapeutics-of-tucson 2025-11-17 07:29:34.35978 2025-11-17 14:46:36.033997 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +77 7 Absolute Health Care Inc Absolute Health Care Inc 175 S Hamilton Pl Bldg 4 Ste 110 Gilbert AZ 85233 Operating · Marijuana Facility · 480-361-0078 https://azcarecheck.azdhs.gov/s/?name=Absolute%20Health%20Care%20Inc \N \N Curaleaf Dispensary Gilbert 4803610078 \N https://curaleaf.com/stores/curaleaf-dispensary-gilbert?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.6 3423 https://dutchie.com/dispensary/curaleaf-gilbert \N \N \N pending absolute-health-care-inc 2025-11-17 07:29:34.200939 2025-11-17 14:46:36.087793 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +248 178 Whoa Qc Inc Whoa Qc Inc 5558 W. Bell Rd. Glendale AZ 85308 Operating · Marijuana Facility · 602-535-0999 https://azcarecheck.azdhs.gov/s/?name=Whoa%20Qc%20Inc \N \N Curaleaf Dispensary Glendale East 6025350999 \N https://curaleaf.com/stores/curaleaf-az-glendale-east?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.8 3990 https://dutchie.com/dispensary/curaleaf-glendale-east \N \N \N pending whoa-qc-inc 2025-11-17 07:29:34.432825 2025-11-17 14:46:36.102624 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +200 130 Pp Wellness Center Pp Wellness Center 8160 W Union Hills Dr Ste A106 Glendale AZ 85308 Operating · Marijuana Facility · (623) 385-1310 https://azcarecheck.azdhs.gov/s/?name=Pp%20Wellness%20Center \N \N Curaleaf Dispensary Glendale 6233851310 \N https://curaleaf.com/stores/curaleaf-dispensary-glendale?utm_source=google&utm_medium=organic&utm_campaign=gmb-menu 4.6 2166 https://dutchie.com/dispensary/curaleaf-glendale \N \N \N pending pp-wellness-center 2025-11-17 07:29:34.366362 2025-11-17 14:46:36.04065 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +235 165 The Kind Relief Inc The Kind Relief Inc 18423 E San Tan Blvd Ste #1 Queen Creek AZ 85142 Operating · Marijuana Facility · 480-550-9121 https://azcarecheck.azdhs.gov/s/?name=The%20Kind%20Relief%20Inc \N \N Curaleaf Dispensary Queen Creek 4805509121 \N https://curaleaf.com/stores/curaleaf-az-queen-creek?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.7 4110 https://dutchie.com/dispensary/curaleaf-queen-creek \N \N \N pending the-kind-relief-inc 2025-11-17 07:29:34.415378 2025-11-17 14:46:36.081927 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +180 110 Natural Remedy Patient Center Natural Remedy Patient Center 16277 N Greenway Hayden Loop, 1st Floor Scottsdale AZ 85260 Operating · Marijuana Facility · 602-842-0020 https://azcarecheck.azdhs.gov/s/?name=Natural%20Remedy%20Patient%20Center \N \N Curaleaf Dispensary Scottsdale 6028420020 \N https://curaleaf.com/stores/curaleaf-dispensary-scottsdale?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.6 849 https://dutchie.com/dispensary/curaleaf-dispensary-scottsdale \N \N \N pending natural-remedy-patient-center 2025-11-17 07:29:34.338426 2025-11-17 14:46:36.13068 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +207 137 Sol Flower S Flower SE 2, Inc. 3000 West Valencia Road, Suite 210 Tucson AZ 85746 Operating · Marijuana Establishment · (602) 828-7204 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N Sol Flower Dispensary South Tucson 6028287204 \N https://www.livewithsol.com/locations/south-tucson/?utm_source=gmb&utm_medium=organic 4.7 1914 https://dutchie.com/dispensary/sol-flower-dispensary-south-tucson \N \N \N pending sol-flower-tucson-2 2025-11-17 07:29:34.376613 2025-11-17 14:46:36.046812 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +153 83 Sol Flower Kannaboost Technology Inc 2424 W University Dr, Ste. 101 & 119 Tempe AZ 85281 Operating · Marijuana Facility · 480-644-2071 https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower \N \N Sol Flower Dispensary University 4806442071 \N https://www.livewithsol.com/locations/tempe-university/?utm_source=gmb&utm_medium=organic 4.6 1149 https://dutchie.com/dispensary/sol-flower-dispensary-mcclintock \N \N \N pending sol-flower-tempe 2025-11-17 07:29:34.302196 2025-11-17 14:46:35.995733 \N 0 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +255 \N Curaleaf - 48th Street \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-dispensary-48th-street \N \N \N pending curaleaf-dispensary-48th-street 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +81 11 All Rebel Rockers Inc All Rebel Rockers Inc 4730 S 48th St Phoenix AZ 85040 Operating · Marijuana Facility · 602-807-5005 https://azcarecheck.azdhs.gov/s/?name=All%20Rebel%20Rockers%20Inc \N \N Curaleaf Dispensary 48th Street 6028075005 \N https://curaleaf.com/stores/curaleaf-dispensary-48th-street?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.6 1381 \N \N \N \N pending all-rebel-rockers-inc 2025-11-17 07:29:34.208023 2025-12-01 07:41:59.449786 \N 0 production idle \N \N {} unknown 0 sandbox \N {} unknown 0 sandbox \N {"specials_html_pattern": true} unknown 0 sandbox \N {} unknown 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +115 45 Devine Desert Healing Inc Devine Desert Healing Inc 17201 N 19th Ave Phoenix AZ 85023 Operating · Marijuana Facility · 602-388-4400 https://azcarecheck.azdhs.gov/s/?name=Devine%20Desert%20Healing%20Inc \N \N Curaleaf Dispensary Bell 6023884400 \N https://curaleaf.com/stores/curaleaf-dispensary-bell?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.5 2873 https://dutchie.com/dispensary/curaleaf-bell-road \N \N \N pending devine-desert-healing-inc 2025-11-17 07:29:34.254693 2025-12-01 07:42:04.602585 \N 0 production idle \N \N {} dutchie 100 production 2025-12-01 07:42:04.602585+00 {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "specials_html_pattern": true, "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} unknown f \N \N pending \N 0 0 0 +140 70 Healing Healthcare 3 Inc Healing Healthcare 3 Inc 1040 E Camelback Rd, Ste A Phoenix AZ 85014 Operating · Marijuana Facility · 602-354-3094 https://azcarecheck.azdhs.gov/s/?name=Healing%20Healthcare%203%20Inc \N \N Curaleaf Dispensary Camelback 6023543094 \N https://curaleaf.com/stores/curaleaf-dispensary-camelback?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.5 2853 https://dutchie.com/dispensary/curaleaf-camelback \N \N \N pending healing-healthcare-3-inc 2025-11-17 07:29:34.285217 2025-12-01 07:42:09.688135 \N 0 production idle \N \N {} dutchie 100 production 2025-12-01 07:42:09.688135+00 {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "specials_html_pattern": true, "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} unknown f \N \N pending \N 0 0 0 +177 107 Natural Herbal Remedies Inc Natural Herbal Remedies Inc 3333 S Central Ave Phoenix AZ 85040 Operating · Marijuana Facility · 480-739-0366 https://azcarecheck.azdhs.gov/s/?name=Natural%20Herbal%20Remedies%20Inc \N \N Curaleaf Dispensary Central Phoenix 4807390366 \N https://curaleaf.com/stores/curaleaf-dispensary-central?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu 4.6 2577 https://dutchie.com/dispensary/curaleaf-central \N \N \N pending natural-herbal-remedies-inc 2025-11-17 07:29:34.334407 2025-12-01 07:42:14.800931 \N 0 production idle \N \N {} dutchie 100 production 2025-12-01 07:42:14.800931+00 {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "specials_html_pattern": true, "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/dispensary/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} unknown f \N \N pending \N 0 0 0 +78 8 Trulieve of Phoenix Alhambra Ad, Llc 2630 W Indian School Rd Phoenix AZ 85017 Operating · Marijuana Facility · 770-330-0831 https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Phoenix%20Alhambra \N \N Trulieve Phoenix Dispensary Alhambra 7703300831 \N https://www.trulieve.com/dispensaries/arizona/phoenix-alhambra?utm_source=gmb&utm_medium=organic&utm_campaign=alhambra 4.5 2917 \N \N \N \N pending trulieve-of-phoenix-alhambra 2025-11-17 07:29:34.203168 2025-12-01 15:16:43.860532 \N 0 production queued_detection \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} unknown f \N \N pending \N 0 0 0 +265 \N Curaleaf - Peoria \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-dispensary-peoria \N \N \N pending curaleaf-dispensary-peoria 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +266 \N Curaleaf - Phoenix Airport \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-phoenix \N \N \N pending curaleaf-phoenix 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +256 \N Curaleaf - 83rd Ave \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-83rd-ave \N \N \N pending curaleaf-83rd-ave 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +257 \N Curaleaf - Gilbert \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-gilbert \N \N \N pending curaleaf-gilbert 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +258 \N Curaleaf - Glendale East \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-glendale-east \N \N \N pending curaleaf-glendale-east 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +259 \N Curaleaf - Glendale East Kind Relief \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-glendale-east-kind-relief \N \N \N pending curaleaf-glendale-east-kind-relief 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +260 \N Curaleaf - Bell \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-bell-road \N \N \N pending curaleaf-bell-road 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +261 \N Curaleaf - Camelback \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-camelback \N \N \N pending curaleaf-camelback 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +262 \N Curaleaf - Central \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-central \N \N \N pending curaleaf-central 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +263 \N Curaleaf - Glendale \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-glendale \N \N \N pending curaleaf-glendale 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +264 \N Curaleaf - Midtown \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-dispensary-midtown \N \N \N pending curaleaf-dispensary-midtown 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +267 \N Curaleaf - Queen Creek \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-queen-creek \N \N \N pending curaleaf-queen-creek 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +268 \N Curaleaf - Queen Creek WHOA \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-queen-creek-whoa \N \N \N pending curaleaf-queen-creek-whoa 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +269 \N Curaleaf - Scottsdale Natural Remedy \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-dispensary-scottsdale \N \N \N pending curaleaf-dispensary-scottsdale 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +270 \N Curaleaf - Sedona \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-dispensary-sedona \N \N \N pending curaleaf-dispensary-sedona 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +271 \N Curaleaf - Tucson \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-tucson \N \N \N pending curaleaf-tucson 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +272 \N Curaleaf - Youngtown \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/curaleaf-youngtown \N \N \N pending curaleaf-youngtown 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +273 \N Sol Flower - Sun City \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/sol-flower-dispensary \N \N \N pending sol-flower-dispensary 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +274 \N Sol Flower - South Tucson \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/sol-flower-dispensary-south-tucson \N \N \N pending sol-flower-dispensary-south-tucson 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +275 \N Sol Flower - North Tucson \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/sol-flower-dispensary-north-tucson \N \N \N pending sol-flower-dispensary-north-tucson 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +276 \N Sol Flower - McClintock (Tempe) \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/sol-flower-dispensary-mcclintock \N \N \N pending sol-flower-dispensary-mcclintock 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +277 \N Sol Flower - Deer Valley (Phoenix) \N TBD TBD AZ \N \N \N \N \N \N \N \N \N \N \N https://dutchie.com/dispensary/sol-flower-dispensary-deer-valley \N \N \N pending sol-flower-dispensary-deer-valley 2025-12-01 15:14:19.185936 2025-12-01 15:14:19.185936 dutchie 100 production idle \N \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} \N 0 sandbox \N {} dutchie f \N \N pending \N 0 0 0 +112 42 Deeply Rooted Boutique Cannabis Company Desert Boyz 11725 NW Grand Ave El Mirage AZ 85335 Operating · Marijuana Establishment · (480) 708-0296 https://azcarecheck.azdhs.gov/s/?name=Deeply%20Rooted%20Boutique%20Cannabis%20Company \N \N Deeply Rooted Boutique Cannabis Company Dispensary 4807080296 \N http://azdeeplyrooted.com/ 4.8 568 https://dutchie.com/embedded-menu/AZ-Deeply-Rooted dutchie \N \N pending deeply-rooted-boutique-cannabis-company 2025-11-17 07:29:34.250572 2025-12-01 07:41:48.779274 dutchie 100 production idle \N \N {} dutchie 100 production 2025-12-01 07:41:48.779274+00 {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/embedded-menu/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "specials_html_pattern": true, "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/embedded-menu/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/embedded-menu/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie 100 sandbox \N {"dutchie_script_/menu": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/shop": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/deals": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/order": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/brands": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_mainPage": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/embedded-menu/[cName]\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/products": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}", "dutchie_script_/specials": "inline:{\\"props\\":{\\"pageProps\\":{}},\\"page\\":\\"/_error\\",\\"query\\":{},\\"buildId\\":\\"zhX-_Y9x0IPLY21dJNESA\\",\\"assetPrefix\\":\\"https://assets2.dutchie.com\\",\\"nextExport\\":true,\\"autoExport\\":true,\\"isFallback\\":false,\\"scriptLoader\\":[]}"} dutchie f \N \N pending \N 0 0 0 +\. + + +-- +-- Data for Name: dispensary_changes; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.dispensary_changes (id, dispensary_id, field_name, old_value, new_value, source, confidence_score, change_notes, status, requires_recrawl, created_at, reviewed_at, reviewed_by, rejection_reason) FROM stdin; +1 79 website \N https://example-dispensary.com test high Test change record for workflow verification approved t 2025-11-17 17:26:00.23668 2025-11-17 17:32:14.009388 1 \N +\. + + +-- +-- Data for Name: dispensary_crawl_jobs; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.dispensary_crawl_jobs (id, dispensary_id, schedule_id, job_type, trigger_type, status, priority, scheduled_at, started_at, completed_at, duration_ms, detection_ran, crawl_ran, crawl_type, products_found, products_new, products_updated, detected_provider, detected_confidence, detected_mode, error_message, worker_id, run_id, created_at, updated_at, in_stock_count, out_of_stock_count, limited_count, unknown_count, availability_changed_count) FROM stdin; +1 112 \N orchestrator manual failed 100 2025-12-01 07:05:29.483029+00 2025-12-01 07:05:29.483029+00 2025-12-01 07:05:29.483029+00 11740 t t production \N \N \N dutchie 100 production column s.dutchie_plus_url does not exist \N 5ce79234-6fce-49fe-8ca9-75b296bdf049 2025-12-01 07:05:29.483029+00 2025-12-01 07:05:29.483029+00 \N \N \N \N \N +2 81 \N orchestrator manual completed 100 2025-12-01 07:05:40.184666+00 2025-12-01 07:05:40.184666+00 2025-12-01 07:05:40.184666+00 10687 t f \N \N \N \N unknown \N sandbox \N \N 7d78d7bc-556c-4691-ba53-03b034847764 2025-12-01 07:05:40.184666+00 2025-12-01 07:05:40.184666+00 \N \N \N \N \N +3 115 \N orchestrator manual failed 100 2025-12-01 07:05:50.409783+00 2025-12-01 07:05:50.409783+00 2025-12-01 07:05:50.409783+00 10208 t t production \N \N \N dutchie 100 production column s.dutchie_plus_url does not exist \N 8cd7eab4-510d-4bcf-ac15-b2f38e658108 2025-12-01 07:05:50.409783+00 2025-12-01 07:05:50.409783+00 \N \N \N \N \N +4 140 \N orchestrator manual failed 100 2025-12-01 07:06:00.384169+00 2025-12-01 07:06:00.384169+00 2025-12-01 07:06:00.384169+00 9955 t t production \N \N \N dutchie 100 production column s.dutchie_plus_url does not exist \N 88ba0fd4-26e9-473d-a4d5-58f5e0b04839 2025-12-01 07:06:00.384169+00 2025-12-01 07:06:00.384169+00 \N \N \N \N \N +5 177 \N orchestrator manual failed 100 2025-12-01 07:06:10.405679+00 2025-12-01 07:06:10.405679+00 2025-12-01 07:06:10.405679+00 10002 t t production \N \N \N dutchie 100 production column s.dutchie_plus_url does not exist \N 0b6743a2-7cde-4b9e-a21a-305582ff7bc7 2025-12-01 07:06:10.405679+00 2025-12-01 07:06:10.405679+00 \N \N \N \N \N +6 112 \N orchestrator manual completed 100 2025-12-01 07:41:48.793517+00 2025-12-01 07:41:48.793517+00 2025-12-01 07:41:48.793517+00 2118615 f t production \N \N \N \N \N \N \N \N 313bbe96-d621-4fcc-84aa-4f1bdf9526c9 2025-12-01 07:41:48.793517+00 2025-12-01 07:41:48.793517+00 \N \N \N \N \N +7 81 \N orchestrator manual completed 100 2025-12-01 07:41:59.465657+00 2025-12-01 07:41:59.465657+00 2025-12-01 07:41:59.465657+00 10660 t f \N \N \N \N unknown \N sandbox \N \N 3820d3d0-9ff0-4a35-a682-81195c410e1a 2025-12-01 07:41:59.465657+00 2025-12-01 07:41:59.465657+00 \N \N \N \N \N +8 115 \N orchestrator manual completed 100 2025-12-01 07:42:04.605624+00 2025-12-01 07:42:04.605624+00 2025-12-01 07:42:04.605624+00 5134 f t production \N \N \N \N \N \N \N \N a8a8f66d-0e97-4ade-a485-9dc0e40e94d0 2025-12-01 07:42:04.605624+00 2025-12-01 07:42:04.605624+00 \N \N \N \N \N +9 140 \N orchestrator manual completed 100 2025-12-01 07:42:09.691446+00 2025-12-01 07:42:09.691446+00 2025-12-01 07:42:09.691446+00 5081 f t production \N \N \N \N \N \N \N \N 3eb03a6e-525b-42b2-a986-432e01c3fa27 2025-12-01 07:42:09.691446+00 2025-12-01 07:42:09.691446+00 \N \N \N \N \N +10 177 \N orchestrator manual completed 100 2025-12-01 07:42:14.8046+00 2025-12-01 07:42:14.8046+00 2025-12-01 07:42:14.8046+00 5109 f t production \N \N \N \N \N \N \N \N 68a441e0-10ab-48a3-a1d0-71293d7feead 2025-12-01 07:42:14.8046+00 2025-12-01 07:42:14.8046+00 \N \N \N \N \N +\. + + +-- +-- Data for Name: dispensary_crawl_schedule; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.dispensary_crawl_schedule (id, dispensary_id, is_active, interval_minutes, priority, last_run_at, next_run_at, last_status, last_summary, last_error, last_duration_ms, consecutive_failures, total_runs, successful_runs, created_at, updated_at) FROM stdin; +1 71 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +2 72 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +3 73 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +4 74 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +5 75 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +6 76 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +7 77 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +8 78 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +9 80 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +11 82 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +12 83 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +13 84 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +14 85 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +15 86 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +16 87 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +17 88 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +18 89 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +19 90 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +20 91 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +21 92 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +22 93 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +23 94 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +24 95 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +25 96 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +26 97 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +27 98 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +28 99 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +29 100 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +30 101 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +31 102 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +32 103 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +33 104 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +34 105 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +35 106 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +36 107 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +37 108 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +38 109 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +39 110 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +40 111 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +41 113 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +42 114 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +44 116 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +45 117 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +46 118 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +47 119 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +48 120 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +49 121 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +50 122 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +51 123 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +52 124 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +53 125 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +54 126 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +55 127 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +56 128 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +57 129 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +58 130 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +59 131 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +60 132 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +61 231 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +62 133 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +63 134 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +64 135 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +65 136 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +66 137 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +67 138 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +68 139 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +70 141 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +71 142 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +72 143 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +73 144 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +74 145 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +75 146 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +76 147 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +77 148 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +78 150 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +79 151 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +80 152 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +81 153 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +82 154 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +83 155 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +84 156 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +85 157 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +86 158 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +87 159 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +88 160 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +89 161 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +90 162 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +91 163 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +92 164 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +93 165 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +94 166 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +95 167 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +96 168 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +97 169 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +98 170 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +99 171 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +100 172 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +101 173 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +102 174 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +103 175 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +104 176 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +106 178 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +107 179 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +108 180 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +109 181 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +110 182 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +111 183 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +112 184 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +113 185 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +114 186 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +115 187 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +116 188 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +117 189 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +118 190 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +119 191 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +120 192 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +121 193 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +122 232 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +123 194 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +124 195 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +125 196 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +126 197 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +127 198 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +128 199 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +129 200 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +130 201 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +131 202 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +132 203 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +133 204 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +134 205 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +135 206 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +136 207 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +137 208 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +138 209 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +139 210 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +140 211 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +141 212 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +142 252 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +143 213 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +144 214 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +145 215 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +146 216 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +147 217 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +148 218 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +149 219 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +150 220 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +151 221 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +152 222 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +153 223 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +154 224 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +155 225 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +156 226 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +157 227 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +158 228 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +159 229 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +160 230 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +161 233 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +162 234 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +163 235 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +164 236 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +165 237 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +166 238 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +167 239 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +168 240 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +169 241 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +170 242 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +171 243 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +172 244 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +173 245 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +174 246 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +175 247 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +176 248 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +177 249 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +178 250 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +179 251 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +180 79 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +182 149 t 240 0 \N \N \N \N \N \N 0 0 0 2025-12-01 00:29:42.918747+00 2025-12-01 00:29:42.918747+00 +10 81 t 240 0 2025-12-01 07:41:59.463225+00 2025-12-01 11:41:59.46807+00 detection_only Detection complete: provider=unknown, confidence=0% \N 10660 0 2 2 2025-12-01 00:29:42.918747+00 2025-12-01 07:41:59.463225+00 +43 115 t 240 0 2025-12-01 07:42:04.604287+00 2025-12-01 11:42:04.607178+00 success Dutchie products crawl completed \N 5134 0 2 1 2025-12-01 00:29:42.918747+00 2025-12-01 07:42:04.604287+00 +69 140 t 240 0 2025-12-01 07:42:09.689985+00 2025-12-01 11:42:09.692909+00 success Dutchie products crawl completed \N 5081 0 2 1 2025-12-01 00:29:42.918747+00 2025-12-01 07:42:09.689985+00 +181 112 t 240 0 2025-12-01 07:47:00.379547+00 2025-12-01 11:41:48.798374+00 running Running Dutchie production crawl... \N 2118615 0 2 1 2025-12-01 00:29:42.918747+00 2025-12-01 07:47:00.379547+00 +105 177 t 240 0 2025-12-01 07:42:14.803193+00 2025-12-01 11:42:14.806404+00 success Dutchie products crawl completed \N 5109 0 2 1 2025-12-01 00:29:42.918747+00 2025-12-01 07:42:14.803193+00 +\. + + +-- +-- Data for Name: failed_proxies; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.failed_proxies (id, host, port, protocol, username, password, failure_count, last_error, failed_at, created_at, city, state, country, country_code, location_updated_at) FROM stdin; +\. + + +-- +-- Data for Name: jobs; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.jobs (id, type, status, store_id, progress, total_items, processed_items, error, started_at, completed_at, created_at) FROM stdin; +\. + + +-- +-- Data for Name: price_history; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.price_history (id, product_id, regular_price, sale_price, recorded_at) FROM stdin; +\. + + +-- +-- Data for Name: product_categories; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.product_categories (id, product_id, category_slug, first_seen_at, last_seen_at) FROM stdin; +828 2067 brands 2025-11-18 03:16:27.275158 2025-11-18 03:16:27.275158 +829 2067 specials 2025-11-18 03:16:27.275158 2025-11-18 03:16:27.275158 +830 2067 all products 2025-11-18 03:16:27.275158 2025-11-18 03:16:27.275158 +831 2067 flower 2025-11-18 03:16:27.275158 2025-11-18 03:16:27.275158 +\. + + +-- +-- Data for Name: products; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.products (id, store_id, category_id, dutchie_product_id, name, slug, description, price, original_price, strain_type, thc_percentage, cbd_percentage, brand, weight, image_url, local_image_path, dutchie_url, in_stock, is_special, metadata, first_seen_at, last_seen_at, created_at, updated_at, dispensary_id, variant, special_ends_at, special_text, special_type, terpenes, effects, flavors, regular_price, sale_price, stock_quantity, stock_status, discount_type, discount_value, availability_status, availability_raw, last_seen_in_stock_at, last_seen_out_of_stock_at) FROM stdin; +3195 1 61 deeply-rooted-az-accessories-1764475012057-0 Accessories | 510 Buttonless Stylus Battery with USB Charger accessories-510-buttonless-stylus-battery-with-usb-charger-1764475012066-fpoyek \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-510-buttonless-stylus-battery-with-usb-charger f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3196 1 61 deeply-rooted-az-accessories-1764475012057-1 Accessories | Blazy Susan | Poker and Roach Tool accessories-blazy-susan-poker-and-roach-tool-1764475012070-403m98 \N 2.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-blazy-susan-poker-and-roach-tool f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +2435 \N \N \N Savvy | Guap | 100mg RSO Gummy 1:1 CBD:THC | Peachy PunchSavvyTHCTHC: 0.84%CBD: 0.86% savvy-guap-100mg-rso-gummy-1-1-cbd-thc-peachy-punch \N \N \N \N 0.84 0.86 Savvy \N https://images.dutchie.com/6c16042846c4a294181ed33b1305daef?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/savvy-guap-100mg-rso-gummy-1-1-cbd-thc-peachy-punch t f \N 2025-11-18 03:57:30.551824 2025-11-18 04:25:04.600448 2025-11-18 03:57:30.551824 2025-11-18 05:24:56.185788 112 100mg RSO Gummy 1:1 \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.600448+00 \N +3197 1 61 deeply-rooted-az-accessories-1764475012057-2 Accessories | Dime | Battery Mini | White accessories-dime-battery-mini-white-1764475012072-72iah0 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-dime-battery-mini-white f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3198 1 61 deeply-rooted-az-accessories-1764475012057-3 Accessories | Dime | Battery | Black accessories-dime-battery-black-1764475012074-wj1434 \N 18.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-dime-battery-black f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3199 1 61 deeply-rooted-az-accessories-1764475012057-4 Accessories | Dime | Battery | White accessories-dime-battery-white-1764475012075-aqagdd \N 18.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-dime-battery-white f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3200 1 61 deeply-rooted-az-accessories-1764475012057-5 Accessories | Loose Leaf | 2-Pack Wraps | Desean Jackson Long Leaf accessories-loose-leaf-2-pack-wraps-desean-jackson-long-leaf-1764475012077-j4tg3b \N 3.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-2-pack-wraps-desean-jackson-long-leaf f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3201 1 61 deeply-rooted-az-accessories-1764475012057-6 Accessories | Loose Leaf | 5-Pack Wraps Mini | Russian Cream accessories-loose-leaf-5-pack-wraps-mini-russian-cream-1764475012078-rq0xn6 \N 5.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-mini-russian-cream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3202 1 61 deeply-rooted-az-accessories-1764475012057-7 Accessories | Loose Leaf | 5-Pack Wraps | Banana Dream accessories-loose-leaf-5-pack-wraps-banana-dream-1764475012079-jpbyeg \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-banana-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3203 1 61 deeply-rooted-az-accessories-1764475012057-8 Accessories | Loose Leaf | 5-Pack Wraps | Grape Dream accessories-loose-leaf-5-pack-wraps-grape-dream-1764475012080-rkfwl9 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-grape-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3204 1 61 deeply-rooted-az-accessories-1764475012058-9 Accessories | Loose Leaf | 5-Pack Wraps | Watermelon Dream accessories-loose-leaf-5-pack-wraps-watermelon-dream-1764475012083-wcc32t \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-watermelon-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3205 1 61 deeply-rooted-az-accessories-1764475012058-10 Accessories | Octo Box | 510 Thread Discrete Battery accessories-octo-box-510-thread-discrete-battery-1764475012084-uetl4k \N 14.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-octo-box-510-thread-discrete-battery f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3206 1 61 deeply-rooted-az-accessories-1764475012058-11 Accessories | Smoxy Loki Stand and Torch accessories-smoxy-loki-stand-and-torch-1764475012085-8yxrnd \N 17.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-smoxy-loki-stand-and-torch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3207 1 61 deeply-rooted-az-accessories-1764475012058-12 Accessories | Stiiizy Pro Battery | BlackSTIIIZY accessories-stiiizy-pro-battery-blackstiiizy-1764475012086-50ziwn \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-battery-black f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3208 1 61 deeply-rooted-az-accessories-1764475012058-13 Accessories | Stiiizy Pro Battery | CheetahSTIIIZY accessories-stiiizy-pro-battery-cheetahstiiizy-1764475012087-wo11fb \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-battery-cheetah f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3209 1 61 deeply-rooted-az-accessories-1764475012058-14 Accessories | Stiiizy Pro XL Battery | BlackSTIIIZY accessories-stiiizy-pro-xl-battery-blackstiiizy-1764475012088-j1832l \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-xl-battery-black f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3234 1 58 deeply-rooted-az-concentrates-1764475146326-19 Canamo Cured Batter | CuracaoCanamo ConcentratesHybridTHC: 77.66%CBD: 0.15%Special Offer canamo-cured-batter-curacaocanamo-concentrateshybridthc-77-66-cbd-0-15-special-offer-1764475146358-iknni6 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-cured-batter-curacao f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3235 1 58 deeply-rooted-az-concentrates-1764475146326-20 Canamo Cured Batter | Gelly PieCanamo ConcentratesIndica-HybridTHC: 78.45%CBD: 0.14%Special Offer canamo-cured-batter-gelly-piecanamo-concentratesindica-hybridthc-78-45-cbd-0-14-special-offer-1764475146359-2wplaj \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-cured-batter-gelly-pie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3232 1 58 deeply-rooted-az-concentrates-1764475146326-17 Bud Bros Big Cloud Live AIO | Lemon JackBud BrosTHC: 85.87%CBD: 1.92% bud-bros-big-cloud-live-aio-lemon-jackbud-brosthc-85-87-cbd-1-92-1764475146356-p7s0o1 \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-live-aio-lemon-jack f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3233 1 58 deeply-rooted-az-concentrates-1764475146326-18 Canamo Cured Batter | Blue Scream #27Canamo ConcentratesIndica-HybridTHC: 77.22%CBD: 0.19%Special Offer canamo-cured-batter-blue-scream-27canamo-concentratesindica-hybridthc-77-22-cbd-0-19-special-offer-1764475146357-qdosmz \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-cured-batter-blue-scream-27 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3670 1 60 deeply-rooted-az-topicals-1764475535685-0 1:1 CBD/CBG Sport ICE Muscle Recovery Lotion 2ozDrip OilsCBD: 0.91% 1-1-cbd-cbg-sport-ice-muscle-recovery-lotion-2ozdrip-oilscbd-0-91-1764475535690-tkn761 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-cbd-cbg-sport-ice-muscle-recovery-lotion-2oz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 2oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3707 1 57 deeply-rooted-az-vaporizers-1764475641905-24 Dime Distillate AIO | Tropical KiwiDime IndustriesTHC: 94.75%CBD: 0.18% dime-distillate-aio-tropical-kiwidime-industriesthc-94-75-cbd-0-18-1764475641943-354yfa \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-tropical-kiwi-45270 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3683 1 57 deeply-rooted-az-vaporizers-1764475641905-0 Alien Labs Cured Resin Cart | BiskanteAlien LabsHybridTHC: 83.04%CBD: 0.18% alien-labs-cured-resin-cart-biskantealien-labshybridthc-83-04-cbd-0-18-1764475641910-x5mk6w \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-biskante f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3684 1 57 deeply-rooted-az-vaporizers-1764475641905-1 Alien Labs Cured Resin Cart | Dark WebAlien LabsHybridTHC: 76.47%CBD: 0.11% alien-labs-cured-resin-cart-dark-webalien-labshybridthc-76-47-cbd-0-11-1764475641914-5r9fkn \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-dark-web f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3685 1 57 deeply-rooted-az-vaporizers-1764475641905-2 Alien Labs Cured Resin Cart | GemeniAlien LabsIndica-HybridTHC: 72.18% alien-labs-cured-resin-cart-gemenialien-labsindica-hybridthc-72-18-1764475641915-g2uw9s \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-gemeni f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +2438 \N \N \N Savvy | Guap | 100mg RSO Gummy | Tangie CrushSavvyTHC: 0.9% savvy-guap-100mg-rso-gummy-tangie-crush \N \N \N \N 0.90 \N Savvy \N https://images.dutchie.com/3018ec315955b8a908772b2df070b692?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/savvy-guap-100mg-rso-gummy-tangie-crush t f \N 2025-11-18 03:57:30.563949 2025-11-18 04:25:04.612069 2025-11-18 03:57:30.563949 2025-11-18 05:25:07.923434 112 Tangie CrushSavvy \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.612069+00 \N +3686 1 57 deeply-rooted-az-vaporizers-1764475641905-3 Alien Labs Cured Resin Cart | Y2KAlien LabsIndica-HybridTHC: 80.38% alien-labs-cured-resin-cart-y2kalien-labsindica-hybridthc-80-38-1764475641917-djon3w \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-y2k f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +2935 \N \N \N NIGHTSHADE connected-nightshade-3-5g \N \N \N I/S 23.55 \N CONNECTED \N \N \N https://best.treez.io/onlinemenu/category/flower/item/d231c828-a7f5-451b-a790-b783eef8ede0?customerType=ADULT t f \N 2025-11-18 14:42:08.822086 2025-11-18 14:42:08.822086 2025-11-18 14:42:08.822086 2025-11-18 14:42:08.822086 149 3.5G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.822086+00 \N +2102 \N \N \N Daze Off Flower Jar | Everybody's HereDaze OffTHC: 22.18% daze-off-flower-jar-everybody-s-here \N \N \N \N 22.18 \N Daze Off \N https://images.dutchie.com/d30aaf86f0c80884ea8f4dc51458048f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/daze-off-flower-jar-everybody-s-here t f \N 2025-11-18 03:52:33.462346 2025-11-18 04:19:16.49205 2025-11-18 03:52:33.462346 2025-11-18 05:05:17.362262 112 Everybody's HereDaze Off \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:16.49205+00 \N +2936 \N \N \N BLUE PAVE cult-blue-pave-3-5g \N \N \N S/I 23.87 \N CULT \N \N \N https://best.treez.io/onlinemenu/category/flower/item/70717575-f4cf-477c-aed8-d9de53db85b4?customerType=ADULT t f \N 2025-11-18 14:42:08.8259 2025-11-18 14:42:08.8259 2025-11-18 14:42:08.8259 2025-11-18 14:42:08.8259 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.8259+00 \N +3687 1 57 deeply-rooted-az-vaporizers-1764475641905-4 Alien Labs Cured Resin Cart | ZpectrumAlien LabsHybridTHC: 77.06%CBD: 0.17% alien-labs-cured-resin-cart-zpectrumalien-labshybridthc-77-06-cbd-0-17-1764475641918-9crv4v \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-zpectrum f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3688 1 57 deeply-rooted-az-vaporizers-1764475641905-5 Breeze Distillate AIO | GrapeBREEZE CannaTHC: 80.66%CBD: 3.16% breeze-distillate-aio-grapebreeze-cannathc-80-66-cbd-3-16-1764475641919-bbyflz \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/breeze-distillate-aio-grape f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3210 1 61 deeply-rooted-az-accessories-1764475012058-15 Accessories | Stiiizy Pro XL Battery | CheetahSTIIIZY accessories-stiiizy-pro-xl-battery-cheetahstiiizy-1764475012089-6yzpk0 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-xl-battery-cheetah f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +2937 \N \N \N CHEMLATTO cult-chemlatto-3-5g \N \N \N HYBRID 24.40 \N CULT \N \N \N https://best.treez.io/onlinemenu/category/flower/item/10161ff6-6541-4ab9-bbdd-f2e034e781c9?customerType=ADULT t f \N 2025-11-18 14:42:08.829702 2025-11-18 14:42:08.829702 2025-11-18 14:42:08.829702 2025-11-18 14:42:08.829702 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.829702+00 \N +2938 \N \N \N MAGIC MARKER cult-magic-marker-3-5g \N \N \N HYBRID 22.75 \N CULT \N \N \N https://best.treez.io/onlinemenu/category/flower/item/b6365938-04bf-4367-ba57-7c38e6e28764?customerType=ADULT t f \N 2025-11-18 14:42:08.8328 2025-11-18 14:42:08.8328 2025-11-18 14:42:08.8328 2025-11-18 14:42:08.8328 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.8328+00 \N +3236 1 58 deeply-rooted-az-concentrates-1764475146326-21 Canamo Cured Batter | Lemon SkunkCanamo ConcentratesSativaTHC: 73.93%CBD: 0.15%Special Offer canamo-cured-batter-lemon-skunkcanamo-concentratessativathc-73-93-cbd-0-15-special-offer-1764475146360-ke2h7k \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-cured-batter-lemon-skunk f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3237 1 58 deeply-rooted-az-concentrates-1764475146326-22 Canamo Cured Batter | Lemon SorbetCanamo ConcentratesHybridTHC: 75.43%CBD: 0.15%Special Offer canamo-cured-batter-lemon-sorbetcanamo-concentrateshybridthc-75-43-cbd-0-15-special-offer-1764475146362-pls88l \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-cured-batter-lemon-sorbet f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3238 1 58 deeply-rooted-az-concentrates-1764475146326-23 Canamo Cured Batter | OctangCanamo ConcentratesSativaTHC: 73.72%CBD: 0.15%Special Offer canamo-cured-batter-octangcanamo-concentratessativathc-73-72-cbd-0-15-special-offer-1764475146363-c24t2p \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-cured-batter-octang f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3240 1 58 deeply-rooted-az-concentrates-1764475146326-25 Canamo Live Batter | Chem DCanamo ConcentratesHybridTHC: 77.98%CBD: 0.15% canamo-live-batter-chem-dcanamo-concentrateshybridthc-77-98-cbd-0-15-1764475146365-ad0irh \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-batter-chem-d f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3241 1 58 deeply-rooted-az-concentrates-1764475146326-26 Canamo Live Batter | CuracaoCanamo ConcentratesHybridTHC: 76.47%CBD: 0.15% canamo-live-batter-curacaocanamo-concentrateshybridthc-76-47-cbd-0-15-1764475146366-v2cyyo \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-batter-curacao f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3239 1 58 deeply-rooted-az-concentrates-1764475146326-24 Canamo Cured Batter | SherbtangCanamo ConcentratesHybridTHC: 78.13%CBD: 0.14%Special Offer canamo-cured-batter-sherbtangcanamo-concentrateshybridthc-78-13-cbd-0-14-special-offer-1764475146364-jifhfz \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-cured-batter-sherbtang f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3242 1 58 deeply-rooted-az-concentrates-1764475146326-27 Canamo Live Batter | Ghost OGCanamo ConcentratesIndica-HybridTHC: 75.1%CBD: 0.15% canamo-live-batter-ghost-ogcanamo-concentratesindica-hybridthc-75-1-cbd-0-15-1764475146367-s53or4 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-batter-ghost-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3243 1 58 deeply-rooted-az-concentrates-1764475146326-28 Canamo Live Batter | Green GuavaCanamo ConcentratesSativa-HybridTHC: 73.2%CBD: 0.14% canamo-live-batter-green-guavacanamo-concentratessativa-hybridthc-73-2-cbd-0-14-1764475146368-7stlba \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-batter-green-guava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3247 1 58 deeply-rooted-az-concentrates-1764475146326-32 Canamo Live Resin AIO | Alien SkrewdriverCanamo ConcentratesIndica-HybridTHC: 79.53%CBD: 0.14%Special Offer canamo-live-resin-aio-alien-skrewdrivercanamo-concentratesindica-hybridthc-79-53-cbd-0-14-special-offer-1764475146372-r1u852 \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-aio-alien-skrewdriver f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3244 1 58 deeply-rooted-az-concentrates-1764475146326-29 Canamo Live Batter | MACCanamo ConcentratesHybridTHC: 74.12%CBD: 0.15% canamo-live-batter-maccanamo-concentrateshybridthc-74-12-cbd-0-15-1764475146369-nfj8os \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-batter-mac f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3245 1 58 deeply-rooted-az-concentrates-1764475146326-30 Canamo Live Batter | RTZCanamo ConcentratesHybridTHC: 76.29%CBD: 0.15% canamo-live-batter-rtzcanamo-concentrateshybridthc-76-29-cbd-0-15-1764475146370-np2cle \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-batter-rtz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3246 1 58 deeply-rooted-az-concentrates-1764475146326-31 Canamo Live Batter | The BridgeCanamo ConcentratesHybridTHC: 74.81%CBD: 0.15% canamo-live-batter-the-bridgecanamo-concentrateshybridthc-74-81-cbd-0-15-1764475146371-5rtd00 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-batter-the-bridge f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3248 1 58 deeply-rooted-az-concentrates-1764475146326-33 Canamo Live Resin AIO | Larry BubbaCanamo ConcentratesIndica-HybridTHC: 75.9%CBD: 0.15%Special Offer canamo-live-resin-aio-larry-bubbacanamo-concentratesindica-hybridthc-75-9-cbd-0-15-special-offer-1764475146373-xk8kbl \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-aio-larry-bubba f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +2639 \N \N \N Select Essential Cart | Sour Tangie select-essential-cart-sour-tangie-99220 With Select Essentials, you don't need to choose between the strains you love and quality oil. Essentials delivers a high potency oil with exceptional flavor and a wide variety of your favorite strains. Available in 1g the variety of strains you love. \N \N Sativa 85.36 1.07 Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/a50b81806eb105e5e2ade0c6ebe5fce0 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-essential-cart-sour-tangie-99220 t f \N 2025-11-18 05:08:07.860207 2025-11-18 05:08:07.860207 2025-11-18 05:08:07.860207 2025-11-18 05:08:07.860207 112 \N \N \N \N {} {} {} 35.00 \N \N in stock \N \N in_stock \N 2025-11-18 05:08:07.860207+00 \N +3249 1 58 deeply-rooted-az-concentrates-1764475146326-34 Canamo Live Resin AIO | Martian PunchCanamo ConcentratesIndica-HybridTHC: 79.54%CBD: 0.13%Special Offer canamo-live-resin-aio-martian-punchcanamo-concentratesindica-hybridthc-79-54-cbd-0-13-special-offer-1764475146374-p5qbus \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-aio-martian-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3250 1 58 deeply-rooted-az-concentrates-1764475146326-35 Canamo Live Resin AIO | Sour LimeCanamo ConcentratesSativa-HybridTHC: 75.89%CBD: 0.14%Special Offer canamo-live-resin-aio-sour-limecanamo-concentratessativa-hybridthc-75-89-cbd-0-14-special-offer-1764475146375-5hufai \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-aio-sour-lime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3254 1 58 deeply-rooted-az-concentrates-1764475146326-39 Canamo Live Resin Cart | Larry BubbaCanamo ConcentratesIndica-HybridTHC: 75.9%CBD: 0.15% canamo-live-resin-cart-larry-bubbacanamo-concentratesindica-hybridthc-75-9-cbd-0-15-1764475146379-irhbc4 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-larry-bubba f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3251 1 58 deeply-rooted-az-concentrates-1764475146326-36 Canamo Live Resin AIO | Strawberry BananaCanamo ConcentratesIndica-HybridTHC: 78.05%CBD: 0.13%Special Offer canamo-live-resin-aio-strawberry-bananacanamo-concentratesindica-hybridthc-78-05-cbd-0-13-special-offer-1764475146376-g6mfm7 \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-aio-strawberry-banana f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3252 1 58 deeply-rooted-az-concentrates-1764475146326-37 Canamo Live Resin AIO | Two by FourCanamo ConcentratesTHC: 75.33%CBD: 0.15%Special Offer canamo-live-resin-aio-two-by-fourcanamo-concentratesthc-75-33-cbd-0-15-special-offer-1764475146377-zau5ct \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-aio-two-by-four f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3253 1 58 deeply-rooted-az-concentrates-1764475146326-38 Canamo Live Resin Cart | Cherry CosmoCanamo ConcentratesHybridTHC: 79.39%CBD: 0.13% canamo-live-resin-cart-cherry-cosmocanamo-concentrateshybridthc-79-39-cbd-0-13-1764475146378-gtoe4w \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-cherry-cosmo f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3255 1 58 deeply-rooted-az-concentrates-1764475146326-40 Canamo Live Resin Cart | Mean MugCanamo ConcentratesHybridTHC: 76.15%CBD: 0.15% canamo-live-resin-cart-mean-mugcanamo-concentrateshybridthc-76-15-cbd-0-15-1764475146380-7k7gw3 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-mean-mug f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3256 1 58 deeply-rooted-az-concentrates-1764475146326-41 Canamo Live Resin Cart | Passion Fruit ReserveCanamo ConcentratesSativa-HybridTHC: 76.02%CBD: 0.13% canamo-live-resin-cart-passion-fruit-reservecanamo-concentratessativa-hybridthc-76-02-cbd-0-13-1764475146382-22pzrs \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-passion-fruit-reserve f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3259 1 58 deeply-rooted-az-concentrates-1764475146326-44 Canamo Shatter | Grateful BreathCanamo ConcentratesIndica-HybridTHC: 78.79%CBD: 0.14% canamo-shatter-grateful-breathcanamo-concentratesindica-hybridthc-78-79-cbd-0-14-1764475146385-d078fk \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-grateful-breath f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3260 1 58 deeply-rooted-az-concentrates-1764475146326-45 Canamo Shatter | High School SweetheartsCanamo ConcentratesHybridTHC: 77.53%CBD: 0.13% canamo-shatter-high-school-sweetheartscanamo-concentrateshybridthc-77-53-cbd-0-13-1764475146386-7zjc68 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-high-school-sweethearts f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3261 1 58 deeply-rooted-az-concentrates-1764475146326-46 Canamo Shatter | Modified RootbeerCanamo ConcentratesIndica-HybridTHC: 77.19%CBD: 0.13% canamo-shatter-modified-rootbeercanamo-concentratesindica-hybridthc-77-19-cbd-0-13-1764475146387-w4ukwe \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-modified-rootbeer f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3269 1 58 deeply-rooted-az-concentrates-1764475146326-54 DR Live Hash Rosin | Arctic Gummies (ET)Deeply RootedSativa-HybridTHC: 73.23%CBD: 0.16% dr-live-hash-rosin-arctic-gummies-et-deeply-rootedsativa-hybridthc-73-23-cbd-0-16-1764475146397-q5eztr \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-arctic-gummies-et f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3270 1 58 deeply-rooted-az-concentrates-1764475146326-55 DR Live Hash Rosin | Fire and Rain (ET)Deeply RootedHybridTHC: 72.52%CBD: 0.13% dr-live-hash-rosin-fire-and-rain-et-deeply-rootedhybridthc-72-52-cbd-0-13-1764475146398-kzqura \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-fire-and-rain-et f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3271 1 58 deeply-rooted-az-concentrates-1764475146326-56 DR Live Hash Rosin | Gruntz (ENVY)Deeply RootedIndica-HybridTHC: 73.01% dr-live-hash-rosin-gruntz-envy-deeply-rootedindica-hybridthc-73-01-1764475146399-znx4b0 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-gruntz-envy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3272 1 58 deeply-rooted-az-concentrates-1764475146326-57 DR Live Hash Rosin | Monsoon (ET)Deeply RootedIndica-HybridTHC: 69.01%CBD: 0.14% dr-live-hash-rosin-monsoon-et-deeply-rootedindica-hybridthc-69-01-cbd-0-14-1764475146400-eiyo8p \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-monsoon-et f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3153 \N \N \N CLEAR PUSH BOWL 14MM clear-push-bowl-14mm \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/d9955575-6c0e-4b65-8505-7f12d904b0e9?customerType=ADULT t f \N 2025-11-18 14:42:09.259192 2025-11-18 14:42:09.259192 2025-11-18 14:42:09.259192 2025-11-18 14:42:09.259192 149 \N \N \N \N \N \N 2.20 2.20 \N \N \N \N in_stock \N 2025-11-18 14:42:09.259192+00 \N +3273 1 58 deeply-rooted-az-concentrates-1764475146326-58 DR Live Hash Rosin | Oishii (ENVY)Deeply RootedIndica-HybridTHC: 77.66% dr-live-hash-rosin-oishii-envy-deeply-rootedindica-hybridthc-77-66-1764475146402-qlh87d \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-oishii-envy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3274 1 58 deeply-rooted-az-concentrates-1764475146326-59 DR Live Hash Rosin | P Di Sole (ET)Deeply RootedSativaTHC: 80.06%CBD: 0.16% dr-live-hash-rosin-p-di-sole-et-deeply-rootedsativathc-80-06-cbd-0-16-1764475146403-d4gzj4 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-p-di-sole-et f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3275 1 58 deeply-rooted-az-concentrates-1764475146326-60 DR Live Hash Rosin | Petes Peaches (ET)Deeply RootedSativa-HybridTHC: 76.06%CBD: 0.16% dr-live-hash-rosin-petes-peaches-et-deeply-rootedsativa-hybridthc-76-06-cbd-0-16-1764475146404-denbg6 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-petes-peaches-et f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3215 1 58 deeply-rooted-az-concentrates-1764475146326-0 SponsoredTru Infusion | Live Resin Batter | GMOZKTRU InfusionHybridTHC: 78.26%CBD: 0.14% sponsoredtru-infusion-live-resin-batter-gmozktru-infusionhybridthc-78-26-cbd-0-14-1764475146330-uzdltw \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-batter-gmozk f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3216 1 58 deeply-rooted-az-concentrates-1764475146326-1 SponsoredTru Infusion Live Resin AIO | Ice Box PieTRU InfusionIndica-HybridTHC: 80.12%CBD: 0.16% sponsoredtru-infusion-live-resin-aio-ice-box-pietru-infusionindica-hybridthc-80-12-cbd-0-16-1764475146333-1b3ykc \N 24.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-aio-ice-box-pie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3217 1 58 deeply-rooted-az-concentrates-1764475146326-2 Achieve Live Resin AIO | Animal TreeAchieveSativa-HybridTHC: 74.31%CBD: 0.15% achieve-live-resin-aio-animal-treeachievesativa-hybridthc-74-31-cbd-0-15-1764475146335-9jiy25 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/achieve-live-resin-aio-animal-tree f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3218 1 58 deeply-rooted-az-concentrates-1764475146326-3 Achieve Live Resin AIO | Berry DreamAchieveHybridTHC: 72.87%CBD: 0.17% achieve-live-resin-aio-berry-dreamachievehybridthc-72-87-cbd-0-17-1764475146337-lvschs \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/achieve-live-resin-aio-berry-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3219 1 58 deeply-rooted-az-concentrates-1764475146326-4 Achieve Live Resin AIO | Guava HeavenAchieveSativa-HybridTHC: 71.31%CBD: 0.17% achieve-live-resin-aio-guava-heavenachievesativa-hybridthc-71-31-cbd-0-17-1764475146339-oomvfq \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/achieve-live-resin-aio-guava-heaven f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3220 1 58 deeply-rooted-az-concentrates-1764475146326-5 Alien Labs Live Resin Cart | XJ-13Alien LabsIndica-HybridTHC: 77.23%Special Offer alien-labs-live-resin-cart-xj-13alien-labsindica-hybridthc-77-23-special-offer-1764475146340-l4tu92 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-live-resin-cart-xj-13 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3285 1 58 deeply-rooted-az-concentrates-1764475146326-70 Dankbar Distillate AIO | VioletDankbarTHC: 84.22%CBD: 2.69% dankbar-distillate-aio-violetdankbarthc-84-22-cbd-2-69-1764475146416-tr3pxs \N 60.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-distillate-aio-violet f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3221 1 58 deeply-rooted-az-concentrates-1764475146326-6 SponsoredTru Infusion Live Resin AIO | GMOZKTRU InfusionHybridTHC: 79.17%CBD: 0.13% sponsoredtru-infusion-live-resin-aio-gmozktru-infusionhybridthc-79-17-cbd-0-13-1764475146342-7kik5i \N 24.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-aio-gmozk f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3222 1 58 deeply-rooted-az-concentrates-1764475146326-7 Bud Bros Big Cloud Fuzion AIO | Baja BurstBud BrosTHC: 84.46%CBD: 1.88% bud-bros-big-cloud-fuzion-aio-baja-burstbud-brosthc-84-46-cbd-1-88-1764475146343-1mjjpd \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-fuzion-aio-baja-burst f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3284 1 58 deeply-rooted-az-concentrates-1764475146326-69 Dankbar Distillate AIO | Pirate MilkDankbarTHC: 85.3%CBD: 2.78% dankbar-distillate-aio-pirate-milkdankbarthc-85-3-cbd-2-78-1764475146414-46wmj8 \N 60.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-distillate-aio-pirate-milk f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3223 1 58 deeply-rooted-az-concentrates-1764475146326-8 Bud Bros Big Cloud Liquid Diamonds AIO | GelonaidBud BrosSativa-HybridTHC: 79.3%CBD: 0.13% bud-bros-big-cloud-liquid-diamonds-aio-gelonaidbud-brossativa-hybridthc-79-3-cbd-0-13-1764475146344-33xpg9 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-liquid-diamonds-aio-gelonaid f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3224 1 58 deeply-rooted-az-concentrates-1764475146326-9 Bud Bros Big Cloud Liquid Diamonds AIO | Lazer FuelBud BrosIndica-HybridTHC: 79.52%CBD: 0.1% bud-bros-big-cloud-liquid-diamonds-aio-lazer-fuelbud-brosindica-hybridthc-79-52-cbd-0-1-1764475146345-a2m3b9 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-liquid-diamonds-aio-lazer-fuel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3225 1 58 deeply-rooted-az-concentrates-1764475146326-10 Bud Bros Big Cloud Liquid Diamonds AIO | Neon SunshineBud BrosHybridTHC: 67.33%CBD: 0.18% bud-bros-big-cloud-liquid-diamonds-aio-neon-sunshinebud-broshybridthc-67-33-cbd-0-18-1764475146347-9sf7ym \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-liquid-diamonds-aio-neon-sunshine f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3226 1 58 deeply-rooted-az-concentrates-1764475146326-11 SponsoredTru Infusion | Live Resin Batter | Ice Box PieTRU InfusionIndica-HybridTHC: 79.73%CBD: 0.15% sponsoredtru-infusion-live-resin-batter-ice-box-pietru-infusionindica-hybridthc-79-73-cbd-0-15-1764475146348-i8j2dc \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-batter-ice-box-pie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3307 1 58 deeply-rooted-az-concentrates-1764475146326-92 Green Dot Labs Live Resin Badder | DaliGreen Dot LabsHybridTHC: 72.31% green-dot-labs-live-resin-badder-daligreen-dot-labshybridthc-72-31-1764475146439-m2o9m6 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-badder-dali f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3227 1 58 deeply-rooted-az-concentrates-1764475146326-12 Bud Bros Big Cloud Liquid Diamonds AIO | Toxic MarriageBud BrosTHC: 80.63%CBD: 0.14% bud-bros-big-cloud-liquid-diamonds-aio-toxic-marriagebud-brosthc-80-63-cbd-0-14-1764475146349-y4rj5v \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-liquid-diamonds-aio-toxic-marriage f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3228 1 58 deeply-rooted-az-concentrates-1764475146326-13 Bud Bros Big Cloud Liquid Diamonds AIO | Vice CityBud BrosTHC: 72.23%CBD: 0.14% bud-bros-big-cloud-liquid-diamonds-aio-vice-citybud-brosthc-72-23-cbd-0-14-1764475146350-nselsw \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-liquid-diamonds-aio-vice-city f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3229 1 58 deeply-rooted-az-concentrates-1764475146326-14 Bud Bros Big Cloud Live AIO | Blue LobsterBud BrosTHC: 86.84%CBD: 1.94% bud-bros-big-cloud-live-aio-blue-lobsterbud-brosthc-86-84-cbd-1-94-1764475146352-7xhnzk \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-live-aio-blue-lobster f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3230 1 58 deeply-rooted-az-concentrates-1764475146326-15 SponsoredTru Infusion | Live Resin Batter | Silver ProjectTRU InfusionHybridTHC: 77.23%CBD: 0.15% sponsoredtru-infusion-live-resin-batter-silver-projecttru-infusionhybridthc-77-23-cbd-0-15-1764475146353-b7yugq \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-batter-silver-project f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3231 1 58 deeply-rooted-az-concentrates-1764475146326-16 Bud Bros Big Cloud Live AIO | GarlicaneBud BrosTHC: 85.59%CBD: 1.92% bud-bros-big-cloud-live-aio-garlicanebud-brosthc-85-59-cbd-1-92-1764475146355-abml9m \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/bud-bros-big-cloud-live-aio-garlicane f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3276 1 58 deeply-rooted-az-concentrates-1764475146326-61 DR Live Hash Rosin | Rainbow PP (ET)Deeply RootedSativa-HybridTHC: 71.92%CBD: 0.14% dr-live-hash-rosin-rainbow-pp-et-deeply-rootedsativa-hybridthc-71-92-cbd-0-14-1764475146405-jac11b \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-rainbow-pp-et f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3277 1 58 deeply-rooted-az-concentrates-1764475146326-62 DR Live Hash Rosin | Super Buff CherryDeeply RootedHybridTHC: 73.54% dr-live-hash-rosin-super-buff-cherrydeeply-rootedhybridthc-73-54-1764475146406-c7ilyd \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-super-buff-cherry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3278 1 58 deeply-rooted-az-concentrates-1764475146326-63 DR Live Hash Rosin | White RontzDeeply RootedIndica-HybridTHC: 77.79% dr-live-hash-rosin-white-rontzdeeply-rootedindica-hybridthc-77-79-1764475146407-56rnlm \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-white-rontz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3279 1 58 deeply-rooted-az-concentrates-1764475146326-64 DR Live Hash Rosin | Winter SunsetDeeply RootedSativa-HybridTHC: 69.69%CBD: 0.14% dr-live-hash-rosin-winter-sunsetdeeply-rootedsativa-hybridthc-69-69-cbd-0-14-1764475146408-q3s4gp \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-winter-sunset f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3280 1 58 deeply-rooted-az-concentrates-1764475146326-65 DR Live Resin Sauce | Forbidden RTZDeeply RootedIndica-HybridTHC: 70.43%Special Offer dr-live-resin-sauce-forbidden-rtzdeeply-rootedindica-hybridthc-70-43-special-offer-1764475146410-fn5dk6 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-sauce-forbidden-rtz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3281 1 58 deeply-rooted-az-concentrates-1764475146326-66 DR Live Resin Sauce | Neon SunshineDeeply RootedHybridTHC: 72.7%Special Offer dr-live-resin-sauce-neon-sunshinedeeply-rootedhybridthc-72-7-special-offer-1764475146411-h903gz \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-sauce-neon-sunshine f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3282 1 58 deeply-rooted-az-concentrates-1764475146326-67 Dankbar Distillate AIO | GMODankbarTHC: 87.31%CBD: 2.76% dankbar-distillate-aio-gmodankbarthc-87-31-cbd-2-76-1764475146412-fzgr1v \N 60.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-distillate-aio-gmo f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3283 1 58 deeply-rooted-az-concentrates-1764475146326-68 Dankbar Distillate AIO | OH BlitzDankbarTHC: 90.64%CBD: 2.84% dankbar-distillate-aio-oh-blitzdankbarthc-90-64-cbd-2-84-1764475146413-o8r0us \N 60.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-distillate-aio-oh-blitz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3286 1 58 deeply-rooted-az-concentrates-1764475146326-71 Dankbar Live Resin AIO | BiscottiDankbarIndicaTHC: 81.53%CBD: 0.18% dankbar-live-resin-aio-biscottidankbarindicathc-81-53-cbd-0-18-1764475146417-3b3hgg \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-live-resin-aio-biscotti f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3287 1 58 deeply-rooted-az-concentrates-1764475146326-72 Dankbar Live Resin AIO | Cookies n CreamDankbarIndica-HybridTHC: 79.94% dankbar-live-resin-aio-cookies-n-creamdankbarindica-hybridthc-79-94-1764475146418-f6hzdd \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-live-resin-aio-cookies-n-cream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3288 1 58 deeply-rooted-az-concentrates-1764475146326-73 Dankbar Live Resin AIO | GelatoDankbarHybridTHC: 77.15% dankbar-live-resin-aio-gelatodankbarhybridthc-77-15-1764475146419-620qr6 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-live-resin-aio-gelato f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3289 1 58 deeply-rooted-az-concentrates-1764475146326-74 Dankbar | 2g Live Resin AIO | HiFiDankbarHybridTHC: 80.48% dankbar-2g-live-resin-aio-hifidankbarhybridthc-80-48-1764475146420-fo1ubr \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dankbar-2g-live-resin-aio-hifi f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3290 1 58 deeply-rooted-az-concentrates-1764475146326-75 Dime Live Resin AIO | Dime OGDime IndustriesIndicaTHC: 75.59%CBD: 0.19% dime-live-resin-aio-dime-ogdime-industriesindicathc-75-59-cbd-0-19-1764475146421-6rgp63 \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-live-resin-aio-dime-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3291 1 58 deeply-rooted-az-concentrates-1764475146326-76 Drip 1:1 THC/CBD Full Spectrum | RSODrip OilsTHCCBD: 57.53% drip-1-1-thc-cbd-full-spectrum-rsodrip-oilsthccbd-57-53-1764475146422-rl99zs \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-1-1-thc-cbd-full-spectrum-rso-62891 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3292 1 58 deeply-rooted-az-concentrates-1764475146326-77 Drip Live Resin Batter | First Class FunkDrip OilsIndica-HybridTHC: 71.73%CBD: 0.17%Special Offer drip-live-resin-batter-first-class-funkdrip-oilsindica-hybridthc-71-73-cbd-0-17-special-offer-1764475146423-ws3zmh \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-live-resin-batter-first-class-funk f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3293 1 58 deeply-rooted-az-concentrates-1764475146326-78 Drip Live Resin Batter | Gastro PopDrip OilsIndica-HybridTHC: 71.73%CBD: 0.17%Special Offer drip-live-resin-batter-gastro-popdrip-oilsindica-hybridthc-71-73-cbd-0-17-special-offer-1764475146424-w5o6yj \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-live-resin-batter-gastro-pop f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3294 1 58 deeply-rooted-az-concentrates-1764475146326-79 Drip Live Resin Batter | GelonadeDrip OilsSativa-HybridTHC: 71.31%CBD: 0.15%Special Offer drip-live-resin-batter-gelonadedrip-oilssativa-hybridthc-71-31-cbd-0-15-special-offer-1764475146425-jjjnhy \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-live-resin-batter-gelonade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3295 1 58 deeply-rooted-az-concentrates-1764475146326-80 Gelato Baller Brush | Cherry PieGelatoHybridTHC: 46.3%CBD: 1.47% gelato-baller-brush-cherry-piegelatohybridthc-46-3-cbd-1-47-1764475146426-c2zwhs \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-baller-brush-cherry-pie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3296 1 58 deeply-rooted-az-concentrates-1764475146326-81 Gelato Baller Brush | Wedding CakeGelatoHybridTHC: 44.66%CBD: 1.47% gelato-baller-brush-wedding-cakegelatohybridthc-44-66-cbd-1-47-1764475146427-t8th5c \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-baller-brush-wedding-cake f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3297 1 58 deeply-rooted-az-concentrates-1764475146326-82 Goldsmith Distillate Syringe | AK-47Goldsmith ExtractsSativa-HybridTHC: 91.02%CBD: 4.45% goldsmith-distillate-syringe-ak-47goldsmith-extractssativa-hybridthc-91-02-cbd-4-45-1764475146428-tao4we \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-ak-47 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 47G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3298 1 58 deeply-rooted-az-concentrates-1764475146326-83 Goldsmith Distillate Syringe | Blackberry KushGoldsmith ExtractsIndica-HybridTHC: 89.42%CBD: 4.12% goldsmith-distillate-syringe-blackberry-kushgoldsmith-extractsindica-hybridthc-89-42-cbd-4-12-1764475146429-mlpgkg \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-blackberry-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3299 1 58 deeply-rooted-az-concentrates-1764475146326-84 Goldsmith Distillate Syringe | California OrangeGoldsmith ExtractsHybridTHC: 89.35% goldsmith-distillate-syringe-california-orangegoldsmith-extractshybridthc-89-35-1764475146430-lc1yo1 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-california-orange f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3300 1 58 deeply-rooted-az-concentrates-1764475146326-85 Goldsmith Distillate Syringe | Forbidden FruitGoldsmith ExtractsIndica-HybridTHC: 90.02%CBD: 4.4% goldsmith-distillate-syringe-forbidden-fruitgoldsmith-extractsindica-hybridthc-90-02-cbd-4-4-1764475146431-m7ofuh \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-forbidden-fruit-37103 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3301 1 58 deeply-rooted-az-concentrates-1764475146326-86 Goldsmith Distillate Syringe | Mango KushGoldsmith ExtractsIndica-HybridTHC: 89.14%CBD: 4.32% goldsmith-distillate-syringe-mango-kushgoldsmith-extractsindica-hybridthc-89-14-cbd-4-32-1764475146432-12l0t5 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-mango-kush-5943 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3302 1 58 deeply-rooted-az-concentrates-1764475146326-87 Goldsmith Distillate Syringe | Sour DieselGoldsmith ExtractsSativa-HybridTHC: 90.05% goldsmith-distillate-syringe-sour-dieselgoldsmith-extractssativa-hybridthc-90-05-1764475146433-wi7jtr \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-sour-diesel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3303 1 58 deeply-rooted-az-concentrates-1764475146326-88 Goldsmith Distillate Syringe | Sour TangieGoldsmith ExtractsSativaTHC: 88.67% goldsmith-distillate-syringe-sour-tangiegoldsmith-extractssativathc-88-67-1764475146434-ggo91m \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-sour-tangie-33013 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3304 1 58 deeply-rooted-az-concentrates-1764475146326-89 Green Dot Labs Live Resin Badder | Belgium BluGreen Dot LabsHybridTHC: 74.42% green-dot-labs-live-resin-badder-belgium-blugreen-dot-labshybridthc-74-42-1764475146436-m4zfdw \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-badder-belgium-blu f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3689 1 57 deeply-rooted-az-vaporizers-1764475641905-6 Cake 'She Hits Different' | Designer Distillate AIO | Passion Orange GuavaCakeTHC: 87.17%CBD: 0.21% cake-she-hits-different-designer-distillate-aio-passion-orange-guavacakethc-87-17-cbd-0-21-1764475641921-ftsb5e \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cake-she-hits-different-designer-distillate-aio-passion-orange-guava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3690 1 57 deeply-rooted-az-vaporizers-1764475641905-7 Cake Designer Distillate AIO | Pineapple ParadiseCakeTHC: 82.57%CBD: 0.36% cake-designer-distillate-aio-pineapple-paradisecakethc-82-57-cbd-0-36-1764475641922-3w4yaf \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cake-designer-distillate-aio-pineapple-paradise f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3691 1 57 deeply-rooted-az-vaporizers-1764475641905-8 Cake Designer Distillate AIO | White RaspberryCakeTHC: 87.74%CBD: 0.22% cake-designer-distillate-aio-white-raspberrycakethc-87-74-cbd-0-22-1764475641923-szb55w \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cake-designer-distillate-aio-white-raspberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3692 1 57 deeply-rooted-az-vaporizers-1764475641905-9 Collective Distillate Cartridge | Grapefruit KushCollectiveTHC: 81.75%CBD: 2.78% collective-distillate-cartridge-grapefruit-kushcollectivethc-81-75-cbd-2-78-1764475641925-tmbshi \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/collective-distillate-cartridge-grapefruit-kush-53200 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3693 1 57 deeply-rooted-az-vaporizers-1764475641905-10 Connected Cured Resin Cart | AmbroziaConnected CannabisHybridTHC: 76.12%CBD: 0.12% connected-cured-resin-cart-ambroziaconnected-cannabishybridthc-76-12-cbd-0-12-1764475641926-6i644r \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-ambrozia f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3694 1 57 deeply-rooted-az-vaporizers-1764475641905-11 Connected Cured Resin Cart | Cherry FadeConnected CannabisHybridTHC: 79.59%CBD: 0.09% connected-cured-resin-cart-cherry-fadeconnected-cannabishybridthc-79-59-cbd-0-09-1764475641928-43xmey \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-cherry-fade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +2972 \N \N \N TROPICAL COOKIES io-extracts-tropical-cookies-1g \N \N \N S/I 93.44 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/5641fd89-968b-4f4c-a115-8cb23e4d7c87?customerType=ADULT t f \N 2025-11-18 14:42:08.922796 2025-11-18 14:42:08.922796 2025-11-18 14:42:08.922796 2025-11-18 14:42:08.922796 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.922796+00 \N +2447 \N \N \N Select Elite Terpologist Cart | Mountain Diesel select-elite-terpologist-cart-mountain-diesel Gas in its many iterations is the Terpologist’s obsession, and this strain crosses the iconic NYC Diesel strain with Green Crack. Fuel expectations get flipped with funky, sharp kerosene notes and a sour kick that captures the taste of urban grit. Freshly peeled grapefruit and floral haze punctuates the flavor for a bright, sweet edge to balance out the gassy punch. \N \N Indica 82.57 1.25 Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/c3b42ad4212998d7ac6153dd95520605 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-elite-terpologist-cart-mountain-diesel t f \N 2025-11-18 03:57:55.437813 2025-11-18 05:08:07.834754 2025-11-18 03:57:55.437813 2025-11-18 05:08:07.834754 112 \N \N \N \N {} {} {} 40.00 \N \N in stock \N \N in_stock \N 2025-11-18 05:08:07.834754+00 \N +3325 1 59 deeply-rooted-az-edibles-1764475244783-5 1:1 Peach Prosecco Pearls - CBD/THC - HybridGrönTHCTHC: 0.28%CBD: 0.31% 1-1-peach-prosecco-pearls-cbd-thc-hybridgr-nthcthc-0-28-cbd-0-31-1764475244798-5468h1 \N 14.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-peach-prosecco-pearls-cbd-thc-hybrid-87123 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3326 1 59 deeply-rooted-az-edibles-1764475244783-6 2:1:1 Tangelo Pearls - THC/CBC/CBG - SativaGrönTHCTHC: 0.28% 2-1-1-tangelo-pearls-thc-cbc-cbg-sativagr-nthcthc-0-28-1764475244799-whghdq \N 14.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/2-1-1-tangelo-pearls-thc-cbc-cbg-sativa-40846 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3327 1 59 deeply-rooted-az-edibles-1764475244783-7 3:1 Blueberry Lemonade Pearls - CBG/THC - Daytime SativaGrönTHCTAC: 400 mgTHC: 0.27% 3-1-blueberry-lemonade-pearls-cbg-thc-daytime-sativagr-nthctac-400-mgthc-0-27-1764475244800-kfl9wc \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/3-1-blueberry-lemonade-pearls-cbg-thc-daytime-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 400 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3328 1 59 deeply-rooted-az-edibles-1764475244783-8 4:1 Pomegranate Pearls - CBD/THC - HybridGrönTHCTAC: 500 mgTHC: 0.28%CBD: 1.15% 4-1-pomegranate-pearls-cbd-thc-hybridgr-nthctac-500-mgthc-0-28-cbd-1-15-1764475244801-ajdnsp \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/4-1-pomegranate-pearls-cbd-thc-hybrid f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 500 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3329 1 59 deeply-rooted-az-edibles-1764475244783-9 Aloha Tymemachine | 100mg Beverage | ArnieAloha TymemachineTHC: 0.03%Special Offer aloha-tymemachine-100mg-beverage-arniealoha-tymemachinethc-0-03-special-offer-1764475244802-ipdpkn \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/aloha-tymemachine-100mg-beverage-arnie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3330 1 59 deeply-rooted-az-edibles-1764475244783-10 Aloha Tymemachine | 100mg Beverage | Cold BrewAloha TymemachineTHC: 108.58 mgSpecial Offer aloha-tymemachine-100mg-beverage-cold-brewaloha-tymemachinethc-108-58-mgspecial-offer-1764475244804-fgvzy3 \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/aloha-tymemachine-100mg-beverage-cold-brew f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3331 1 59 deeply-rooted-az-edibles-1764475244783-11 Aloha Tymemachine | 100mg Beverage | Peach BelliniAloha TymemachineTHC: 0.03%Special Offer aloha-tymemachine-100mg-beverage-peach-bellinialoha-tymemachinethc-0-03-special-offer-1764475244805-js73bi \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/aloha-tymemachine-100mg-beverage-peach-bellini f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3332 1 59 deeply-rooted-az-edibles-1764475244783-12 CQ | 100mg THC Shot | Old Fashioned LemonadeCQTHCTHC: 0.18%Special Offer cq-100mg-thc-shot-old-fashioned-lemonadecqthcthc-0-18-special-offer-1764475244806-2kemns \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cq-100mg-thc-shot-old-fashioned-lemonade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3333 1 59 deeply-rooted-az-edibles-1764475244783-13 CQ | 100mg THC Shot | Strawberry LemonadeCQTHCTHC: 0.17%Special Offer cq-100mg-thc-shot-strawberry-lemonadecqthcthc-0-17-special-offer-1764475244807-h23f7y \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cq-100mg-thc-shot-strawberry-lemonade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3334 1 59 deeply-rooted-az-edibles-1764475244783-14 Chill Pill | 100mg THC 10pk | ANYTIMEChill PillTHCTHC: 3.08% chill-pill-100mg-thc-10pk-anytimechill-pillthcthc-3-08-1764475244809-s6o9pe \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-anytime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3335 1 59 deeply-rooted-az-edibles-1764475244783-15 Chill Pill | 100mg THC 10pk | DAYTIMEChill PillTHCTHC: 3.25% chill-pill-100mg-thc-10pk-daytimechill-pillthcthc-3-25-1764475244810-hvzy41 \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-daytime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3336 1 59 deeply-rooted-az-edibles-1764475244783-16 Chill Pill | 100mg THC 10pk | FLEXTIMEChill PillTHCTHC: 3.04% chill-pill-100mg-thc-10pk-flextimechill-pillthcthc-3-04-1764475244811-zoinh3 \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-flextime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3337 1 59 deeply-rooted-az-edibles-1764475244783-17 Chill Pill | 100mg THC 10pk | RESINTIMEChill PillTHCTHC: 3.19% chill-pill-100mg-thc-10pk-resintimechill-pillthcthc-3-19-1764475244813-k2frf0 \N 13.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-resintime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3338 1 59 deeply-rooted-az-edibles-1764475244783-18 Chill Pill | 100mg THC 20pk | ANYTIMEChill PillTHCTHC: 1.54% chill-pill-100mg-thc-20pk-anytimechill-pillthcthc-1-54-1764475244814-pwhlej \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-anytime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3339 1 59 deeply-rooted-az-edibles-1764475244783-19 Chill Pill | 100mg THC 20pk | DAYTIMEChill PillTHCTHC: 1.46% chill-pill-100mg-thc-20pk-daytimechill-pillthcthc-1-46-1764475244815-jdmb2v \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-daytime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3341 1 59 deeply-rooted-az-edibles-1764475244783-21 Chill Pill | 100mg THC 20pk | NIGHTTIMEChill PillTHCTHC: 3.26% chill-pill-100mg-thc-20pk-nighttimechill-pillthcthc-3-26-1764475244818-ztqc6w \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-nighttime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3342 1 59 deeply-rooted-az-edibles-1764475244783-22 Chill Pill | 100mg THC | Chill DropsChill PillTHCTHC: 97.32 mgCBD: 1.12 mg chill-pill-100mg-thc-chill-dropschill-pillthcthc-97-32-mgcbd-1-12-mg-1764475244819-bda6hd \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-chill-drops f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3343 1 59 deeply-rooted-az-edibles-1764475244783-23 Dialed In | Classic Rosin Gummies 100mg | Blue IceDialed In GummiesSativaTHC: 0.17% dialed-in-classic-rosin-gummies-100mg-blue-icedialed-in-gummiessativathc-0-17-1764475244820-szqsy2 \N 9.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-classic-rosin-gummies-100mg-blue-ice f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3344 1 59 deeply-rooted-az-edibles-1764475244783-24 Dialed In | Classic Rosin Gummies 100mg | BlueberryDialed In GummiesIndicaTHC: 0.17% dialed-in-classic-rosin-gummies-100mg-blueberrydialed-in-gummiesindicathc-0-17-1764475244821-ra6y66 \N 9.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-classic-rosin-gummies-100mg-blueberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3345 1 59 deeply-rooted-az-edibles-1764475244783-25 Dialed In | Classic Rosin Gummies 100mg | Grape PunchDialed In GummiesIndicaTHC: 0.18% dialed-in-classic-rosin-gummies-100mg-grape-punchdialed-in-gummiesindicathc-0-18-1764475244822-6xewtn \N 9.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-classic-rosin-gummies-100mg-grape-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3346 1 59 deeply-rooted-az-edibles-1764475244783-26 Dialed In | Classic Rosin Gummies 100mg | StrawberryDialed In GummiesHybridTHC: 0.18% dialed-in-classic-rosin-gummies-100mg-strawberrydialed-in-gummieshybridthc-0-18-1764475244823-x6zdd4 \N 9.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-classic-rosin-gummies-100mg-strawberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3347 1 59 deeply-rooted-az-edibles-1764475244783-27 Dialed In | Classic Rosin Gummies 100mg | TangerineDialed In GummiesSativaTHC: 0.17% dialed-in-classic-rosin-gummies-100mg-tangerinedialed-in-gummiessativathc-0-17-1764475244824-8w3xaf \N 9.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-classic-rosin-gummies-100mg-tangerine f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3348 1 59 deeply-rooted-az-edibles-1764475244783-28 Dialed In | Classic Rosin Gummies 100mg | WatermelonDialed In GummiesHybridTHC: 0.17% dialed-in-classic-rosin-gummies-100mg-watermelondialed-in-gummieshybridthc-0-17-1764475244825-mitboi \N 9.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-classic-rosin-gummies-100mg-watermelon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3349 1 59 deeply-rooted-az-edibles-1764475244783-29 Dialed In | Innovation Rosin Gummies 100mg 1:1:1 CBC:THCV:THC | Blood OrangeDialed In GummiesHybridTHC: 0.18% dialed-in-innovation-rosin-gummies-100mg-1-1-1-cbc-thcv-thc-blood-orangedialed-in-gummieshybridthc-0-18-1764475244827-e9oso3 \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-innovation-rosin-gummies-100mg-1-1-1-cbc-thcv-thc-blood-orange f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2973 \N \N \N PINEAPPLE PARADISE cake-pineapple-paradise-1g \N \N \N INDICA \N \N CAKE \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/4a383d97-3cd4-4b87-8648-268b7163dab4?customerType=ADULT t f \N 2025-11-18 14:42:08.92542 2025-11-18 14:42:08.92542 2025-11-18 14:42:08.92542 2025-11-18 14:42:08.92542 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.92542+00 \N +2368 \N \N \N Made Flower Jar | BiscottiMadeIndicaTHC: 28.25% made-flower-jar-biscotti \N \N \N \N 28.25 \N Made \N https://images.dutchie.com/9edb129feb301171db5298e2956006b9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/made-flower-jar-biscotti t f \N 2025-11-18 03:56:12.288489 2025-11-18 04:23:37.291161 2025-11-18 03:56:12.288489 2025-11-18 05:20:55.694931 112 BiscottiMade \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:37.291161+00 \N +2369 \N \N \N Made Flower Jar | GMO FunkMadeIndicaTHC: 28.52% made-flower-jar-gmo-funk \N \N \N \N 28.52 \N Made \N https://images.dutchie.com/974bc9bf626b8edfc4be9a28e97fea34?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/made-flower-jar-gmo-funk t f \N 2025-11-18 03:56:12.296956 2025-11-18 04:23:37.300293 2025-11-18 03:56:12.296956 2025-11-18 05:20:58.768682 112 GMO FunkMade \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:37.300293+00 \N +2371 \N \N \N Made Flower Jar | Sugar ShackMadeIndica-HybridTHC: 28.87% made-flower-jar-sugar-shack-31744 \N \N \N \N 28.87 \N Made \N https://images.dutchie.com/99941f0bdf3f23f5041a5683b3f70ea2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/made-flower-jar-sugar-shack-31744 t f \N 2025-11-18 03:56:12.301457 2025-11-18 04:23:37.306923 2025-11-18 03:56:12.301457 2025-11-18 05:21:06.806339 112 Sugar ShackMade \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:37.306923+00 \N +2974 \N \N \N GRAPE GOJI elevate-grape-goji-1g \N \N \N SATIVA 83.98 \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/2253883f-b8ed-4e6c-80f3-f4388bb54164?customerType=ADULT t f \N 2025-11-18 14:42:08.928082 2025-11-18 14:42:08.928082 2025-11-18 14:42:08.928082 2025-11-18 14:42:08.928082 149 1G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.928082+00 \N +3350 1 59 deeply-rooted-az-edibles-1764475244783-30 Dialed In | Innovation Rosin Gummies 100mg 2:1 CBG:THC | PomegranateDialed In GummiesHybridTHC: 0.17% dialed-in-innovation-rosin-gummies-100mg-2-1-cbg-thc-pomegranatedialed-in-gummieshybridthc-0-17-1764475244828-30d0cv \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-innovation-rosin-gummies-100mg-2-1-cbg-thc-pomegranate f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3351 1 59 deeply-rooted-az-edibles-1764475244783-31 Dialed In | Innovation Rosin Gummies 100mg 5:1 CBD:THC | Rocket BerryDialed In GummiesHybrid dialed-in-innovation-rosin-gummies-100mg-5-1-cbd-thc-rocket-berrydialed-in-gummieshybrid-1764475244829-lvnn4g \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dialed-in-innovation-rosin-gummies-100mg-5-1-cbd-thc-rocket-berry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3352 1 59 deeply-rooted-az-edibles-1764475244783-32 Gelato | 100mg Last Bite | Cookies and CreamGelatoTHC: 0.1% gelato-100mg-last-bite-cookies-and-creamgelatothc-0-1-1764475244830-olk79w \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-100mg-last-bite-cookies-and-cream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3353 1 59 deeply-rooted-az-edibles-1764475244783-33 Gelato | 100mg Last Bite | Milk Chocolate CarmelGelatoTHC: 0.11% gelato-100mg-last-bite-milk-chocolate-carmelgelatothc-0-11-1764475244831-hxmwei \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-100mg-last-bite-milk-chocolate-carmel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3354 1 59 deeply-rooted-az-edibles-1764475244783-34 Gelato | 100mg Last Bite | Mint Chocolate ChipGelatoTHC: 0.1% gelato-100mg-last-bite-mint-chocolate-chipgelatothc-0-1-1764475244832-y8f7oz \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-100mg-last-bite-mint-chocolate-chip f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3355 1 59 deeply-rooted-az-edibles-1764475244783-35 Gelato | 100mg Last Bite | Strawberry ChocolateGelatoTHC: 0.1% gelato-100mg-last-bite-strawberry-chocolategelatothc-0-1-1764475244834-9rkv2k \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-100mg-last-bite-strawberry-chocolate f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3356 1 59 deeply-rooted-az-edibles-1764475244783-36 Good Tide | 100mg Hash Rosin Gummies | MangoGood TideTHC: 0.26%CBD: 0.27% good-tide-100mg-hash-rosin-gummies-mangogood-tidethc-0-26-cbd-0-27-1764475244835-i1zocp \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/good-tide-100mg-hash-rosin-gummies-mango-18321 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3357 1 59 deeply-rooted-az-edibles-1764475244783-37 Good Tide | 100mg Hash Rosin Gummies | PineappleGood TideSativa-HybridTHC: 0.28% good-tide-100mg-hash-rosin-gummies-pineapplegood-tidesativa-hybridthc-0-28-1764475244836-riiagi \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/good-tide-100mg-hash-rosin-gummies-pineapple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3358 1 59 deeply-rooted-az-edibles-1764475244783-38 Good Tide | 100mg Hash Rosin Gummies | Guava (I)Good TideIndica-HybridTHC: 0.25% good-tide-100mg-hash-rosin-gummies-guava-i-good-tideindica-hybridthc-0-25-1764475244837-v24yjd \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/good-tide-100mg-hash-rosin-gummies-guava-i f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3359 1 59 deeply-rooted-az-edibles-1764475244783-39 Good Tide | 100mg THC: 100mg CBD: 100mg CBC Hash Rosin Gummies | GrapefruitGood TideTHCTHC: 0.26%CBD: 0.28% good-tide-100mg-thc-100mg-cbd-100mg-cbc-hash-rosin-gummies-grapefruitgood-tidethcthc-0-26-cbd-0-28-1764475244838-l7y6zt \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/good-tide-100mg-thc-100mg-cbd-100mg-cbc-hash-rosin-gummies-grapefruit f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3360 1 59 deeply-rooted-az-edibles-1764475244783-40 Grow Sciences | 100mg Rosin Fruit Chew | Green AppleGrow SciencesTHC: 0.23%Special Offer grow-sciences-100mg-rosin-fruit-chew-green-applegrow-sciencesthc-0-23-special-offer-1764475244839-m446h2 \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-100mg-rosin-fruit-chew-green-apple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2975 \N \N \N LOUD JEFE XL- PACIFIC PUNCH mfused-loud-jefe-xl-pacific-punch-2g \N \N \N SATIVA 90.79 \N MFUSED \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/1e876609-7296-4a36-8472-38e7090b91a3?customerType=ADULT t f \N 2025-11-18 14:42:08.930913 2025-11-18 14:42:08.930913 2025-11-18 14:42:08.930913 2025-11-18 14:42:08.930913 149 2G \N \N \N \N \N \N 52.00 52.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.930913+00 \N +2312 \N \N \N Jeeter | 1.3g Solventless Live Rosin Infused Baby Cannon | Neptune KushJeeterIndica-HybridTHC: 41.44% jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-neptune-kush \N \N \N \N 41.44 \N Jeeter \N https://images.dutchie.com/2b4eabb8eba95fde81e65448b7ce7801?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-neptune-kush t f \N 2025-11-18 03:55:13.067205 2025-11-18 04:22:50.688913 2025-11-18 03:55:13.067205 2025-11-18 05:17:31.890248 112 Neptune KushJeeter \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.688913+00 \N +2321 \N \N \N Keef Classic Soda C.R.E.A.M. XTREME | 100mgKeefHybridTHC: 90.81% keef-classic-soda-c-r-e-a-m-xtreme-100mg-1161 \N \N \N \N 90.81 \N Keef \N https://images.dutchie.com/4d966f0810f27738f7c5e25c00fb4296?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-c-r-e-a-m-xtreme-100mg-1161 t f \N 2025-11-18 03:55:31.861741 2025-11-18 04:22:52.975734 2025-11-18 03:55:31.861741 2025-11-18 05:18:17.807455 112 100mgKeef \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.975734+00 \N +2310 \N \N \N Jeeter Diamond Cartridge | Guava BurstJeeterTHC: 91.9%CBD: 0.14% jeeter-diamond-cartridge-guava-burst \N \N \N \N 91.90 0.14 Jeeter \N https://images.dutchie.com/2b5fdbee64c037f7ff6e175a453092ba?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-diamond-cartridge-guava-burst t f \N 2025-11-18 03:55:13.062975 2025-11-18 04:22:50.684072 2025-11-18 03:55:13.062975 2025-11-18 05:17:25.709464 112 Guava BurstJeeter \N \N \N {} {} {} 44.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.684072+00 \N +3361 1 59 deeply-rooted-az-edibles-1764475244783-41 Grow Sciences | 100mg Rosin Fruit Chew | Sour Blue RazzGrow SciencesTHC: 0.22%Special Offer grow-sciences-100mg-rosin-fruit-chew-sour-blue-razzgrow-sciencesthc-0-22-special-offer-1764475244841-n7ncfe \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-100mg-rosin-fruit-chew-sour-blue-razz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3362 1 59 deeply-rooted-az-edibles-1764475244783-42 Grow Sciences | 100mg Rosin Fruit Chew | Strawberry TangerineGrow SciencesTHC: 0.21%Special Offer grow-sciences-100mg-rosin-fruit-chew-strawberry-tangerinegrow-sciencesthc-0-21-special-offer-1764475244842-j51a4c \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-100mg-rosin-fruit-chew-strawberry-tangerine f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3363 1 59 deeply-rooted-az-edibles-1764475244783-43 Grow Sciences | 100mg Rosin Fruit Chew | WatermelonGrow SciencesTHC: 0.23%Special Offer grow-sciences-100mg-rosin-fruit-chew-watermelongrow-sciencesthc-0-23-special-offer-1764475244843-pgprb3 \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-100mg-rosin-fruit-chew-watermelon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3364 1 59 deeply-rooted-az-edibles-1764475244783-44 Halo | Aunt Ellie's | 100mg Classic Brownie | SativaHalo InfusionsSativaTHC: 0.24% halo-aunt-ellie-s-100mg-classic-brownie-sativahalo-infusionssativathc-0-24-1764475244844-0lass2 \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-aunt-ellie-s-100mg-classic-brownie-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3365 1 59 deeply-rooted-az-edibles-1764475244783-45 Halo | Canna Confections | 100mg Vanilla Caramels | IndicaHalo InfusionsIndicaTHC: 0.07% halo-canna-confections-100mg-vanilla-caramels-indicahalo-infusionsindicathc-0-07-1764475244846-7c3oqz \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-canna-confections-100mg-vanilla-caramels-indica f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3366 1 59 deeply-rooted-az-edibles-1764475244783-46 Halo | Canna Confections | 100mg Vanilla Caramels | SativaHalo InfusionsSativaTHC: 0.08% halo-canna-confections-100mg-vanilla-caramels-sativahalo-infusionssativathc-0-08-1764475244847-cl8hl8 \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-canna-confections-100mg-vanilla-caramels-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3367 1 59 deeply-rooted-az-edibles-1764475244783-47 Halo | Chronic Health | 100mg 1:1 Sleep Well TinctureHalo InfusionsTHC: 0.25%CBD: 0.28% halo-chronic-health-100mg-1-1-sleep-well-tincturehalo-infusionsthc-0-25-cbd-0-28-1764475244848-m1hi8z \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-100mg-1-1-sleep-well-tincture f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2977 \N \N \N LOUD JEFE XL - STOOPID GAS mfused-loud-jefe-xl-stoopid-gas-2g \N \N \N HYBRID 92.69 \N MFUSED \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/a10ec3d8-8545-42a0-843c-e6e01b0f37a1?customerType=ADULT t f \N 2025-11-18 14:42:08.936135 2025-11-18 14:42:08.936135 2025-11-18 14:42:08.936135 2025-11-18 14:42:08.936135 149 2G \N \N \N \N \N \N 52.00 52.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.936135+00 \N +2405 \N \N \N NWD Flower Smalls | Blueberry PaveNWDHybridTHC: 19.4%Special Offer nwd-flower-smalls-blueberry-pave \N \N \N \N 19.40 \N NWD \N https://images.dutchie.com/flower-stock-10-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/nwd-flower-smalls-blueberry-pave t f \N 2025-11-18 03:56:45.761899 2025-11-18 04:24:10.49385 2025-11-18 03:56:45.761899 2025-11-18 05:23:08.749306 112 Blueberry PaveNWD \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:10.49385+00 \N +2978 \N \N \N FIRE JEFE XL- GRAND DADDY PURPLE mfused-fire-jefe-xl-grand-daddy-purple-2g \N \N \N INDICA 90.89 \N MFUSED \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/08cc3dad-3e1e-4bd2-a8aa-5be83d7ae8af?customerType=ADULT t f \N 2025-11-18 14:42:08.938838 2025-11-18 14:42:08.938838 2025-11-18 14:42:08.938838 2025-11-18 14:42:08.938838 149 2G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.938838+00 \N +2403 \N \N \N NWD Flower Mylar | SFV OGNWDSativa-HybridTHC: 18.23% nwd-flower-mylar-sfv-og \N \N \N \N 18.23 \N NWD \N https://images.dutchie.com/flower-stock-12-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/nwd-flower-mylar-sfv-og t f \N 2025-11-18 03:56:45.750479 2025-11-18 04:24:10.485572 2025-11-18 03:56:45.750479 2025-11-18 05:23:02.437229 112 SFV OGNWD \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:10.485572+00 \N +2391 \N \N \N Mr. Honey Budder | Black RazzberryMr. HoneyHybridTHC: 68.33%CBD: 0.2%Special Offer mr-honey-budder-black-razzberry \N \N \N \N 68.33 0.20 Mr. Honey \N https://images.dutchie.com/dce6707bb97afa925f0678f27e40f63c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-budder-black-razzberry t f \N 2025-11-18 03:56:43.184961 2025-11-18 04:24:04.708902 2025-11-18 03:56:43.184961 2025-11-18 05:22:23.017071 112 Black RazzberryMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.708902+00 \N +2591 \N \N \N Varz Pre-Roll | Glitter BombVarzIndica-HybridTHC: 20.56%CBD: 0.04%Special Offer varz-pre-roll-glitter-bomb \N \N \N \N 20.56 0.04 Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-glitter-bomb t f \N 2025-11-18 04:00:18.652908 2025-11-18 04:27:44.040565 2025-11-18 04:00:18.652908 2025-11-18 05:34:17.037908 112 Glitter BombVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.040565+00 \N +2593 \N \N \N Varz Pre-Roll | KaliVarzIndicaTHC: 20.07%Special Offer varz-pre-roll-kali \N \N \N \N 20.07 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-kali t f \N 2025-11-18 04:00:18.657714 2025-11-18 04:27:44.04556 2025-11-18 04:00:18.657714 2025-11-18 05:34:23.08413 112 KaliVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.04556+00 \N +2979 \N \N \N VIBES JEFE- TURN DOWN mfused-vibes-jefe-turn-down-2g \N \N \N HYBRID 29.45 \N MFUSED \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/843af2f8-ead1-41c9-8c7b-975ec5b0edbf?customerType=ADULT t f \N 2025-11-18 14:42:08.941248 2025-11-18 14:42:08.941248 2025-11-18 14:42:08.941248 2025-11-18 14:42:08.941248 149 2G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.941248+00 \N +2980 \N \N \N BLACK TRIANGLE cure-injoy-black-triangle-1g \N \N \N HYBRID 83.19 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/2863ad6e-4194-4541-9fc8-0d58f1daa5c3?customerType=ADULT t f \N 2025-11-18 14:42:08.943664 2025-11-18 14:42:08.943664 2025-11-18 14:42:08.943664 2025-11-18 14:42:08.943664 149 1G \N \N \N \N \N \N 46.00 46.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.943664+00 \N +3369 1 59 deeply-rooted-az-edibles-1764475244783-49 Honey Bomb | (1:1:1) 10mg THC:CBD:CBN | IndicaHoney BombHybridTHC: 0.15%CBD: 0.15% honey-bomb-1-1-1-10mg-thc-cbd-cbn-indicahoney-bombhybridthc-0-15-cbd-0-15-1764475244850-vxbzh9 \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/honey-bomb-1-1-1-10mg-thc-cbd-cbn-indica f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 10mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3370 1 59 deeply-rooted-az-edibles-1764475244783-50 Honey Bomb | 100mg THC:300mg CBD | HybridHoney BombIndica-HybridTHC: 0.14%CBD: 0.42% honey-bomb-100mg-thc-300mg-cbd-hybridhoney-bombindica-hybridthc-0-14-cbd-0-42-1764475244851-hgv5dd \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/honey-bomb-100mg-thc-300mg-cbd-hybrid f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3371 1 59 deeply-rooted-az-edibles-1764475244783-51 Honey Bomb | 200mg THC:100mg CBG | SativaHoney BombSativaTHC: 0.14% honey-bomb-200mg-thc-100mg-cbg-sativahoney-bombsativathc-0-14-1764475244852-fcfe36 \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/honey-bomb-200mg-thc-100mg-cbg-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 200mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3372 1 59 deeply-rooted-az-edibles-1764475244783-52 Jams | 100mg E(Tart) 40PK | Raspberry LemonadeSelectTHC: 0.32% jams-100mg-e-tart-40pk-raspberry-lemonadeselectthc-0-32-1764475244853-twa0kw \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jams-100mg-e-tart-40pk-raspberry-lemonade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3373 1 59 deeply-rooted-az-edibles-1764475244783-53 Jams | 100mg E(Tart) 40PK | StrawberrySelectTHC: 0.32% jams-100mg-e-tart-40pk-strawberryselectthc-0-32-1764475244854-v1z8uz \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jams-100mg-e-tart-40pk-strawberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3374 1 59 deeply-rooted-az-edibles-1764475244783-54 Jams | 100mg E(Tart) 40PK | TangerineSelectTHC: 0.28% jams-100mg-e-tart-40pk-tangerineselectthc-0-28-1764475244855-y015li \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jams-100mg-e-tart-40pk-tangerine f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3375 1 59 deeply-rooted-az-edibles-1764475244783-55 Keef Classic Soda Blue Razz XTREME | 100mgKeefHybridTHC: 104.27 mg keef-classic-soda-blue-razz-xtreme-100mgkeefhybridthc-104-27-mg-1764475244856-9gwb6o \N 12.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-blue-razz-xtreme-100mg-82544 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3376 1 59 deeply-rooted-az-edibles-1764475244783-56 Keef Classic Soda Blue Razz | 10mgKeefHybridTHC: 9.55 mg keef-classic-soda-blue-razz-10mgkeefhybridthc-9-55-mg-1764475244857-zo3fns \N 4.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-blue-razz-10mg-37618 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 10mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3377 1 59 deeply-rooted-az-edibles-1764475244783-57 Keef Classic Soda Orange Kush XTREME | 100mgKeefHybridTHC: 0.03% keef-classic-soda-orange-kush-xtreme-100mgkeefhybridthc-0-03-1764475244858-h8ru5l \N 12.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-orange-kush-xtreme-100mg-85098 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2636 \N \N \N Select Essential Briq AIO | Lychee DreamSelectHybridTHC: 86.4%CBD: 2.73% select-essential-briq-aio-lychee-dream-7323 \N \N \N \N 86.40 2.73 Select \N https://images.dutchie.com/dcf6e24cfc9c02dd8985e5441d540194?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-essential-briq-aio-lychee-dream-7323 t f \N 2025-11-18 04:25:16.183053 2025-11-18 04:25:16.183053 2025-11-18 04:25:16.183053 2025-11-18 05:37:03.76391 112 Lychee DreamSelect \N \N \N {} {} {} 45.00 33.75 \N \N \N \N in_stock \N 2025-11-18 04:25:16.183053+00 \N +2637 \N \N \N Select Essential Cart | Sour TangieSelectIndica-HybridTHC: 85.82%CBD: 0.16% select-essential-cart-sour-tangie-72521 \N \N \N \N 85.82 0.16 Select \N https://images.dutchie.com/a50b81806eb105e5e2ade0c6ebe5fce0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-essential-cart-sour-tangie-72521 t f \N 2025-11-18 04:25:16.188968 2025-11-18 04:25:16.188968 2025-11-18 04:25:16.188968 2025-11-18 05:37:03.929108 112 Sour TangieSelect \N \N \N {} {} {} 35.00 26.25 \N \N \N \N in_stock \N 2025-11-18 04:25:16.188968+00 \N +3378 1 59 deeply-rooted-az-edibles-1764475244783-58 Keef Classic Soda Orange Kush | 10mgKeefHybridTHC: 9.27% keef-classic-soda-orange-kush-10mgkeefhybridthc-9-27-1764475244860-bki5kz \N 4.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-orange-kush-10mg-91778 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 10mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2478 \N \N \N Smokiez | 100mg Sweet Gummy | Single | Blue RaspberrySmokiez EdiblesTHC: 93.38 mgSpecial Offer smokiez-100mg-sweet-gummy-single-blue-raspberry \N \N \N \N \N \N Smokiez Edibles \N https://images.dutchie.com/a1865c80e4a2acbbbea3ddac94c2c140?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sweet-gummy-single-blue-raspberry t f \N 2025-11-18 03:58:33.167279 2025-11-18 04:25:58.265642 2025-11-18 03:58:33.167279 2025-11-18 05:27:10.578428 112 Blue RaspberrySmokiez Edibles \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:58.265642+00 \N +2492 \N \N \N Sticky Saguaro Flower Mylar | Cajun BerriesSticky SaguaroSativa-HybridTHC: 23.09%Special Offer sticky-saguaro-flower-mylar-cajun-berries \N \N \N \N 23.09 \N Sticky Saguaro \N https://images.dutchie.com/9224a88322bad14aaaa630d97a0b6853?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-flower-mylar-cajun-berries t f \N 2025-11-18 03:58:47.564515 2025-11-18 04:26:27.095359 2025-11-18 03:58:47.564515 2025-11-18 05:28:00.148616 112 Cajun BerriesSticky Saguaro \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.095359+00 \N +2494 \N \N \N Sticky Saguaro Flower Mylar | PineappleSticky SaguaroHybridTHC: 25.56%Special Offer sticky-saguaro-flower-mylar-pineapple \N \N \N \N 25.56 \N Sticky Saguaro \N https://images.dutchie.com/f1fa35aa3222713cff7b374aa6019389?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-flower-mylar-pineapple t f \N 2025-11-18 03:58:47.570033 2025-11-18 04:26:27.100908 2025-11-18 03:58:47.570033 2025-11-18 05:28:09.641686 112 PineappleSticky Saguaro \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.100908+00 \N +3379 1 59 deeply-rooted-az-edibles-1764475244783-59 Keef Classic Soda Original Cola XTREME | 100mgKeefHybridTHC: 0.03% keef-classic-soda-original-cola-xtreme-100mgkeefhybridthc-0-03-1764475244861-ck7ywv \N 12.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-original-cola-xtreme-100mg-7852 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3380 1 59 deeply-rooted-az-edibles-1764475244783-60 Keef Classic Soda Original Cola | 10mgKeefHybridTHC: 10.02 mg keef-classic-soda-original-cola-10mgkeefhybridthc-10-02-mg-1764475244861-495vp7 \N 4.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-original-cola-10mg-80418 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 10mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3381 1 59 deeply-rooted-az-edibles-1764475244783-61 Mellow Vibes | 100mg Rosin Jelly | Prickly PearMellow VibesTHC: 0.82% mellow-vibes-100mg-rosin-jelly-prickly-pearmellow-vibesthc-0-82-1764475244862-b1fec7 \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mellow-vibes-100mg-rosin-jelly-prickly-pear f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3382 1 59 deeply-rooted-az-edibles-1764475244783-62 Mellow Vibes | 100mg Rosin Jelly | WatermelonMellow VibesTHC: 0.83% mellow-vibes-100mg-rosin-jelly-watermelonmellow-vibesthc-0-83-1764475244863-ctabv5 \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mellow-vibes-100mg-rosin-jelly-watermelon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3383 1 59 deeply-rooted-az-edibles-1764475244783-63 Micro Drops | 100mg Daytime THC TinctureDrip OilsTHCTHC: 0.39% micro-drops-100mg-daytime-thc-tincturedrip-oilsthcthc-0-39-1764475244864-7wlzsi \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/micro-drops-100mg-daytime-thc-tincture f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3384 1 59 deeply-rooted-az-edibles-1764475244783-64 Micro Drops | 2:1 200/100mg CBN/THC Night Time TinctureDrip OilsTHCTHC: 0.36% micro-drops-2-1-200-100mg-cbn-thc-night-time-tincturedrip-oilsthcthc-0-36-1764475244865-ghn65v \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/micro-drops-2-1-200-100mg-cbn-thc-night-time-tincture f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3385 1 59 deeply-rooted-az-edibles-1764475244783-65 Milk Chocolate High-Dose Mini BarGrönTHCTAC: 100 mgTHC: 1.39% milk-chocolate-high-dose-mini-bargr-nthctac-100-mgthc-1-39-1764475244866-sclhqm \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/milk-chocolate-high-dose-mini-bar-36665 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3386 1 59 deeply-rooted-az-edibles-1764475244783-66 Milk Chocolate Pips - THC - SativaGrönSativaTHC: 0.45% milk-chocolate-pips-thc-sativagr-nsativathc-0-45-1764475244867-krdni3 \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/milk-chocolate-pips-thc-sativa-80073 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3387 1 59 deeply-rooted-az-edibles-1764475244783-67 Ogeez | 100mg Gummy | The Creams Mellow IndicaOGeez!IndicaTHC: 0.18% ogeez-100mg-gummy-the-creams-mellow-indicaogeez-indicathc-0-18-1764475244868-grwrzl \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-creams-mellow-indica f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3388 1 59 deeply-rooted-az-edibles-1764475244783-68 Ogeez | 100mg Gummy | The Creams Sunny SativaOGeez!SativaTHC: 0.18% ogeez-100mg-gummy-the-creams-sunny-sativaogeez-sativathc-0-18-1764475244870-2f9ljv \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-creams-sunny-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3389 1 59 deeply-rooted-az-edibles-1764475244783-69 Ogeez | 100mg Gummy | Sugar Free Tropical IndicaOGEEZIndicaTHC: 0.19% ogeez-100mg-gummy-sugar-free-tropical-indicaogeezindicathc-0-19-1764475244871-1snv03 \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-sugar-free-tropical-indica f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3390 1 59 deeply-rooted-az-edibles-1764475244783-70 Ogeez | 100mg Gummy | Sugar Free Tropical SativaOGEEZSativaTHC: 0.18% ogeez-100mg-gummy-sugar-free-tropical-sativaogeezsativathc-0-18-1764475244872-y348as \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-sugar-free-tropical-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3391 1 59 deeply-rooted-az-edibles-1764475244783-71 Ogeez | 100mg Gummy | The Fruits Mellow IndicaOGeez!IndicaTHC: 0.19% ogeez-100mg-gummy-the-fruits-mellow-indicaogeez-indicathc-0-19-1764475244874-8ia1nd \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-fruits-mellow-indica f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3392 1 59 deeply-rooted-az-edibles-1764475244783-72 Ogeez | 100mg Gummy | The Fruits Sunny SativaOGeez!SativaTHC: 0.18% ogeez-100mg-gummy-the-fruits-sunny-sativaogeez-sativathc-0-18-1764475244875-n1nwmn \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-fruits-sunny-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3393 1 59 deeply-rooted-az-edibles-1764475244783-73 Ogeez | 100mg THC: 100mg CBD 1: ummy | Happy Balance Strawberries and CreaOGeez!THCTHC: 0.19%CBD: 0.17% ogeez-100mg-thc-100mg-cbd-1-ummy-happy-balance-strawberries-and-creaogeez-thcthc-0-19-cbd-0-17-1764475244876-3crc82 \N 10.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-thc-100mg-cbd-1-ummy-happy-balance-strawberries-and-crea f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3394 1 59 deeply-rooted-az-edibles-1764475244783-74 Ogeez | 100mg/50mg THC/CBN MEGA DOSE 1: ummy | BIG Sleep EditionOGEEZTHCTHC: 0.65% ogeez-100mg-50mg-thc-cbn-mega-dose-1-ummy-big-sleep-editionogeezthcthc-0-65-1764475244877-r02ilc \N 5.40 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-50mg-thc-cbn-mega-dose-1-ummy-big-sleep-edition f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3395 1 59 deeply-rooted-az-edibles-1764475244783-75 Ogeez | 75mg Minis Gummy | The Fruits Sunny SativaOGEEZSativaTHC: 0.13% ogeez-75mg-minis-gummy-the-fruits-sunny-sativaogeezsativathc-0-13-1764475244878-jmxsmh \N 7.80 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-75mg-minis-gummy-the-fruits-sunny-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 75mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3396 1 59 deeply-rooted-az-edibles-1764475244783-76 SIP Elixir | 100mg Nano Beverage + Caffeine | Blue RazzSipTHC: 0.21%CBD: 0.01% sip-elixir-100mg-nano-beverage-caffeine-blue-razzsipthc-0-21-cbd-0-01-1764475244879-y6mcdl \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-caffeine-blue-razz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3397 1 59 deeply-rooted-az-edibles-1764475244783-77 SIP Elixir | 100mg Nano Beverage + Caffeine | Citrus SparkSipTHC: 0.21% sip-elixir-100mg-nano-beverage-caffeine-citrus-sparksipthc-0-21-1764475244880-b49uqg \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-caffeine-citrus-spark f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3398 1 59 deeply-rooted-az-edibles-1764475244783-78 SIP Elixir | 100mg Nano Beverage + Caffeine | XpressoSipTHC: 95.16 mg sip-elixir-100mg-nano-beverage-caffeine-xpressosipthc-95-16-mg-1764475244881-9hrv6h \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-caffeine-xpresso f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3399 1 59 deeply-rooted-az-edibles-1764475244783-79 SIP Elixir | 100mg Nano Beverage | Electric LemonSipHybridTHC: 116.76 mg sip-elixir-100mg-nano-beverage-electric-lemonsiphybridthc-116-76-mg-1764475244882-wk2jmr \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-electric-lemon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3400 1 59 deeply-rooted-az-edibles-1764475244783-80 SIP Elixir | 100mg Nano Beverage | HurricaneSipTHC: 0.19% sip-elixir-100mg-nano-beverage-hurricanesipthc-0-19-1764475244883-rozce4 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-hurricane f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2455 \N \N \N Session Live Resin AIO | Mango MintalitySessionHybridTHC: 79.51%CBD: 0.11%Special Offer session-live-resin-aio-mango-mintality \N \N \N \N 79.51 0.11 Session \N https://images.dutchie.com/2103e7652bba4a1491f9bad87be2fb3c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/session-live-resin-aio-mango-mintality t f \N 2025-11-18 03:57:57.46303 2025-11-18 04:25:22.23182 2025-11-18 03:57:57.46303 2025-11-18 05:25:46.462773 112 Mango MintalitySession \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:22.23182+00 \N +2993 \N \N \N BLUEBERRY LEMON dime-blueberry-lemon-2g \N \N \N SATIVA 88.66 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/bb6861cd-cbe6-4358-aed9-8b04fdc21002?customerType=ADULT t f \N 2025-11-18 14:42:08.971562 2025-11-18 14:42:08.971562 2025-11-18 14:42:08.971562 2025-11-18 14:42:08.971562 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.971562+00 \N +3401 1 59 deeply-rooted-az-edibles-1764475244783-81 SIP Elixir | 100mg Nano Beverage | Sunset PunchSipTHC: 104 mg sip-elixir-100mg-nano-beverage-sunset-punchsipthc-104-mg-1764475244884-4gbslp \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-sunset-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2541 \N \N \N Thunder Bud Pre-Roll | FAM 95Thunder BudIndica-HybridTHC: 30.1%Special Offer thunder-bud-pre-roll-fam-95 \N \N \N \N 30.10 \N Thunder Bud \N https://images.dutchie.com/7ccebf8db7dd13f7f2a265847bd15d6b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-fam-95 t f \N 2025-11-18 03:59:42.466038 2025-11-18 04:27:08.335896 2025-11-18 03:59:42.466038 2025-11-18 05:31:02.958386 112 FAM 95Thunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.335896+00 \N +3402 1 59 deeply-rooted-az-edibles-1764475244783-82 SIP Elixir | 100mg Nano Beverage | Tropical CrushSipTHC: 108.83 mg sip-elixir-100mg-nano-beverage-tropical-crushsipthc-108-83-mg-1764475244885-43za27 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-tropical-crush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3403 1 59 deeply-rooted-az-edibles-1764475244783-83 SIP Elixir | 100mg Nano Beverage | WatermelonSipHybridTHC: 104.57 mg sip-elixir-100mg-nano-beverage-watermelonsiphybridthc-104-57-mg-1764475244886-qagdej \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-watermelon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2994 \N \N \N GUAVALICIOUS dime-guavalicious-2g \N \N \N SATIVA 91.59 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/06fe78c7-321a-4a3e-9d22-a840f7c3b0cc?customerType=ADULT t f \N 2025-11-18 14:42:08.973358 2025-11-18 14:42:08.973358 2025-11-18 14:42:08.973358 2025-11-18 14:42:08.973358 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.973358+00 \N +2481 \N \N \N Space Rocks Infused Flower | Alice in SpacelandSpace RocksSativa-HybridTHC: 40.47% space-rocks-infused-flower-alice-in-spaceland \N \N \N \N 40.47 \N Space Rocks \N https://images.dutchie.com/143dbd241e177058c61ca37c7c87cef4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-infused-flower-alice-in-spaceland t f \N 2025-11-18 03:58:41.835618 2025-11-18 04:26:15.993801 2025-11-18 03:58:41.835618 2025-11-18 05:27:20.166769 112 Alice in SpacelandSpace Rocks \N \N \N {} {} {} 60.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:15.993801+00 \N +2484 \N \N \N Space Rocks Infused Flower | Crater KushimiSpace RocksIndica-HybridTHC: 50.45% space-rocks-infused-flower-crater-kushimi \N \N \N \N 50.45 \N Space Rocks \N https://images.dutchie.com/143dbd241e177058c61ca37c7c87cef4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-infused-flower-crater-kushimi t f \N 2025-11-18 03:58:41.85123 2025-11-18 04:26:16.004568 2025-11-18 03:58:41.85123 2025-11-18 05:27:40.170911 112 Crater KushimiSpace Rocks \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.004568+00 \N +3404 1 59 deeply-rooted-az-edibles-1764475244783-84 SIP Elixir | 100mg Nano Beverage | Wild BerrySipHybridTHC: 113.18 mg sip-elixir-100mg-nano-beverage-wild-berrysiphybridthc-113-18-mg-1764475244886-i51ce6 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-wild-berry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3405 1 59 deeply-rooted-az-edibles-1764475244783-85 Savvy | Guap | 100mg RSO Gummy 1:2 CBN:THC | Melon CrashSavvyTHCTHC: 0.82%Special Offer savvy-guap-100mg-rso-gummy-1-2-cbn-thc-melon-crashsavvythcthc-0-82-special-offer-1764475244887-ou3mfu \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/savvy-guap-100mg-rso-gummy-1-2-cbn-thc-melon-crash f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3406 1 59 deeply-rooted-az-edibles-1764475244783-86 Savvy | Guap | 50mg Gummy | Jungle JuiceSavvyTHC: 0.43% savvy-guap-50mg-gummy-jungle-juicesavvythc-0-43-1764475244889-tpy25b \N 5.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/savvy-guap-50mg-gummy-jungle-juice f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 50mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3407 1 59 deeply-rooted-az-edibles-1764475244783-87 Smokiez | 100mg LR Sour Gummy | MangoSmokiez EdiblesTHC: 0.22%Special Offer smokiez-100mg-lr-sour-gummy-mangosmokiez-ediblesthc-0-22-special-offer-1764475244890-ahf4in \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-lr-sour-gummy-mango f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3408 1 59 deeply-rooted-az-edibles-1764475244783-88 Smokiez | 100mg Sour Gummy | 1:1 THC to CBD | TropicalSmokiez EdiblesTHCTHC: 0.17%Special Offer smokiez-100mg-sour-gummy-1-1-thc-to-cbd-tropicalsmokiez-ediblesthcthc-0-17-special-offer-1764475244891-u4cqw6 \N 12.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-1-1-thc-to-cbd-tropical f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3409 1 59 deeply-rooted-az-edibles-1764475244783-89 Smokiez | 100mg Sour Gummy | Single | Blue RaspberrySmokiez EdiblesTHC: 93.15 mgCBD: 0.2 mgSpecial Offer smokiez-100mg-sour-gummy-single-blue-raspberrysmokiez-ediblesthc-93-15-mgcbd-0-2-mgspecial-offer-1764475244892-el1no5 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-single-blue-raspberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2457 \N \N \N Session Live Resin AIO | AK 1995Session Cannabis Co.Sativa-HybridTHC: 70.22%CBD: 0.15%Special Offer session-live-resin-aio-ak-1995 \N \N \N Sativa 70.22 0.15 Session Cannabis Co. \N https://images.dutchie.com/2103e7652bba4a1491f9bad87be2fb3c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/session-live-resin-aio-ak-1995 t f \N 2025-11-18 03:57:59.081641 2025-11-18 04:25:25.33347 2025-11-18 03:57:59.081641 2025-11-18 05:25:52.549281 112 AK 1995Session Cannabis Co. \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:25.33347+00 \N +2617 \N \N \N Wana | Fast Asleep Gummies 10:2:2:2 CBD:CBN:CBG:THC | Dream BerryWanaTHCTHC: 0.05%CBD: 0.21% wana-fast-asleep-gummies-10-2-2-2-cbd-cbn-cbg-thc-dream-berry \N \N \N \N 0.05 0.21 Wana \N https://images.dutchie.com/76ea622e53f1b0765925a08cdcad58f3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-fast-asleep-gummies-10-2-2-2-cbd-cbn-cbg-thc-dream-berry t f \N 2025-11-18 04:00:24.827482 2025-11-18 04:27:48.254627 2025-11-18 04:00:24.827482 2025-11-18 05:35:54.384827 112 Fast Asleep Gummies 10:2:2:2 \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.254627+00 \N +2088 \N \N \N Clout King Flower Jar | Milk SteakClout KingIndicaTHC: 36.72% clout-king-flower-jar-milk-steak \N \N \N \N 36.72 \N Clout King \N https://images.dutchie.com/49aac19bceeac9dd55945d192859ddb1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-flower-jar-milk-steak t f \N 2025-11-18 03:51:59.978887 2025-11-18 04:18:31.514366 2025-11-18 03:51:59.978887 2025-11-18 05:04:19.295626 112 Milk SteakClout King \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:31.514366+00 \N +2190 \N \N \N Drip Live Rosin AIO Citrus | Pineapple PetroldripSativaTHC: 79.94%CBD: 0.1% drip-live-rosin-aio-citrus-pineapple-petrol \N \N \N \N 79.94 0.10 drip \N https://images.dutchie.com/fe3005bbfc2388410554e069a2cd41ed?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-live-rosin-aio-citrus-pineapple-petrol t f \N 2025-11-18 03:53:11.776357 2025-11-18 04:20:03.369152 2025-11-18 03:53:11.776357 2025-11-18 05:10:26.028005 112 Pineapple Petroldrip \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:03.369152+00 \N +3004 \N \N \N KR BLACK BAR 100MG korova-kr-black-bar-100mg \N \N \N HYBRID \N \N KOROVA \N \N \N https://best.treez.io/onlinemenu/category/edible/item/6e6d56de-65d5-4b8b-8982-43c61082e10e?customerType=ADULT t f \N 2025-11-18 14:42:08.990952 2025-11-18 14:42:08.990952 2025-11-18 14:42:08.990952 2025-11-18 14:42:08.990952 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.990952+00 \N +3005 \N \N \N KICKS-BALANCE 100MG FULL SPECTRUM pure-everest-kicks-balance-100mg-full-spectrum \N \N \N HYBRID \N \N PURE/EVEREST \N \N \N https://best.treez.io/onlinemenu/category/edible/item/d317d48d-8629-46d1-a5f8-128320a23e3c?customerType=ADULT t f \N 2025-11-18 14:42:08.99286 2025-11-18 14:42:08.99286 2025-11-18 14:42:08.99286 2025-11-18 14:42:08.99286 149 \N \N \N \N \N \N 12.00 12.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.99286+00 \N +3410 1 59 deeply-rooted-az-edibles-1764475244783-90 Smokiez | 100mg Sour Gummy | Single | PeachSmokiez EdiblesTHC: 102 mgCBD: 0.21 mgSpecial Offer smokiez-100mg-sour-gummy-single-peachsmokiez-ediblesthc-102-mgcbd-0-21-mgspecial-offer-1764475244893-urkl94 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-single-peach f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3411 1 59 deeply-rooted-az-edibles-1764475244783-91 Smokiez | 100mg Sour Gummy | Single | WatermelonSmokiez EdiblesTHC: 95.95 mgSpecial Offer smokiez-100mg-sour-gummy-single-watermelonsmokiez-ediblesthc-95-95-mgspecial-offer-1764475244894-ra7b0h \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-single-watermelon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3412 1 59 deeply-rooted-az-edibles-1764475244783-92 Smokiez | 100mg Sweet Gummy | Single | Blue RaspberrySmokiez EdiblesTHC: 93.38 mgSpecial Offer smokiez-100mg-sweet-gummy-single-blue-raspberrysmokiez-ediblesthc-93-38-mgspecial-offer-1764475244895-f0yl0y \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sweet-gummy-single-blue-raspberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3563 1 56 deeply-rooted-az-pre-rolls-1764475447223-43 Jeeter Infused Pre-Rolls | Pink LemonJeeterTHC: 40.5% jeeter-infused-pre-rolls-pink-lemonjeeterthc-40-5-1764475447290-mzuet1 \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-infused-pre-rolls-pink-lemon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3615 1 56 deeply-rooted-az-pre-rolls-1764475447223-95 Stiiizy Infused Pre-Roll 40's | Magic MelonSTIIIZYTHC: 46.19% stiiizy-infused-pre-roll-40-s-magic-melonstiiizythc-46-19-1764475447350-kiso33 \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-magic-melon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3564 1 56 deeply-rooted-az-pre-rolls-1764475447223-44 Jeeter Infused Pre-Rolls | Sugar PlumJeeterTHC: 37.71% jeeter-infused-pre-rolls-sugar-plumjeeterthc-37-71-1764475447292-6k2zmr \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-infused-pre-rolls-sugar-plum f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3520 1 56 deeply-rooted-az-pre-rolls-1764475447223-0 SponsoredExotic Blend Diamond-Infused Joint (1g)LeafersHybridTHC: 49.76% sponsoredexotic-blend-diamond-infused-joint-1g-leafershybridthc-49-76-1764475447230-ky6ncw \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/exotic-blend-diamond-infused-joint-1g f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3521 1 56 deeply-rooted-az-pre-rolls-1764475447223-1 SponsoredFruit Blend Diamond-Infused Joint (1g)LeafersSativa-HybridTHC: 46.83% sponsoredfruit-blend-diamond-infused-joint-1g-leaferssativa-hybridthc-46-83-1764475447234-hlree8 \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/fruit-blend-diamond-infused-joint-1g f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3522 1 56 deeply-rooted-az-pre-rolls-1764475447223-2 3-Pack x (2.1g) Bubble Hash Infused Pre-Roll | Cherry Paloma x Permanent MarkerDrip OilsHybridTHC: 43.05% 3-pack-x-2-1g-bubble-hash-infused-pre-roll-cherry-paloma-x-permanent-markerdrip-oilshybridthc-43-05-1764475447236-m7g9gz \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/3-pack-x-2-1g-bubble-hash-infused-pre-roll-cherry-paloma-x-permanent-marker-96850 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3695 1 57 deeply-rooted-az-vaporizers-1764475641905-12 Connected Cured Resin Cart | Gelato 41Connected CannabisIndica-HybridTHC: 71.1%CBD: 0.13% connected-cured-resin-cart-gelato-41connected-cannabisindica-hybridthc-71-1-cbd-0-13-1764475641929-k9ihvm \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-gelato-41 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3524 1 56 deeply-rooted-az-pre-rolls-1764475447223-4 Alien Labs Single Pre-Roll | Dark WebAlien LabsHybridTHC: 19.05%Special Offer alien-labs-single-pre-roll-dark-webalien-labshybridthc-19-05-special-offer-1764475447239-hn74b3 \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-single-pre-roll-dark-web f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3525 1 56 deeply-rooted-az-pre-rolls-1764475447223-5 Alien Labs Single Pre-Roll | GeminiAlien LabsIndica-HybridTHC: 22.63%Special Offer alien-labs-single-pre-roll-geminialien-labsindica-hybridthc-22-63-special-offer-1764475447241-toehdp \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-single-pre-roll-gemini f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3526 1 56 deeply-rooted-az-pre-rolls-1764475447223-6 SponsoredExotic Blend Diamond-Infused (3-pack of .5g joints)LeafersHybridTHC: 46.74% sponsoredexotic-blend-diamond-infused-3-pack-of-5g-joints-leafershybridthc-46-74-1764475447242-pvpk4h \N 24.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/exotic-blend-diamond-infused-3-pack-of-5g-joints f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3527 1 56 deeply-rooted-az-pre-rolls-1764475447223-7 Blue ZKZ Baby Jeeter Infused Pre-Roll 5-pack | 2.5gJeeterIndicaTHC: 39.71% blue-zkz-baby-jeeter-infused-pre-roll-5-pack-2-5gjeeterindicathc-39-71-1764475447244-0d7qss \N 26.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/blue-zkz-baby-jeeter-infused-pre-roll-5-pack-2-5g-70923 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3528 1 56 deeply-rooted-az-pre-rolls-1764475447223-8 Clout King | 2-Pack x 1g Pre-roll | Blue WagyuClout KingTHC: 33.43% clout-king-2-pack-x-1g-pre-roll-blue-wagyuclout-kingthc-33-43-1764475447245-x3jqs8 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-2-pack-x-1g-pre-roll-blue-wagyu f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3529 1 56 deeply-rooted-az-pre-rolls-1764475447223-9 Clout King | 2-Pack x 1g Pre-roll | Bubble ButtClout KingHybridTHC: 38.06% clout-king-2-pack-x-1g-pre-roll-bubble-buttclout-kinghybridthc-38-06-1764475447246-1xr7xx \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-2-pack-x-1g-pre-roll-bubble-butt f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3006 \N \N \N STRAWBERRY CARAMEL CHEWS 100MG pure-everest-strawberry-caramel-chews-100mg \N \N \N HYBRID \N \N PURE/EVEREST \N \N \N https://best.treez.io/onlinemenu/category/edible/item/8654b055-8aae-459f-ae91-5fe1a487b20a?customerType=ADULT t f \N 2025-11-18 14:42:08.994684 2025-11-18 14:42:08.994684 2025-11-18 14:42:08.994684 2025-11-18 14:42:08.994684 149 \N \N \N \N \N \N 12.00 12.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.994684+00 \N +3007 \N \N \N CARAMEL CHEWS KONA SEA SALT 100MG sublime-caramel-chews-kona-sea-salt-100mg \N \N \N HYBRID \N \N SUBLIME \N \N \N https://best.treez.io/onlinemenu/category/edible/item/864f844a-2a8b-4f76-b320-30acab09f27a?customerType=ADULT t f \N 2025-11-18 14:42:08.996491 2025-11-18 14:42:08.996491 2025-11-18 14:42:08.996491 2025-11-18 14:42:08.996491 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.996491+00 \N +2460 \N \N \N Shango Flower Jar | Scotties CakeShangoIndica-HybridTHC: 30.72% shango-flower-jar-scotties-cake \N \N \N \N 30.72 \N Shango \N https://images.dutchie.com/9ef8598b2407303193a147317cba3cf3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/shango-flower-jar-scotties-cake t f \N 2025-11-18 03:58:06.16852 2025-11-18 04:25:40.384577 2025-11-18 03:58:06.16852 2025-11-18 05:26:05.146055 112 Scotties CakeShango \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:40.384577+00 \N +3420 1 55 deeply-rooted-az-flower-1764475343320-0 Abundant Organics Flower Mylar | Abundant HorizonAbundant OrganicsTHC: 26.32% abundant-organics-flower-mylar-abundant-horizonabundant-organicsthc-26-32-1764475343324-a2b9w9 \N 91.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-flower-mylar-abundant-horizon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3421 1 55 deeply-rooted-az-flower-1764475343320-1 Abundant Organics Flower Mylar | Star QueenAbundant OrganicsIndica-HybridTHC: 29.61% abundant-organics-flower-mylar-star-queenabundant-organicsindica-hybridthc-29-61-1764475343328-mf4aa2 \N 91.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-flower-mylar-star-queen f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3422 1 55 deeply-rooted-az-flower-1764475343320-2 Abundant Organics Flower Mylar | ViennettaAbundant OrganicsIndicaTHC: 28.47% abundant-organics-flower-mylar-viennettaabundant-organicsindicathc-28-47-1764475343330-m39j45 \N 91.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-flower-mylar-viennetta f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +2462 \N \N \N Simply Twisted | 2-Pack x (1g) Pre-Rolls | Banana PunchSimply TwistedHybridTHC: 31.03%Special Offer simply-twisted-2-pack-x-1g-pre-rolls-banana-punch \N \N \N \N 31.03 \N Simply Twisted \N https://images.dutchie.com/5d8fb0ed4703238fa13c44b487971102?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/simply-twisted-2-pack-x-1g-pre-rolls-banana-punch t f \N 2025-11-18 03:58:11.399035 2025-11-18 04:25:51.413781 2025-11-18 03:58:11.399035 2025-11-18 05:26:10.953432 112 Banana PunchSimply Twisted \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.413781+00 \N +3423 1 55 deeply-rooted-az-flower-1764475343320-3 Abundant Organics | 4.5g Flower Jar | Space SasquatchAbundant OrganicsHybridTHC: 28.2% abundant-organics-4-5g-flower-jar-space-sasquatchabundant-organicshybridthc-28-2-1764475343331-x3ppce \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-4-5g-flower-jar-space-sasquatch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3424 1 55 deeply-rooted-az-flower-1764475343320-4 Alien Labs Flower Jar | Atomic AppleAlien LabsHybridTHC: 23.85%Special Offer alien-labs-flower-jar-atomic-applealien-labshybridthc-23-85-special-offer-1764475343332-l8hinb \N 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-atomic-apple-1437 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3425 1 55 deeply-rooted-az-flower-1764475343320-5 Alien Labs Flower Jar | BiskanteAlien LabsHybridTHC: 24.68%Special Offer alien-labs-flower-jar-biskantealien-labshybridthc-24-68-special-offer-1764475343333-qan2hj \N 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-biskante-44323 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3426 1 55 deeply-rooted-az-flower-1764475343320-6 Alien Labs Flower Jar | BiskanteAlien LabsHybridTHC: 22.77%Special Offer alien-labs-flower-jar-biskantealien-labshybridthc-22-77-special-offer-1764475343335-ic51hd \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-biskante f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3427 1 55 deeply-rooted-az-flower-1764475343320-7 Alien Labs Flower Jar | Dark WebAlien LabsHybridTHC: 20.71%Special Offer alien-labs-flower-jar-dark-webalien-labshybridthc-20-71-special-offer-1764475343336-r860j6 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-dark-web f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3428 1 55 deeply-rooted-az-flower-1764475343320-8 Alien Labs Flower Jar | GUAVA 2.0Alien LabsIndica-HybridTHC: 20.69%Special Offer alien-labs-flower-jar-guava-2-0alien-labsindica-hybridthc-20-69-special-offer-1764475343337-bl6utz \N 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-guava-2-0 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3010 \N \N \N RSO MILK CHOCOLATE BAR 100MG sublime-rso-milk-chocolate-bar-100mg \N \N \N \N \N \N SUBLIME \N \N \N https://best.treez.io/onlinemenu/category/edible/item/3dd30b62-8d5b-48fd-ae46-138d30d811e5?customerType=ADULT t f \N 2025-11-18 14:42:09.001669 2025-11-18 14:42:09.001669 2025-11-18 14:42:09.001669 2025-11-18 14:42:09.001669 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.001669+00 \N +3429 1 55 deeply-rooted-az-flower-1764475343320-9 Alien Labs Flower Jar | GeminiAlien LabsIndica-HybridTHC: 24.9%Special Offer alien-labs-flower-jar-geminialien-labsindica-hybridthc-24-9-special-offer-1764475343338-bq06t2 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-gemini f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3430 1 55 deeply-rooted-az-flower-1764475343320-10 Alien Labs Flower Jar | XJ-13Alien LabsIndica-HybridTHC: 26.51%Special Offer alien-labs-flower-jar-xj-13alien-labsindica-hybridthc-26-51-special-offer-1764475343339-qi2hxr \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-xj-13 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3431 1 55 deeply-rooted-az-flower-1764475343320-11 Alien Labs Flower Jar | ZangriaAlien LabsHybridTHC: 23.97%Special Offer alien-labs-flower-jar-zangriaalien-labshybridthc-23-97-special-offer-1764475343340-cfj745 \N 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-zangria-26907 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3432 1 55 deeply-rooted-az-flower-1764475343320-12 Barrio Cannabis Co. Flower Jar | Permanent ChimeraBarrio Cannabis Co.Indica-HybridTHC: 18.55% barrio-cannabis-co-flower-jar-permanent-chimerabarrio-cannabis-co-indica-hybridthc-18-55-1764475343341-phvf57 \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/barrio-cannabis-co-flower-jar-permanent-chimera f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3413 1 59 deeply-rooted-az-edibles-1764475244783-93 Smokiez | 100mg Sweet Gummy | Single | PeachSmokiez EdiblesTHC: 0.43%Special Offer smokiez-100mg-sweet-gummy-single-peachsmokiez-ediblesthc-0-43-special-offer-1764475244896-2s6p6v \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sweet-gummy-single-peach f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3414 1 59 deeply-rooted-az-edibles-1764475244783-94 Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | CherrySticky SaguaroTHC: 111.27 mgCBD: 2.75 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-cherrysticky-saguarothc-111-27-mgcbd-2-75-mg-1764475244897-sttzyy \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-cherry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3415 1 59 deeply-rooted-az-edibles-1764475244783-95 Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | MandarinSticky SaguaroTHC: 107.06 mgCBD: 2.6 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-mandarinsticky-saguarothc-107-06-mgcbd-2-6-mg-1764475244898-12upk8 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-mandarin f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3416 1 59 deeply-rooted-az-edibles-1764475244783-96 Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | MangoSticky SaguaroTHC: 94.27 mgCBD: 2.36 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-mangosticky-saguarothc-94-27-mgcbd-2-36-mg-1764475244899-jpijkn \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-mango f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3417 1 59 deeply-rooted-az-edibles-1764475244783-97 Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | The Blue OneSticky SaguaroTHC: 104.7 mgCBD: 2.35 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-the-blue-onesticky-saguarothc-104-7-mgcbd-2-35-mg-1764475244900-6bmfgc \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-the-blue-one f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3418 1 59 deeply-rooted-az-edibles-1764475244783-98 Sublime | 100mg Hard Candy | Pina ColadaFeel SublimeTHC: 0.23% sublime-100mg-hard-candy-pina-coladafeel-sublimethc-0-23-1764475244901-m4ixyb \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sublime-100mg-hard-candy-pina-colada f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3419 1 59 deeply-rooted-az-edibles-1764475244783-99 THC Raspberry Lemonade Pearls - SativaGrönSativaTAC: 100 mgTHC: 0.27% thc-raspberry-lemonade-pearls-sativagr-nsativatac-100-mgthc-0-27-1764475244902-o9zwda \N 15.40 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thc-raspberry-lemonade-pearls-sativa-21478 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3696 1 57 deeply-rooted-az-vaporizers-1764475641905-13 Connected Cured Resin Cart | Ghost OGConnected CannabisIndica-HybridTHC: 80.84%CBD: 0.15% connected-cured-resin-cart-ghost-ogconnected-cannabisindica-hybridthc-80-84-cbd-0-15-1764475641930-cwg2nu \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-ghost-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3697 1 57 deeply-rooted-az-vaporizers-1764475641905-14 Connected Cured Resin Cart | Guava 2.0Connected CannabisIndica-HybridTHC: 75.94%CBD: 0.09% connected-cured-resin-cart-guava-2-0connected-cannabisindica-hybridthc-75-94-cbd-0-09-1764475641931-qx16cq \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-guava-2-0 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3698 1 57 deeply-rooted-az-vaporizers-1764475641905-15 Connected Cured Resin Cart | Permanent MarkerConnected CannabisIndicaTHC: 79.23%CBD: 0.16% connected-cured-resin-cart-permanent-markerconnected-cannabisindicathc-79-23-cbd-0-16-1764475641933-fyzh6t \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-permanent-marker f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3699 1 57 deeply-rooted-az-vaporizers-1764475641905-16 Connected Cured Resin Cart | Silver SpoonConnected CannabisSativa-HybridTHC: 74.88% connected-cured-resin-cart-silver-spoonconnected-cannabissativa-hybridthc-74-88-1764475641934-tbelxu \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-silver-spoon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3700 1 57 deeply-rooted-az-vaporizers-1764475641905-17 Cure Injoy Distillate AIO | Guava LavaCure InjoyTHC: 84.5%CBD: 0.2%Special Offer cure-injoy-distillate-aio-guava-lavacure-injoythc-84-5-cbd-0-2-special-offer-1764475641935-9d2uan \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-guava-lava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3701 1 57 deeply-rooted-az-vaporizers-1764475641905-18 Cure Injoy Distillate AIO | Papaya KushCure InjoyTHC: 85.31%CBD: 0.2%Special Offer cure-injoy-distillate-aio-papaya-kushcure-injoythc-85-31-cbd-0-2-special-offer-1764475641936-s46wey \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-papaya-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3702 1 57 deeply-rooted-az-vaporizers-1764475641905-19 Cure Injoy Distillate AIO | RS11Cure InjoyTHC: 83.02%CBD: 0.2%Special Offer cure-injoy-distillate-aio-rs11cure-injoythc-83-02-cbd-0-2-special-offer-1764475641937-a4rptj \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-rs11 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3530 1 56 deeply-rooted-az-pre-rolls-1764475447223-10 Clout King | 2-Pack x 1g Pre-roll | Peanut Butter CupClout KingHybridTHC: 37.7% clout-king-2-pack-x-1g-pre-roll-peanut-butter-cupclout-kinghybridthc-37-7-1764475447248-zvtrpx \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-2-pack-x-1g-pre-roll-peanut-butter-cup f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3531 1 56 deeply-rooted-az-pre-rolls-1764475447223-11 SponsoredCookie Blend Diamond-Infused Joint (1g)LeafersIndica-HybridTHC: 42.59% sponsoredcookie-blend-diamond-infused-joint-1g-leafersindica-hybridthc-42-59-1764475447249-lg186q \N 16.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cookie-blend-diamond-infused-joint-1g f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3532 1 56 deeply-rooted-az-pre-rolls-1764475447223-12 Connected Single Pre-Roll | ChromeConnected CannabisHybridTHC: 23.64%Special Offer connected-single-pre-roll-chromeconnected-cannabishybridthc-23-64-special-offer-1764475447251-e7kkkw \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-single-pre-roll-chrome f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3533 1 56 deeply-rooted-az-pre-rolls-1764475447223-13 Connected Single Pre-Roll | NightshadeConnected CannabisIndica-HybridTHC: 21.3% connected-single-pre-roll-nightshadeconnected-cannabisindica-hybridthc-21-3-1764475447252-27i5eh \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-single-pre-roll-nightshade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3534 1 56 deeply-rooted-az-pre-rolls-1764475447223-14 Diamond Dusties | 1.3G Live Resin Infused Pre-Roll | Ruby SativaThe PharmHybridTHC: 41.43% diamond-dusties-1-3g-live-resin-infused-pre-roll-ruby-sativathe-pharmhybridthc-41-43-1764475447253-8crob0 \N 18.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/diamond-dusties-1-3g-live-resin-infused-pre-roll-ruby-sativa-87808 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1.3G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3535 1 56 deeply-rooted-az-pre-rolls-1764475447223-15 Dusties | 1.3G Infused Pre-Roll | DOUBLE YUMThe PharmTHC: 38.68% dusties-1-3g-infused-pre-roll-double-yumthe-pharmthc-38-68-1764475447254-90zycn \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-1-3g-infused-pre-roll-double-yum f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1.3G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3536 1 56 deeply-rooted-az-pre-rolls-1764475447223-16 Dusties | 1.3G Infused Pre-Roll | HorchataThe PharmTHC: 40.33%CBD: 0.22% dusties-1-3g-infused-pre-roll-horchatathe-pharmthc-40-33-cbd-0-22-1764475447255-kgvew2 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-1-3g-infused-pre-roll-horchata f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1.3G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3537 1 56 deeply-rooted-az-pre-rolls-1764475447223-17 Dusties | 1.3G Infused Pre-Roll | Watermelon AKThe PharmSativaTHC: 38.19%CBD: 0.6% dusties-1-3g-infused-pre-roll-watermelon-akthe-pharmsativathc-38-19-cbd-0-6-1764475447257-kttea6 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-1-3g-infused-pre-roll-watermelon-ak f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1.3G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3538 1 56 deeply-rooted-az-pre-rolls-1764475447223-18 Evolution Pharms Hash and Rosin Mini Infused Pre-Roll | Ghost Train HazeEvolution PharmsSativa-HybridTHC: 35%Special Offer evolution-pharms-hash-and-rosin-mini-infused-pre-roll-ghost-train-hazeevolution-pharmssativa-hybridthc-35-special-offer-1764475447258-c1o4dc \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/evolution-pharms-hash-and-rosin-mini-infused-pre-roll-ghost-train-haze f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3539 1 56 deeply-rooted-az-pre-rolls-1764475447223-19 Evolution Pharms Hash and Rosin Mini Infused Pre-Roll | Natural | Watermelon PunchEvolution PharmsHybridTHC: 34.4%Special Offer evolution-pharms-hash-and-rosin-mini-infused-pre-roll-natural-watermelon-punchevolution-pharmshybridthc-34-4-special-offer-1764475447259-jv4psc \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/evolution-pharms-hash-and-rosin-mini-infused-pre-roll-natural-watermelon-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3540 1 56 deeply-rooted-az-pre-rolls-1764475447223-20 Evolution Pharms Hash and Rosin Mini Infused Pre-Roll | SunsetEvolution PharmsIndica-HybridTHC: 41.94%Special Offer evolution-pharms-hash-and-rosin-mini-infused-pre-roll-sunsetevolution-pharmsindica-hybridthc-41-94-special-offer-1764475447260-gyalt6 \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/evolution-pharms-hash-and-rosin-mini-infused-pre-roll-sunset f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3541 1 56 deeply-rooted-az-pre-rolls-1764475447223-21 Evolution Pharms Hash and Rosin Mini Infused Pre-Roll | Thai ChiEvolution PharmsHybridTHC: 35.9%Special Offer evolution-pharms-hash-and-rosin-mini-infused-pre-roll-thai-chievolution-pharmshybridthc-35-9-special-offer-1764475447262-r34qti \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/evolution-pharms-hash-and-rosin-mini-infused-pre-roll-thai-chi f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3542 1 56 deeply-rooted-az-pre-rolls-1764475447223-22 Evolution Pharms Hash and Rosin Mini Infused Pre-Roll | ZkywalkerEvolution PharmsIndica-HybridTHC: 41.15%Special Offer evolution-pharms-hash-and-rosin-mini-infused-pre-roll-zkywalkerevolution-pharmsindica-hybridthc-41-15-special-offer-1764475447263-llmbhu \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/evolution-pharms-hash-and-rosin-mini-infused-pre-roll-zkywalker f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3543 1 56 deeply-rooted-az-pre-rolls-1764475447223-23 Ghost OGConnected CannabisIndica-HybridTHC: 23.33% ghost-ogconnected-cannabisindica-hybridthc-23-33-1764475447264-dv2yda \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ghost-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3544 1 56 deeply-rooted-az-pre-rolls-1764475447223-24 Goldsmith | 3-Pack Iced Out Infused Pre-Roll | Blueberry HeadbandGoldsmith ExtractsHybridTHC: 35.52% goldsmith-3-pack-iced-out-infused-pre-roll-blueberry-headbandgoldsmith-extractshybridthc-35-52-1764475447265-3hhclj \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-3-pack-iced-out-infused-pre-roll-blueberry-headband f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3545 1 56 deeply-rooted-az-pre-rolls-1764475447223-25 Goldsmith | 3-Pack Iced Out Infused Pre-Roll | UrsulaGoldsmith ExtractsIndicaTHC: 38.15% goldsmith-3-pack-iced-out-infused-pre-roll-ursulagoldsmith-extractsindicathc-38-15-1764475447267-uypi4p \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-3-pack-iced-out-infused-pre-roll-ursula f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3546 1 56 deeply-rooted-az-pre-rolls-1764475447223-26 Green Dot Labs Pre-roll | FuchsiaGreen Dot LabsTHC: 22.81% green-dot-labs-pre-roll-fuchsiagreen-dot-labsthc-22-81-1764475447269-2s0kyq \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-pre-roll-fuchsia f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3547 1 56 deeply-rooted-az-pre-rolls-1764475447223-27 Green Dot Labs Pre-roll | Thunder DomeGreen Dot LabsHybridTHC: 24.04% green-dot-labs-pre-roll-thunder-domegreen-dot-labshybridthc-24-04-1764475447270-gel3mf \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-pre-roll-thunder-dome f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3548 1 56 deeply-rooted-az-pre-rolls-1764475447223-28 Grow Sciences Hash Hole Pre-Roll | Prickly Pear x RambutanGrow SciencesTHC: 29.08% grow-sciences-hash-hole-pre-roll-prickly-pear-x-rambutangrow-sciencesthc-29-08-1764475447272-a1j0vq \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-hash-hole-pre-roll-prickly-pear-x-rambutan f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3549 1 56 deeply-rooted-az-pre-rolls-1764475447223-29 High Five | 5-Pack x 1g Pre-Rolls | Gorilla CookiesHigh West FarmsSativa-HybridTHC: 28.42% high-five-5-pack-x-1g-pre-rolls-gorilla-cookieshigh-west-farmssativa-hybridthc-28-42-1764475447273-roffp8 \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-five-5-pack-x-1g-pre-rolls-gorilla-cookies f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3550 1 56 deeply-rooted-az-pre-rolls-1764475447223-30 High Five | 5-Pack x 1g Pre-Rolls | Memory LossHigh West FarmsSativa-HybridTHC: 28.24% high-five-5-pack-x-1g-pre-rolls-memory-losshigh-west-farmssativa-hybridthc-28-24-1764475447274-3jg97x \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-five-5-pack-x-1g-pre-rolls-memory-loss f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3551 1 56 deeply-rooted-az-pre-rolls-1764475447223-31 High Tide Baby Jeeter Infused Pre-Roll 5-pack | 2.5gJeeterSativaTHC: 35.96% high-tide-baby-jeeter-infused-pre-roll-5-pack-2-5gjeetersativathc-35-96-1764475447275-wa1xg8 \N 26.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-tide-baby-jeeter-infused-pre-roll-5-pack-2-5g-3718 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3552 1 56 deeply-rooted-az-pre-rolls-1764475447223-32 High West Farms Diamond Infused Pre-Rolls | IlluminatiHigh West FarmsIndica-HybridTHC: 41.32% high-west-farms-diamond-infused-pre-rolls-illuminatihigh-west-farmsindica-hybridthc-41-32-1764475447277-sjvc3z \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-diamond-infused-pre-rolls-illuminati f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3553 1 56 deeply-rooted-az-pre-rolls-1764475447223-33 High West Farms Diamond Infused Pre-Rolls | YahemiHigh West FarmsSativa-HybridTHC: 28.2% high-west-farms-diamond-infused-pre-rolls-yahemihigh-west-farmssativa-hybridthc-28-2-1764475447278-ipwjec \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-diamond-infused-pre-rolls-yahemi-31391 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3554 1 56 deeply-rooted-az-pre-rolls-1764475447223-34 High West Farms Diamond Infused Pre-Rolls | YahemiHigh West FarmsSativa-HybridTHC: 50.04% high-west-farms-diamond-infused-pre-rolls-yahemihigh-west-farmssativa-hybridthc-50-04-1764475447279-w16qs6 \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-diamond-infused-pre-rolls-yahemi f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3555 1 56 deeply-rooted-az-pre-rolls-1764475447223-35 Hot Rod Infused Pre-Roll | Blue DreamHot RodSativa-HybridTHC: 37.56%CBD: 0.93% hot-rod-infused-pre-roll-blue-dreamhot-rodsativa-hybridthc-37-56-cbd-0-93-1764475447281-p1linn \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/hot-rod-infused-pre-roll-blue-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3556 1 56 deeply-rooted-az-pre-rolls-1764475447223-36 Hot Rod | Infused Pre-Roll-Berry BlackoutHot RodIndicaTHC: 34.44%CBD: 0.86% hot-rod-infused-pre-roll-berry-blackouthot-rodindicathc-34-44-cbd-0-86-1764475447282-w1hzn4 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/hot-rod-infused-pre-roll-berry-blackout-97708 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3557 1 56 deeply-rooted-az-pre-rolls-1764475447223-37 Hot Rod | Infused Pre-Roll-Pony PizzazzHot RodSativaTHC: 49.77% hot-rod-infused-pre-roll-pony-pizzazzhot-rodsativathc-49-77-1764475447283-1q6xzc \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/hot-rod-infused-pre-roll-pony-pizzazz-83458 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3558 1 56 deeply-rooted-az-pre-rolls-1764475447223-38 Infused Five | 5-Pack x 1g Pre-Rolls | Citrus KushHigh West FarmsIndica-HybridTHC: 60.11% infused-five-5-pack-x-1g-pre-rolls-citrus-kushhigh-west-farmsindica-hybridthc-60-11-1764475447284-usdzek \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/infused-five-5-pack-x-1g-pre-rolls-citrus-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3559 1 56 deeply-rooted-az-pre-rolls-1764475447223-39 Infused Five | 5-Pack x 1g Pre-Rolls | Gas PedalHigh West FarmsHybridTHC: 62.09% infused-five-5-pack-x-1g-pre-rolls-gas-pedalhigh-west-farmshybridthc-62-09-1764475447286-27n4du \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/infused-five-5-pack-x-1g-pre-rolls-gas-pedal f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3560 1 56 deeply-rooted-az-pre-rolls-1764475447223-40 Infused Five | 5-Pack x 1g Pre-Rolls | Lemon Cherry GelatoHigh West FarmsIndica-HybridTHC: 69.06% infused-five-5-pack-x-1g-pre-rolls-lemon-cherry-gelatohigh-west-farmsindica-hybridthc-69-06-1764475447287-y29qi6 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/infused-five-5-pack-x-1g-pre-rolls-lemon-cherry-gelato f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3561 1 56 deeply-rooted-az-pre-rolls-1764475447223-41 Infused Five | 5-Pack x 1g Pre-Rolls | Purple PunchHigh West FarmsIndica-HybridTHC: 53.13% infused-five-5-pack-x-1g-pre-rolls-purple-punchhigh-west-farmsindica-hybridthc-53-13-1764475447288-0mv2m4 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/infused-five-5-pack-x-1g-pre-rolls-purple-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3562 1 56 deeply-rooted-az-pre-rolls-1764475447223-42 Jeeter Infused Pre-Rolls | Mai TaiJeeterTHC: 38.36% jeeter-infused-pre-rolls-mai-taijeeterthc-38-36-1764475447289-8f1giu \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-infused-pre-rolls-mai-tai f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3567 1 56 deeply-rooted-az-pre-rolls-1764475447223-47 Jeeter Quad Infused Pre-Rolls | Kiwi KushJeeterTHC: 41.02% jeeter-quad-infused-pre-rolls-kiwi-kushjeeterthc-41-02-1764475447295-4tmwow \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-quad-infused-pre-rolls-kiwi-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3568 1 56 deeply-rooted-az-pre-rolls-1764475447223-48 Jeeter Quad Infused Pre-Rolls | MojilatoJeeterTHC: 40.72% jeeter-quad-infused-pre-rolls-mojilatojeeterthc-40-72-1764475447297-tao9fd \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-quad-infused-pre-rolls-mojilato-29973 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3569 1 56 deeply-rooted-az-pre-rolls-1764475447223-49 Jeeter | 1.3g Solventless Live Rosin Infused Baby Cannon | Neptune KushJeeterIndica-HybridTHC: 41.44% jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-neptune-kushjeeterindica-hybridthc-41-44-1764475447298-01i1s7 \N 18.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-neptune-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1.3g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3570 1 56 deeply-rooted-az-pre-rolls-1764475447223-50 Jeeter | 3-Pack x 0.5g Live Resin Infused Pre-Roll | RS11JeeterTHC: 41.33%Special Offer jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-rs11jeeterthc-41-33-special-offer-1764475447299-0hghom \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-rs11 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 0.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3571 1 56 deeply-rooted-az-pre-rolls-1764475447223-51 Jeeter | 5-Pack x 0.5g Baby Jeeter Infused Pre-Rolls | Granddaddy PurpJeeterTHC: 43.47% jeeter-5-pack-x-0-5g-baby-jeeter-infused-pre-rolls-granddaddy-purpjeeterthc-43-47-1764475447300-2bikox \N 26.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-5-pack-x-0-5g-baby-jeeter-infused-pre-rolls-granddaddy-purp f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 0.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3572 1 56 deeply-rooted-az-pre-rolls-1764475447223-52 Jeeter | 5-Pack x 0.5g Quad Infused Pre-Roll | Double RainbowJeeterTHC: 40.99% jeeter-5-pack-x-0-5g-quad-infused-pre-roll-double-rainbowjeeterthc-40-99-1764475447302-utckbn \N 26.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-5-pack-x-0-5g-quad-infused-pre-roll-double-rainbow f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 0.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3573 1 56 deeply-rooted-az-pre-rolls-1764475447223-53 Legends Doinks | 2-Pack x 1g Pre-roll | Fire CrotchLegendsHybridTHC: 20.51%CBD: 0.03% legends-doinks-2-pack-x-1g-pre-roll-fire-crotchlegendshybridthc-20-51-cbd-0-03-1764475447303-6z4uhi \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-doinks-2-pack-x-1g-pre-roll-fire-crotch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3574 1 56 deeply-rooted-az-pre-rolls-1764475447223-54 Legends Doinks | 2-Pack x 1g Pre-roll | Papaya PowerLegendsIndica-HybridTHC: 20.1%CBD: 0.03% legends-doinks-2-pack-x-1g-pre-roll-papaya-powerlegendsindica-hybridthc-20-1-cbd-0-03-1764475447304-3vlqzj \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-doinks-2-pack-x-1g-pre-roll-papaya-power f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3575 1 56 deeply-rooted-az-pre-rolls-1764475447223-55 Lunch Box Hash Hole | Cherryzona X Grape Cream CakeLunch BoxIndica-HybridTHC: 37.84% lunch-box-hash-hole-cherryzona-x-grape-cream-cakelunch-boxindica-hybridthc-37-84-1764475447305-qjo203 \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-hash-hole-cherryzona-x-grape-cream-cake f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3576 1 56 deeply-rooted-az-pre-rolls-1764475447223-56 Lunch Box Hash Hole | Cherryzona X Off White StrawberryLunch BoxHybridTHC: 37.75% lunch-box-hash-hole-cherryzona-x-off-white-strawberrylunch-boxhybridthc-37-75-1764475447306-apz5ry \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-hash-hole-cherryzona-x-off-white-strawberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3577 1 56 deeply-rooted-az-pre-rolls-1764475447223-57 Lunch Box Hash Hole | Cherryzona X Original GlueLunch BoxIndica-HybridTHC: 31.77% lunch-box-hash-hole-cherryzona-x-original-gluelunch-boxindica-hybridthc-31-77-1764475447307-97fwm7 \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-hash-hole-cherryzona-x-original-glue f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3578 1 56 deeply-rooted-az-pre-rolls-1764475447223-58 Lunch Box Hash Hole | Cherryzona X PapayaLunch BoxIndica-HybridTHC: 34.64% lunch-box-hash-hole-cherryzona-x-papayalunch-boxindica-hybridthc-34-64-1764475447308-33unx2 \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-hash-hole-cherryzona-x-papaya f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3579 1 56 deeply-rooted-az-pre-rolls-1764475447223-59 Lunch Box Pre-Roll | The KimberLunch BoxIndica-HybridTHC: 23.38% lunch-box-pre-roll-the-kimberlunch-boxindica-hybridthc-23-38-1764475447310-xvg9fr \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-pre-roll-the-kimber f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1.2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3580 1 56 deeply-rooted-az-pre-rolls-1764475447223-60 Lunch Box | 2pk x (2.4g) Infused Mini Pre-Rolls | Cherryzona Lemonade X Grape GasLunch BoxHybridTHC: 32.69% lunch-box-2pk-x-2-4g-infused-mini-pre-rolls-cherryzona-lemonade-x-grape-gaslunch-boxhybridthc-32-69-1764475447311-rzcnp6 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-infused-mini-pre-rolls-cherryzona-lemonade-x-grape-gas f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3581 1 56 deeply-rooted-az-pre-rolls-1764475447223-61 Lunch Box | 2pk x (2.4g) Pre-Rolls | Camelback KushLunch BoxHybridTHC: 26.25% lunch-box-2pk-x-2-4g-pre-rolls-camelback-kushlunch-boxhybridthc-26-25-1764475447312-8lihu0 \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-pre-rolls-camelback-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3582 1 56 deeply-rooted-az-pre-rolls-1764475447223-62 Lunch Box | 2pk x (2.4g) Pre-Rolls | Lemon SquirtLunch BoxSativa-HybridTHC: 26.36% lunch-box-2pk-x-2-4g-pre-rolls-lemon-squirtlunch-boxsativa-hybridthc-26-36-1764475447313-riwbv8 \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-pre-rolls-lemon-squirt f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3583 1 56 deeply-rooted-az-pre-rolls-1764475447223-63 Lunch Box | 2pk x (2.4g) Pre-Rolls | MelonzLunch BoxHybridTHC: 25.99% lunch-box-2pk-x-2-4g-pre-rolls-melonzlunch-boxhybridthc-25-99-1764475447314-w8yimm \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-pre-rolls-melonz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3584 1 56 deeply-rooted-az-pre-rolls-1764475447223-64 Lunch Box | 2pk x (2.4g) Pre-Rolls | OkashiLunch BoxHybridTHC: 27.63% lunch-box-2pk-x-2-4g-pre-rolls-okashilunch-boxhybridthc-27-63-1764475447314-lsu1rh \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-pre-rolls-okashi f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3585 1 56 deeply-rooted-az-pre-rolls-1764475447223-65 Lunch Box | 2pk x (2.4g) Pre-Rolls | Peach SquirtLunch BoxHybridTHC: 24.79% lunch-box-2pk-x-2-4g-pre-rolls-peach-squirtlunch-boxhybridthc-24-79-1764475447316-ynprbs \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-pre-rolls-peach-squirt f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3586 1 56 deeply-rooted-az-pre-rolls-1764475447223-66 Lunch Box | 2pk x (2.4g) Pre-Rolls | Sour GrapesLunch BoxHybridTHC: 18.77% lunch-box-2pk-x-2-4g-pre-rolls-sour-grapeslunch-boxhybridthc-18-77-1764475447317-wzebrc \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-pre-rolls-sour-grapes f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3587 1 56 deeply-rooted-az-pre-rolls-1764475447223-67 Lunch Box | 2pk x (2.4g) Pre-Rolls | Wyatt HerbLunch BoxHybridTHC: 19.05% lunch-box-2pk-x-2-4g-pre-rolls-wyatt-herblunch-boxhybridthc-19-05-1764475447318-zodfkw \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-2pk-x-2-4g-pre-rolls-wyatt-herb f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2.4g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3588 1 56 deeply-rooted-az-pre-rolls-1764475447223-68 Mfused | 2-Pack x (1g) Infused Pre-roll Fatty | Swirly TempleMfusedTHC: 46.77% mfused-2-pack-x-1g-infused-pre-roll-fatty-swirly-templemfusedthc-46-77-1764475447319-ihp2s7 \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-2-pack-x-1g-infused-pre-roll-fatty-swirly-temple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3589 1 56 deeply-rooted-az-pre-rolls-1764475447223-69 Mfused | 2-Pack x (1g) Infused Pre-roll Fatty | Wicked ApplezMfusedTHC: 31.86% mfused-2-pack-x-1g-infused-pre-roll-fatty-wicked-applezmfusedthc-31-86-1764475447320-mp21w1 \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-2-pack-x-1g-infused-pre-roll-fatty-wicked-applez f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3590 1 56 deeply-rooted-az-pre-rolls-1764475447223-70 Mfused | 5-Pack x 0.5g Infused Pre-roll Fatty | Galactic GrapeMfusedTHC: 36.21% mfused-5-pack-x-0-5g-infused-pre-roll-fatty-galactic-grapemfusedthc-36-21-1764475447321-7wm2nb \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-5-pack-x-0-5g-infused-pre-roll-fatty-galactic-grape f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 0.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3591 1 56 deeply-rooted-az-pre-rolls-1764475447223-71 Mfused | 5-Pack x 0.5g Infused Pre-roll Fatty | Lemon LoopzMfusedTHC: 35.04% mfused-5-pack-x-0-5g-infused-pre-roll-fatty-lemon-loopzmfusedthc-35-04-1764475447323-1nh21r \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-5-pack-x-0-5g-infused-pre-roll-fatty-lemon-loopz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 0.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3592 1 56 deeply-rooted-az-pre-rolls-1764475447223-72 Seed Junky Pre-Roll | Magic MarkerSeed JunkyHybridTHC: 24.47%Special Offer seed-junky-pre-roll-magic-markerseed-junkyhybridthc-24-47-special-offer-1764475447324-y2rwyp \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/seed-junky-pre-roll-magic-marker f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3593 1 56 deeply-rooted-az-pre-rolls-1764475447223-73 Seed Junky Pre-Roll | MalibuSeed JunkyHybridTHC: 26.63%Special Offer seed-junky-pre-roll-malibuseed-junkyhybridthc-26-63-special-offer-1764475447324-mqxkbp \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/seed-junky-pre-roll-malibu f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3594 1 56 deeply-rooted-az-pre-rolls-1764475447223-74 Seed Junky Pre-Roll | Pineapple FruzSeed JunkyIndica-HybridTHC: 23.13%Special Offer seed-junky-pre-roll-pineapple-fruzseed-junkyindica-hybridthc-23-13-special-offer-1764475447326-j87z7l \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/seed-junky-pre-roll-pineapple-fruz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3595 1 56 deeply-rooted-az-pre-rolls-1764475447223-75 Seed Junky Pre-Roll | Purple Push PopSeed JunkyIndica-HybridTHC: 30.37%CBD: 0.09%Special Offer seed-junky-pre-roll-purple-push-popseed-junkyindica-hybridthc-30-37-cbd-0-09-special-offer-1764475447327-dd8cx8 \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/seed-junky-pre-roll-purple-push-pop f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3048 \N \N \N MARIONBERRY 100MG wyld-marionberry-100mg \N \N \N INDICA \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/767b7e43-2cae-40ae-a8be-9e5261570fab?customerType=ADULT t f \N 2025-11-18 14:42:09.070552 2025-11-18 14:42:09.070552 2025-11-18 14:42:09.070552 2025-11-18 14:42:09.070552 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.070552+00 \N +3596 1 56 deeply-rooted-az-pre-rolls-1764475447223-76 Simply Twisted | 2-Pack x (1g) Pre-Rolls | Cactus BreathSimply TwistedIndicaTHC: 23.86%Special Offer simply-twisted-2-pack-x-1g-pre-rolls-cactus-breathsimply-twistedindicathc-23-86-special-offer-1764475447328-huletx \N 4.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/simply-twisted-2-pack-x-1g-pre-rolls-cactus-breath f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3597 1 56 deeply-rooted-az-pre-rolls-1764475447223-77 Space Rocks | Space Rocketz Infused Pre-Roll | Apples & BananasSpace RocksHybridTHC: 49.81% space-rocks-space-rocketz-infused-pre-roll-apples-bananasspace-rockshybridthc-49-81-1764475447329-d3c9vb \N 13.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-space-rocketz-infused-pre-roll-apples-bananas f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3598 1 56 deeply-rooted-az-pre-rolls-1764475447223-78 Space Rocks | Space Rocketz Infused Pre-Roll | LuciliciousSpace RocksIndica-HybridTHC: 44.1% space-rocks-space-rocketz-infused-pre-roll-luciliciousspace-rocksindica-hybridthc-44-1-1764475447330-h2sujk \N 13.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-space-rocketz-infused-pre-roll-lucilicious f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3599 1 56 deeply-rooted-az-pre-rolls-1764475447223-79 Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | BlueberrySticky SaguaroIndica-HybridTHC: 22.75% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-blueberrysticky-saguaroindica-hybridthc-22-75-1764475447331-8j51z3 \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-blueberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3600 1 56 deeply-rooted-az-pre-rolls-1764475447223-80 Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | GluejitsuSticky SaguaroIndica-HybridTHC: 22.4% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-gluejitsusticky-saguaroindica-hybridthc-22-4-1764475447333-tzdq2p \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-gluejitsu f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3601 1 56 deeply-rooted-az-pre-rolls-1764475447223-81 Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | PineappleSticky SaguaroHybridTHC: 25.56% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-pineapplesticky-saguarohybridthc-25-56-1764475447334-bxglwj \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-pineapple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3602 1 56 deeply-rooted-az-pre-rolls-1764475447223-82 Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | ShibuiSticky SaguaroSativa-HybridTHC: 20.39% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-shibuisticky-saguarosativa-hybridthc-20-39-1764475447335-iymzmy \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-shibui f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3603 1 56 deeply-rooted-az-pre-rolls-1764475447223-83 Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | Sour MacSticky SaguaroHybridTHC: 15.16% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-sour-macsticky-saguarohybridthc-15-16-1764475447336-tfix52 \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-sour-mac f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3066 \N \N \N JAM JAM achieve-jam-jam-1g \N \N \N S/I 56.72 \N ACHIEVE \N \N \N https://best.treez.io/onlinemenu/category/extract/item/a82dc144-6010-4020-845b-6ea393bd7d71?customerType=ADULT t f \N 2025-11-18 14:42:09.102419 2025-11-18 14:42:09.102419 2025-11-18 14:42:09.102419 2025-11-18 14:42:09.102419 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.102419+00 \N +3604 1 56 deeply-rooted-az-pre-rolls-1764475447223-84 Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | TropkickSticky SaguaroIndica-HybridTHC: 23.44% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-tropkicksticky-saguaroindica-hybridthc-23-44-1764475447337-fl8vmu \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-tropkick f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3605 1 56 deeply-rooted-az-pre-rolls-1764475447223-85 Stiiizy Infused Blunt 40's | BiscottiSTIIIZYIndicaTHC: 47.94% stiiizy-infused-blunt-40-s-biscottistiiizyindicathc-47-94-1764475447338-8hdtho \N 21.60 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-blunt-40-s-biscotti f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3606 1 56 deeply-rooted-az-pre-rolls-1764475447223-86 Stiiizy Infused Blunt 40's | GelatoSTIIIZYTHC: 44.94% stiiizy-infused-blunt-40-s-gelatostiiizythc-44-94-1764475447339-ktc8hi \N 21.60 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-blunt-40-s-gelato f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3607 1 56 deeply-rooted-az-pre-rolls-1764475447223-87 Stiiizy Infused Blunt 40's | Pineapple ExpressSTIIIZYSativa-HybridTHC: 48.23% stiiizy-infused-blunt-40-s-pineapple-expressstiiizysativa-hybridthc-48-23-1764475447341-at93k6 \N 21.60 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-blunt-40-s-pineapple-express f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3608 1 56 deeply-rooted-az-pre-rolls-1764475447223-88 Stiiizy Infused Blunt 40's | Sour DieselSTIIIZYTHC: 44.4% stiiizy-infused-blunt-40-s-sour-dieselstiiizythc-44-4-1764475447342-7htbs4 \N 21.60 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-blunt-40-s-sour-diesel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3609 1 56 deeply-rooted-az-pre-rolls-1764475447223-89 Stiiizy Infused Blunt 40's | Super Lemon HazeSTIIIZYSativaTHC: 41.55% stiiizy-infused-blunt-40-s-super-lemon-hazestiiizysativathc-41-55-1764475447343-g6c712 \N 21.60 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-blunt-40-s-super-lemon-haze f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3610 1 56 deeply-rooted-az-pre-rolls-1764475447223-90 Stiiizy Infused Blunt 40's | Watermelon ZSTIIIZYIndica-HybridTHC: 43.22% stiiizy-infused-blunt-40-s-watermelon-zstiiizyindica-hybridthc-43-22-1764475447344-bkqcrj \N 21.60 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-blunt-40-s-watermelon-z f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3611 1 56 deeply-rooted-az-pre-rolls-1764475447223-91 Stiiizy Infused Pre-Roll 40's | BiscottiSTIIIZYTHC: 41.31% stiiizy-infused-pre-roll-40-s-biscottistiiizythc-41-31-1764475447346-g5ro1w \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-biscotti f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3612 1 56 deeply-rooted-az-pre-rolls-1764475447223-92 Stiiizy Infused Pre-Roll 40's | Blue DreamSTIIIZYTHC: 39.43% stiiizy-infused-pre-roll-40-s-blue-dreamstiiizythc-39-43-1764475447347-yls2yj \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-blue-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3613 1 56 deeply-rooted-az-pre-rolls-1764475447223-93 Stiiizy Infused Pre-Roll 40's | Cereal MilkSTIIIZYTHC: 40.5% stiiizy-infused-pre-roll-40-s-cereal-milkstiiizythc-40-5-1764475447348-5177e9 \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-cereal-milk f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3614 1 56 deeply-rooted-az-pre-rolls-1764475447223-94 Stiiizy Infused Pre-Roll 40's | King Louis XIIISTIIIZYTHC: 48.51% stiiizy-infused-pre-roll-40-s-king-louis-xiiistiiizythc-48-51-1764475447349-efkjt7 \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-king-louis-xiii f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3703 1 57 deeply-rooted-az-vaporizers-1764475641905-20 Dime Distillate AIO | Blueberry Lemon HazeDime IndustriesTHC: 94.27%CBD: 0.22% dime-distillate-aio-blueberry-lemon-hazedime-industriesthc-94-27-cbd-0-22-1764475641938-weqo70 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-blueberry-lemon-haze-12072 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3704 1 57 deeply-rooted-az-vaporizers-1764475641905-21 Dime Distillate AIO | Cactus ChillDime IndustriesTHC: 90.49%CBD: 0.21% dime-distillate-aio-cactus-chilldime-industriesthc-90-49-cbd-0-21-1764475641939-fe0q6g \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-cactus-chill f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3705 1 57 deeply-rooted-az-vaporizers-1764475641905-22 Dime Distillate AIO | Key Lime PieDime IndustriesTHC: 90.84%CBD: 0.16% dime-distillate-aio-key-lime-piedime-industriesthc-90-84-cbd-0-16-1764475641940-kxa74c \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-key-lime-pie-93335 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3706 1 57 deeply-rooted-az-vaporizers-1764475641905-23 Dime Distillate AIO | Peach Ice-TDime IndustriesTHC: 93.21%CBD: 0.2% dime-distillate-aio-peach-ice-tdime-industriesthc-93-21-cbd-0-2-1764475641941-qzwa3q \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-peach-ice-t f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3433 1 55 deeply-rooted-az-flower-1764475343320-13 Barrio Cannabis Co. Flower Jar | Carbon FiberBarrio Cannabis Co.HybridTHC: 18.6% barrio-cannabis-co-flower-jar-carbon-fiberbarrio-cannabis-co-hybridthc-18-6-1764475343342-tws3lt \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/barrio-cannabis-co-flower-jar-carbon-fiber f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3434 1 55 deeply-rooted-az-flower-1764475343320-14 Barrio Cannabis Co. Flower Jar | Rainbow RTZBarrio Cannabis Co.Indica-HybridTHC: 18% barrio-cannabis-co-flower-jar-rainbow-rtzbarrio-cannabis-co-indica-hybridthc-18-1764475343343-fexxzh \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/barrio-cannabis-co-flower-jar-rainbow-rtz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3435 1 55 deeply-rooted-az-flower-1764475343320-15 Cannabish Flower Jar | Grande GuavaCannabishSativa-HybridTHC: 24.74% cannabish-flower-jar-grande-guavacannabishsativa-hybridthc-24-74-1764475343345-bm2way \N 17.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cannabish-flower-jar-grande-guava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3436 1 55 deeply-rooted-az-flower-1764475343320-16 Clout King Cloutlettes | Hood SnacksClout KingHybridTHC: 33.9%Special Offer clout-king-cloutlettes-hood-snacksclout-kinghybridthc-33-9-special-offer-1764475343346-e5d7tt \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-cloutlettes-hood-snacks f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3437 1 55 deeply-rooted-az-flower-1764475343320-17 Clout King Cloutlettes | LCZClout KingHybridTHC: 28.73%Special Offer clout-king-cloutlettes-lczclout-kinghybridthc-28-73-special-offer-1764475343348-aq63rq \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-cloutlettes-lcz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3438 1 55 deeply-rooted-az-flower-1764475343320-18 Clout King Cloutlettes | SlusheeClout KingHybridTHC: 35.79%Special Offer clout-king-cloutlettes-slusheeclout-kinghybridthc-35-79-special-offer-1764475343349-89v20a \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-cloutlettes-slushee f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3439 1 55 deeply-rooted-az-flower-1764475343320-19 Clout King Cloutlettes | ZZ BowClout KingIndica-HybridTHC: 37.1%Special Offer clout-king-cloutlettes-zz-bowclout-kingindica-hybridthc-37-1-special-offer-1764475343350-jfxxx9 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-cloutlettes-zz-bow f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3440 1 55 deeply-rooted-az-flower-1764475343320-20 Clout King Cloutlettes | ZaddyClout KingIndicaTHC: 23.87%Special Offer clout-king-cloutlettes-zaddyclout-kingindicathc-23-87-special-offer-1764475343351-r0fxxo \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-cloutlettes-zaddy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3441 1 55 deeply-rooted-az-flower-1764475343320-21 Clout King Cloutlettes | ZaklavaClout KingHybridTHC: 31.98%Special Offer clout-king-cloutlettes-zaklavaclout-kinghybridthc-31-98-special-offer-1764475343353-rbrbp0 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-cloutlettes-zaklava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3442 1 55 deeply-rooted-az-flower-1764475343320-22 Clout King Cloutlettes | ZelonaClout KingHybridTHC: 38.32%Special Offer clout-king-cloutlettes-zelonaclout-kinghybridthc-38-32-special-offer-1764475343354-8q7ycp \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-cloutlettes-zelona f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 4 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3443 1 55 deeply-rooted-az-flower-1764475343320-23 Clout King Flower Jar | Cream PieClout KingHybridTHC: 33.26% clout-king-flower-jar-cream-pieclout-kinghybridthc-33-26-1764475343355-msksxo \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-flower-jar-cream-pie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3444 1 55 deeply-rooted-az-flower-1764475343320-24 Clout King Flower Jar | Hood SnacksClout KingHybridTHC: 33.9% clout-king-flower-jar-hood-snacksclout-kinghybridthc-33-9-1764475343356-v3cbtp \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-flower-jar-hood-snacks f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3445 1 55 deeply-rooted-az-flower-1764475343320-25 Clout King Flower Jar | Milk SteakClout KingIndicaTHC: 36.72%Special Offer clout-king-flower-jar-milk-steakclout-kingindicathc-36-72-special-offer-1764475343357-94ed2v \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-flower-jar-milk-steak f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3446 1 55 deeply-rooted-az-flower-1764475343320-26 Clout King Flower Jar | SlusheeClout KingHybridTHC: 35.79%Special Offer clout-king-flower-jar-slusheeclout-kinghybridthc-35-79-special-offer-1764475343358-chlzcj \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-flower-jar-slushee f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3447 1 55 deeply-rooted-az-flower-1764475343320-27 Clout King Flower Jar | ZaklavaClout KingHybridTHC: 31.98%Special Offer clout-king-flower-jar-zaklavaclout-kinghybridthc-31-98-special-offer-1764475343359-sx445l \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/clout-king-flower-jar-zaklava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3448 1 55 deeply-rooted-az-flower-1764475343320-28 Connected Flower Jar | AmbroziaAlien LabsHybridTHC: 23.54%Special Offer connected-flower-jar-ambroziaalien-labshybridthc-23-54-special-offer-1764475343360-gbvnhu \N 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-ambrozia-31383 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3449 1 55 deeply-rooted-az-flower-1764475343320-29 Connected Flower Jar | Bad AppleConnected CannabisSativa-HybridTHC: 24.57% connected-flower-jar-bad-appleconnected-cannabissativa-hybridthc-24-57-1764475343361-tcv3qa \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-bad-apple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3450 1 55 deeply-rooted-az-flower-1764475343320-30 Connected Flower Jar | GelonadeConnected CannabisSativa-HybridTHC: 20.65%Special Offer connected-flower-jar-gelonadeconnected-cannabissativa-hybridthc-20-65-special-offer-1764475343362-zy76u8 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-gelonade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3451 1 55 deeply-rooted-az-flower-1764475343320-31 Connected Flower Jar | Ghost OGConnected CannabisIndica-HybridTHC: 24.82%Special Offer connected-flower-jar-ghost-ogconnected-cannabisindica-hybridthc-24-82-special-offer-1764475343363-12cj34 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-ghost-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3452 1 55 deeply-rooted-az-flower-1764475343320-32 Connected Flower Jar | Jack of DiamondsConnected CannabisHybridTHC: 22.39%Special Offer connected-flower-jar-jack-of-diamondsconnected-cannabishybridthc-22-39-special-offer-1764475343364-w7086a \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-jack-of-diamonds f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3453 1 55 deeply-rooted-az-flower-1764475343320-33 Connected Flower Jar | Permanent MarkerConnected CannabisIndicaTHC: 21.06%Special Offer connected-flower-jar-permanent-markerconnected-cannabisindicathc-21-06-special-offer-1764475343365-yzioan \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-permanent-marker f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3454 1 55 deeply-rooted-az-flower-1764475343320-34 DR Flower Mylar | AK 1995 (PH)Deeply RootedSativa-HybridTHC: 18.41%Special Offer dr-flower-mylar-ak-1995-ph-deeply-rootedsativa-hybridthc-18-41-special-offer-1764475343366-nhczfl \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-ak-1995-ph-67783 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3455 1 55 deeply-rooted-az-flower-1764475343320-35 DR Flower Mylar | AK 1995(PH)Deeply RootedSativa-HybridTHC: 21.02%Special Offer dr-flower-mylar-ak-1995-ph-deeply-rootedsativa-hybridthc-21-02-special-offer-1764475343367-wxeih7 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-ak-1995-ph f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3456 1 55 deeply-rooted-az-flower-1764475343320-36 DR Flower Mylar | AngelicaDeeply RootedIndica-HybridTHC: 31.66% dr-flower-mylar-angelicadeeply-rootedindica-hybridthc-31-66-1764475343369-jxul92 \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-angelica f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3457 1 55 deeply-rooted-az-flower-1764475343320-37 DR Flower Mylar | Black MapleDeeply RootedHybridTHC: 25.67%Special Offer dr-flower-mylar-black-mapledeeply-rootedhybridthc-25-67-special-offer-1764475343371-g5tsw2 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-black-maple-8834 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3458 1 55 deeply-rooted-az-flower-1764475343320-38 DR Flower Mylar | Blue NDZ (Living Soil)Deeply RootedHybridTHC: 19.51%Special Offer dr-flower-mylar-blue-ndz-living-soil-deeply-rootedhybridthc-19-51-special-offer-1764475343372-1jqg9c \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-blue-ndz-living-soil-80727 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3459 1 55 deeply-rooted-az-flower-1764475343320-39 DR Flower Mylar | D-Inferno (HG)Deeply RootedIndica-HybridTHC: 22.72%Special Offer dr-flower-mylar-d-inferno-hg-deeply-rootedindica-hybridthc-22-72-special-offer-1764475343373-cdxtgv \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-d-inferno-hg-62724 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3460 1 55 deeply-rooted-az-flower-1764475343320-40 DR Flower Mylar | GMO (PH)Deeply RootedIndicaTHC: 30.44% dr-flower-mylar-gmo-ph-deeply-rootedindicathc-30-44-1764475343375-dqpcd1 \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-gmo-ph-13002 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3461 1 55 deeply-rooted-az-flower-1764475343320-41 DR Flower Mylar | GMO (PH)Deeply RootedIndicaTHC: 29%Special Offer dr-flower-mylar-gmo-ph-deeply-rootedindicathc-29-special-offer-1764475343376-ml1d5r \N 56.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-gmo-ph f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3462 1 55 deeply-rooted-az-flower-1764475343320-42 DR Flower Mylar | Guava Heaven (Living Soil)Deeply RootedSativa-HybridTHC: 17.79%Special Offer dr-flower-mylar-guava-heaven-living-soil-deeply-rootedsativa-hybridthc-17-79-special-offer-1764475343377-ztrabq \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-guava-heaven-living-soil f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3463 1 55 deeply-rooted-az-flower-1764475343320-43 DR Flower Mylar | ICC X Biker OG (ASY)Deeply RootedIndica-HybridTHC: 22.99%Special Offer dr-flower-mylar-icc-x-biker-og-asy-deeply-rootedindica-hybridthc-22-99-special-offer-1764475343379-fhhojd \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-icc-x-biker-og-asy-4260 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3464 1 55 deeply-rooted-az-flower-1764475343320-44 DR Flower Mylar | ICC x Biker OG (ASY)Deeply RootedIndica-HybridTHC: 22.99% dr-flower-mylar-icc-x-biker-og-asy-deeply-rootedindica-hybridthc-22-99-1764475343380-vca9wo \N 56.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-icc-x-biker-og-asy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3465 1 55 deeply-rooted-az-flower-1764475343320-45 DR Flower Mylar | Jealousy Mintz (ARZ)Deeply RootedIndica-HybridTHC: 29.25% dr-flower-mylar-jealousy-mintz-arz-deeply-rootedindica-hybridthc-29-25-1764475343381-j5h8vi \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-jealousy-mintz-arz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3466 1 55 deeply-rooted-az-flower-1764475343320-46 DR Flower Mylar | LSD (ASY)Deeply RootedIndica-HybridTHC: 25.91%Special Offer dr-flower-mylar-lsd-asy-deeply-rootedindica-hybridthc-25-91-special-offer-1764475343383-o90hwk \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-lsd-asy-81792 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3467 1 55 deeply-rooted-az-flower-1764475343320-47 DR Flower Mylar | Lemon Cherry RTZ (ASY)Deeply RootedHybridTHC: 24.37%CBD: 0.03%Special Offer dr-flower-mylar-lemon-cherry-rtz-asy-deeply-rootedhybridthc-24-37-cbd-0-03-special-offer-1764475343384-y142qz \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-lemon-cherry-rtz-asy-80506 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3468 1 55 deeply-rooted-az-flower-1764475343320-48 DR Flower Mylar | Lemon Vuitton 36 (PH)The PharmSativa-HybridTHC: 15.43%Special Offer dr-flower-mylar-lemon-vuitton-36-ph-the-pharmsativa-hybridthc-15-43-special-offer-1764475343385-u31itj \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-lemon-vuitton-36-ph f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3469 1 55 deeply-rooted-az-flower-1764475343320-49 DR Flower Mylar | Leopard Head (Pharm)Deeply RootedTHC: 17.8% dr-flower-mylar-leopard-head-pharm-deeply-rootedthc-17-8-1764475343386-n0iqz8 \N 7.98 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-leopard-head-pharm f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3470 1 55 deeply-rooted-az-flower-1764475343320-50 DR Flower Mylar | London Fog (ASY)AsylumIndica-HybridTHC: 29.07% dr-flower-mylar-london-fog-asy-asylumindica-hybridthc-29-07-1764475343387-i1y58n \N 82.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-london-fog-asy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3471 1 55 deeply-rooted-az-flower-1764475343320-51 DR Flower Mylar | Mac 1 (ASY)Deeply RootedIndica-HybridTHC: 28.28% dr-flower-mylar-mac-1-asy-deeply-rootedindica-hybridthc-28-28-1764475343389-p7p248 \N 82.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-mac-1-asy-91912 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3472 1 55 deeply-rooted-az-flower-1764475343320-52 DR Flower Mylar | Mule Fuel (PH)The PharmIndicaTHC: 29.56%Special Offer dr-flower-mylar-mule-fuel-ph-the-pharmindicathc-29-56-special-offer-1764475343390-q81qul \N 56.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-mule-fuel-ph f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3473 1 55 deeply-rooted-az-flower-1764475343320-53 DR Flower Mylar | Ronny Burger (Living Soil)Deeply RootedIndicaTHC: 21.67%Special Offer dr-flower-mylar-ronny-burger-living-soil-deeply-rootedindicathc-21-67-special-offer-1764475343391-dra893 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-ronny-burger-living-soil f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3474 1 55 deeply-rooted-az-flower-1764475343320-54 DR Flower Mylar | Royal Cake (PH)Deeply RootedHybridTHC: 25.37%Special Offer dr-flower-mylar-royal-cake-ph-deeply-rootedhybridthc-25-37-special-offer-1764475343392-ya9tlp \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-royal-cake-ph f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3475 1 55 deeply-rooted-az-flower-1764475343320-55 DR Flower Mylar | SuperBoof (Sueno)Deeply RootedHybridTHC: 29.55% dr-flower-mylar-superboof-sueno-deeply-rootedhybridthc-29-55-1764475343393-6i7oi4 \N 82.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-superboof-sueno-81974 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3476 1 55 deeply-rooted-az-flower-1764475343320-56 DR Flower Mylar | Tropical Punch (ASY)AsylumSativa-HybridTHC: 22.5%CBD: 0.02%Special Offer dr-flower-mylar-tropical-punch-asy-asylumsativa-hybridthc-22-5-cbd-0-02-special-offer-1764475343395-3x4nnb \N 56.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-tropical-punch-asy-73538 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3477 1 55 deeply-rooted-az-flower-1764475343320-57 DR Flower Mylar | Tropical Punch (ASY)Deeply RootedSativa-HybridTHC: 22.5%CBD: 0.02%Special Offer dr-flower-mylar-tropical-punch-asy-deeply-rootedsativa-hybridthc-22-5-cbd-0-02-special-offer-1764475343396-ssenxv \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-tropical-punch-asy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3478 1 55 deeply-rooted-az-flower-1764475343320-58 DR Flower POP | Wedding Cake (TP)Deeply RootedIndica-HybridTHC: 23.02%Special Offer dr-flower-pop-wedding-cake-tp-deeply-rootedindica-hybridthc-23-02-special-offer-1764475343398-v2nc63 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-pop-wedding-cake-tp f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3479 1 55 deeply-rooted-az-flower-1764475343320-59 DR Flower | Wedding Cake (TP)Deeply RootedIndica-HybridTHC: 21.56%Special Offer dr-flower-wedding-cake-tp-deeply-rootedindica-hybridthc-21-56-special-offer-1764475343399-ecmcng \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-wedding-cake-tp f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3480 1 55 deeply-rooted-az-flower-1764475343320-60 Dr. Zodiak Hash Hole | Blue Sherb x Rainbow PPDr. ZodiakTHC: 33.1% dr-zodiak-hash-hole-blue-sherb-x-rainbow-ppdr-zodiakthc-33-1-1764475343400-4elpri \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-hash-hole-blue-sherb-x-rainbow-pp f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 1.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3481 1 55 deeply-rooted-az-flower-1764475343320-61 Dr. Zodiak Infused Blunt | Hybrid BlendDr. ZodiakHybridTHC: 46.27%CBD: 0.45% dr-zodiak-infused-blunt-hybrid-blenddr-zodiakhybridthc-46-27-cbd-0-45-1764475343401-5xv4vl \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-infused-blunt-hybrid-blend f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3482 1 55 deeply-rooted-az-flower-1764475343320-62 Drip Top-Shelf Flower | MelonattidripSativaTHC: 26.67%Special Offer drip-top-shelf-flower-melonattidripsativathc-26-67-special-offer-1764475343402-9ilovu \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-top-shelf-flower-melonatti f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3483 1 55 deeply-rooted-az-flower-1764475343320-63 Drip Top-Shelf Flower | Waffle ConedripHybridTHC: 20.53%Special Offer drip-top-shelf-flower-waffle-conedriphybridthc-20-53-special-offer-1764475343403-g4qew9 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-top-shelf-flower-waffle-cone-20293 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3484 1 55 deeply-rooted-az-flower-1764475343320-64 Elevate Flower Mylar | Black MapleElevateHybridTHC: 24.65% elevate-flower-mylar-black-mapleelevatehybridthc-24-65-1764475343404-3q74zu \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-black-maple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3485 1 55 deeply-rooted-az-flower-1764475343320-65 Elevate Flower Mylar | Headband CookiesElevateSativaTHC: 28.14% elevate-flower-mylar-headband-cookieselevatesativathc-28-14-1764475343405-p5t9f8 \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-headband-cookies f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3486 1 55 deeply-rooted-az-flower-1764475343320-66 Elevate Flower Mylar | Hell's OGElevateIndica-HybridTHC: 28.63% elevate-flower-mylar-hell-s-ogelevateindica-hybridthc-28-63-1764475343406-c2hspl \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-hell-s-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3487 1 55 deeply-rooted-az-flower-1764475343320-67 Elevate Flower Mylar | Strawberry GuavaElevateHybridTHC: 24.98% elevate-flower-mylar-strawberry-guavaelevatehybridthc-24-98-1764475343407-18ry1d \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-strawberry-guava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3488 1 55 deeply-rooted-az-flower-1764475343320-68 Elevate Flower Mylar | Super BoofElevateHybridTHC: 22.7% elevate-flower-mylar-super-boofelevatehybridthc-22-7-1764475343409-qe609k \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-super-boof f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3489 1 55 deeply-rooted-az-flower-1764475343320-69 Elevate Flower Mylar | White PoisonElevateSativaTHC: 22.93% elevate-flower-mylar-white-poisonelevatesativathc-22-93-1764475343410-4uixtm \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-white-poison f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3490 1 55 deeply-rooted-az-flower-1764475343320-70 Green Dot Labs Flower Jar | Blu FrootGreen Dot LabsTHC: 28.21%CBD: 0.12% green-dot-labs-flower-jar-blu-frootgreen-dot-labsthc-28-21-cbd-0-12-1764475343411-d03le0 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-flower-jar-blu-froot f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3491 1 55 deeply-rooted-az-flower-1764475343320-71 Green Dot Labs Flower Jar | Pink FrootGreen Dot LabsHybridTHC: 21.91%CBD: 0.03% green-dot-labs-flower-jar-pink-frootgreen-dot-labshybridthc-21-91-cbd-0-03-1764475343413-8bfgxs \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-flower-jar-pink-froot f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3492 1 55 deeply-rooted-az-flower-1764475343320-72 Grow Sciences Flower Mylar | Apples and BananasGrow SciencesHybridTHC: 25.2% grow-sciences-flower-mylar-apples-and-bananasgrow-scienceshybridthc-25-2-1764475343414-66p0z9 \N 84.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-flower-mylar-apples-and-bananas f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3708 1 57 deeply-rooted-az-vaporizers-1764475641905-25 Dime Distillate AIO | Watermelon KushDime IndustriesTHC: 93.97%CBD: 0.19% dime-distillate-aio-watermelon-kushdime-industriesthc-93-97-cbd-0-19-1764475641944-iqe70k \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-watermelon-kush-89404 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3493 1 55 deeply-rooted-az-flower-1764475343320-73 Grow Sciences Flower Mylar | Banana Cream Cake x JealousyGrow SciencesHybridTHC: 28.59% grow-sciences-flower-mylar-banana-cream-cake-x-jealousygrow-scienceshybridthc-28-59-1764475343415-ouhwu5 \N 84.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-flower-mylar-banana-cream-cake-x-jealousy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3494 1 55 deeply-rooted-az-flower-1764475343320-74 Grow Sciences Flower Mylar | Trop CherryGrow SciencesHybridTHC: 29.48% grow-sciences-flower-mylar-trop-cherrygrow-scienceshybridthc-29-48-1764475343415-jw3ccm \N 84.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-flower-mylar-trop-cherry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3495 1 55 deeply-rooted-az-flower-1764475343320-75 Grow Sciences | 3.7g Flower Jar | Lemon SherbetGrow SciencesHybridTHC: 24.71% grow-sciences-3-7g-flower-jar-lemon-sherbetgrow-scienceshybridthc-24-71-1764475343417-jfdlfz \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-lemon-sherbet f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 3.7g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3496 1 55 deeply-rooted-az-flower-1764475343320-76 Grow Sciences | 3.7g Flower Jar | Orange ZqueezeGrow SciencesSativa-HybridTHC: 23.48% grow-sciences-3-7g-flower-jar-orange-zqueezegrow-sciencessativa-hybridthc-23-48-1764475343418-q1pe0o \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-orange-zqueeze f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 3.7g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3497 1 55 deeply-rooted-az-flower-1764475343320-77 Grow Sciences | 3.7g Flower Jar | Pineapple FruzGrow SciencesIndica-HybridTHC: 21% grow-sciences-3-7g-flower-jar-pineapple-fruzgrow-sciencesindica-hybridthc-21-1764475343419-kc6mky \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-pineapple-fruz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 3.7g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3498 1 55 deeply-rooted-az-flower-1764475343320-78 Grow Sciences | 3.7g Flower Jar | Prickly PearGrow SciencesHybridTHC: 21.61% grow-sciences-3-7g-flower-jar-prickly-peargrow-scienceshybridthc-21-61-1764475343420-nseij2 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-prickly-pear f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 3.7g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3499 1 55 deeply-rooted-az-flower-1764475343320-79 Grow Sciences | 3.7g Flower Jar | Z CubedGrow SciencesHybridTHC: 21.59% grow-sciences-3-7g-flower-jar-z-cubedgrow-scienceshybridthc-21-59-1764475343420-zm9i9x \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-z-cubed f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 3.7g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3500 1 55 deeply-rooted-az-flower-1764475343320-80 Halo Cannabis Flower Mylar | L.A BakerHalo CannabisIndica-HybridTHC: 28.4% halo-cannabis-flower-mylar-l-a-bakerhalo-cannabisindica-hybridthc-28-4-1764475343421-uy5vid \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-cannabis-flower-mylar-l-a-baker f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3501 1 55 deeply-rooted-az-flower-1764475343320-81 High Rollin Cannabis Flower Jar | Peach PieHigh Rollin CannabisIndica-HybridTHC: 22.64%CBD: 0.03% high-rollin-cannabis-flower-jar-peach-piehigh-rollin-cannabisindica-hybridthc-22-64-cbd-0-03-1764475343422-8y5iq3 \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-rollin-cannabis-flower-jar-peach-pie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3502 1 55 deeply-rooted-az-flower-1764475343320-82 High West Farms Flower Mylar | AK-47High West FarmsSativa-HybridTHC: 23.79%Special Offer high-west-farms-flower-mylar-ak-47high-west-farmssativa-hybridthc-23-79-special-offer-1764475343423-l7klts \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-flower-mylar-ak-47 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3503 1 55 deeply-rooted-az-flower-1764475343320-83 High West Farms Flower Mylar | Memory LossHigh West FarmsSativa-HybridTHC: 28.24% high-west-farms-flower-mylar-memory-losshigh-west-farmssativa-hybridthc-28-24-1764475343424-odkhrr \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-flower-mylar-memory-loss f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3504 1 55 deeply-rooted-az-flower-1764475343320-84 HighMart Flower Mylar | 97HighMartHybridTHC: 24.05% highmart-flower-mylar-97highmarthybridthc-24-05-1764475343425-0oupkt \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/highmart-flower-mylar-97 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3505 1 55 deeply-rooted-az-flower-1764475343320-85 HighMart Flower Mylar | Family Cut 41HighMartHybridTHC: 25.01% highmart-flower-mylar-family-cut-41highmarthybridthc-25-01-1764475343426-taragq \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/highmart-flower-mylar-family-cut-41-73730 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3506 1 55 deeply-rooted-az-flower-1764475343320-86 HighMart Flower Mylar | SonoraHighMartHybridTHC: 27.3%CBD: 0.16% highmart-flower-mylar-sonorahighmarthybridthc-27-3-cbd-0-16-1764475343427-h01kkq \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/highmart-flower-mylar-sonora-57334 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3507 1 55 deeply-rooted-az-flower-1764475343320-87 Legends Flower Jar | Chem D WreckerLegendsIndica-HybridTHC: 28.83%CBD: 0.04%Special Offer legends-flower-jar-chem-d-wreckerlegendsindica-hybridthc-28-83-cbd-0-04-special-offer-1764475343428-w968kw \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-chem-d-wrecker f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3508 1 55 deeply-rooted-az-flower-1764475343320-88 Legends Flower Jar | Chin CheckLegendsTHC: 27.43%CBD: 0.04%Special Offer legends-flower-jar-chin-checklegendsthc-27-43-cbd-0-04-special-offer-1764475343429-43zlmb \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-chin-check f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3509 1 55 deeply-rooted-az-flower-1764475343320-89 Legends Flower Jar | Fire CrotchLegendsHybridTHC: 20.51%CBD: 0.03%Special Offer legends-flower-jar-fire-crotchlegendshybridthc-20-51-cbd-0-03-special-offer-1764475343430-pc7s9s \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-fire-crotch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3510 1 55 deeply-rooted-az-flower-1764475343320-90 Legends Flower Jar | Honey BananaLegendsIndica-HybridTHC: 24.59%CBD: 0.04%Special Offer legends-flower-jar-honey-bananalegendsindica-hybridthc-24-59-cbd-0-04-special-offer-1764475343431-8hknbh \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-honey-banana f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3511 1 55 deeply-rooted-az-flower-1764475343320-91 Legends Flower Jar | Nitro FumezLegendsTHC: 30.13%Special Offer legends-flower-jar-nitro-fumezlegendsthc-30-13-special-offer-1764475343432-f5tnt9 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-nitro-fumez f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3512 1 55 deeply-rooted-az-flower-1764475343320-92 Legends Flower Jar | Oil TankerLegendsIndica-HybridTHC: 21.1%CBD: 0.03%Special Offer legends-flower-jar-oil-tankerlegendsindica-hybridthc-21-1-cbd-0-03-special-offer-1764475343433-ik6q2n \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-oil-tanker f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3513 1 55 deeply-rooted-az-flower-1764475343320-93 Legends Flower Jar | Papaya PowerLegendsIndica-HybridTHC: 20.1%CBD: 0.03%Special Offer legends-flower-jar-papaya-powerlegendsindica-hybridthc-20-1-cbd-0-03-special-offer-1764475343434-100ou4 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-papaya-power f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3514 1 55 deeply-rooted-az-flower-1764475343320-94 Legends Flower Jar | Sunset SherbLegendsIndica-HybridTHC: 24.8%CBD: 0.04%Special Offer legends-flower-jar-sunset-sherblegendsindica-hybridthc-24-8-cbd-0-04-special-offer-1764475343435-p2olbj \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-sunset-sherb f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3515 1 55 deeply-rooted-az-flower-1764475343320-95 Legends Flower Jar | Z KushLegendsTHC: 23.66%Special Offer legends-flower-jar-z-kushlegendsthc-23-66-special-offer-1764475343436-id4vfz \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-z-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +2624 \N \N \N Wyld | 100mg THC Gummies | RaspberryWyldTHCTHC: 0.25% wyld-100mg-thc-gummies-raspberry-29099 \N \N \N \N 0.25 \N Wyld \N https://images.dutchie.com/28135938e0faaef4d951cfaf88a67f97?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-100mg-thc-gummies-raspberry-29099 t f \N 2025-11-18 04:00:34.952379 2025-11-18 04:28:14.299342 2025-11-18 04:00:34.952379 2025-11-18 05:36:20.232289 112 100mg \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.299342+00 \N +2626 \N \N \N Wyld | 1:1 THC/CBD Gummies | PomegranteWyldTHCTHC: 0.28% wyld-1-1-thc-cbd-gummies-pomegrante-48156 \N \N \N \N 0.28 \N Wyld \N https://images.dutchie.com/a11175af3885260afa70b07d1a281087?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-1-1-thc-cbd-gummies-pomegrante-48156 t f \N 2025-11-18 04:00:34.956642 2025-11-18 04:28:14.303913 2025-11-18 04:00:34.956642 2025-11-18 05:36:26.484483 112 1:1 \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.303913+00 \N +2627 \N \N \N Wyld | 1:1 THC/CBG Gummies | Passion Orange GuavaWyldTHCTHC: 0.53% wyld-1-1-thc-cbg-gummies-passion-orange-guava \N \N \N \N 0.53 \N Wyld \N https://images.dutchie.com/95129946b23d383d9a865a6de0dc23f6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-1-1-thc-cbg-gummies-passion-orange-guava t f \N 2025-11-18 04:00:34.958818 2025-11-18 04:28:14.306225 2025-11-18 04:00:34.958818 2025-11-18 05:36:45.019389 112 1:1 \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.306225+00 \N +2629 \N \N \N Wyld | One 100mg THC Gummies | GrapeWyldTHCTHC: 0.55% wyld-one-100mg-thc-gummies-grape \N \N \N \N 0.55 \N Wyld \N https://images.dutchie.com/7f14b69afac30954162c6a4a84e78d17?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-one-100mg-thc-gummies-grape t f \N 2025-11-18 04:00:34.963876 2025-11-18 04:28:14.310583 2025-11-18 04:00:34.963876 2025-11-18 05:36:36.046717 112 One 100mg \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.310583+00 \N +2631 \N \N \N Wyld | One 100mg THC Gummies | Sour Peach MangoWyldTHCTHC: 0.55% wyld-one-100mg-thc-gummies-sour-peach-mango \N \N \N \N 0.55 \N Wyld \N https://images.dutchie.com/c1af5616cf8aa9c52db1b56f16d64d0d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-one-100mg-thc-gummies-sour-peach-mango t f \N 2025-11-18 04:00:34.96814 2025-11-18 04:28:14.314404 2025-11-18 04:00:34.96814 2025-11-18 05:36:48.276281 112 One 100mg \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.314404+00 \N +2632 \N \N \N Wyld | One 100mg THC Gummies | Strawberry LemonadeWyldTHCTHC: 0.53% wyld-one-100mg-thc-gummies-strawberry-lemonade \N \N \N \N 0.53 \N Wyld \N https://images.dutchie.com/379c3f3c8a3239e0b6474cbafa7cc5af?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-one-100mg-thc-gummies-strawberry-lemonade t f \N 2025-11-18 04:00:34.969799 2025-11-18 04:28:14.316473 2025-11-18 04:00:34.969799 2025-11-18 05:36:51.356635 112 One 100mg \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.316473+00 \N +3117 \N \N \N T-(S) t-s \N \N \N SATIVA \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/32bd3c2f-e1e2-45bd-bf3d-6ce891c97a94?customerType=ADULT t f \N 2025-11-18 14:42:09.194891 2025-11-18 14:42:09.194891 2025-11-18 14:42:09.194891 2025-11-18 14:42:09.194891 149 \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.194891+00 \N +3118 \N \N \N JEWEL CHILLUM jewel-chillum \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/97e8e542-cdbf-4d28-a026-2fd294f6a728?customerType=ADULT t f \N 2025-11-18 14:42:09.196552 2025-11-18 14:42:09.196552 2025-11-18 14:42:09.196552 2025-11-18 14:42:09.196552 149 \N \N \N \N \N \N 5.50 5.50 \N \N \N \N in_stock \N 2025-11-18 14:42:09.196552+00 \N +3516 1 55 deeply-rooted-az-flower-1764475343320-96 Lost Dutchmen Flower Mylar | Champagne RoomLost DutchmenHybridTHC: 25.48%Special Offer lost-dutchmen-flower-mylar-champagne-roomlost-dutchmenhybridthc-25-48-special-offer-1764475343437-15zmf6 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lost-dutchmen-flower-mylar-champagne-room f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3517 1 55 deeply-rooted-az-flower-1764475343320-97 Lost Dutchmen Flower Mylar | Guava TartLost DutchmenSativa-HybridTHC: 25.83%Special Offer lost-dutchmen-flower-mylar-guava-tartlost-dutchmensativa-hybridthc-25-83-special-offer-1764475343439-0dp3j0 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lost-dutchmen-flower-mylar-guava-tart f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3518 1 55 deeply-rooted-az-flower-1764475343320-98 Lost Dutchmen Flower Mylar | Meyers LemonLost DutchmenTHC: 29.01%Special Offer lost-dutchmen-flower-mylar-meyers-lemonlost-dutchmenthc-29-01-special-offer-1764475343440-szokc0 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lost-dutchmen-flower-mylar-meyers-lemon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3519 1 55 deeply-rooted-az-flower-1764475343320-99 Lost Dutchmen Flower Mylar | Scoops of ChemLost DutchmenIndicaTHC: 18.98%CBD: 0.04%Special Offer lost-dutchmen-flower-mylar-scoops-of-chemlost-dutchmenindicathc-18-98-cbd-0-04-special-offer-1764475343441-vx7jgw \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lost-dutchmen-flower-mylar-scoops-of-chem f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 2025-11-30 04:02:23.319808 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:02:23.319808+00 \N +3709 1 57 deeply-rooted-az-vaporizers-1764475641905-26 Dime Distillate AIO | Wedding CakeDime IndustriesTHC: 92.99%CBD: 0.2% dime-distillate-aio-wedding-cakedime-industriesthc-92-99-cbd-0-2-1764475641945-5a0pi0 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-wedding-cake-74989 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3710 1 57 deeply-rooted-az-vaporizers-1764475641905-27 Dime Distillate Cart | Blackberry OGDime IndustriesTHC: 95.15%CBD: 0.25%Special Offer dime-distillate-cart-blackberry-ogdime-industriesthc-95-15-cbd-0-25-special-offer-1764475641947-9vcde9 \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-blackberry-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3711 1 57 deeply-rooted-az-vaporizers-1764475641905-28 Dime Distillate Cart | Blueberry Lemon HazeDime IndustriesSativaTHC: 91.01%CBD: 0.21%Special Offer dime-distillate-cart-blueberry-lemon-hazedime-industriessativathc-91-01-cbd-0-21-special-offer-1764475641948-zc5q9q \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-blueberry-lemon-haze f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3712 1 57 deeply-rooted-az-vaporizers-1764475641905-29 Dime Distillate Cart | Forbidden AppleDime IndustriesTHC: 92.97%CBD: 0.21%Special Offer dime-distillate-cart-forbidden-appledime-industriesthc-92-97-cbd-0-21-special-offer-1764475641949-mqwgfm \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-forbidden-apple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3713 1 57 deeply-rooted-az-vaporizers-1764475641905-30 Dime Distillate Cart | Mango DieselDime IndustriesTHC: 90.03%CBD: 0.21%Special Offer dime-distillate-cart-mango-dieseldime-industriesthc-90-03-cbd-0-21-special-offer-1764475641950-v1kg33 \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-mango-diesel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3714 1 57 deeply-rooted-az-vaporizers-1764475641905-31 Dime Distillate Cart | Passion ParadiseDime IndustriesTHC: 94.13%CBD: 0.3%Special Offer dime-distillate-cart-passion-paradisedime-industriesthc-94-13-cbd-0-3-special-offer-1764475641951-bioweg \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-passion-paradise f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3715 1 57 deeply-rooted-az-vaporizers-1764475641905-32 Dime Distillate Cart | Pina ColadaDime IndustriesTHC: 92.32%CBD: 0.26%Special Offer dime-distillate-cart-pina-coladadime-industriesthc-92-32-cbd-0-26-special-offer-1764475641952-s8jar5 \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-pina-colada f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +2068 \N \N \N Canamo Live Resin Cart | Larry BubbaCanamo ConcentratesIndica-HybridTHC: 75.9%CBD: 0.15% canamo-live-resin-cart-larry-bubba \N \N \N \N 75.90 0.15 Canamo Concentrates \N https://images.dutchie.com/5a8ae870c27ab98b6fc0d71a4831c7ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-larry-bubba t f \N 2025-11-18 03:51:25.798188 2025-11-18 04:17:55.603947 2025-11-18 03:51:25.798188 2025-11-18 05:03:09.093681 112 Larry BubbaCanamo Concentrates \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.603947+00 \N +3121 \N \N \N (NEW) PROXY BUBBLER ONYX GLASS REPLACEMENT puffco-new-proxy-bubbler-onyx-glass-replacement \N \N \N \N \N \N PUFFCO \N \N \N https://best.treez.io/onlinemenu/category/merch/item/57f8b4d6-3669-4854-a0e5-303688cde232?customerType=ADULT t f \N 2025-11-18 14:42:09.201569 2025-11-18 14:42:09.201569 2025-11-18 14:42:09.201569 2025-11-18 14:42:09.201569 149 \N \N \N \N \N \N 133.00 133.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.201569+00 \N +3122 \N \N \N BEST 510 BATTERY BLUE W/ USB CHARGER best-best-510-battery-blue-w-usb-charger \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/7813f6a3-90d9-42b1-8264-8d143d168f07?customerType=ADULT t f \N 2025-11-18 14:42:09.203267 2025-11-18 14:42:09.203267 2025-11-18 14:42:09.203267 2025-11-18 14:42:09.203267 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.203267+00 \N +3123 \N \N \N BEST 510 BATTERY WHITE W/ ORANGE BD - USB CHARGER best-best-510-battery-white-w-orange-bd-usb-charger \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/935b7157-cca8-4c1a-84ea-1b6467f2e73f?customerType=ADULT t f \N 2025-11-18 14:42:09.205113 2025-11-18 14:42:09.205113 2025-11-18 14:42:09.205113 2025-11-18 14:42:09.205113 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.205113+00 \N +3124 \N \N \N BEST 510 BATTERY ORANGE W/ WHITE BD - USB CHARGER best-best-510-battery-orange-w-white-bd-usb-charger \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/e5d6b372-7641-46e7-ac52-3edf0b8b4b45?customerType=ADULT t f \N 2025-11-18 14:42:09.206775 2025-11-18 14:42:09.206775 2025-11-18 14:42:09.206775 2025-11-18 14:42:09.206775 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.206775+00 \N +3523 1 56 deeply-rooted-az-pre-rolls-1764475447223-3 Alien Labs Single Pre-Roll | Brain WashAlien LabsHybridTHC: 24.09%Special Offer alien-labs-single-pre-roll-brain-washalien-labshybridthc-24-09-special-offer-1764475447238-zeq34e This item is included in a special today! Add it to your cart to work towards completing the offer. 10.50 \N Indica 24.09 0.15 Alien Labs specializes in high-quality cannabis flower that seem almost otherworldly. We've taken our standards into space and beyond, ensuring Alien Labs customers receive top-notch product. Although Alien Labs standards are lofty, our grow is small, as \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format&fit=max&q=95&w=2000&h=2000 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-single-pre-roll-brain-wash t f {"terpenes": ["Caryophyllene"]} 2025-11-30 04:04:07.224235 2025-12-01 07:18:50.30267 2025-11-30 04:04:07.224235 2025-12-01 07:18:50.30267 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3716 1 57 deeply-rooted-az-vaporizers-1764475641905-33 Dime Distillate Cart | Royal PearDime IndustriesTHC: 94.7%CBD: 0.32%Special Offer dime-distillate-cart-royal-peardime-industriesthc-94-7-cbd-0-32-special-offer-1764475641953-1kdxjb \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-royal-pear f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3717 1 57 deeply-rooted-az-vaporizers-1764475641905-34 Dime Distillate Cart | Sour GrapeDime IndustriesTHC: 93.54%CBD: 0.2%Special Offer dime-distillate-cart-sour-grapedime-industriesthc-93-54-cbd-0-2-special-offer-1764475641955-akfw0b \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-sour-grape f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3718 1 57 deeply-rooted-az-vaporizers-1764475641905-35 Dime Distillate Cart | Tropical KiwiDime IndustriesTHC: 90.82%CBD: 0.21%Special Offer dime-distillate-cart-tropical-kiwidime-industriesthc-90-82-cbd-0-21-special-offer-1764475641956-2i91dw \N 27.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-tropical-kiwi f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3130 \N \N \N BEST DISPENSARY BLACK HOODIE best-best-dispensary-black-hoodie \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/f1fe7dcf-39a1-4da3-a671-5b7a810e0568?customerType=ADULT t f \N 2025-11-18 14:42:09.218728 2025-11-18 14:42:09.218728 2025-11-18 14:42:09.218728 2025-11-18 14:42:09.218728 149 \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.218728+00 \N +3719 1 57 deeply-rooted-az-vaporizers-1764475641905-36 Dime Live Resin AIO | KushmintsDime IndustriesIndicaTHC: 76.13%CBD: 0.16% dime-live-resin-aio-kushmintsdime-industriesindicathc-76-13-cbd-0-16-1764475641957-neybkd \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-live-resin-aio-kushmints f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3720 1 57 deeply-rooted-az-vaporizers-1764475641905-37 Distillate AIO | Kiwi BurstDrip OilsIndica-HybridTHC: 88.76%CBD: 1.5% distillate-aio-kiwi-burstdrip-oilsindica-hybridthc-88-76-cbd-1-5-1764475641958-dgxq87 \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-kiwi-burst f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3721 1 57 deeply-rooted-az-vaporizers-1764475641905-38 Distillate AIO | Orange Cream FizzDrip OilsHybridTHC: 92.65%CBD: 0.87% distillate-aio-orange-cream-fizzdrip-oilshybridthc-92-65-cbd-0-87-1764475641959-6zkup4 \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-orange-cream-fizz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3722 1 57 deeply-rooted-az-vaporizers-1764475641905-39 Distillate AIO | Pink GuavaDrip OilsHybridTHC: 92.21%CBD: 0.24% distillate-aio-pink-guavadrip-oilshybridthc-92-21-cbd-0-24-1764475641960-niraac \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-pink-guava f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3723 1 57 deeply-rooted-az-vaporizers-1764475641905-40 Distillate AIO | Strawberry WatermelonDrip OilsHybridTHC: 86.4%CBD: 1.45% distillate-aio-strawberry-watermelondrip-oilshybridthc-86-4-cbd-1-45-1764475641961-xfv67y \N 41.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-strawberry-watermelon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3724 1 57 deeply-rooted-az-vaporizers-1764475641905-41 Goldsmith Distillate Cartridge | AK-47Goldsmith ExtractsSativa-HybridTHC: 91.02%CBD: 4.45% goldsmith-distillate-cartridge-ak-47goldsmith-extractssativa-hybridthc-91-02-cbd-4-45-1764475641962-e64ii1 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-ak-47-26412 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 47G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3725 1 57 deeply-rooted-az-vaporizers-1764475641905-42 Goldsmith Distillate Cartridge | Banana CreamGoldsmith ExtractsTHC: 89.4%CBD: 4.09% goldsmith-distillate-cartridge-banana-creamgoldsmith-extractsthc-89-4-cbd-4-09-1764475641963-ep4h7n \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-banana-cream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3726 1 57 deeply-rooted-az-vaporizers-1764475641905-43 Goldsmith Distillate Cartridge | Blackberry KushGoldsmith ExtractsTHC: 89.42%CBD: 4.12% goldsmith-distillate-cartridge-blackberry-kushgoldsmith-extractsthc-89-42-cbd-4-12-1764475641965-lw4jqt \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-blackberry-kush-16992 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3727 1 57 deeply-rooted-az-vaporizers-1764475641905-44 Goldsmith Distillate Cartridge | California OrangeGoldsmith ExtractsTHC: 89.36%CBD: 4.42% goldsmith-distillate-cartridge-california-orangegoldsmith-extractsthc-89-36-cbd-4-42-1764475641966-ees98m \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-california-orange f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3728 1 57 deeply-rooted-az-vaporizers-1764475641905-45 Goldsmith Distillate Cartridge | Dos-Si-DosGoldsmith ExtractsTHC: 88.4%CBD: 4.34% goldsmith-distillate-cartridge-dos-si-dosgoldsmith-extractsthc-88-4-cbd-4-34-1764475641967-ya8rjf \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-dos-si-dos f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3729 1 57 deeply-rooted-az-vaporizers-1764475641905-46 Goldsmith Distillate Cartridge | Forbidden FruitGoldsmith ExtractsTHC: 90.02%CBD: 4.4% goldsmith-distillate-cartridge-forbidden-fruitgoldsmith-extractsthc-90-02-cbd-4-4-1764475641968-lcet0d \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-forbidden-fruit f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3730 1 57 deeply-rooted-az-vaporizers-1764475641905-47 Goldsmith Distillate Cartridge | Grand Daddy PurpleGoldsmith ExtractsTHC: 88.24%CBD: 4.01% goldsmith-distillate-cartridge-grand-daddy-purplegoldsmith-extractsthc-88-24-cbd-4-01-1764475641969-8qhi5w \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-grand-daddy-purple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3731 1 57 deeply-rooted-az-vaporizers-1764475641905-48 Goldsmith Distillate Cartridge | Ice Cream CakeGoldsmith ExtractsTHC: 89.23%CBD: 4.5% goldsmith-distillate-cartridge-ice-cream-cakegoldsmith-extractsthc-89-23-cbd-4-5-1764475641970-61n6fs \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-ice-cream-cake f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3732 1 57 deeply-rooted-az-vaporizers-1764475641905-49 Goldsmith Distillate Cartridge | Lemon LimeGoldsmith ExtractsTHC: 90.31%CBD: 4.1% goldsmith-distillate-cartridge-lemon-limegoldsmith-extractsthc-90-31-cbd-4-1-1764475641971-is6jb2 \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-lemon-lime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3733 1 57 deeply-rooted-az-vaporizers-1764475641905-50 Goldsmith Distillate Cartridge | Mango KushGoldsmith ExtractsTHC: 89.14%CBD: 4.3% goldsmith-distillate-cartridge-mango-kushgoldsmith-extractsthc-89-14-cbd-4-3-1764475641972-jfu3lk \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-mango-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3734 1 57 deeply-rooted-az-vaporizers-1764475641905-51 Goldsmith Distillate Cartridge | MelonadeGoldsmith ExtractsTHC: 90.84%CBD: 4.18% goldsmith-distillate-cartridge-melonadegoldsmith-extractsthc-90-84-cbd-4-18-1764475641973-puypk6 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-melonade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3735 1 57 deeply-rooted-az-vaporizers-1764475641905-52 Goldsmith Distillate Cartridge | Peach RingsGoldsmith ExtractsTHC: 90.7%CBD: 4.5% goldsmith-distillate-cartridge-peach-ringsgoldsmith-extractsthc-90-7-cbd-4-5-1764475641974-uii0ad \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-peach-rings f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3736 1 57 deeply-rooted-az-vaporizers-1764475641905-53 Goldsmith Distillate Cartridge | Pina ColadaGoldsmith ExtractsTHC: 92.53% goldsmith-distillate-cartridge-pina-coladagoldsmith-extractsthc-92-53-1764475641975-i6u5zb \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-pina-colada f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3737 1 57 deeply-rooted-az-vaporizers-1764475641905-54 Goldsmith Distillate Cartridge | Purple PunchGoldsmith ExtractsTHC: 88.4%CBD: 4.08% goldsmith-distillate-cartridge-purple-punchgoldsmith-extractsthc-88-4-cbd-4-08-1764475641976-mj5xap \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-purple-punch-23759 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3738 1 57 deeply-rooted-az-vaporizers-1764475641905-55 Goldsmith Distillate Cartridge | Sour TangieGoldsmith ExtractsTHC: 88.67% goldsmith-distillate-cartridge-sour-tangiegoldsmith-extractsthc-88-67-1764475641977-ec3s9b \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-sour-tangie f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3739 1 57 deeply-rooted-az-vaporizers-1764475641905-56 Goldsmith Distillate Cartridge | Strawberry CoughGoldsmith ExtractsTHC: 89.66%CBD: 4.33% goldsmith-distillate-cartridge-strawberry-coughgoldsmith-extractsthc-89-66-cbd-4-33-1764475641978-utycob \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-strawberry-cough-16561 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3740 1 57 deeply-rooted-az-vaporizers-1764475641905-57 Goldsmith Distillate Cartridge | WatermelonGoldsmith ExtractsTHC: 88.99% goldsmith-distillate-cartridge-watermelongoldsmith-extractsthc-88-99-1764475641979-22u9c1 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-watermelon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3741 1 57 deeply-rooted-az-vaporizers-1764475641905-58 Island Papaya Premium Diamonds Vape Cartridge | 1gJeeterHybridTHC: 92.89%CBD: 0.31% island-papaya-premium-diamonds-vape-cartridge-1gjeeterhybridthc-92-89-cbd-0-31-1764475641980-o1jmho \N 22.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/island-papaya-premium-diamonds-vape-cartridge-1g-68354 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3742 1 57 deeply-rooted-az-vaporizers-1764475641905-59 Jeeter Diamond Cartridge | Acapulco GoldJeeterTHC: 89.74%CBD: 0.22% jeeter-diamond-cartridge-acapulco-goldjeeterthc-89-74-cbd-0-22-1764475641981-rxapjr \N 22.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-diamond-cartridge-acapulco-gold f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3743 1 57 deeply-rooted-az-vaporizers-1764475641905-60 Jeeter Diamond Cartridge | Guava BurstJeeterTHC: 91.9%CBD: 0.14% jeeter-diamond-cartridge-guava-burstjeeterthc-91-9-cbd-0-14-1764475641982-1fc44b \N 22.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-diamond-cartridge-guava-burst f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3744 1 57 deeply-rooted-az-vaporizers-1764475641905-61 Kiwi Kush Premium Diamonds Vape Cartridge | 1gJeeterIndicaTHC: 86.3%CBD: 0.22% kiwi-kush-premium-diamonds-vape-cartridge-1gjeeterindicathc-86-3-cbd-0-22-1764475641983-wxgonl \N 22.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/kiwi-kush-premium-diamonds-vape-cartridge-1g-42228 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3745 1 57 deeply-rooted-az-vaporizers-1764475641905-62 MFUSED Fire Liquid Diamonds AIO | Cereal MilkMfusedTHC: 92.49%CBD: 0.16% mfused-fire-liquid-diamonds-aio-cereal-milkmfusedthc-92-49-cbd-0-16-1764475641984-c6z1eh \N 35.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-cereal-milk f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3746 1 57 deeply-rooted-az-vaporizers-1764475641905-63 MFUSED Fire Liquid Diamonds AIO | ChocolopeMfusedTHC: 93.55%CBD: 0.21% mfused-fire-liquid-diamonds-aio-chocolopemfusedthc-93-55-cbd-0-21-1764475641985-y01pvv \N 35.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-chocolope f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3747 1 57 deeply-rooted-az-vaporizers-1764475641905-64 MFUSED Fire Liquid Diamonds AIO | DJ Short BlueberryMfusedTHC: 91.13%CBD: 0.16% mfused-fire-liquid-diamonds-aio-dj-short-blueberrymfusedthc-91-13-cbd-0-16-1764475641986-cwy2tm \N 35.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-dj-short-blueberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3748 1 57 deeply-rooted-az-vaporizers-1764475641905-65 MFUSED Fire Liquid Diamonds AIO | I-95 CookiesMfusedTHC: 84.01%CBD: 0.16% mfused-fire-liquid-diamonds-aio-i-95-cookiesmfusedthc-84-01-cbd-0-16-1764475641987-3c4qjq \N 35.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-i-95-cookies f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3749 1 57 deeply-rooted-az-vaporizers-1764475641905-66 MFUSED Fire Liquid Diamonds AIO | Maui PineappleMfusedTHC: 94.31%CBD: 0.17% mfused-fire-liquid-diamonds-aio-maui-pineapplemfusedthc-94-31-cbd-0-17-1764475641988-mmpm2k \N 35.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-maui-pineapple f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3750 1 57 deeply-rooted-az-vaporizers-1764475641905-67 MFUSED Fire Liquid Diamonds AIO | Tropical Sapphire KushMfusedTHC: 93.51%CBD: 0.18% mfused-fire-liquid-diamonds-aio-tropical-sapphire-kushmfusedthc-93-51-cbd-0-18-1764475641989-wya2ho \N 35.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-tropical-sapphire-kush f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3751 1 57 deeply-rooted-az-vaporizers-1764475641905-68 MFUSED Loud Liquid Diamonds AIO | Acapulco GoldMfusedTHC: 90.84%CBD: 0.15% mfused-loud-liquid-diamonds-aio-acapulco-goldmfusedthc-90-84-cbd-0-15-1764475641990-2q3u9x \N 38.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-acapulco-gold f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3752 1 57 deeply-rooted-az-vaporizers-1764475641905-69 MFUSED Loud Liquid Diamonds AIO | Blue ZushiMfusedTHC: 92.18%CBD: 0.15% mfused-loud-liquid-diamonds-aio-blue-zushimfusedthc-92-18-cbd-0-15-1764475641991-fnigf4 \N 38.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-blue-zushi f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3753 1 57 deeply-rooted-az-vaporizers-1764475641905-70 MFUSED Loud Liquid Diamonds AIO | Pacific PunchMfusedTHC: 94.31%CBD: 2.04% mfused-loud-liquid-diamonds-aio-pacific-punchmfusedthc-94-31-cbd-2-04-1764475641992-87el22 \N 38.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-pacific-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3754 1 57 deeply-rooted-az-vaporizers-1764475641905-71 MFUSED Loud Liquid Diamonds AIO | Planet of the GrapesMfusedTHC: 91.26%CBD: 0.15% mfused-loud-liquid-diamonds-aio-planet-of-the-grapesmfusedthc-91-26-cbd-0-15-1764475641993-bu0vqd \N 38.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-planet-of-the-grapes f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3755 1 57 deeply-rooted-az-vaporizers-1764475641905-72 MFUSED Loud Liquid Diamonds AIO | Sour DieselMfusedTHC: 91.75%CBD: 0.15% mfused-loud-liquid-diamonds-aio-sour-dieselmfusedthc-91-75-cbd-0-15-1764475641994-mpigjk \N 38.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-sour-diesel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3756 1 57 deeply-rooted-az-vaporizers-1764475641905-73 MFUSED Loud Liquid Diamonds AIO | Stoopid GasMfusedTHC: 92.69%CBD: 0.15% mfused-loud-liquid-diamonds-aio-stoopid-gasmfusedthc-92-69-cbd-0-15-1764475641995-rdcrkb \N 38.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-stoopid-gas f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3626 1 3 deeply-rooted-az-specials-1764475522731-6 1:1 Peach Prosecco Pearls - CBD/THC - HybridGrönTHCTHC: 0.28%CBD: 0.31% 1-1-peach-prosecco-pearls-cbd-thc-hybridgr-nthcthc-0-28-cbd-0-31-1764475522746-9iqtjx \N 14.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-peach-prosecco-pearls-cbd-thc-hybrid-87123 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3627 1 3 deeply-rooted-az-specials-1764475522731-7 2:1:1 Tangelo Pearls - THC/CBC/CBG - SativaGrönTHCTHC: 0.28% 2-1-1-tangelo-pearls-thc-cbc-cbg-sativagr-nthcthc-0-28-1764475522748-uk3jdl \N 14.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/2-1-1-tangelo-pearls-thc-cbc-cbg-sativa-40846 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3644 1 3 deeply-rooted-az-specials-1764475522731-24 Accessories | Loose Leaf | 5-Pack Wraps | Watermelon Dream accessories-loose-leaf-5-pack-wraps-watermelon-dream-1764475522770-608sy5 \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-watermelon-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3628 1 3 deeply-rooted-az-specials-1764475522731-8 3-Pack x (2.1g) Bubble Hash Infused Pre-Roll | Cherry Paloma x Permanent MarkerDrip OilsHybridTHC: 43.05% 3-pack-x-2-1g-bubble-hash-infused-pre-roll-cherry-paloma-x-permanent-markerdrip-oilshybridthc-43-05-1764475522749-n81kpv \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/3-pack-x-2-1g-bubble-hash-infused-pre-roll-cherry-paloma-x-permanent-marker-96850 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 2.1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3629 1 3 deeply-rooted-az-specials-1764475522731-9 3:1 Blueberry Lemonade Pearls - CBG/THC - Daytime SativaGrönTHCTAC: 400 mgTHC: 0.27% 3-1-blueberry-lemonade-pearls-cbg-thc-daytime-sativagr-nthctac-400-mgthc-0-27-1764475522750-esfrp2 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/3-1-blueberry-lemonade-pearls-cbg-thc-daytime-sativa f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 400 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3630 1 3 deeply-rooted-az-specials-1764475522731-10 4:1 Pomegranate Pearls - CBD/THC - HybridGrönTHCTAC: 500 mgTHC: 0.28%CBD: 1.15% 4-1-pomegranate-pearls-cbd-thc-hybridgr-nthctac-500-mgthc-0-28-cbd-1-15-1764475522752-kqn92g \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/4-1-pomegranate-pearls-cbd-thc-hybrid f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 500 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3631 1 3 deeply-rooted-az-specials-1764475522731-11 Abundant Organics Flower Mylar | Abundant HorizonAbundant OrganicsTHC: 26.32% abundant-organics-flower-mylar-abundant-horizonabundant-organicsthc-26-32-1764475522753-uvul1q \N 91.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-flower-mylar-abundant-horizon f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3632 1 3 deeply-rooted-az-specials-1764475522731-12 Abundant Organics Flower Mylar | Star QueenAbundant OrganicsIndica-HybridTHC: 29.61% abundant-organics-flower-mylar-star-queenabundant-organicsindica-hybridthc-29-61-1764475522754-oenyqz \N 91.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-flower-mylar-star-queen f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3633 1 3 deeply-rooted-az-specials-1764475522731-13 Abundant Organics Flower Mylar | ViennettaAbundant OrganicsIndicaTHC: 28.47% abundant-organics-flower-mylar-viennettaabundant-organicsindicathc-28-47-1764475522755-pto8vq \N 91.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-flower-mylar-viennetta f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 2 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3634 1 3 deeply-rooted-az-specials-1764475522731-14 Abundant Organics | 4.5g Flower Jar | Space SasquatchAbundant OrganicsHybridTHC: 28.2% abundant-organics-4-5g-flower-jar-space-sasquatchabundant-organicshybridthc-28-2-1764475522756-0ixfhl \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/abundant-organics-4-5g-flower-jar-space-sasquatch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 4.5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3635 1 3 deeply-rooted-az-specials-1764475522731-15 Accessories | 510 Buttonless Stylus Battery with USB Charger accessories-510-buttonless-stylus-battery-with-usb-charger-1764475522757-9p6jlh \N 6.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-510-buttonless-stylus-battery-with-usb-charger f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3636 1 3 deeply-rooted-az-specials-1764475522731-16 Accessories | Blazy Susan | Poker and Roach Tool accessories-blazy-susan-poker-and-roach-tool-1764475522759-z8q12r \N 2.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-blazy-susan-poker-and-roach-tool f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3637 1 3 deeply-rooted-az-specials-1764475522731-17 Accessories | Dime | Battery Mini | White accessories-dime-battery-mini-white-1764475522761-icrs46 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-dime-battery-mini-white f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3638 1 3 deeply-rooted-az-specials-1764475522731-18 Accessories | Dime | Battery | Black accessories-dime-battery-black-1764475522762-zzckdm \N 18.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-dime-battery-black f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3639 1 3 deeply-rooted-az-specials-1764475522731-19 Accessories | Dime | Battery | White accessories-dime-battery-white-1764475522763-t4vzdm \N 18.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-dime-battery-white f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3671 1 60 deeply-rooted-az-topicals-1764475535685-1 Dermafreeze | 600mg Roll-On TopicalDermafreezeTHC: 114.76 mgCBD: 423.9 mg dermafreeze-600mg-roll-on-topicaldermafreezethc-114-76-mgcbd-423-9-mg-1764475535693-hp5pu1 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-600mg-roll-on-topical f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 600mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3672 1 60 deeply-rooted-az-topicals-1764475535685-2 Dermafreeze | 600mg TopicalDermafreezeTHC: 97 mgCBD: 527 mg dermafreeze-600mg-topicaldermafreezethc-97-mgcbd-527-mg-1764475535695-hrlkom \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-600mg-topical f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 600mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3673 1 60 deeply-rooted-az-topicals-1764475535685-3 Dermafreeze | Dermaheat | 600mg Roll-On TopicalDermafreezeTHC: 93.69 mgCBD: 461.42 mg dermafreeze-dermaheat-600mg-roll-on-topicaldermafreezethc-93-69-mgcbd-461-42-mg-1764475535696-7q4666 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-dermaheat-600mg-roll-on-topical f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 600mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3674 1 60 deeply-rooted-az-topicals-1764475535685-4 Dermafreeze | Dermaheat | 600mg TopicalDermafreezeTHC: 103 mgCBD: 499 mg dermafreeze-dermaheat-600mg-topicaldermafreezethc-103-mgcbd-499-mg-1764475535698-verme5 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-dermaheat-600mg-topical f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 600mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3675 1 60 deeply-rooted-az-topicals-1764475535685-5 Drip 1:1 THC/CBD Salve 1ozDrip OilsTHCTHC: 1.82%CBD: 1.72% drip-1-1-thc-cbd-salve-1ozdrip-oilsthcthc-1-82-cbd-1-72-1764475535699-2z097n \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-1-1-thc-cbd-salve-1oz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 1oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3676 1 60 deeply-rooted-az-topicals-1764475535685-6 Halo | Chronic Health | 100mg Pain Relief LotionHalo InfusionsTHC: 0.2% halo-chronic-health-100mg-pain-relief-lotionhalo-infusionsthc-0-2-1764475535700-889311 \N 10.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-100mg-pain-relief-lotion f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3677 1 60 deeply-rooted-az-topicals-1764475535685-7 Halo | Chronic Health | 1400mg THC Pain Relief Ointment 4ozHalo InfusionsTHCTHC: 1481.26 mgCBD: 10.5 mg halo-chronic-health-1400mg-thc-pain-relief-ointment-4ozhalo-infusionsthcthc-1481-26-mgcbd-10-5-mg-1764475535701-xnfjj9 \N 60.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-1400mg-thc-pain-relief-ointment-4oz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 1400mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3678 1 60 deeply-rooted-az-topicals-1764475535685-8 Halo | Chronic Health | 350/350mg 1:1 THC:CBD Pain Relief Ointment 4ozHalo InfusionsTHCTHC: 0.33%CBD: 0.27% halo-chronic-health-350-350mg-1-1-thc-cbd-pain-relief-ointment-4ozhalo-infusionsthcthc-0-33-cbd-0-27-1764475535703-uv5t87 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-350-350mg-1-1-thc-cbd-pain-relief-ointment-4oz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 350mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3679 1 60 deeply-rooted-az-topicals-1764475535685-9 Halo | Chronic Health | 350mg Pain Relief OintmentHalo InfusionsTHC: 0.59% halo-chronic-health-350mg-pain-relief-ointmenthalo-infusionsthc-0-59-1764475535704-3ul2m0 \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-350mg-pain-relief-ointment f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 350mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3680 1 60 deeply-rooted-az-topicals-1764475535685-10 Halo | Chronic Health | 45/45mg 1:1 THC:CBD Pain Relief Ointment 0.5ozHalo InfusionsTHCTHC: 0.31%CBD: 0.31% halo-chronic-health-45-45mg-1-1-thc-cbd-pain-relief-ointment-0-5ozhalo-infusionsthcthc-0-31-cbd-0-31-1764475535705-as3h79 \N 7.13 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-45-45mg-1-1-thc-cbd-pain-relief-ointment-0-5oz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 45mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3681 1 60 deeply-rooted-az-topicals-1764475535685-11 Halo | Chronic Health | 50mg 1:1 Pain Relief Roll-OnHalo InfusionsTHC: 0.17%CBD: 0.16% halo-chronic-health-50mg-1-1-pain-relief-roll-onhalo-infusionsthc-0-17-cbd-0-16-1764475535706-2rva8g \N 14.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-50mg-1-1-pain-relief-roll-on f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 50mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3682 1 60 deeply-rooted-az-topicals-1764475535685-12 Halo | Chronic Health | 90mg Pain Relief Ointment 0.5ozHalo InfusionsTHC: 0.59% halo-chronic-health-90mg-pain-relief-ointment-0-5ozhalo-infusionsthc-0-59-1764475535708-tojemf \N 7.13 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-90mg-pain-relief-ointment-0-5oz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 2025-11-30 04:05:35.685547 \N 90mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:35.685547+00 \N +3305 1 58 deeply-rooted-az-concentrates-1764475146326-90 Green Dot Labs Live Resin Badder | Bicycle DayGreen Dot LabsHybridTHC: 74.15%CBD: 0.13% green-dot-labs-live-resin-badder-bicycle-daygreen-dot-labshybridthc-74-15-cbd-0-13-1764475146437-e7jcu8 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-badder-bicycle-day f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3306 1 58 deeply-rooted-az-concentrates-1764475146326-91 Green Dot Labs Live Resin Badder | Candy CakeGreen Dot LabsIndica-HybridTHC: 71.53% green-dot-labs-live-resin-badder-candy-cakegreen-dot-labsindica-hybridthc-71-53-1764475146438-xksz5u \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-badder-candy-cake f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3311 1 58 deeply-rooted-az-concentrates-1764475146326-96 Green Dot Labs Live Resin Cart | Pink FrootGreen Dot LabsHybridTHC: 70.39% green-dot-labs-live-resin-cart-pink-frootgreen-dot-labshybridthc-70-39-1764475146443-wgajix \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-cart-pink-froot f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3757 1 57 deeply-rooted-az-vaporizers-1764475641905-74 MFUSED Twisted Liquid Diamonds AIO | Baja BlazedMfusedTHC: 93.51%CBD: 0.16% mfused-twisted-liquid-diamonds-aio-baja-blazedmfusedthc-93-51-cbd-0-16-1764475641996-guui27 \N 31.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-baja-blazed f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3758 1 57 deeply-rooted-az-vaporizers-1764475641905-75 MFUSED Twisted Liquid Diamonds AIO | Cosmic CosmosMfusedTHC: 95.3%CBD: 0.13% mfused-twisted-liquid-diamonds-aio-cosmic-cosmosmfusedthc-95-3-cbd-0-13-1764475641997-0aqynl \N 31.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-cosmic-cosmos f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3759 1 57 deeply-rooted-az-vaporizers-1764475641905-76 MFUSED Twisted Liquid Diamonds AIO | FaderadeMfusedTHC: 94.33%CBD: 0.11% mfused-twisted-liquid-diamonds-aio-faderademfusedthc-94-33-cbd-0-11-1764475641998-ezz1bh \N 31.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-faderade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3760 1 57 deeply-rooted-az-vaporizers-1764475641905-77 MFUSED Twisted Liquid Diamonds AIO | Lemon LoopzMfusedTHC: 96.26% mfused-twisted-liquid-diamonds-aio-lemon-loopzmfusedthc-96-26-1764475641999-tt7iaa \N 31.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-lemon-loopz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3761 1 57 deeply-rooted-az-vaporizers-1764475641905-78 MFUSED Twisted Liquid Diamonds AIO | Rainbow CloudMfusedTHC: 93.51%CBD: 0.16% mfused-twisted-liquid-diamonds-aio-rainbow-cloudmfusedthc-93-51-cbd-0-16-1764475642001-0hpufz \N 31.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-rainbow-cloud f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3762 1 57 deeply-rooted-az-vaporizers-1764475641905-79 MFUSED Twisted Liquid Diamonds | Fruit PunchMfusedTHC: 94.1%CBD: 2.49% mfused-twisted-liquid-diamonds-fruit-punchmfusedthc-94-1-cbd-2-49-1764475642002-mmmygt \N 31.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-fruit-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3763 1 57 deeply-rooted-az-vaporizers-1764475641905-80 Mac Pharms Distillate AIO | Apple MintzMac PharmsTHC: 87.32%CBD: 0.79%Special Offer mac-pharms-distillate-aio-apple-mintzmac-pharmsthc-87-32-cbd-0-79-special-offer-1764475642003-2r20j3 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-distillate-aio-apple-mintz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3764 1 57 deeply-rooted-az-vaporizers-1764475641905-81 Papa's Herb Distillate AIO Vapes | GDPPapa's HerbTHC: 86.86%CBD: 1.12%Special Offer papa-s-herb-distillate-aio-vapes-gdppapa-s-herbthc-86-86-cbd-1-12-special-offer-1764475642004-lmgz5b \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-gdp f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3765 1 57 deeply-rooted-az-vaporizers-1764475641905-82 Papa's Herb Distillate AIO Vapes | Lemon Cherry GelatoPapa's HerbTHC: 85.37%CBD: 0.88%Special Offer papa-s-herb-distillate-aio-vapes-lemon-cherry-gelatopapa-s-herbthc-85-37-cbd-0-88-special-offer-1764475642005-r032dr \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-lemon-cherry-gelato f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3766 1 57 deeply-rooted-az-vaporizers-1764475641905-83 Papa's Herb Distillate AIO Vapes | Starwalker OGPapa's HerbTHC: 87.32%CBD: 0.85%Special Offer papa-s-herb-distillate-aio-vapes-starwalker-ogpapa-s-herbthc-87-32-cbd-0-85-special-offer-1764475642006-xtq8b9 \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-starwalker-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3767 1 57 deeply-rooted-az-vaporizers-1764475641905-84 Papa's Herb Distillate AIO Vapes | Watermelon ZPapa's HerbTHC: 86.95%CBD: 1.1%Special Offer papa-s-herb-distillate-aio-vapes-watermelon-zpapa-s-herbthc-86-95-cbd-1-1-special-offer-1764475642007-e4bu3k \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-watermelon-z f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3770 1 57 deeply-rooted-az-vaporizers-1764475641905-87 Sauce Essentials Distillate AIO | White BlueberrySauceTHC: 85.64%CBD: 0.22% sauce-essentials-distillate-aio-white-blueberrysaucethc-85-64-cbd-0-22-1764475642009-iiev1c \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-distillate-aio-white-blueberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3771 1 57 deeply-rooted-az-vaporizers-1764475641905-88 Sauce Essentials Live Resin AIO | GelatoSauceIndica-HybridTHC: 94.44%Special Offer sauce-essentials-live-resin-aio-gelatosauceindica-hybridthc-94-44-special-offer-1764475642011-s17flk \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-live-resin-aio-gelato f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1.25g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3772 1 57 deeply-rooted-az-vaporizers-1764475641905-89 Sauce Essentials Live Resin AIO | Strawberry CoughSauceSativa-HybridTHC: 79.5%Special Offer sauce-essentials-live-resin-aio-strawberry-coughsaucesativa-hybridthc-79-5-special-offer-1764475642012-uf86na \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-live-resin-aio-strawberry-cough f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3640 1 3 deeply-rooted-az-specials-1764475522731-20 Accessories | Loose Leaf | 2-Pack Wraps | Desean Jackson Long Leaf accessories-loose-leaf-2-pack-wraps-desean-jackson-long-leaf-1764475522764-gco0q3 \N 3.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-2-pack-wraps-desean-jackson-long-leaf f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3641 1 3 deeply-rooted-az-specials-1764475522731-21 Accessories | Loose Leaf | 5-Pack Wraps Mini | Russian Cream accessories-loose-leaf-5-pack-wraps-mini-russian-cream-1764475522766-hotm0l \N 5.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-mini-russian-cream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3642 1 3 deeply-rooted-az-specials-1764475522731-22 Accessories | Loose Leaf | 5-Pack Wraps | Banana Dream accessories-loose-leaf-5-pack-wraps-banana-dream-1764475522767-paywja \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-banana-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3643 1 3 deeply-rooted-az-specials-1764475522731-23 Accessories | Loose Leaf | 5-Pack Wraps | Grape Dream accessories-loose-leaf-5-pack-wraps-grape-dream-1764475522768-tdkdhx \N 7.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-loose-leaf-5-pack-wraps-grape-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3645 1 3 deeply-rooted-az-specials-1764475522731-25 Accessories | Octo Box | 510 Thread Discrete Battery accessories-octo-box-510-thread-discrete-battery-1764475522772-xjp5jk \N 14.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-octo-box-510-thread-discrete-battery f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3646 1 3 deeply-rooted-az-specials-1764475522731-26 Accessories | Smoxy Loki Stand and Torch accessories-smoxy-loki-stand-and-torch-1764475522773-h34tpg \N 17.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-smoxy-loki-stand-and-torch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3648 1 3 deeply-rooted-az-specials-1764475522731-28 Accessories | Stiiizy Pro Battery | CheetahSTIIIZY accessories-stiiizy-pro-battery-cheetahstiiizy-1764475522776-zva8fi \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-battery-cheetah f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3649 1 3 deeply-rooted-az-specials-1764475522731-29 Accessories | Stiiizy Pro XL Battery | BlackSTIIIZY accessories-stiiizy-pro-xl-battery-blackstiiizy-1764475522777-4ezqss \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-xl-battery-black f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3650 1 3 deeply-rooted-az-specials-1764475522731-30 Accessories | Stiiizy Pro XL Battery | CheetahSTIIIZY accessories-stiiizy-pro-xl-battery-cheetahstiiizy-1764475522779-pye2w8 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-xl-battery-cheetah f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3651 1 3 deeply-rooted-az-specials-1764475522731-31 Accessory | Bic | Lighters accessory-bic-lighters-1764475522780-fb8nx7 \N 2.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-bic-lighters f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3652 1 3 deeply-rooted-az-specials-1764475522731-32 Accessory | Classic Wild and Free | Flip Top Torch Lighter accessory-classic-wild-and-free-flip-top-torch-lighter-1764475522781-4jodub \N 2.63 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-classic-wild-and-free-flip-top-torch-lighter f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3653 1 3 deeply-rooted-az-specials-1764475522731-33 Accessory | MK Jet Torch | Lighters accessory-mk-jet-torch-lighters-1764475522782-0egdl9 \N 1.88 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-mk-jet-torch-lighters f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3654 1 3 deeply-rooted-az-specials-1764475522731-34 Accessory | MK Outdoor | Torch Lighter accessory-mk-outdoor-torch-lighter-1764475522783-8f3t2t \N 1.88 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-mk-outdoor-torch-lighter f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3655 1 3 deeply-rooted-az-specials-1764475522731-35 Achieve Live Resin AIO | Animal TreeAchieveSativa-HybridTHC: 74.31%CBD: 0.15% achieve-live-resin-aio-animal-treeachievesativa-hybridthc-74-31-cbd-0-15-1764475522784-z9fhwy \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/achieve-live-resin-aio-animal-tree f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3656 1 3 deeply-rooted-az-specials-1764475522731-36 Achieve Live Resin AIO | Berry DreamAchieveHybridTHC: 72.87%CBD: 0.17% achieve-live-resin-aio-berry-dreamachievehybridthc-72-87-cbd-0-17-1764475522786-x80zwi \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/achieve-live-resin-aio-berry-dream f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3657 1 3 deeply-rooted-az-specials-1764475522731-37 Achieve Live Resin AIO | Guava HeavenAchieveSativa-HybridTHC: 71.31%CBD: 0.17% achieve-live-resin-aio-guava-heavenachievesativa-hybridthc-71-31-cbd-0-17-1764475522787-7omhrx \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/achieve-live-resin-aio-guava-heaven f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3658 1 3 deeply-rooted-az-specials-1764475522731-38 Alien Labs Cured Resin Cart | BiskanteAlien LabsHybridTHC: 83.04%CBD: 0.18% alien-labs-cured-resin-cart-biskantealien-labshybridthc-83-04-cbd-0-18-1764475522788-fzc6bl \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-biskante f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3659 1 3 deeply-rooted-az-specials-1764475522731-39 Alien Labs Cured Resin Cart | Dark WebAlien LabsHybridTHC: 76.47%CBD: 0.11% alien-labs-cured-resin-cart-dark-webalien-labshybridthc-76-47-cbd-0-11-1764475522789-gfble4 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-dark-web f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3660 1 3 deeply-rooted-az-specials-1764475522731-40 Alien Labs Cured Resin Cart | GemeniAlien LabsIndica-HybridTHC: 72.18% alien-labs-cured-resin-cart-gemenialien-labsindica-hybridthc-72-18-1764475522790-08vlkw \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-gemeni f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3661 1 3 deeply-rooted-az-specials-1764475522731-41 Alien Labs Cured Resin Cart | Y2KAlien LabsIndica-HybridTHC: 80.38% alien-labs-cured-resin-cart-y2kalien-labsindica-hybridthc-80-38-1764475522791-csqpik \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-y2k f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3662 1 3 deeply-rooted-az-specials-1764475522731-42 Alien Labs Cured Resin Cart | ZpectrumAlien LabsHybridTHC: 77.06%CBD: 0.17% alien-labs-cured-resin-cart-zpectrumalien-labshybridthc-77-06-cbd-0-17-1764475522793-1two7g \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-cured-resin-cart-zpectrum f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3663 1 3 deeply-rooted-az-specials-1764475522731-43 Alien Labs Flower Jar | Atomic AppleAlien LabsHybridTHC: 23.85%Special Offer alien-labs-flower-jar-atomic-applealien-labshybridthc-23-85-special-offer-1764475522794-mqo52f \N 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-atomic-apple-1437 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3664 1 3 deeply-rooted-az-specials-1764475522731-44 Alien Labs Flower Jar | BiskanteAlien LabsHybridTHC: 22.77%Special Offer alien-labs-flower-jar-biskantealien-labshybridthc-22-77-special-offer-1764475522795-zjqlaj \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-biskante f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3666 1 3 deeply-rooted-az-specials-1764475522731-46 Alien Labs Flower Jar | Dark WebAlien LabsHybridTHC: 20.71%Special Offer alien-labs-flower-jar-dark-webalien-labshybridthc-20-71-special-offer-1764475522798-ubnrff \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-dark-web f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3667 1 3 deeply-rooted-az-specials-1764475522731-47 Alien Labs Flower Jar | GUAVA 2.0Alien LabsIndica-HybridTHC: 20.69%Special Offer alien-labs-flower-jar-guava-2-0alien-labsindica-hybridthc-20-69-special-offer-1764475522799-0v3u5l \N 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-guava-2-0 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +2327 \N \N \N Keef Classic Soda Pineapple X-Press XTREME | 100mgKeefHybridTHC: 101 mg keef-classic-soda-pineapple-x-press-xtreme-100mg-8010 \N \N \N \N \N \N Keef \N https://images.dutchie.com/b58f3eef3c540b96e3639a0cb3f04b9e?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-pineapple-x-press-xtreme-100mg-8010 t f \N 2025-11-18 03:55:31.876418 2025-11-18 04:22:52.994954 2025-11-18 03:55:31.876418 2025-11-18 05:18:33.480785 112 100mgKeef \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.994954+00 \N +2343 \N \N \N Legends Flower Jar | Nitro FumezLegendsTHC: 30.13%Special Offer legends-flower-jar-nitro-fumez \N \N \N \N 30.13 \N Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-nitro-fumez t f \N 2025-11-18 03:55:36.54462 2025-11-18 04:23:01.356164 2025-11-18 03:55:36.54462 2025-11-18 05:19:30.231791 112 Nitro FumezLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.356164+00 \N +3668 1 3 deeply-rooted-az-specials-1764475522731-48 Alien Labs Flower Jar | GeminiAlien LabsIndica-HybridTHC: 24.9%Special Offer alien-labs-flower-jar-geminialien-labsindica-hybridthc-24-9-special-offer-1764475522800-mrjfmr \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-gemini f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3669 1 3 deeply-rooted-az-specials-1764475522731-49 Alien Labs Flower Jar | XJ-13Alien LabsIndica-HybridTHC: 26.51%Special Offer alien-labs-flower-jar-xj-13alien-labsindica-hybridthc-26-51-special-offer-1764475522801-s6jsa6 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-xj-13 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 8 oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3665 1 3 deeply-rooted-az-specials-1764475522731-45 Alien Labs Flower Jar | BiskanteAlien LabsHybridTHC: 24.68%Special Offer alien-labs-flower-jar-biskantealien-labshybridthc-24-68-special-offer-1764475522796-qi6bbk 90.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/alien-labs-flower-jar-biskante-44323 t f {} 2025-11-30 04:05:22.731246 2025-12-01 07:35:18.608035 2025-11-30 04:05:22.731246 2025-12-01 07:35:18.608035 \N 10g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3312 1 58 deeply-rooted-az-concentrates-1764475146326-97 Green Dot Labs Live Resin Cart | Rainbow Belts V2Green Dot LabsIndica-HybridTHC: 74.84%CBD: 0.24% green-dot-labs-live-resin-cart-rainbow-belts-v2green-dot-labsindica-hybridthc-74-84-cbd-0-24-1764475146444-7cw3z6 \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-cart-rainbow-belts-v2 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 2G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3773 1 57 deeply-rooted-az-vaporizers-1764475641905-90 Select Elite Terpologist BRIQ | Mountain DieselSelectIndicaTHC: 89.83%CBD: 0.3% select-elite-terpologist-briq-mountain-dieselselectindicathc-89-83-cbd-0-3-1764475642013-txcpr9 \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-elite-terpologist-briq-mountain-diesel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3774 1 57 deeply-rooted-az-vaporizers-1764475641905-91 Select Elite Terpologist BRIQ | Pina GlueladaSelectIndicaTHC: 88.77%CBD: 1.48% select-elite-terpologist-briq-pina-glueladaselectindicathc-88-77-cbd-1-48-1764475642014-wvu5tt \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-elite-terpologist-briq-pina-gluelada f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3775 1 57 deeply-rooted-az-vaporizers-1764475641905-92 Select Elite Terpologist BRIQ | Tropic HazeSelectSativaTHC: 88.98%CBD: 0.16% select-elite-terpologist-briq-tropic-hazeselectsativathc-88-98-cbd-0-16-1764475642015-uyb16t \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-elite-terpologist-briq-tropic-haze f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3776 1 57 deeply-rooted-az-vaporizers-1764475641905-93 Select Elite Terpologist Cart | Mountain DieselSelectIndicaTHC: 82.57%CBD: 1.25% select-elite-terpologist-cart-mountain-dieselselectindicathc-82-57-cbd-1-25-1764475642016-sq2y53 \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-elite-terpologist-cart-mountain-diesel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3777 1 57 deeply-rooted-az-vaporizers-1764475641905-94 Select Essential Cart | Sour TangieSelectSativa-HybridTHC: 85.36%CBD: 1.07% select-essential-cart-sour-tangieselectsativa-hybridthc-85-36-cbd-1-07-1764475642017-6penir \N 26.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-essential-cart-sour-tangie-99220 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3778 1 57 deeply-rooted-az-vaporizers-1764475641905-95 Select Fruit Stiq AIO | Go Go GuavaSelectTHC: 88.07%CBD: 0.21% select-fruit-stiq-aio-go-go-guavaselectthc-88-07-cbd-0-21-1764475642018-em3qq2 \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-fruit-stiq-aio-go-go-guava-67760 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3779 1 57 deeply-rooted-az-vaporizers-1764475641905-96 Select Fruit Stiq AIO | Pink LemonadeSelectSativaTHC: 88.12%CBD: 0.23% select-fruit-stiq-aio-pink-lemonadeselectsativathc-88-12-cbd-0-23-1764475642019-dxs0p3 \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-fruit-stiq-aio-pink-lemonade f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3780 1 57 deeply-rooted-az-vaporizers-1764475641905-97 Select Fruit Stiq AIO | Sweet StrawberrySelectIndicaTHC: 86.9%CBD: 0.15% select-fruit-stiq-aio-sweet-strawberryselectindicathc-86-9-cbd-0-15-1764475642020-00u9ms \N 20.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-fruit-stiq-aio-sweet-strawberry f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3781 1 57 deeply-rooted-az-vaporizers-1764475641905-98 Sticky Saguaro Sticky Disty Cartridge | Pure OGSticky SaguaroTHC: 91.05%Special Offer sticky-saguaro-sticky-disty-cartridge-pure-ogsticky-saguarothc-91-05-special-offer-1764475642021-9i8hhb \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-sticky-disty-cartridge-pure-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3782 1 57 deeply-rooted-az-vaporizers-1764475641905-99 Sticky Saguaro StickyDisty Cartridge | Purple UrkleSticky SaguaroTHC: 89.25%Special Offer sticky-saguaro-stickydisty-cartridge-purple-urklesticky-saguarothc-89-25-special-offer-1764475642022-lbtcaw \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-stickydisty-cartridge-purple-urkle f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +3313 1 58 deeply-rooted-az-concentrates-1764475146326-98 Green Dot Labs Live Resin Cart | Red RoseGreen Dot LabsHybridTHC: 74.89% green-dot-labs-live-resin-cart-red-rosegreen-dot-labshybridthc-74-89-1764475146445-h2pp2u \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-cart-red-rose f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3319 1 58 deeply-rooted-az-concentrates-1764475146326-104 Green Dot Labs Live Rosin Badder | Bourbon StreetGreen Dot LabsHybridTHC: 77.7% green-dot-labs-live-rosin-badder-bourbon-streetgreen-dot-labshybridthc-77-7-1764475146451-sfb1lk \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-rosin-badder-bourbon-street f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +2954 \N \N \N HOLY GRAIL OG 420-to-yuma-holy-grail-og-28g \N \N \N INDICA 24.76 \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/flower/item/a607a234-6f36-4843-ab4b-7d976e9929a6?customerType=ADULT t f \N 2025-11-18 14:42:08.876769 2025-11-18 14:42:08.876769 2025-11-18 14:42:08.876769 2025-11-18 14:42:08.876769 149 28G \N \N \N \N \N \N 100.00 100.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.876769+00 \N +3796 1 61 deeply-rooted-az-accessories-1764577445876-0.24942421929913539 Accessory | MK Jet Torch | LightersSpecial Offer accessory-mk-jet-torch-lightersspecial-offer 2.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-mk-jet-torch-lighters t f {} 2025-12-01 08:24:05.884296 2025-12-01 08:24:05.884296 2025-12-01 08:24:05.884296 2025-12-01 08:24:05.884296 \N \N \N \N \N \N \N \N \N \N \N \N \N \N unknown \N \N \N +3211 1 61 deeply-rooted-az-accessories-1764475012058-16 Accessory | Bic | Lighters accessory-bic-lighters-1764475012091-ftwdsk \N 2.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-bic-lighters f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3212 1 61 deeply-rooted-az-accessories-1764475012058-17 Accessory | Classic Wild and Free | Flip Top Torch Lighter accessory-classic-wild-and-free-flip-top-torch-lighter-1764475012092-wezwd3 \N 2.63 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-classic-wild-and-free-flip-top-torch-lighter f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3213 1 61 deeply-rooted-az-accessories-1764475012058-18 Accessory | MK Jet Torch | Lighters accessory-mk-jet-torch-lighters-1764475012093-sr20ua \N 1.88 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-mk-jet-torch-lighters f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +3214 1 61 deeply-rooted-az-accessories-1764475012058-19 Accessory | MK Outdoor | Torch Lighter accessory-mk-outdoor-torch-lighter-1764475012094-bgic2m \N 1.88 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessory-mk-outdoor-torch-lighter f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 2025-11-30 03:56:52.058518 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:56:52.058518+00 \N +2955 \N \N \N JET FUEL GELATO 420-to-yuma-jet-fuel-gelato-14g \N \N \N S/I \N \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/flower/item/2383489a-019c-4f99-8796-9eb8e031b314?customerType=ADULT t f \N 2025-11-18 14:42:08.87917 2025-11-18 14:42:08.87917 2025-11-18 14:42:08.87917 2025-11-18 14:42:08.87917 149 14G \N \N \N \N \N \N 60.00 60.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.87917+00 \N +2956 \N \N \N JET FUEL GELATO 420-to-yuma-jet-fuel-gelato-28g \N \N \N S/I \N \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/flower/item/c3287e92-6aa0-4bfd-ba7f-57f3c05ab1c6?customerType=ADULT t f \N 2025-11-18 14:42:08.881538 2025-11-18 14:42:08.881538 2025-11-18 14:42:08.881538 2025-11-18 14:42:08.881538 149 28G \N \N \N \N \N \N 100.00 100.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.881538+00 \N +2957 \N \N \N FACE OFF OG best-face-off-og-1g \N \N \N \N 27.36 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/flower/item/05aa65e7-82ed-4200-8432-2b978cc4fa50?customerType=ADULT t f \N 2025-11-18 14:42:08.883697 2025-11-18 14:42:08.883697 2025-11-18 14:42:08.883697 2025-11-18 14:42:08.883697 149 1G \N \N \N \N \N \N 5.00 5.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.883697+00 \N +3314 1 58 deeply-rooted-az-concentrates-1764475146326-99 Green Dot Labs Live Rosin AIO | Bourbon StreetGreen Dot LabsHybridTHC: 81.43% green-dot-labs-live-rosin-aio-bourbon-streetgreen-dot-labshybridthc-81-43-1764475146446-gnntdz \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-rosin-aio-bourbon-street f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3315 1 58 deeply-rooted-az-concentrates-1764475146326-100 Green Dot Labs Live Rosin AIO | Candy CakeGreen Dot LabsIndica-HybridTHC: 76.17% green-dot-labs-live-rosin-aio-candy-cakegreen-dot-labsindica-hybridthc-76-17-1764475146447-4dbn6c \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-rosin-aio-candy-cake f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3316 1 58 deeply-rooted-az-concentrates-1764475146326-101 Green Dot Labs Live Rosin AIO | FAFOGreen Dot LabsHybridTHC: 81.43% green-dot-labs-live-rosin-aio-fafogreen-dot-labshybridthc-81-43-1764475146448-1bm3h1 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-rosin-aio-fafo f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3317 1 58 deeply-rooted-az-concentrates-1764475146326-102 Green Dot Labs Live Rosin AIO | Final BossGreen Dot LabsHybridTHC: 75.48% green-dot-labs-live-rosin-aio-final-bossgreen-dot-labshybridthc-75-48-1764475146449-0ttj7d \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-rosin-aio-final-boss f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3318 1 58 deeply-rooted-az-concentrates-1764475146326-103 Green Dot Labs Live Rosin AIO | OtoroGreen Dot LabsHybridTHC: 78.69%CBD: 0.14% green-dot-labs-live-rosin-aio-otorogreen-dot-labshybridthc-78-69-cbd-0-14-1764475146450-jbphr3 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-rosin-aio-otoro f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3793 1 61 deeply-rooted-az-accessories-1764574648077-0.0763312250911603 Accessories | 510 Buttonless Stylus Battery with USB ChargerSpecial Offer accessories-510-buttonless-stylus-battery-with-usb-chargerspecial-offer This item is included in a special today! Add it to your cart to work towards completing the offer. 8.00 \N Indica 81.59 0.15 TRU Infusion… \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format&fit=max&q=95&w=2000&h=2000 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-510-buttonless-stylus-battery-with-usb-charger t f {} 2025-12-01 07:37:28.084761 2025-12-01 08:12:27.787152 2025-12-01 07:37:28.084761 2025-12-01 08:12:27.787152 \N \N \N \N \N \N \N \N \N \N \N \N \N \N unknown \N \N \N +3794 1 58 deeply-rooted-az-concentrates-1764574650701-0.9867615957745457 SponsoredTru Infusion | Live Resin Batter | GMOZKTRU InfusionHybridTHC: 78.26%CBD: 0.14%Special Offer sponsoredtru-infusion-live-resin-batter-gmozktru-infusionhybridthc-78-26-cbd-0-14-special-offer 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-batter-gmozk t f {} 2025-12-01 07:37:30.704243 2025-12-01 08:12:27.612001 2025-12-01 07:37:30.704243 2025-12-01 08:12:27.612001 \N \N \N \N \N \N \N \N \N \N \N \N \N \N unknown \N \N \N +2960 \N \N \N CHAMPION CITY CHOCOLATE LR best-champion-city-chocolate-lr-1g \N \N \N I/S 81.81 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/6dda4319-867d-49df-956c-866c6caf898d?customerType=ADULT t f \N 2025-11-18 14:42:08.890339 2025-11-18 14:42:08.890339 2025-11-18 14:42:08.890339 2025-11-18 14:42:08.890339 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.890339+00 \N +3308 1 58 deeply-rooted-az-concentrates-1764475146326-93 Green Dot Labs Live Resin Badder | Final BossGreen Dot LabsHybridTHC: 74.15% green-dot-labs-live-resin-badder-final-bossgreen-dot-labshybridthc-74-15-1764475146440-gc952l \N 30.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-badder-final-boss f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3309 1 58 deeply-rooted-az-concentrates-1764475146326-94 Green Dot Labs Live Resin Cart | Cherry Lime SodaGreen Dot LabsHybridTHC: 67.03% green-dot-labs-live-resin-cart-cherry-lime-sodagreen-dot-labshybridthc-67-03-1764475146441-5mlnwd \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-cart-cherry-lime-soda f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3310 1 58 deeply-rooted-az-concentrates-1764475146326-95 Green Dot Labs Live Resin Cart | I-95Green Dot LabsIndica-HybridTHC: 71.23%CBD: 0.13% green-dot-labs-live-resin-cart-i-95green-dot-labsindica-hybridthc-71-23-cbd-0-13-1764475146442-6saps4 \N 33.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-resin-cart-i-95 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 95G \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +2961 \N \N \N BLACKBERRY OG dime-industries-blackberry-og-1g \N \N \N I/S 95.15 \N DIME INDUSTRIES \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/2fd43a22-8847-414f-b8b0-1b8f3a498cff?customerType=ADULT t f \N 2025-11-18 14:42:08.892792 2025-11-18 14:42:08.892792 2025-11-18 14:42:08.892792 2025-11-18 14:42:08.892792 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.892792+00 \N +2968 \N \N \N FRUIT COCKTAIL io-extracts-fruit-cocktail-1g \N \N \N HYBRID 93.50 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/9ae594aa-7613-4ec1-a9ca-287f421bd1aa?customerType=ADULT t f \N 2025-11-18 14:42:08.912029 2025-11-18 14:42:08.912029 2025-11-18 14:42:08.912029 2025-11-18 14:42:08.912029 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.912029+00 \N +2946 \N \N \N CALI SUNSET ROSE elevate-cali-sunset-rose-3-5g \N \N \N I/S \N \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/flower/item/929467dc-9cfc-4dcd-9c3e-37cb6c81f330?customerType=ADULT t f \N 2025-11-18 14:42:08.856739 2025-11-18 14:42:08.856739 2025-11-18 14:42:08.856739 2025-11-18 14:42:08.856739 149 3.5G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.856739+00 \N +3616 1 56 deeply-rooted-az-pre-rolls-1764475447223-96 Stiiizy Infused Pre-Roll 40's | PIneapple ExpressSTIIIZYTHC: 42.16% stiiizy-infused-pre-roll-40-s-pineapple-expressstiiizythc-42-16-1764475447351-yl1ipr \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-pineapple-express f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3617 1 56 deeply-rooted-az-pre-rolls-1764475447223-97 Stiiizy Infused Pre-Roll 40's | Pink AcaiSTIIIZYTHC: 49.99% stiiizy-infused-pre-roll-40-s-pink-acaistiiizythc-49-99-1764475447352-hw4wz2 \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-pink-acai f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3618 1 56 deeply-rooted-az-pre-rolls-1764475447223-98 Stiiizy Infused Pre-Roll 40's | Purple PunchSTIIIZYTHC: 38.79% stiiizy-infused-pre-roll-40-s-purple-punchstiiizythc-38-79-1764475447353-v9ehp5 \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-purple-punch f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +3619 1 56 deeply-rooted-az-pre-rolls-1764475447223-99 Stiiizy Infused Pre-Roll 40's | Sour DieselSTIIIZYTHC: 43.69% stiiizy-infused-pre-roll-40-s-sour-dieselstiiizythc-43-69-1764475447354-dkbujz \N 13.20 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-infused-pre-roll-40-s-sour-diesel f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +2947 \N \N \N GRAPEFRUIT DURBAN elevate-grapefruit-durban-3-5g \N \N \N SATIVA \N \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/flower/item/bf0a341c-8d9d-40eb-9691-dc0f8755f05c?customerType=ADULT t f \N 2025-11-18 14:42:08.859537 2025-11-18 14:42:08.859537 2025-11-18 14:42:08.859537 2025-11-18 14:42:08.859537 149 3.5G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.859537+00 \N +2948 \N \N \N PINEAPPLE PLANET elevate-pineapple-planet-3-5g \N \N \N SATIVA \N \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/flower/item/64e0cd2d-108e-49f3-8893-8ea1d82da261?customerType=ADULT t f \N 2025-11-18 14:42:08.862144 2025-11-18 14:42:08.862144 2025-11-18 14:42:08.862144 2025-11-18 14:42:08.862144 149 3.5G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.862144+00 \N +2949 \N \N \N SUPERBOOF elevate-superboof-3-5g \N \N \N HYBRID 25.27 \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/flower/item/2da78298-471a-4b65-bdab-3ee2575fb427?customerType=ADULT t f \N 2025-11-18 14:42:08.864932 2025-11-18 14:42:08.864932 2025-11-18 14:42:08.864932 2025-11-18 14:42:08.864932 149 3.5G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.864932+00 \N +2950 \N \N \N THE CHOP elevate-the-chop-3-5g \N \N \N HYBRID 30.03 \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/flower/item/420e7dd7-64e3-488a-aa1a-e4d8a6f7ec71?customerType=ADULT t f \N 2025-11-18 14:42:08.867298 2025-11-18 14:42:08.867298 2025-11-18 14:42:08.867298 2025-11-18 14:42:08.867298 149 3.5G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.867298+00 \N +2951 \N \N \N WRECKING BALL elevate-wrecking-ball-3-5g \N \N \N I/S \N \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/flower/item/35202744-cc26-4ca1-be6e-f823f4a99b7f?customerType=ADULT t f \N 2025-11-18 14:42:08.86963 2025-11-18 14:42:08.86963 2025-11-18 14:42:08.86963 2025-11-18 14:42:08.86963 149 3.5G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.86963+00 \N +2952 \N \N \N ITALIAN ICE 420-to-yuma-italian-ice-3-5g \N \N \N HYBRID 29.32 \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/flower/item/f0250f92-5eec-4f7b-af3d-f316fde4e3eb?customerType=ADULT t f \N 2025-11-18 14:42:08.871955 2025-11-18 14:42:08.871955 2025-11-18 14:42:08.871955 2025-11-18 14:42:08.871955 149 3.5G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.871955+00 \N +2257 \N \N \N Grow Sciences | 100mg Rosin Fruit Chew | Green AppleGrow SciencesTHC: 0.23 mgSpecial Offer grow-sciences-100mg-rosin-fruit-chew-green-apple \N \N \N \N \N \N Grow Sciences \N https://images.dutchie.com/e56c6b93e82b50c08fe3b3c4611e9e7b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-100mg-rosin-fruit-chew-green-apple t f \N 2025-11-18 03:54:01.8562 2025-11-18 04:21:14.577304 2025-11-18 03:54:01.8562 2025-11-18 05:14:25.537657 112 Green AppleGrow Sciences \N \N \N {} {} {} 16.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.577304+00 \N +2933 \N \N \N XJ -13 alien-labs-xj-13-3-5g \N \N \N SATIVA 26.50 \N ALIEN LABS \N \N \N https://best.treez.io/onlinemenu/category/flower/item/aa1f45a3-6272-47f1-a6d4-60756719a4d8?customerType=ADULT t f \N 2025-11-18 14:42:08.813988 2025-11-18 14:42:08.813988 2025-11-18 14:42:08.813988 2025-11-18 14:42:08.813988 149 3.5G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.813988+00 \N +2205 \N \N \N Easy Tiger Live Rosin AIO | GMOEasy TigerIndicaTHC: 80.3%CBD: 0.12% easy-tiger-live-rosin-aio-gmo \N \N \N \N 80.30 0.12 Easy Tiger \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/easy-tiger-live-rosin-aio-gmo t f \N 2025-11-18 03:53:20.431376 2025-11-18 04:20:27.90352 2025-11-18 03:53:20.431376 2025-11-18 05:11:14.428489 112 GMOEasy Tiger \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:27.90352+00 \N +2206 \N \N \N Easy Tiger Live Rosin AIO | Pete's PeachEasy TigerSativa-HybridTHC: 79.18%CBD: 0.18% easy-tiger-live-rosin-aio-pete-s-peach \N \N \N \N 79.18 0.18 Easy Tiger \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/easy-tiger-live-rosin-aio-pete-s-peach t f \N 2025-11-18 03:53:20.434013 2025-11-18 04:20:27.906525 2025-11-18 03:53:20.434013 2025-11-18 05:11:17.511291 112 Pete's PeachEasy Tiger \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:27.906525+00 \N +2254 \N \N \N Grow Sciences Live Resin Cartridge | Scented Marker #1Grow SciencesTHC: 80.42% grow-sciences-live-resin-cartridge-scented-marker-1 \N \N \N \N 80.42 \N Grow Sciences \N https://images.dutchie.com/9fccfa91147d41b6618ce14641429168?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-live-resin-cartridge-scented-marker-1 t f \N 2025-11-18 03:54:01.848592 2025-11-18 04:21:14.571106 2025-11-18 03:54:01.848592 2025-11-18 05:14:11.768191 112 Scented Marker #1Grow Sciences \N \N \N {} {} {} 33.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.571106+00 \N +2256 \N \N \N Grow Sciences Live Resin Cartridge | ZKZ CubedGrow SciencesTHC: 70.34% grow-sciences-live-resin-cartridge-zkz-cubed \N \N \N \N 70.34 \N Grow Sciences \N https://images.dutchie.com/9fccfa91147d41b6618ce14641429168?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-live-resin-cartridge-zkz-cubed t f \N 2025-11-18 03:54:01.854051 2025-11-18 04:21:14.575547 2025-11-18 03:54:01.854051 2025-11-18 05:14:22.569459 112 ZKZ CubedGrow Sciences \N \N \N {} {} {} 33.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.575547+00 \N +2259 \N \N \N Grow Sciences | 3.7g Flower Jar | Lemon SherbetGrow SciencesHybridTHC: 24.71% grow-sciences-3-7g-flower-jar-lemon-sherbet \N \N \N \N 24.71 \N Grow Sciences \N https://images.dutchie.com/00d05364e10d01765ebb95bc214b5254?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-lemon-sherbet t f \N 2025-11-18 03:54:01.861295 2025-11-18 04:21:14.580996 2025-11-18 03:54:01.861295 2025-11-18 05:14:31.743239 112 Lemon SherbetGrow Sciences \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.580996+00 \N +2953 \N \N \N DEVIL DRIVER 420-to-yuma-devil-driver-3-5g \N \N \N SATIVA 26.62 \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/flower/item/3456939f-79c1-4598-b611-0da3592d76eb?customerType=ADULT t f \N 2025-11-18 14:42:08.874207 2025-11-18 14:42:08.874207 2025-11-18 14:42:08.874207 2025-11-18 14:42:08.874207 149 3.5G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.874207+00 \N +3086 \N \N \N WEDDING PIE PR (2 X . best-wedding-pie-pr-2-x-5g \N \N \N I/S \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/646912a5-e84f-4cf6-8c12-57c5b8cea6ea?customerType=ADULT t f \N 2025-11-18 14:42:09.139771 2025-11-18 14:42:09.139771 2025-11-18 14:42:09.139771 2025-11-18 14:42:09.139771 149 5G \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.139771+00 \N +3099 \N \N \N NAPOLEON DYNAMITE PR napoleons-napoleon-dynamite-pr-3g \N \N \N S/I 39.09 \N NAPOLEONS \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/07734184-0da6-48a4-a3e4-4aea01093a0f?customerType=ADULT t f \N 2025-11-18 14:42:09.16228 2025-11-18 14:42:09.16228 2025-11-18 14:42:09.16228 2025-11-18 14:42:09.16228 149 3G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.16228+00 \N +3100 \N \N \N APPLE FRITTER twisties-apple-fritter-1-25g \N \N \N HYBRID 41.62 \N TWISTIES \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/aeab49df-aee3-477d-92c6-2b5839e361ec?customerType=ADULT t f \N 2025-11-18 14:42:09.163959 2025-11-18 14:42:09.163959 2025-11-18 14:42:09.163959 2025-11-18 14:42:09.163959 149 1.25G \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.163959+00 \N +3131 \N \N \N CROWN BD LIGHTERS W/BOTTLE OPENER best-crown-bd-lighters-w-bottle-opener \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/d9bbaa4b-95e7-4e53-a2ac-4444a500bc11?customerType=ADULT t f \N 2025-11-18 14:42:09.220554 2025-11-18 14:42:09.220554 2025-11-18 14:42:09.220554 2025-11-18 14:42:09.220554 149 \N \N \N \N \N \N 5.00 5.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.220554+00 \N +3132 \N \N \N GOLD CLIPPER LIGHTER clipper-gold-clipper-lighter \N \N \N \N \N \N CLIPPER \N \N \N https://best.treez.io/onlinemenu/category/merch/item/071fd954-b64e-433d-9f92-3d8c20da520d?customerType=ADULT t f \N 2025-11-18 14:42:09.222343 2025-11-18 14:42:09.222343 2025-11-18 14:42:09.222343 2025-11-18 14:42:09.222343 149 \N \N \N \N \N \N 14.00 14.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.222343+00 \N +3133 \N \N \N SINGLE FLAME TORCH single-flame-torch \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/657d9cb5-b53c-44ab-93c1-ee7aa20eb6ea?customerType=ADULT t f \N 2025-11-18 14:42:09.223892 2025-11-18 14:42:09.223892 2025-11-18 14:42:09.223892 2025-11-18 14:42:09.223892 149 \N \N \N \N \N \N 46.00 46.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.223892+00 \N +2976 \N \N \N LOUD JEFE XL- NORTHERN LIGHTS mfused-loud-jefe-xl-northern-lights-2g \N \N \N INDICA 92.35 \N MFUSED \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/5ab97cbe-9b67-4cc5-9fe2-22428c2ad56d?customerType=ADULT t f \N 2025-11-18 14:42:08.933597 2025-11-18 14:42:08.933597 2025-11-18 14:42:08.933597 2025-11-18 14:42:08.933597 149 2G \N \N \N \N \N \N 52.00 52.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.933597+00 \N +2944 \N \N \N EVERCRISP source-one-meds-evercrisp-3-5g \N \N \N HYBRID 25.07 \N SOURCE ONE MEDS \N \N \N https://best.treez.io/onlinemenu/category/flower/item/5bdd9efa-d405-4f0f-bd60-4da33dbbdfec?customerType=ADULT t f \N 2025-11-18 14:42:08.850891 2025-11-18 14:42:08.850891 2025-11-18 14:42:08.850891 2025-11-18 14:42:08.850891 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.850891+00 \N +2945 \N \N \N CAKE CRASHER elevate-cake-crasher-3-5g \N \N \N HYBRID 23.86 \N ELEVATE \N \N \N https://best.treez.io/onlinemenu/category/flower/item/1e767c91-d2f1-49f9-aaf1-dae2209c8bab?customerType=ADULT t f \N 2025-11-18 14:42:08.853949 2025-11-18 14:42:08.853949 2025-11-18 14:42:08.853949 2025-11-18 14:42:08.853949 149 3.5G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.853949+00 \N +2991 \N \N \N SUPER LEMON HAZE cure-injoy-super-lemon-haze-1g \N \N \N S/I 88.11 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/0f4f15b5-adfa-4eec-a5a6-2c0bf450528d?customerType=ADULT t f \N 2025-11-18 14:42:08.968185 2025-11-18 14:42:08.968185 2025-11-18 14:42:08.968185 2025-11-18 14:42:08.968185 149 1G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.968185+00 \N +2992 \N \N \N BLACKBERRY dime-blackberry-2g \N \N \N INDICA 93.73 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/a3a1d5ee-3eee-4da7-8aef-80967f92f6e1?customerType=ADULT t f \N 2025-11-18 14:42:08.969903 2025-11-18 14:42:08.969903 2025-11-18 14:42:08.969903 2025-11-18 14:42:08.969903 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.969903+00 \N +3134 \N \N \N LIGHTER lighter \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/5814ae99-8eb7-43fa-85bb-90465e7df81a?customerType=ADULT t f \N 2025-11-18 14:42:09.225823 2025-11-18 14:42:09.225823 2025-11-18 14:42:09.225823 2025-11-18 14:42:09.225823 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.225823+00 \N +3135 \N \N \N "THE ELITE" WHITE MINI BEAKER SET aleaf-the-elite-white-mini-beaker-set \N \N \N \N \N \N ALEAF \N \N \N https://best.treez.io/onlinemenu/category/merch/item/90d33ec0-2557-48d0-b89a-f296404f1e05?customerType=ADULT t f \N 2025-11-18 14:42:09.227546 2025-11-18 14:42:09.227546 2025-11-18 14:42:09.227546 2025-11-18 14:42:09.227546 149 \N \N \N \N \N \N 36.30 36.30 \N \N \N \N in_stock \N 2025-11-18 14:42:09.227546+00 \N +3136 \N \N \N 6" STAR WING ORANGE BUBBLER g2-6-star-wing-orange-bubbler \N \N \N \N \N \N G2 \N \N \N https://best.treez.io/onlinemenu/category/merch/item/39bbb5ba-4478-4e39-9f43-3a6a2f6f16cd?customerType=ADULT t f \N 2025-11-18 14:42:09.229048 2025-11-18 14:42:09.229048 2025-11-18 14:42:09.229048 2025-11-18 14:42:09.229048 149 \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.229048+00 \N +3137 \N \N \N PULSE DUO 510 TANK VAPE BATTERY hb-king-pulse-duo-510-tank-vape-battery \N \N \N \N \N \N HB KING \N \N \N https://best.treez.io/onlinemenu/category/merch/item/1d4f66ac-bebb-4a4a-8856-c53575e04a42?customerType=ADULT t f \N 2025-11-18 14:42:09.230658 2025-11-18 14:42:09.230658 2025-11-18 14:42:09.230658 2025-11-18 14:42:09.230658 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.230658+00 \N +3138 \N \N \N DUTCH CREAM ORGANIC CBD WRAP 25PK high-hemp-dutch-cream-organic-cbd-wrap-25pk \N \N \N \N \N \N HIGH HEMP \N \N \N https://best.treez.io/onlinemenu/category/merch/item/cc080ea8-fa09-4873-8a3b-d58d13efd33b?customerType=ADULT t f \N 2025-11-18 14:42:09.232241 2025-11-18 14:42:09.232241 2025-11-18 14:42:09.232241 2025-11-18 14:42:09.232241 149 \N \N \N \N \N \N 1.00 1.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.232241+00 \N +3139 \N \N \N PINEAPPLE PARADISE ORGANIC CBD WRAP 2PK high-hemp-pineapple-paradise-organic-cbd-wrap-2pk \N \N \N \N \N \N HIGH HEMP \N \N \N https://best.treez.io/onlinemenu/category/merch/item/59769845-0e82-484a-994f-1415f9a538d8?customerType=ADULT t f \N 2025-11-18 14:42:09.233798 2025-11-18 14:42:09.233798 2025-11-18 14:42:09.233798 2025-11-18 14:42:09.233798 149 \N \N \N \N \N \N 1.00 1.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.233798+00 \N +3140 \N \N \N PINK CLOUDS ORGANIC CBD WRAPS 2PK high-hemp-pink-clouds-organic-cbd-wraps-2pk \N \N \N \N \N \N HIGH HEMP \N \N \N https://best.treez.io/onlinemenu/category/merch/item/de3952fd-9343-4343-8a04-1414d09d2a93?customerType=ADULT t f \N 2025-11-18 14:42:09.235418 2025-11-18 14:42:09.235418 2025-11-18 14:42:09.235418 2025-11-18 14:42:09.235418 149 \N \N \N \N \N \N 1.00 1.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.235418+00 \N +2313 \N \N \N Jeeter | 1.3g Solventless Live Rosin Infused Baby Cannon | Wrecking BallJeeterTHC: 39.29% jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-wrecking-ball \N \N \N \N 39.29 \N Jeeter \N https://images.dutchie.com/a94b822ccf42815deb7e3f2da7a23ada?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-wrecking-ball t f \N 2025-11-18 03:55:13.06921 2025-11-18 04:22:50.69099 2025-11-18 03:55:13.06921 2025-11-18 05:17:34.876109 112 Wrecking BallJeeter \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.69099+00 \N +2089 \N \N \N Connected Cured Resin Cart | AmbroziaConnected CannabisHybridTHC: 76.12%CBD: 0.12% connected-cured-resin-cart-ambrozia \N \N \N \N 76.12 0.12 Connected Cannabis \N https://images.dutchie.com/4a63523ab2cf2f2c1646669a930d7b37?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-ambrozia t f \N 2025-11-18 03:52:02.304636 2025-11-18 04:18:48.497953 2025-11-18 03:52:02.304636 2025-11-18 05:04:22.31799 112 AmbroziaConnected Cannabis \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.497953+00 \N +2090 \N \N \N Connected Cured Resin Cart | Cherry FadeConnected CannabisHybridTHC: 79.59%CBD: 0.09% connected-cured-resin-cart-cherry-fade \N \N \N \N 79.59 0.09 Connected Cannabis \N https://images.dutchie.com/9a5a2ae45ddaad6b8a6bdf2b34975d3a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-cherry-fade t f \N 2025-11-18 03:52:02.313314 2025-11-18 04:18:48.506691 2025-11-18 03:52:02.313314 2025-11-18 05:04:25.419294 112 Cherry FadeConnected Cannabis \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.506691+00 \N +2091 \N \N \N Connected Cured Resin Cart | Ghost OGConnected CannabisIndica-HybridTHC: 80.84%CBD: 0.15% connected-cured-resin-cart-ghost-og \N \N \N \N 80.84 0.15 Connected Cannabis \N https://images.dutchie.com/9a5a2ae45ddaad6b8a6bdf2b34975d3a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-ghost-og t f \N 2025-11-18 03:52:02.315755 2025-11-18 04:18:48.509459 2025-11-18 03:52:02.315755 2025-11-18 05:05:08.51097 112 Ghost OGConnected Cannabis \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.509459+00 \N +2092 \N \N \N Connected Cured Resin Cart | Guava 2.0Connected CannabisIndica-HybridTHC: 75.94%CBD: 0.09% connected-cured-resin-cart-guava-2-0 \N \N \N \N 75.94 0.09 Connected Cannabis \N https://images.dutchie.com/9a5a2ae45ddaad6b8a6bdf2b34975d3a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-guava-2-0 t f \N 2025-11-18 03:52:02.318575 2025-11-18 04:18:48.511772 2025-11-18 03:52:02.318575 2025-11-18 05:05:11.282616 112 Guava 2.0Connected Cannabis \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.511772+00 \N +3125 \N \N \N 8" ORANGE BUBBLER g2-8-orange-bubbler \N \N \N \N \N \N G2 \N \N \N https://best.treez.io/onlinemenu/category/merch/item/f1728cde-4862-4b89-8eaa-883f900990d3?customerType=ADULT t f \N 2025-11-18 14:42:09.208398 2025-11-18 14:42:09.208398 2025-11-18 14:42:09.208398 2025-11-18 14:42:09.208398 149 \N \N \N \N \N \N 70.00 70.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.208398+00 \N +3126 \N \N \N GREEN & BLUE DONUT BEAKER W/DICHRO MARBLE green-blue-donut-beaker-w-dichro-marble \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/859ed775-5dd4-4506-9f31-b9d2ccdb4da4?customerType=ADULT t f \N 2025-11-18 14:42:09.210454 2025-11-18 14:42:09.210454 2025-11-18 14:42:09.210454 2025-11-18 14:42:09.210454 149 \N \N \N \N \N \N 75.00 75.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.210454+00 \N +3127 \N \N \N BLUE FLOWER JEWEL BONG 10" blue-flower-jewel-bong-10 \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/9c16e9b2-f718-4860-bcc4-390f36f229c8?customerType=ADULT t f \N 2025-11-18 14:42:09.212434 2025-11-18 14:42:09.212434 2025-11-18 14:42:09.212434 2025-11-18 14:42:09.212434 149 \N \N \N \N \N \N 15.00 15.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.212434+00 \N +3128 \N \N \N PUFFCO PRO 2.0 3DXL-ONYX puffco-puffco-pro-2-0-3dxl-onyx \N \N \N \N \N \N PUFFCO \N \N \N https://best.treez.io/onlinemenu/category/merch/item/0d39d812-e8ce-4979-8db8-0d5f439fd5a4?customerType=ADULT t f \N 2025-11-18 14:42:09.214456 2025-11-18 14:42:09.214456 2025-11-18 14:42:09.214456 2025-11-18 14:42:09.214456 149 \N \N \N \N \N \N 425.00 425.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.214456+00 \N +3129 \N \N \N BD LOGO HAT best-bd-logo-hat \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/268e9449-3a46-44c1-b8d7-70c70794701f?customerType=ADULT t f \N 2025-11-18 14:42:09.216833 2025-11-18 14:42:09.216833 2025-11-18 14:42:09.216833 2025-11-18 14:42:09.216833 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.216833+00 \N +2260 \N \N \N Grow Sciences | 3.7g Flower Jar | Orange ZqueezeGrow SciencesSativa-HybridTHC: 23.48% grow-sciences-3-7g-flower-jar-orange-zqueeze \N \N \N \N 23.48 \N Grow Sciences \N https://images.dutchie.com/00d05364e10d01765ebb95bc214b5254?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-orange-zqueeze t f \N 2025-11-18 03:54:01.864537 2025-11-18 04:21:14.582931 2025-11-18 03:54:01.864537 2025-11-18 05:14:34.745469 112 Orange ZqueezeGrow Sciences \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.582931+00 \N +2934 \N \N \N ZANGRIA alien-labs-zangria-3-5g \N \N \N HYBRID 23.17 \N ALIEN LABS \N \N \N https://best.treez.io/onlinemenu/category/flower/item/02d9b552-40a9-4e7c-a185-8d1cfe433fe6?customerType=ADULT t f \N 2025-11-18 14:42:08.819297 2025-11-18 14:42:08.819297 2025-11-18 14:42:08.819297 2025-11-18 14:42:08.819297 149 3.5G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.819297+00 \N +2210 \N \N \N Elevate Flower Mylar | Cereal MilkElevateHybridTHC: 29.21%Special Offer elevate-flower-mylar-cereal-milk \N \N \N \N 29.21 \N Elevate \N https://images.dutchie.com/e9cf07e62ad92d88a7048ca1aaacc1cb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-cereal-milk t f \N 2025-11-18 03:53:25.888277 2025-11-18 04:20:35.614092 2025-11-18 03:53:25.888277 2025-11-18 05:11:31.740054 112 Cereal MilkElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.614092+00 \N +2211 \N \N \N Elevate Flower Mylar | Headband CookiesElevateSativaTHC: 28.14%Special Offer elevate-flower-mylar-headband-cookies \N \N \N \N 28.14 \N Elevate \N https://images.dutchie.com/e9cf07e62ad92d88a7048ca1aaacc1cb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-headband-cookies t f \N 2025-11-18 03:53:25.890837 2025-11-18 04:20:35.616572 2025-11-18 03:53:25.890837 2025-11-18 05:11:34.788473 112 Headband CookiesElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.616572+00 \N +2212 \N \N \N Elevate Flower Mylar | Hell's OGElevateIndica-HybridTHC: 28.63%Special Offer elevate-flower-mylar-hell-s-og \N \N \N \N 28.63 \N Elevate \N https://images.dutchie.com/e9cf07e62ad92d88a7048ca1aaacc1cb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-hell-s-og t f \N 2025-11-18 03:53:25.893158 2025-11-18 04:20:35.618951 2025-11-18 03:53:25.893158 2025-11-18 05:11:38.924232 112 Hell's OGElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.618951+00 \N +2213 \N \N \N Elevate Flower Mylar | Star KillerElevateHybridTHC: 26.24%Special Offer elevate-flower-mylar-star-killer \N \N \N \N 26.24 \N Elevate \N https://images.dutchie.com/e9cf07e62ad92d88a7048ca1aaacc1cb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-star-killer t f \N 2025-11-18 03:53:25.895238 2025-11-18 04:20:35.621054 2025-11-18 03:53:25.895238 2025-11-18 05:11:42.167511 112 Star KillerElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.621054+00 \N +2109 \N \N \N DR Flower Mylar | D-Inferno (HG)Deeply RootedIndica-HybridTHC: 22.72% dr-flower-mylar-d-inferno-hg-62724 \N \N \N \N 22.72 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-d-inferno-hg-62724 t f \N 2025-11-18 03:52:35.821334 2025-11-18 04:19:24.151672 2025-11-18 03:52:35.821334 2025-11-18 05:05:41.456583 112 D-Inferno (HG)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.151672+00 \N +2440 \N \N \N Strut - Pineapple Mimosa [2g]SavvyHybridTHC: 88.37%CBD: 0.2% strut-pineapple-mimosa-2g \N \N \N \N 88.37 0.20 Savvy \N https://images.dutchie.com/6a8a8b267121862ece7f6c025f1a03db?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/strut-pineapple-mimosa-2g t f \N 2025-11-18 03:57:30.568525 2025-11-18 04:25:04.616622 2025-11-18 03:57:30.568525 2025-11-18 05:25:15.518231 112 \N \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.616622+00 \N +3141 \N \N \N DRIP ART W/CRYSTAL PERC nectar-collector-drip-art-w-crystal-perc \N \N \N \N \N \N NECTAR COLLECTOR \N \N \N https://best.treez.io/onlinemenu/category/merch/item/9ab4840b-400d-48de-8124-caca5fbde7dd?customerType=ADULT t f \N 2025-11-18 14:42:09.237092 2025-11-18 14:42:09.237092 2025-11-18 14:42:09.237092 2025-11-18 14:42:09.237092 149 \N \N \N \N \N \N 11.00 11.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.237092+00 \N +2472 \N \N \N SIP Elixir | 100mg Nano Beverage | Wild BerrySipHybridTHC: 113.18 mg sip-elixir-100mg-nano-beverage-wild-berry \N \N \N \N \N \N Sip \N https://images.dutchie.com/bccd3d9e36caa738d021999980363e3f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-wild-berry t f \N 2025-11-18 03:58:31.119654 2025-11-18 04:25:51.892182 2025-11-18 03:58:31.119654 2025-11-18 05:26:46.540005 112 Wild BerrySip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.892182+00 \N +2473 \N \N \N SIP Elixir | 100mg THC: 50mg CBN Nano Beverage | 2:1 DreamberrySipTHCTHC: 0.2% sip-elixir-100mg-thc-50mg-cbn-nano-beverage-2-1-dreamberry \N \N \N \N 0.20 \N Sip \N https://images.dutchie.com/9a2d56cc2367b3e66267448b62b7ca4e?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-thc-50mg-cbn-nano-beverage-2-1-dreamberry t f \N 2025-11-18 03:58:31.121908 2025-11-18 04:25:51.894927 2025-11-18 03:58:31.121908 2025-11-18 05:26:53.109861 112 100mg \N \N \N {} {} {} 11.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.894927+00 \N +2107 \N \N \N DR Flower Mylar | Block Berry (Living Soil)Deeply RootedHybridTHC: 21.34% dr-flower-mylar-block-berry-living-soil \N \N \N \N 21.34 \N Deeply Rooted \N https://images.dutchie.com/flower-stock-9-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-block-berry-living-soil t f \N 2025-11-18 03:52:35.81534 2025-11-18 04:19:24.14766 2025-11-18 03:52:35.81534 2025-11-18 05:05:33.333563 112 Block Berry (Living Soil)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.14766+00 \N +2103 \N \N \N DR Cured Sugar | Vice City (AZOL)Deeply RootedHybridTHC: 69.86% dr-cured-sugar-vice-city-azol \N \N \N \N 69.86 \N Deeply Rooted \N https://images.dutchie.com/6303e8f140d150f269878c0812408673?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-cured-sugar-vice-city-azol t f \N 2025-11-18 03:52:35.794504 2025-11-18 04:19:24.131737 2025-11-18 03:52:35.794504 2025-11-18 05:05:20.36285 112 Vice City (AZOL)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.131737+00 \N +2110 \N \N \N DR Flower Mylar | Guava Heaven (Living Soil)Deeply RootedSativa-HybridTHC: 17.79%Special Offer dr-flower-mylar-guava-heaven-living-soil \N \N \N \N 17.79 \N Deeply Rooted \N https://images.dutchie.com/flower-stock-12-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-guava-heaven-living-soil t f \N 2025-11-18 03:52:35.824606 2025-11-18 04:19:24.153414 2025-11-18 03:52:35.824606 2025-11-18 05:05:44.67964 112 Guava Heaven (Living Soil)Deeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.153414+00 \N +2577 \N \N \N Varz Flower Jar | DerbyVarzIndica-HybridTHC: 26.52%Special Offer varz-flower-jar-derby \N \N \N \N 26.52 \N Varz \N https://images.dutchie.com/e85dd12dd5854a4952a3c686fb8e8e35?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-flower-jar-derby t f \N 2025-11-18 04:00:18.619272 2025-11-18 04:27:43.998734 2025-11-18 04:00:18.619272 2025-11-18 05:33:22.34655 112 DerbyVarz \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:43.998734+00 \N +2123 \N \N \N DR Flower POP | Wedding Cake (TP)Deeply RootedIndica-HybridTHC: 23.02%Special Offer dr-flower-pop-wedding-cake-tp \N \N \N \N 23.02 \N Deeply Rooted \N https://images.dutchie.com/flower-stock-10-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-pop-wedding-cake-tp t f \N 2025-11-18 03:52:35.870322 2025-11-18 04:19:24.178354 2025-11-18 03:52:35.870322 2025-11-18 05:06:28.733119 112 Wedding Cake (TP)Deeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.178354+00 \N +2939 \N \N \N GRAPE DREAM joel-s-cannabis-grape-dream-3-5g \N \N \N S/I 22.05 \N JOEL'S CANNABIS \N \N \N https://best.treez.io/onlinemenu/category/flower/item/25269b10-8ec6-48fb-a614-4ad811fd91b5?customerType=ADULT t f \N 2025-11-18 14:42:08.836215 2025-11-18 14:42:08.836215 2025-11-18 14:42:08.836215 2025-11-18 14:42:08.836215 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.836215+00 \N +2125 \N \N \N DR Flower Smalls | Permanent Marker (AG)Deeply RootedIndicaTHC: 27.01% dr-flower-smalls-permanent-marker-ag-74095 \N \N \N \N 27.01 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-smalls-permanent-marker-ag-74095 t f \N 2025-11-18 03:52:35.877026 2025-11-18 04:19:24.182542 2025-11-18 03:52:35.877026 2025-11-18 05:06:34.827247 112 Permanent Marker (AG)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.182542+00 \N +2134 \N \N \N DR Live Hash Rosin | Petes Peaches (ET)Deeply RootedSativa-HybridTHC: 76.06%CBD: 0.16% dr-live-hash-rosin-petes-peaches-et \N \N \N \N 76.06 0.16 Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-petes-peaches-et t f \N 2025-11-18 03:52:35.911326 2025-11-18 04:19:24.20067 2025-11-18 03:52:35.911326 2025-11-18 05:07:04.712569 112 Petes Peaches (ET)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.20067+00 \N +2150 \N \N \N Dime Distillate AIO | Berry WhiteDime IndustriesTHC: 95.83%CBD: 0.23% dime-distillate-aio-berry-white-71859 \N \N \N \N 95.83 0.23 Dime Industries \N https://images.dutchie.com/c84d5722213a9c70f9bd21d9ef8b3b65?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-berry-white-71859 t f \N 2025-11-18 03:52:43.824977 2025-11-18 04:19:43.669393 2025-11-18 03:52:43.824977 2025-11-18 05:08:06.388516 112 Berry WhiteDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.669393+00 \N +2176 \N \N \N Doja Pre-roll 2 Pack | StarlatoDOJAIndica-HybridTHC: 26.6% doja-pre-roll-2-pack-starlato \N \N \N \N 26.60 \N DOJA \N https://images.dutchie.com/63d14924a932126439bec249fb4e872a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/doja-pre-roll-2-pack-starlato t f \N 2025-11-18 03:52:50.268999 2025-11-18 04:19:52.227749 2025-11-18 03:52:50.268999 2025-11-18 05:09:36.111146 112 StarlatoDOJA \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:52.227749+00 \N +2188 \N \N \N Dr. Zodiak | 4.2g Infused Flower Buckshots | Lynwood lemonadeDr. ZodiakTHC: 54.66% dr-zodiak-4-2g-infused-flower-buckshots-lynwood-lemonade \N \N \N \N 54.66 \N Dr. Zodiak \N https://images.dutchie.com/flower-stock-1-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-4-2g-infused-flower-buckshots-lynwood-lemonade t f \N 2025-11-18 03:53:09.264018 2025-11-18 04:19:59.949714 2025-11-18 03:53:09.264018 2025-11-18 05:10:19.870252 112 Lynwood lemonadeDr. Zodiak \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.949714+00 \N +2940 \N \N \N SUPER SHERB joel-s-cannabis-super-sherb-3-5g \N \N \N I/S 29.33 \N JOEL'S CANNABIS \N \N \N https://best.treez.io/onlinemenu/category/flower/item/7cf2cbcf-7a94-475e-ab17-c2ff4b6db804?customerType=ADULT t f \N 2025-11-18 14:42:08.839126 2025-11-18 14:42:08.839126 2025-11-18 14:42:08.839126 2025-11-18 14:42:08.839126 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.839126+00 \N +2941 \N \N \N MALIBU seed-junky-malibu-3-5g \N \N \N HYBRID 26.63 \N SEED JUNKY \N \N \N https://best.treez.io/onlinemenu/category/flower/item/b555877e-ba9f-4061-acef-67679719e4e6?customerType=ADULT t f \N 2025-11-18 14:42:08.841884 2025-11-18 14:42:08.841884 2025-11-18 14:42:08.841884 2025-11-18 14:42:08.841884 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.841884+00 \N +2942 \N \N \N PINEAPPLE FRUZ seed-junky-pineapple-fruz-3-5g \N \N \N SATIVA 29.34 \N SEED JUNKY \N \N \N https://best.treez.io/onlinemenu/category/flower/item/5e4e5ace-d7ca-4539-94d2-7d7a9c75f782?customerType=ADULT t f \N 2025-11-18 14:42:08.845123 2025-11-18 14:42:08.845123 2025-11-18 14:42:08.845123 2025-11-18 14:42:08.845123 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.845123+00 \N +2943 \N \N \N PINEAPPLE BANG shango-pineapple-bang-3-5g \N \N \N HYBRID 30.90 \N SHANGO \N \N \N https://best.treez.io/onlinemenu/category/flower/item/f9dfbd00-9e2e-486f-b902-1958b04a3f08?customerType=ADULT t f \N 2025-11-18 14:42:08.848042 2025-11-18 14:42:08.848042 2025-11-18 14:42:08.848042 2025-11-18 14:42:08.848042 149 3.5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.848042+00 \N +2444 \N \N \N Jams | 100mg E(Tart) 40PK | Raspberry Lemonade jams-100mg-e-tart-40pk-raspberry-lemonade Edibles are created either by infusing cooking oil or butter with cannabis extract, or by mixing extract directly into other ingredients. Because edibles are digested and absorbed by your stomach and liver, the activation is often longer than other consumption methods, taking on average 45 minutes, and sometimes up to 2 hours. It is important to start low and slow when consuming edibles so you don't over do it. Take extra caution to ensure edibles are out of the reach of children. \N \N \N 0.28 \N Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/Edibles.jpg \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jams-100mg-e-tart-40pk-raspberry-lemonade t f \N 2025-11-18 03:57:55.424501 2025-11-18 05:08:07.813672 2025-11-18 03:57:55.424501 2025-11-18 05:08:07.813672 112 \N \N \N \N {} {} {} 10.00 \N \N in stock \N \N in_stock \N 2025-11-18 05:08:07.813672+00 \N +2446 \N \N \N Jams | 100mg E(Tart) 40PK | Tangerine jams-100mg-e-tart-40pk-tangerine Edibles are created either by infusing cooking oil or butter with cannabis extract, or by mixing extract directly into other ingredients. Because edibles are digested and absorbed by your stomach and liver, the activation is often longer than other consumption methods, taking on average 45 minutes, and sometimes up to 2 hours. It is important to start low and slow when consuming edibles so you don't over do it. Take extra caution to ensure edibles are out of the reach of children. \N \N \N 0.28 \N Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/Edibles.jpg \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jams-100mg-e-tart-40pk-tangerine t f \N 2025-11-18 03:57:55.435275 2025-11-18 05:08:07.830401 2025-11-18 03:57:55.435275 2025-11-18 05:08:07.830401 112 \N \N \N \N {} {} {} 10.00 \N \N in stock \N \N in_stock \N 2025-11-18 05:08:07.830401+00 \N +2448 \N \N \N Select Elite Terpologist Cart | Pina Gluelada select-elite-terpologist-cart-pina-gluelada The Terpologist blends intoxicating funk with a sweet side in this cross between GG4 and Pineapple Express. Pungent, gassy, and adhesive, the glue notes travel into tropical territory for candied pineapple expressions that help fuel the funk. Flashes of bright citrus flavors mix with a green mango leaf center for a uniquely sessionable strain to sit and rip on. \N \N Indica 86.62 0.93 Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/b933fda03edc36ba5893d5cfb5e04725 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-elite-terpologist-cart-pina-gluelada t f \N 2025-11-18 03:57:55.440413 2025-11-18 05:08:07.838881 2025-11-18 03:57:55.440413 2025-11-18 05:08:07.838881 112 \N \N \N \N {} {} {} 40.00 \N 2 2 left in stock \N \N in_stock \N 2025-11-18 05:08:07.838881+00 \N +2638 \N \N \N Select Essential Briq AIO | Lychee Dream select-essential-briq-aio-lychee-dream-17633 Meet Briq, the new visionary of vapes from Select. Packing 2 GRAMS of our premium Essentials oils in a rechargeable all-in-one, this sleek and compact device fits easily into palms and pockets for on-the-go lifestyles. And with Advanced No Burn Technology you can be sure every effortless pull is packed full of your favorite flavors for more puffs than a pastry shop. \N \N Sativa 88.90 5.43 Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/dcf6e24cfc9c02dd8985e5441d540194 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-essential-briq-aio-lychee-dream-17633 t f \N 2025-11-18 05:08:07.846815 2025-11-18 05:08:07.846815 2025-11-18 05:08:07.846815 2025-11-18 05:08:07.846815 112 \N \N \N \N {} {} {} 45.00 \N \N in stock \N \N in_stock \N 2025-11-18 05:08:07.846815+00 \N +2443 \N \N \N Seed Junky Flower Jar | MalibuSeed JunkyHybridTHC: 26.63%Special Offer seed-junky-flower-jar-malibu \N \N \N \N 26.63 \N Seed Junky \N https://images.dutchie.com/0ac6dbe7c642759bbab20a9e612bcc21?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/seed-junky-flower-jar-malibu t f \N 2025-11-18 03:57:35.806787 2025-11-18 04:25:15.690972 2025-11-18 03:57:35.806787 2025-11-18 05:25:24.571038 112 MalibuSeed Junky \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:15.690972+00 \N +2214 \N \N \N Elevate Flower Mylar | Strawberry GuavaElevateHybridTHC: 24.98%Special Offer elevate-flower-mylar-strawberry-guava \N \N \N \N 24.98 \N Elevate \N https://images.dutchie.com/e9cf07e62ad92d88a7048ca1aaacc1cb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-strawberry-guava t f \N 2025-11-18 03:53:25.897233 2025-11-18 04:20:35.623141 2025-11-18 03:53:25.897233 2025-11-18 05:11:45.860799 112 Strawberry GuavaElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.623141+00 \N +2215 \N \N \N Elevate Flower Mylar | Super BoofElevateHybridTHC: 22.7%Special Offer elevate-flower-mylar-super-boof \N \N \N \N 22.70 \N Elevate \N https://images.dutchie.com/e9cf07e62ad92d88a7048ca1aaacc1cb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-super-boof t f \N 2025-11-18 03:53:25.899455 2025-11-18 04:20:35.625605 2025-11-18 03:53:25.899455 2025-11-18 05:11:57.52595 112 Super BoofElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.625605+00 \N +2262 \N \N \N Grow Sciences | 3.7g Flower Jar | Prickly PearGrow SciencesHybridTHC: 21.61% grow-sciences-3-7g-flower-jar-prickly-pear \N \N \N \N 21.61 \N Grow Sciences \N https://images.dutchie.com/00d05364e10d01765ebb95bc214b5254?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-prickly-pear t f \N 2025-11-18 03:54:01.869835 2025-11-18 04:21:14.586696 2025-11-18 03:54:01.869835 2025-11-18 05:14:41.061282 112 Prickly PearGrow Sciences \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.586696+00 \N +2203 \N \N \N Easy Tiger Live Hash Rosin | G13 SkunkEasy TigerHybridTHC: 74.26%CBD: 0.14% easy-tiger-live-hash-rosin-g13-skunk \N \N \N \N 74.26 0.14 Easy Tiger \N https://images.dutchie.com/7dd476bc64e835f7ed875802381d8bb6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/easy-tiger-live-hash-rosin-g13-skunk t f \N 2025-11-18 03:53:20.420462 2025-11-18 04:20:27.892302 2025-11-18 03:53:20.420462 2025-11-18 05:11:07.780774 112 G13 SkunkEasy Tiger \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:27.892302+00 \N +2209 \N \N \N Elevate Flower Mylar | Black MapleElevateHybridTHC: 24.65%Special Offer elevate-flower-mylar-black-maple \N \N \N \N 24.65 \N Elevate \N https://images.dutchie.com/e9cf07e62ad92d88a7048ca1aaacc1cb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-black-maple t f \N 2025-11-18 03:53:25.880788 2025-11-18 04:20:35.607139 2025-11-18 03:53:25.880788 2025-11-18 05:11:28.640313 112 Black MapleElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.607139+00 \N +2286 \N \N \N High West Farms Diamond Infused Pre-Rolls | IlluminatiHigh West FarmsIndica-HybridTHC: 41.32% high-west-farms-diamond-infused-pre-rolls-illuminati \N \N \N \N 41.32 \N High West Farms \N https://images.dutchie.com/b4c5f0554d1ca9d24a09763bf4f68030?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-diamond-infused-pre-rolls-illuminati t f \N 2025-11-18 03:54:56.124383 2025-11-18 04:22:15.028384 2025-11-18 03:54:56.124383 2025-11-18 05:16:05.986648 112 IlluminatiHigh West Farms \N \N \N {} {} {} 12.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:15.028384+00 \N +2287 \N \N \N High West Farms Diamond Infused Pre-Rolls | YahemiHigh West FarmsSativa-HybridTHC: 50.04% high-west-farms-diamond-infused-pre-rolls-yahemi \N \N \N \N 50.04 \N High West Farms \N https://images.dutchie.com/b4c5f0554d1ca9d24a09763bf4f68030?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-diamond-infused-pre-rolls-yahemi t f \N 2025-11-18 03:54:56.126887 2025-11-18 04:22:15.031387 2025-11-18 03:54:56.126887 2025-11-18 05:16:09.031426 112 YahemiHigh West Farms \N \N \N {} {} {} 12.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:15.031387+00 \N +2293 \N \N \N IO Extracts Cured Batter | Chem DI.O. ExtractsHybridTHC: 74.01%CBD: 0.14% io-extracts-cured-batter-chem-d \N \N \N \N 74.01 0.14 I.O. Extracts \N https://images.dutchie.com/c7a805bcc9a56c1c311ea81102fe103f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-cured-batter-chem-d t f \N 2025-11-18 03:55:07.41304 2025-11-18 04:22:41.772815 2025-11-18 03:55:07.41304 2025-11-18 05:16:27.006053 112 Chem DI.O. Extracts \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.772815+00 \N +2295 \N \N \N IO Extracts Cured Batter | Han's GMO BurgerI.O. ExtractsIndica-HybridTHC: 76.8%CBD: 0.15% io-extracts-cured-batter-han-s-gmo-burger \N \N \N \N 76.80 0.15 I.O. Extracts \N https://images.dutchie.com/c7a805bcc9a56c1c311ea81102fe103f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-cured-batter-han-s-gmo-burger t f \N 2025-11-18 03:55:07.417701 2025-11-18 04:22:41.776768 2025-11-18 03:55:07.417701 2025-11-18 05:16:36.929225 112 Han's GMO BurgerI.O. Extracts \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.776768+00 \N +2296 \N \N \N IO Extracts Live Hash Rosin AIO Vape | Block BerryI.O. ExtractsHybridTHC: 76.97%CBD: 0.2% io-extracts-live-hash-rosin-aio-vape-block-berry \N \N \N \N 76.97 0.20 I.O. Extracts \N https://images.dutchie.com/8230140f9fbb210900567af9fd820b33?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-hash-rosin-aio-vape-block-berry t f \N 2025-11-18 03:55:07.419804 2025-11-18 04:22:41.779171 2025-11-18 03:55:07.419804 2025-11-18 05:16:40.150137 112 Block BerryI.O. Extracts \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.779171+00 \N +2298 \N \N \N IO Extracts Live Hash Rosin AIO Vape | Papaya PunchI.O. ExtractsIndica-HybridTHC: 75.9%CBD: 0.17% io-extracts-live-hash-rosin-aio-vape-papaya-punch \N \N \N \N 75.90 0.17 I.O. Extracts \N https://images.dutchie.com/8230140f9fbb210900567af9fd820b33?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-hash-rosin-aio-vape-papaya-punch t f \N 2025-11-18 03:55:07.424206 2025-11-18 04:22:41.78398 2025-11-18 03:55:07.424206 2025-11-18 05:16:46.342996 112 Papaya PunchI.O. Extracts \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.78398+00 \N +2958 \N \N \N HEADBAND best-headband-5g \N \N \N HYBRID 31.16 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/flower/item/49abed21-8f79-414f-9a58-5f3ff8ef14df?customerType=ADULT t f \N 2025-11-18 14:42:08.88579 2025-11-18 14:42:08.88579 2025-11-18 14:42:08.88579 2025-11-18 14:42:08.88579 149 5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.88579+00 \N +2300 \N \N \N IO Extracts Live Hash Rosin | Insane Pound CakeI.O. ExtractsIndica-HybridTHC: 72.76%CBD: 0.2% io-extracts-live-hash-rosin-insane-pound-cake \N \N \N \N 72.76 0.20 I.O. Extracts \N https://images.dutchie.com/980d7d4f6b6dafce78d3093a0048cfe9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-hash-rosin-insane-pound-cake t f \N 2025-11-18 03:55:07.428215 2025-11-18 04:22:41.787795 2025-11-18 03:55:07.428215 2025-11-18 05:16:52.40853 112 Insane Pound CakeI.O. Extracts \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.787795+00 \N +2959 \N \N \N PINEAPPLE EXPRESS best-pineapple-express-5g \N \N \N SATIVA 32.31 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/flower/item/bc54b899-b56e-4cfd-9465-c54dccce72f3?customerType=ADULT t f \N 2025-11-18 14:42:08.888081 2025-11-18 14:42:08.888081 2025-11-18 14:42:08.888081 2025-11-18 14:42:08.888081 149 5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.888081+00 \N +2445 \N \N \N Jams | 100mg E(Tart) 40PK | Strawberry jams-100mg-e-tart-40pk-strawberry Edibles are created either by infusing cooking oil or butter with cannabis extract, or by mixing extract directly into other ingredients. Because edibles are digested and absorbed by your stomach and liver, the activation is often longer than other consumption methods, taking on average 45 minutes, and sometimes up to 2 hours. It is important to start low and slow when consuming edibles so you don't over do it. Take extra caution to ensure edibles are out of the reach of children. \N \N \N 0.28 \N Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/Edibles.jpg \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jams-100mg-e-tart-40pk-strawberry t f \N 2025-11-18 03:57:55.432688 2025-11-18 05:08:07.826539 2025-11-18 03:57:55.432688 2025-11-18 05:08:07.826539 112 \N \N \N \N {} {} {} 10.00 \N \N in stock \N \N in_stock \N 2025-11-18 05:08:07.826539+00 \N +2217 \N \N \N Sublime Pre-Roll | Charlie's KushFeel SublimeIndica-HybridTHC: 25.77%Special Offer sublime-pre-roll-charlie-s-kush \N \N \N \N 25.77 \N Feel Sublime \N https://images.dutchie.com/293448dc4415cdaca6ddb3287562e3d1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sublime-pre-roll-charlie-s-kush t f \N 2025-11-18 03:53:44.867657 2025-11-18 04:20:38.973642 2025-11-18 03:53:44.867657 2025-11-18 05:12:00.581258 112 Charlie's KushFeel Sublime \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:38.973642+00 \N +2219 \N \N \N Sublime | 100mg Hard Candy | Pina ColadaFeel SublimeTHC: 0.23% sublime-100mg-hard-candy-pina-colada \N \N \N \N 0.23 \N Feel Sublime \N https://images.dutchie.com/496e8656f249361c9258a9ac2498dc1d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sublime-100mg-hard-candy-pina-colada t f \N 2025-11-18 03:53:44.877705 2025-11-18 04:20:38.982567 2025-11-18 03:53:44.877705 2025-11-18 05:12:06.694778 112 Pina ColadaFeel Sublime \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:38.982567+00 \N +2264 \N \N \N Gron | 100mg Pips | Milk ChocolateGrönTHC: 0.45% gron-100mg-pips-milk-chocolate \N \N \N \N 0.45 \N Grön \N https://images.dutchie.com/2a97bd6c5d9c0b6aa2887b9ca58b3ef3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gron-100mg-pips-milk-chocolate t f \N 2025-11-18 03:54:20.454479 2025-11-18 04:21:30.4996 2025-11-18 03:54:20.454479 2025-11-18 05:14:49.878641 112 Milk ChocolateGrön \N \N \N {} {} {} 16.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:30.4996+00 \N +2265 \N \N \N Mega Pearl | 1:1 CBN/THC Gummies | BlackberryGrönTHCTHC: 0.5%Special Offer mega-pearl-1-1-cbn-thc-gummies-blackberry-10049 \N \N \N \N 0.50 \N Grön \N https://images.dutchie.com/2a97bd6c5d9c0b6aa2887b9ca58b3ef3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mega-pearl-1-1-cbn-thc-gummies-blackberry-10049 t f \N 2025-11-18 03:54:20.458365 2025-11-18 04:21:30.501638 2025-11-18 03:54:20.458365 2025-11-18 05:14:52.903987 112 1:1 CBN/ \N \N \N {} {} {} 11.00 8.25 \N \N \N \N in_stock \N 2025-11-18 04:21:30.501638+00 \N +2267 \N \N \N Pearls | 10:1 CBN/THC Gummies | Tart CherryGrönTHCTHC: 0.08%CBD: 0.01% pearls-10-1-cbn-thc-gummies-tart-cherry \N \N \N \N 0.08 0.01 Grön \N https://images.dutchie.com/2a97bd6c5d9c0b6aa2887b9ca58b3ef3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/pearls-10-1-cbn-thc-gummies-tart-cherry t f \N 2025-11-18 03:54:20.462743 2025-11-18 04:21:30.505638 2025-11-18 03:54:20.462743 2025-11-18 05:14:59.087337 112 10:1 CBN/ \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:30.505638+00 \N +2268 \N \N \N Pearls | 2:1:1 THC/CBC/CBG Gummies | TangeloGrönTHCTHC: 0.28% pearls-2-1-1-thc-cbc-cbg-gummies-tangelo \N \N \N \N 0.28 \N Grön \N https://images.dutchie.com/2a97bd6c5d9c0b6aa2887b9ca58b3ef3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/pearls-2-1-1-thc-cbc-cbg-gummies-tangelo t f \N 2025-11-18 03:54:20.465226 2025-11-18 04:21:30.507372 2025-11-18 03:54:20.465226 2025-11-18 05:15:02.135196 112 2:1:1 \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:30.507372+00 \N +3142 \N \N \N PUFFCO HOT KNIFE puffco-puffco-hot-knife \N \N \N \N \N \N PUFFCO \N \N \N https://best.treez.io/onlinemenu/category/merch/item/9b817e07-730b-495d-81bd-63e234918305?customerType=ADULT t f \N 2025-11-18 14:42:09.238706 2025-11-18 14:42:09.238706 2025-11-18 14:42:09.238706 2025-11-18 14:42:09.238706 149 \N \N \N \N \N \N 63.00 63.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.238706+00 \N +2270 \N \N \N THC Watermelon Pearls - IndicaGrönTHCTAC: 100 mgTHC: 0.3% thc-watermelon-pearls-indica \N \N \N \N 0.30 \N Grön \N https://images.dutchie.com/bca4144ac81e0bd61307fa68cab53dc1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thc-watermelon-pearls-indica t f \N 2025-11-18 03:54:20.469665 2025-11-18 04:21:30.511216 2025-11-18 03:54:20.469665 2025-11-18 05:15:12.09875 112 \N \N \N \N {} {} {} 16.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:30.511216+00 \N +2288 \N \N \N HighMart Flower Mylar | 97HighMartHybridTHC: 24.05% highmart-flower-mylar-97 \N \N \N \N 24.05 \N HighMart \N https://images.dutchie.com/0d075f746a5384ed06c1af2624de1c3e?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/highmart-flower-mylar-97 t f \N 2025-11-18 03:54:58.814996 2025-11-18 04:22:22.496377 2025-11-18 03:54:58.814996 2025-11-18 05:16:12.047667 112 97HighMart \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:22.496377+00 \N +2289 \N \N \N HighMart Flower Mylar | Family Cut 41HighMartHybridTHC: 25.01% highmart-flower-mylar-family-cut-41-73730 \N \N \N \N 25.01 \N HighMart \N https://images.dutchie.com/2220c274e1fb499049b10e4b364f6334?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/highmart-flower-mylar-family-cut-41-73730 t f \N 2025-11-18 03:54:58.825306 2025-11-18 04:22:22.503084 2025-11-18 03:54:58.825306 2025-11-18 05:16:14.9913 112 Family Cut 41HighMart \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:22.503084+00 \N +2301 \N \N \N IO Extracts Live Hash Rosin | Papaya PunchI.O. ExtractsIndica-HybridTHC: 68.97%CBD: 0.2% io-extracts-live-hash-rosin-papaya-punch \N \N \N \N 68.97 0.20 I.O. Extracts \N https://images.dutchie.com/ff8b1bdc60cb5ba5d2f95bb01a7651c4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-hash-rosin-papaya-punch t f \N 2025-11-18 03:55:07.429897 2025-11-18 04:22:41.789889 2025-11-18 03:55:07.429897 2025-11-18 05:16:55.407861 112 Papaya PunchI.O. Extracts \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.789889+00 \N +2303 \N \N \N IO Extracts Live Resin Badder | Chocolate BarI.O. ExtractsSativa-HybridTHC: 75.39%CBD: 0.15% io-extracts-live-resin-badder-chocolate-bar \N \N \N \N 75.39 0.15 I.O. Extracts \N https://images.dutchie.com/4d0ecac0b350e7d9d278546f76e63a14?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-resin-badder-chocolate-bar t f \N 2025-11-18 03:55:07.43348 2025-11-18 04:22:41.794263 2025-11-18 03:55:07.43348 2025-11-18 05:17:01.348541 112 Chocolate BarI.O. Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.794263+00 \N +2304 \N \N \N IO Extracts Live Resin Badder | Kitchen SinkI.O. ExtractsHybridTHC: 64.62%CBD: 1.48% io-extracts-live-resin-badder-kitchen-sink \N \N \N \N 64.62 1.48 I.O. Extracts \N https://images.dutchie.com/4d0ecac0b350e7d9d278546f76e63a14?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-resin-badder-kitchen-sink t f \N 2025-11-18 03:55:07.435221 2025-11-18 04:22:41.796073 2025-11-18 03:55:07.435221 2025-11-18 05:17:06.396986 112 Kitchen SinkI.O. Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.796073+00 \N +2962 \N \N \N BLUEBERRY LEMON HAZE dime-industries-blueberry-lemon-haze-1g \N \N \N SATIVA 93.33 \N DIME INDUSTRIES \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/b29cdce9-e75a-4fce-a368-38a9215ac9ad?customerType=ADULT t f \N 2025-11-18 14:42:08.895776 2025-11-18 14:42:08.895776 2025-11-18 14:42:08.895776 2025-11-18 14:42:08.895776 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.895776+00 \N +2963 \N \N \N PEACH KUSH dime-industries-peach-kush-1g \N \N \N INDICA 87.45 \N DIME INDUSTRIES \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/c0402cbd-cf13-43ff-9c3b-debf88a70f11?customerType=ADULT t f \N 2025-11-18 14:42:08.898292 2025-11-18 14:42:08.898292 2025-11-18 14:42:08.898292 2025-11-18 14:42:08.898292 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.898292+00 \N +2152 \N \N \N Dime Distillate AIO | Blueberry Lemon HazeDime IndustriesTHC: 94.27%CBD: 0.22% dime-distillate-aio-blueberry-lemon-haze-12072 \N \N \N \N 94.27 0.22 Dime Industries \N https://images.dutchie.com/b65303995cb9b620571eae8bf3b59bdb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-blueberry-lemon-haze-12072 t f \N 2025-11-18 03:52:43.83559 2025-11-18 04:19:43.678396 2025-11-18 03:52:43.83559 2025-11-18 05:08:12.800748 112 Blueberry Lemon HazeDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.678396+00 \N +2154 \N \N \N Dime Distillate AIO | Key Lime PieDime IndustriesTHC: 90.84%CBD: 0.16% dime-distillate-aio-key-lime-pie-93335 \N \N \N \N 90.84 0.16 Dime Industries \N https://images.dutchie.com/9c46fb7353a28f4dd2540d5fbd387339?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-key-lime-pie-93335 t f \N 2025-11-18 03:52:43.840302 2025-11-18 04:19:43.6827 2025-11-18 03:52:43.840302 2025-11-18 05:08:18.840019 112 Key Lime PieDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.6827+00 \N +2155 \N \N \N Dime Distillate AIO | Peach Ice-TDime IndustriesTHC: 93.21%CBD: 0.2% dime-distillate-aio-peach-ice-t \N \N \N \N 93.21 0.20 Dime Industries \N https://images.dutchie.com/412c818f2729b5a349d2a0bc50625619?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-peach-ice-t t f \N 2025-11-18 03:52:43.84253 2025-11-18 04:19:43.684788 2025-11-18 03:52:43.84253 2025-11-18 05:08:26.128089 112 Peach Ice-TDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.684788+00 \N +2157 \N \N \N Dime Distillate AIO | Strawberry CoughDime IndustriesTHC: 94.44%CBD: 0.18% dime-distillate-aio-strawberry-cough-5517 \N \N \N \N 94.44 0.18 Dime Industries \N https://images.dutchie.com/2cba216a6a2ccb0c705b3ca0d247f246?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-strawberry-cough-5517 t f \N 2025-11-18 03:52:43.846369 2025-11-18 04:19:43.688984 2025-11-18 03:52:43.846369 2025-11-18 05:08:32.206788 112 Strawberry CoughDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.688984+00 \N +2159 \N \N \N Dime Distillate AIO | Watermelon KushDime IndustriesTHC: 93.97%CBD: 0.19% dime-distillate-aio-watermelon-kush-89404 \N \N \N \N 93.97 0.19 Dime Industries \N https://images.dutchie.com/7c6cdd3d910d3866140a828d50171d47?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-watermelon-kush-89404 t f \N 2025-11-18 03:52:43.850575 2025-11-18 04:19:43.693345 2025-11-18 03:52:43.850575 2025-11-18 05:08:40.301219 112 Watermelon KushDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.693345+00 \N +2160 \N \N \N Dime Distillate AIO | Wedding CakeDime IndustriesTHC: 92.99%CBD: 0.2% dime-distillate-aio-wedding-cake-74989 \N \N \N \N 92.99 0.20 Dime Industries \N https://images.dutchie.com/a23f3d736b6aa1e1d479185c8cb22cc4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-wedding-cake-74989 t f \N 2025-11-18 03:52:43.852681 2025-11-18 04:19:43.695362 2025-11-18 03:52:43.852681 2025-11-18 05:08:44.038678 112 Wedding CakeDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.695362+00 \N +2162 \N \N \N Dime Distillate Cart | Blueberry Lemon HazeDime IndustriesSativaTHC: 91.01%CBD: 0.21% dime-distillate-cart-blueberry-lemon-haze \N \N \N \N 91.01 0.21 Dime Industries \N https://images.dutchie.com/5ef5f653d08f2c95204cb521ecd70064?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-blueberry-lemon-haze t f \N 2025-11-18 03:52:43.856933 2025-11-18 04:19:43.699247 2025-11-18 03:52:43.856933 2025-11-18 05:08:50.06066 112 Blueberry Lemon HazeDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.699247+00 \N +2164 \N \N \N Dime Distillate Cart | Mango DieselDime IndustriesTHC: 90.03%CBD: 0.21% dime-distillate-cart-mango-diesel \N \N \N \N 90.03 0.21 Dime Industries \N https://images.dutchie.com/9686994b922de9b6a570d5db79b12360?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-mango-diesel t f \N 2025-11-18 03:52:43.861656 2025-11-18 04:19:43.704063 2025-11-18 03:52:43.861656 2025-11-18 05:08:56.350189 112 Mango DieselDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.704063+00 \N +2165 \N \N \N Dime Distillate Cart | Passion ParadiseDime IndustriesTHC: 94.13%CBD: 0.3% dime-distillate-cart-passion-paradise \N \N \N \N 94.13 0.30 Dime Industries \N https://images.dutchie.com/03efb5383e9bff7d2cdbe1eda2e9fb98?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-passion-paradise t f \N 2025-11-18 03:52:43.863674 2025-11-18 04:19:43.706151 2025-11-18 03:52:43.863674 2025-11-18 05:08:59.385853 112 Passion ParadiseDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.706151+00 \N +2167 \N \N \N Dime Distillate Cart | Royal PearDime IndustriesTHC: 94.7%CBD: 0.32% dime-distillate-cart-royal-pear \N \N \N \N 94.70 0.32 Dime Industries \N https://images.dutchie.com/03efb5383e9bff7d2cdbe1eda2e9fb98?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-royal-pear t f \N 2025-11-18 03:52:43.86917 2025-11-18 04:19:43.710692 2025-11-18 03:52:43.86917 2025-11-18 05:09:05.575029 112 Royal PearDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.710692+00 \N +2169 \N \N \N Dime Distillate Cart | Tropical KiwiDime IndustriesTHC: 90.82%CBD: 0.21% dime-distillate-cart-tropical-kiwi \N \N \N \N 90.82 0.21 Dime Industries \N https://images.dutchie.com/34bdb0a8007bcdfe9e0ec87b60654968?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-tropical-kiwi t f \N 2025-11-18 03:52:43.873897 2025-11-18 04:19:43.715092 2025-11-18 03:52:43.873897 2025-11-18 05:09:13.808669 112 Tropical KiwiDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.715092+00 \N +2171 \N \N \N Dime Live Resin AIO | Grape LimeadeDime IndustriesHybridTHC: 81.6%CBD: 0.25% dime-live-resin-aio-grape-limeade \N \N \N \N 81.60 0.25 Dime Industries \N https://images.dutchie.com/1ae1f03158784d4fc1e68d35b5434bb7?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-live-resin-aio-grape-limeade t f \N 2025-11-18 03:52:43.878543 2025-11-18 04:19:43.719212 2025-11-18 03:52:43.878543 2025-11-18 05:09:20.0823 112 Grape LimeadeDime Industries \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.719212+00 \N +2172 \N \N \N Dime Live Resin AIO | Jet FuelDime IndustriesIndicaTHC: 83.24%CBD: 0.26% dime-live-resin-aio-jet-fuel \N \N \N \N 83.24 0.26 Dime Industries \N https://images.dutchie.com/7c35138c5e8bf8e240a9bd70e8b008a0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-live-resin-aio-jet-fuel t f \N 2025-11-18 03:52:43.880867 2025-11-18 04:19:43.721892 2025-11-18 03:52:43.880867 2025-11-18 05:09:23.104574 112 Jet FuelDime Industries \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.721892+00 \N +2964 \N \N \N WEDDING CAKE dime-industries-wedding-cake-1g \N \N \N HYBRID 86.37 \N DIME INDUSTRIES \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/ddc6d296-a070-4a0a-9314-7c9abd7e7fa5?customerType=ADULT t f \N 2025-11-18 14:42:08.900748 2025-11-18 14:42:08.900748 2025-11-18 14:42:08.900748 2025-11-18 14:42:08.900748 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.900748+00 \N +2965 \N \N \N BLUE DREAM io-extracts-blue-dream-1g \N \N \N S/I 91.15 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/fa17cb92-8fce-4460-96af-690d0dad9b67?customerType=ADULT t f \N 2025-11-18 14:42:08.903316 2025-11-18 14:42:08.903316 2025-11-18 14:42:08.903316 2025-11-18 14:42:08.903316 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.903316+00 \N +2070 \N \N \N Canamo Live Resin Cart | Passion Fruit ReserveCanamo ConcentratesSativa-HybridTHC: 76.02%CBD: 0.13% canamo-live-resin-cart-passion-fruit-reserve \N \N \N \N 76.02 0.13 Canamo Concentrates \N https://images.dutchie.com/5a8ae870c27ab98b6fc0d71a4831c7ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-passion-fruit-reserve t f \N 2025-11-18 03:51:25.804438 2025-11-18 04:17:55.609853 2025-11-18 03:51:25.804438 2025-11-18 05:03:15.262262 112 Passion Fruit ReserveCanamo Concentrates \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.609853+00 \N +2072 \N \N \N Canamo Live Resin Cart | Two By FourCanamo ConcentratesTHC: 75.33%CBD: 0.15% canamo-live-resin-cart-two-by-four \N \N \N \N 75.33 0.15 Canamo Concentrates \N https://images.dutchie.com/5a8ae870c27ab98b6fc0d71a4831c7ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-two-by-four t f \N 2025-11-18 03:51:25.80892 2025-11-18 04:17:55.616109 2025-11-18 03:51:25.80892 2025-11-18 05:03:21.624582 112 Two By FourCanamo Concentrates \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.616109+00 \N +2074 \N \N \N Canamo Shatter | High School SweetheartsCanamo ConcentratesHybridTHC: 77.53%CBD: 0.13%Special Offer canamo-shatter-high-school-sweethearts \N \N \N \N 77.53 0.13 Canamo Concentrates \N https://images.dutchie.com/c81ef2ff9b2e6afafe5a46cbbde24775?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-high-school-sweethearts t f \N 2025-11-18 03:51:25.813699 2025-11-18 04:17:55.623005 2025-11-18 03:51:25.813699 2025-11-18 05:03:32.474001 112 High School SweetheartsCanamo Concentrates \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.623005+00 \N +2075 \N \N \N Canamo Shatter | Modified RootbeerCanamo ConcentratesIndica-HybridTHC: 77.19%CBD: 0.13%Special Offer canamo-shatter-modified-rootbeer \N \N \N \N 77.19 0.13 Canamo Concentrates \N https://images.dutchie.com/c81ef2ff9b2e6afafe5a46cbbde24775?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-modified-rootbeer t f \N 2025-11-18 03:51:25.815908 2025-11-18 04:17:55.626312 2025-11-18 03:51:25.815908 2025-11-18 05:03:35.46589 112 Modified RootbeerCanamo Concentrates \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.626312+00 \N +2173 \N \N \N Dime Live Resin AIO | KushmintsDime IndustriesIndicaTHC: 76.13%CBD: 0.16% dime-live-resin-aio-kushmints \N \N \N \N 76.13 0.16 Dime Industries \N https://images.dutchie.com/539dc6fa7b2e1ff95290d34faeed5f47?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-live-resin-aio-kushmints t f \N 2025-11-18 03:52:43.883341 2025-11-18 04:19:43.724248 2025-11-18 03:52:43.883341 2025-11-18 05:09:26.048173 112 KushmintsDime Industries \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.724248+00 \N +2174 \N \N \N Red PlumDime IndustriesIndicaTHC: 93.03%CBD: 0.18% red-plum \N \N \N \N 93.03 0.18 Dime Industries \N https://images.dutchie.com/fe9121c22796927a68f62b28c659707e?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/red-plum t f \N 2025-11-18 03:52:43.885597 2025-11-18 04:19:43.726548 2025-11-18 03:52:43.885597 2025-11-18 05:09:29.537616 112 \N \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.726548+00 \N +2222 \N \N \N Gelato Baller Brush | Wedding CakeGelatoHybridTHC: 44.66%CBD: 1.47% gelato-baller-brush-wedding-cake \N \N \N \N 44.66 1.47 Gelato \N https://images.dutchie.com/b027905743bde1811fd25072e5a75d13?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-baller-brush-wedding-cake t f \N 2025-11-18 03:53:47.542905 2025-11-18 04:20:54.902581 2025-11-18 03:53:47.542905 2025-11-18 05:12:16.356711 112 Wedding CakeGelato \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:54.902581+00 \N +2306 \N \N \N IO Extracts Live Resin Cart | BlueberryI.O. ExtractsIndica-HybridTHC: 77.01%CBD: 0.18% io-extracts-live-resin-cart-blueberry-62682 \N \N \N \N 77.01 0.18 I.O. Extracts \N https://images.dutchie.com/5efaa0e504137ee1fbb6c234ef403332?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-resin-cart-blueberry-62682 t f \N 2025-11-18 03:55:07.438509 2025-11-18 04:22:41.799944 2025-11-18 03:55:07.438509 2025-11-18 05:17:13.588557 112 BlueberryI.O. Extracts \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.799944+00 \N +2307 \N \N \N IO Extracts Live Resin Cart | GluejitsuI.O. ExtractsIndica-HybridTHC: 74.64%CBD: 0.14% io-extracts-live-resin-cart-gluejitsu-93811 \N \N \N \N 74.64 0.14 I.O. Extracts \N https://images.dutchie.com/5efaa0e504137ee1fbb6c234ef403332?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-resin-cart-gluejitsu-93811 t f \N 2025-11-18 03:55:07.440322 2025-11-18 04:22:41.801648 2025-11-18 03:55:07.440322 2025-11-18 05:17:16.703631 112 GluejitsuI.O. Extracts \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.801648+00 \N +2315 \N \N \N Jeeter | 3-Pack x 0.5g Live Resin Infused Pre-Roll | RS11JeeterTHC: 41.33% jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-rs11 \N \N \N \N 41.33 \N Jeeter \N https://images.dutchie.com/35915bcb0fb5c00733c688efea7d2466?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-rs11 t f \N 2025-11-18 03:55:13.073994 2025-11-18 04:22:50.695396 2025-11-18 03:55:13.073994 2025-11-18 05:17:46.051446 112 RS11Jeeter \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.695396+00 \N +2317 \N \N \N Kiwi Kush Premium Diamonds Vape Cartridge | 1gJeeterIndicaTHC: 86.3%CBD: 0.22% kiwi-kush-premium-diamonds-vape-cartridge-1g-42228 \N \N \N \N 86.30 0.22 Jeeter \N https://images.dutchie.com/0303446f4d8d59fa70af43a39efd01ae?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/kiwi-kush-premium-diamonds-vape-cartridge-1g-42228 t f \N 2025-11-18 03:55:13.077944 2025-11-18 04:22:50.699932 2025-11-18 03:55:13.077944 2025-11-18 05:17:52.189334 112 1gJeeter \N \N \N {} {} {} 44.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.699932+00 \N +2318 \N \N \N Super Lemon Haze Live Resin Cannon 3-pack | 1.5gJeeterSativaTHC: 40.92% super-lemon-haze-live-resin-cannon-3-pack-1-5g-97676 \N \N \N \N 40.92 \N Jeeter \N https://images.dutchie.com/ee0cd8e13fa0907c11a889461e5fd36d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/super-lemon-haze-live-resin-cannon-3-pack-1-5g-97676 t f \N 2025-11-18 03:55:13.079953 2025-11-18 04:22:50.702064 2025-11-18 03:55:13.079953 2025-11-18 05:17:55.134503 112 1.5gJeeter \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.702064+00 \N +2319 \N \N \N Keef Classic Soda Blue Razz XTREME | 100mgKeefHybridTHC: 0.03% keef-classic-soda-blue-razz-xtreme-100mg-82544 \N \N \N \N 0.03 \N Keef \N https://images.dutchie.com/a051ab58d5a1b32366023794cfdf9674?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-blue-razz-xtreme-100mg-82544 t f \N 2025-11-18 03:55:31.850858 2025-11-18 04:22:52.962527 2025-11-18 03:55:31.850858 2025-11-18 05:17:58.134505 112 100mgKeef \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.962527+00 \N +2966 \N \N \N BLUEBERRY COOKIES io-extracts-blueberry-cookies-1g \N \N \N HYBRID 91.81 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/9b72b053-6333-4d7e-88ef-ec25e064b3a0?customerType=ADULT t f \N 2025-11-18 14:42:08.906084 2025-11-18 14:42:08.906084 2025-11-18 14:42:08.906084 2025-11-18 14:42:08.906084 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.906084+00 \N +2967 \N \N \N EXOTIC ORANGE APRICOT io-extracts-exotic-orange-apricot-1g \N \N \N HYBRID 91.88 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/c6d09332-6786-481b-a80d-4ac61f07e3b8?customerType=ADULT t f \N 2025-11-18 14:42:08.908893 2025-11-18 14:42:08.908893 2025-11-18 14:42:08.908893 2025-11-18 14:42:08.908893 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.908893+00 \N +2476 \N \N \N Smokiez | 100mg Sour Gummy | Single | PeachSmokiez EdiblesTHC: 0.43%Special Offer smokiez-100mg-sour-gummy-single-peach \N \N \N \N 0.43 \N Smokiez Edibles \N https://images.dutchie.com/08aba6927e249560fd2d5cc882457855?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-single-peach t f \N 2025-11-18 03:58:33.16278 2025-11-18 04:25:58.261536 2025-11-18 03:58:33.16278 2025-11-18 05:27:02.682392 112 PeachSmokiez Edibles \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:58.261536+00 \N +2969 \N \N \N PINEAPPLE EXPRESS io-extracts-pineapple-express-1g \N \N \N S/I 91.19 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/d0e7483e-ed95-4c7e-82a5-d0b3b4deabbb?customerType=ADULT t f \N 2025-11-18 14:42:08.914757 2025-11-18 14:42:08.914757 2025-11-18 14:42:08.914757 2025-11-18 14:42:08.914757 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.914757+00 \N +2970 \N \N \N SORBET KUSH io-extracts-sorbet-kush-1g \N \N \N HYBRID 91.03 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/a667526d-8258-4893-8379-451723401a3c?customerType=ADULT t f \N 2025-11-18 14:42:08.917317 2025-11-18 14:42:08.917317 2025-11-18 14:42:08.917317 2025-11-18 14:42:08.917317 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.917317+00 \N +2226 \N \N \N Goldsmith Distillate Cartridge | California OrangeGoldsmith ExtractsTHC: 89.36%CBD: 4.42%Special Offer goldsmith-distillate-cartridge-california-orange \N \N \N \N 89.36 4.42 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-california-orange t f \N 2025-11-18 03:53:49.488366 2025-11-18 04:21:03.524302 2025-11-18 03:53:49.488366 2025-11-18 05:12:35.757198 112 California OrangeGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.524302+00 \N +2227 \N \N \N Goldsmith Distillate Cartridge | Dos-Si-DosGoldsmith ExtractsTHC: 88.4%CBD: 4.34% goldsmith-distillate-cartridge-dos-si-dos \N \N \N \N 88.40 4.34 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-dos-si-dos t f \N 2025-11-18 03:53:49.490736 2025-11-18 04:21:03.527372 2025-11-18 03:53:49.490736 2025-11-18 05:13:09.402515 112 Dos-Si-DosGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.527372+00 \N +2229 \N \N \N Goldsmith Distillate Cartridge | Grand Daddy PurpleGoldsmith ExtractsTHC: 88.24%CBD: 4.01% goldsmith-distillate-cartridge-grand-daddy-purple \N \N \N \N 88.24 4.01 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-grand-daddy-purple t f \N 2025-11-18 03:53:49.498224 2025-11-18 04:21:03.534391 2025-11-18 03:53:49.498224 2025-11-18 05:12:44.843618 112 Grand Daddy PurpleGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.534391+00 \N +2231 \N \N \N Goldsmith Distillate Cartridge | Jack HererGoldsmith ExtractsTHC: 90.06%CBD: 4.3% goldsmith-distillate-cartridge-jack-herer-25955 \N \N \N \N 90.06 4.30 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-jack-herer-25955 t f \N 2025-11-18 03:53:49.50251 2025-11-18 04:21:03.539728 2025-11-18 03:53:49.50251 2025-11-18 05:12:50.919247 112 Jack HererGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.539728+00 \N +3143 \N \N \N MINI PASSION FRUIT CIGARILLOS 3PK swisher-mini-passion-fruit-cigarillos-3pk \N \N \N \N \N \N SWISHER \N \N \N https://best.treez.io/onlinemenu/category/merch/item/0345c0a3-a59a-45c5-93e4-b27ccba32cf1?customerType=ADULT t f \N 2025-11-18 14:42:09.240613 2025-11-18 14:42:09.240613 2025-11-18 14:42:09.240613 2025-11-18 14:42:09.240613 149 \N \N \N \N \N \N 4.00 4.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.240613+00 \N +2232 \N \N \N Goldsmith Distillate Cartridge | Lemon LimeGoldsmith ExtractsTHC: 90.31%CBD: 4.1% goldsmith-distillate-cartridge-lemon-lime \N \N \N \N 90.31 4.10 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-lemon-lime t f \N 2025-11-18 03:53:49.504558 2025-11-18 04:21:03.542384 2025-11-18 03:53:49.504558 2025-11-18 05:12:55.072224 112 Lemon LimeGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.542384+00 \N +2234 \N \N \N Goldsmith Distillate Cartridge | MelonadeGoldsmith ExtractsTHC: 90.84%CBD: 4.18%Special Offer goldsmith-distillate-cartridge-melonade \N \N \N \N 90.84 4.18 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-melonade t f \N 2025-11-18 03:53:49.508628 2025-11-18 04:21:03.547176 2025-11-18 03:53:49.508628 2025-11-18 05:13:01.430894 112 MelonadeGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.547176+00 \N +2235 \N \N \N Goldsmith Distillate Cartridge | Peach RingsGoldsmith ExtractsTHC: 90.7%CBD: 4.5% goldsmith-distillate-cartridge-peach-rings \N \N \N \N 90.70 4.50 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-peach-rings t f \N 2025-11-18 03:53:49.511651 2025-11-18 04:21:03.549619 2025-11-18 03:53:49.511651 2025-11-18 05:13:04.589209 112 Peach RingsGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.549619+00 \N +2237 \N \N \N Goldsmith Distillate Cartridge | Pineapple ExpressGoldsmith ExtractsTHC: 89.8%CBD: 4.4% goldsmith-distillate-cartridge-pineapple-express \N \N \N \N 89.80 4.40 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-pineapple-express t f \N 2025-11-18 03:53:49.517228 2025-11-18 04:21:03.555646 2025-11-18 03:53:49.517228 2025-11-18 05:13:15.759982 112 Pineapple ExpressGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.555646+00 \N +2239 \N \N \N Goldsmith Distillate Cartridge | Sour TangieGoldsmith ExtractsTHC: 88.67%Special Offer goldsmith-distillate-cartridge-sour-tangie \N \N \N \N 88.67 \N Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-sour-tangie t f \N 2025-11-18 03:53:49.522124 2025-11-18 04:21:03.562433 2025-11-18 03:53:49.522124 2025-11-18 05:13:22.479521 112 Sour TangieGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.562433+00 \N +2240 \N \N \N Goldsmith Distillate Cartridge | Strawberry CoughGoldsmith ExtractsTHC: 89.66%CBD: 4.33%Special Offer goldsmith-distillate-cartridge-strawberry-cough-16561 \N \N \N \N 89.66 4.33 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-strawberry-cough-16561 t f \N 2025-11-18 03:53:49.524313 2025-11-18 04:21:03.565331 2025-11-18 03:53:49.524313 2025-11-18 05:13:25.521467 112 Strawberry CoughGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.565331+00 \N +2242 \N \N \N Goldsmith Distillate Syringe | AK-47Goldsmith ExtractsSativa-HybridTHC: 91.02%CBD: 4.45% goldsmith-distillate-syringe-ak-47 \N \N \N \N 91.02 4.45 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-ak-47 t f \N 2025-11-18 03:53:49.529224 2025-11-18 04:21:03.571292 2025-11-18 03:53:49.529224 2025-11-18 05:13:31.584871 112 AK-47Goldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.571292+00 \N +2971 \N \N \N SOUR DIESEL io-extracts-sour-diesel-1g \N \N \N S/I 89.08 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/6295ee97-4256-41ff-b280-566ed6c28f44?customerType=ADULT t f \N 2025-11-18 14:42:08.920023 2025-11-18 14:42:08.920023 2025-11-18 14:42:08.920023 2025-11-18 14:42:08.920023 149 1G \N \N \N \N \N \N 35.00 35.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.920023+00 \N +2272 \N \N \N Halo | Aunt Ellie's | 100mg Classic Brownie | SativaHalo InfusionsSativaTHC: 0.24% halo-aunt-ellie-s-100mg-classic-brownie-sativa \N \N \N \N 0.24 \N Halo Infusions \N https://images.dutchie.com/1f82e1cde5a7639a27da4983697a1250?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-aunt-ellie-s-100mg-classic-brownie-sativa t f \N 2025-11-18 03:54:25.276468 2025-11-18 04:21:46.814889 2025-11-18 03:54:25.276468 2025-11-18 05:15:18.428998 112 \N \N \N {} {} {} 12.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.814889+00 \N +2278 \N \N \N Halo | Chronic Health | 350/350mg 1:1 THC:CBD Pain Relief Ointment 4ozHalo InfusionsTHCTHC: 0.33%CBD: 0.27% halo-chronic-health-350-350mg-1-1-thc-cbd-pain-relief-ointment-4oz \N \N \N \N 0.33 0.27 Halo Infusions \N https://images.dutchie.com/f2043abcd0e9f6cdb1311558d1fabe6e?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-350-350mg-1-1-thc-cbd-pain-relief-ointment-4oz t f \N 2025-11-18 03:54:25.296647 2025-11-18 04:21:46.842023 2025-11-18 03:54:25.296647 2025-11-18 05:15:38.850635 112 350/350mg 1:1 \N \N \N {} {} {} 60.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.842023+00 \N +2223 \N \N \N Goldsmith Distillate Cartridge | AK-47Goldsmith ExtractsSativa-HybridTHC: 91.02%CBD: 4.45%Special Offer goldsmith-distillate-cartridge-ak-47-26412 \N \N \N \N 91.02 4.45 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-ak-47-26412 t f \N 2025-11-18 03:53:49.473677 2025-11-18 04:21:03.507496 2025-11-18 03:53:49.473677 2025-11-18 05:12:20.913556 112 AK-47Goldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.507496+00 \N +2224 \N \N \N Goldsmith Distillate Cartridge | Banana CreamGoldsmith ExtractsTHC: 89.4%CBD: 4.09% goldsmith-distillate-cartridge-banana-cream \N \N \N \N 89.40 4.09 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-banana-cream t f \N 2025-11-18 03:53:49.482918 2025-11-18 04:21:03.517105 2025-11-18 03:53:49.482918 2025-11-18 05:12:23.936725 112 Banana CreamGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.517105+00 \N +2178 \N \N \N Dr. Zodiak Distillate Moonrock Astro Pod | Ghost OGDr. ZodiakTHC: 91.92%CBD: 3.13%Special Offer dr-zodiak-distillate-moonrock-astro-pod-ghost-og \N \N \N \N 91.92 3.13 Dr. Zodiak \N https://images.dutchie.com/9cf8a9443f465f7979d3643a7b1861fe?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-distillate-moonrock-astro-pod-ghost-og t f \N 2025-11-18 03:53:09.242108 2025-11-18 04:19:59.925321 2025-11-18 03:53:09.242108 2025-11-18 05:09:44.235499 112 Ghost OGDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.925321+00 \N +2324 \N \N \N Keef Classic Soda Orange Kush | 10mgKeefHybridTHC: 9.27% keef-classic-soda-orange-kush-10mg-91778 \N \N \N \N 9.27 \N Keef \N https://images.dutchie.com/a53c32176d8cceb8adedab5be6a0aaaa?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-orange-kush-10mg-91778 t f \N 2025-11-18 03:55:31.869596 2025-11-18 04:22:52.98632 2025-11-18 03:55:31.869596 2025-11-18 05:18:24.177287 112 10mgKeef \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.98632+00 \N +2225 \N \N \N Goldsmith Distillate Cartridge | Blackberry KushGoldsmith ExtractsTHC: 89.42%CBD: 4.12%Special Offer goldsmith-distillate-cartridge-blackberry-kush-16992 \N \N \N \N 89.42 4.12 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-blackberry-kush-16992 t f \N 2025-11-18 03:53:49.48604 2025-11-18 04:21:03.520378 2025-11-18 03:53:49.48604 2025-11-18 05:12:27.489239 112 Blackberry KushGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.520378+00 \N +2180 \N \N \N Dr. Zodiak Distillate Moonrock Astro Pod | Nite OwlDr. ZodiakTHC: 92.76%CBD: 3.17%Special Offer dr-zodiak-distillate-moonrock-astro-pod-nite-owl \N \N \N \N 92.76 3.17 Dr. Zodiak \N https://images.dutchie.com/1b22ef900e4dfd3957cfa5c92745279b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-distillate-moonrock-astro-pod-nite-owl t f \N 2025-11-18 03:53:09.246663 2025-11-18 04:19:59.930688 2025-11-18 03:53:09.246663 2025-11-18 05:09:50.353768 112 Nite OwlDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.930688+00 \N +2181 \N \N \N Dr. Zodiak Distillate Moonrock Cart | Lion HeartDr. ZodiakTHC: 91.16%CBD: 3.15%Special Offer dr-zodiak-distillate-moonrock-cart-lion-heart \N \N \N \N 91.16 3.15 Dr. Zodiak \N https://images.dutchie.com/vaporizer-stock-1-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-distillate-moonrock-cart-lion-heart t f \N 2025-11-18 03:53:09.248817 2025-11-18 04:19:59.933206 2025-11-18 03:53:09.248817 2025-11-18 05:09:53.491138 112 Lion HeartDr. Zodiak \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.933206+00 \N +2495 \N \N \N Sticky Saguaro Flower Mylar | ShibuiSticky SaguaroSativa-HybridTHC: 20.39%Special Offer sticky-saguaro-flower-mylar-shibui \N \N \N \N 20.39 \N Sticky Saguaro \N https://images.dutchie.com/f804e3be30a506558e6accbd5dd60aba?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-flower-mylar-shibui t f \N 2025-11-18 03:58:47.572373 2025-11-18 04:26:27.103561 2025-11-18 03:58:47.572373 2025-11-18 05:28:13.033677 112 ShibuiSticky Saguaro \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.103561+00 \N +2483 \N \N \N Space Rocks Infused Flower | Berry PieSpace RocksSativaTHC: 49.06% space-rocks-infused-flower-berry-pie \N \N \N \N 49.06 \N Space Rocks \N https://images.dutchie.com/143dbd241e177058c61ca37c7c87cef4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-infused-flower-berry-pie t f \N 2025-11-18 03:58:41.848116 2025-11-18 04:26:16.002454 2025-11-18 03:58:41.848116 2025-11-18 05:27:26.096242 112 Berry PieSpace Rocks \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.002454+00 \N +2496 \N \N \N Sticky Saguaro Flower Mylar | TropkickSticky SaguaroIndica-HybridTHC: 23.44%Special Offer sticky-saguaro-flower-mylar-tropkick \N \N \N \N 23.44 \N Sticky Saguaro \N https://images.dutchie.com/9bfecdd63f409da6590a686a08c33384?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-flower-mylar-tropkick t f \N 2025-11-18 03:58:47.575157 2025-11-18 04:26:27.106557 2025-11-18 03:58:47.575157 2025-11-18 05:28:16.128676 112 TropkickSticky Saguaro \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.106557+00 \N +2981 \N \N \N GOLDEN TANGIE cure-injoy-golden-tangie-1g \N \N \N S/I 79.35 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/6b32d153-2677-4bdb-8195-5bd4bfb05413?customerType=ADULT t f \N 2025-11-18 14:42:08.94605 2025-11-18 14:42:08.94605 2025-11-18 14:42:08.94605 2025-11-18 14:42:08.94605 149 1G \N \N \N \N \N \N 46.00 46.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.94605+00 \N +2982 \N \N \N PURPLE URKLE cure-injoy-purple-urkle-1g \N \N \N INDICA 83.20 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/d81aca68-6a7c-49e1-8074-80a0a9446bbb?customerType=ADULT t f \N 2025-11-18 14:42:08.94847 2025-11-18 14:42:08.94847 2025-11-18 14:42:08.94847 2025-11-18 14:42:08.94847 149 1G \N \N \N \N \N \N 46.00 46.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.94847+00 \N +2452 \N \N \N Session Live Resin AIO | Apple Tartz x Lemon Cherry GelatoSessionHybridTHC: 78.53%CBD: 0.16%Special Offer session-live-resin-aio-apple-tartz-x-lemon-cherry-gelato \N \N \N \N 78.53 0.16 Session \N https://images.dutchie.com/2103e7652bba4a1491f9bad87be2fb3c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/session-live-resin-aio-apple-tartz-x-lemon-cherry-gelato t f \N 2025-11-18 03:57:57.44902 2025-11-18 04:25:22.216854 2025-11-18 03:57:57.44902 2025-11-18 05:25:31.800909 112 Apple Tartz x Lemon Cherry GelatoSession \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:22.216854+00 \N +2997 \N \N \N RED PLUM dime-red-plum-2g \N \N \N INDICA 91.68 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/5a5da5b6-a2f3-43aa-b068-ff1a9bce0cf6?customerType=ADULT t f \N 2025-11-18 14:42:08.978808 2025-11-18 14:42:08.978808 2025-11-18 14:42:08.978808 2025-11-18 14:42:08.978808 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.978808+00 \N +2488 \N \N \N Space Rocks | Space Rocketz Infused Pre-Roll | Apples & BananasSpace RocksHybridTHC: 49.81% space-rocks-space-rocketz-infused-pre-roll-apples-bananas \N \N \N \N 49.81 \N Space Rocks \N https://images.dutchie.com/3cd11b47077dc9d0b2e3ddd22ad1157c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-space-rocketz-infused-pre-roll-apples-bananas t f \N 2025-11-18 03:58:41.861444 2025-11-18 04:26:16.012446 2025-11-18 03:58:41.861444 2025-11-18 05:27:46.180301 112 Apples & BananasSpace Rocks \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.012446+00 \N +2983 \N \N \N FUSHIA LIVE RESIN green-dot-labs-fushia-live-resin-1g \N \N \N HYBRID \N \N GREEN DOT LABS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/c849818d-dc63-47e8-8f17-5c82eac57439?customerType=ADULT t f \N 2025-11-18 14:42:08.950752 2025-11-18 14:42:08.950752 2025-11-18 14:42:08.950752 2025-11-18 14:42:08.950752 149 1G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.950752+00 \N +2984 \N \N \N FRIED APPLES source-one-meds-fried-apples-1g \N \N \N HYBRID 80.87 \N SOURCE ONE MEDS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/030376ab-4f76-4cb4-b1ef-3526a99a38e1?customerType=ADULT t f \N 2025-11-18 14:42:08.952942 2025-11-18 14:42:08.952942 2025-11-18 14:42:08.952942 2025-11-18 14:42:08.952942 149 1G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.952942+00 \N +2985 \N \N \N MAPLE DUNKS source-one-meds-maple-dunks-1g \N \N \N INDICA 85.44 \N SOURCE ONE MEDS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/d3adcd00-6381-4180-907f-aaa42ab78bb5?customerType=ADULT t f \N 2025-11-18 14:42:08.9557 2025-11-18 14:42:08.9557 2025-11-18 14:42:08.9557 2025-11-18 14:42:08.9557 149 1G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.9557+00 \N +2986 \N \N \N SHERBERT ICC source-one-meds-sherbert-icc-1g \N \N \N INDICA 89.94 \N SOURCE ONE MEDS \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/56ef966d-a5ac-4c93-ac8c-1c2881092e1f?customerType=ADULT t f \N 2025-11-18 14:42:08.958594 2025-11-18 14:42:08.958594 2025-11-18 14:42:08.958594 2025-11-18 14:42:08.958594 149 1G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.958594+00 \N +2987 \N \N \N GUAVA LAVA cure-injoy-guava-lava-1g \N \N \N INDICA 84.49 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/1129a212-ee30-4afa-99bd-102e9d72a410?customerType=ADULT t f \N 2025-11-18 14:42:08.961225 2025-11-18 14:42:08.961225 2025-11-18 14:42:08.961225 2025-11-18 14:42:08.961225 149 1G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.961225+00 \N +2988 \N \N \N MANGO TANGO cure-injoy-mango-tango-1g \N \N \N HYBRID 84.37 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/ce4f4375-1455-4449-8603-08cdcda769cc?customerType=ADULT t f \N 2025-11-18 14:42:08.96321 2025-11-18 14:42:08.96321 2025-11-18 14:42:08.96321 2025-11-18 14:42:08.96321 149 1G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.96321+00 \N +2989 \N \N \N PASSION FRUIT cure-injoy-passion-fruit-1g \N \N \N SATIVA 86.08 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/4b0dcd2d-22d1-4c54-b5ab-85da70797285?customerType=ADULT t f \N 2025-11-18 14:42:08.965042 2025-11-18 14:42:08.965042 2025-11-18 14:42:08.965042 2025-11-18 14:42:08.965042 149 1G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.965042+00 \N +2990 \N \N \N RAINBOW BREEZE cure-injoy-rainbow-breeze-1g \N \N \N INDICA 84.59 \N CURE INJOY \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/c1105129-f0d6-4c5b-a1d2-695ec6e0cdce?customerType=ADULT t f \N 2025-11-18 14:42:08.966643 2025-11-18 14:42:08.966643 2025-11-18 14:42:08.966643 2025-11-18 14:42:08.966643 149 1G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.966643+00 \N +2087 \N \N \N Collective Distillate Cartridge | Grapefruit KushCollectiveTHC: 81.75%CBD: 2.78%Special Offer collective-distillate-cartridge-grapefruit-kush-53200 \N \N \N \N 81.75 2.78 Collective \N https://images.dutchie.com/781049e3aa6c6ca21a4b8c9beffcb80f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/collective-distillate-cartridge-grapefruit-kush-53200 t f \N 2025-11-18 03:51:57.551651 2025-11-18 04:18:40.765345 2025-11-18 03:51:57.551651 2025-11-18 05:04:16.304531 112 Grapefruit KushCollective \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:40.765345+00 \N +2183 \N \N \N Dr. Zodiak Infused Blunt | Hybrid BlendDr. ZodiakHybridTHC: 46.27%CBD: 0.45% dr-zodiak-infused-blunt-hybrid-blend \N \N \N Hybrid 46.27 0.45 Dr. Zodiak \N https://images.dutchie.com/flower-stock-3-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-infused-blunt-hybrid-blend t f \N 2025-11-18 03:53:09.252897 2025-11-18 04:19:59.93858 2025-11-18 03:53:09.252897 2025-11-18 05:10:16.768171 112 \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.93858+00 \N +2184 \N \N \N Dr. Zodiak Infused Flower Snowballz | Bobby BlueDr. ZodiakTHC: 52.51%Special Offer dr-zodiak-infused-flower-snowballz-bobby-blue \N \N \N \N 52.51 \N Dr. Zodiak \N https://images.dutchie.com/flower-stock-12-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-infused-flower-snowballz-bobby-blue t f \N 2025-11-18 03:53:09.254702 2025-11-18 04:19:59.940929 2025-11-18 03:53:09.254702 2025-11-18 05:10:02.91381 112 Bobby BlueDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.940929+00 \N +2186 \N \N \N Dr. Zodiak Infused Flower Snowballz | Razzle DazzleDr. ZodiakTHC: 53.87%Special Offer dr-zodiak-infused-flower-snowballz-razzle-dazzle \N \N \N \N 53.87 \N Dr. Zodiak \N https://images.dutchie.com/flower-stock-4-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-infused-flower-snowballz-razzle-dazzle t f \N 2025-11-18 03:53:09.259641 2025-11-18 04:19:59.945127 2025-11-18 03:53:09.259641 2025-11-18 05:10:08.89013 112 Razzle DazzleDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.945127+00 \N +2187 \N \N \N Dr. Zodiak | 4.2g Infused Flower Buckshots | GhostDr. ZodiakTHC: 89.79%CBD: 1.72% dr-zodiak-4-2g-infused-flower-buckshots-ghost \N \N \N \N 89.79 1.72 Dr. Zodiak \N https://images.dutchie.com/flower-stock-14-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-4-2g-infused-flower-buckshots-ghost t f \N 2025-11-18 03:53:09.261839 2025-11-18 04:19:59.947349 2025-11-18 03:53:09.261839 2025-11-18 05:10:11.903859 112 GhostDr. Zodiak \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.947349+00 \N +2189 \N \N \N Dr. Zodiak | 4.2g Infused Flower Buckshots | Wedding CakeDr. ZodiakIndica-HybridTHC: 64.15% dr-zodiak-4-2g-infused-flower-buckshots-wedding-cake \N \N \N \N 64.15 \N Dr. Zodiak \N https://images.dutchie.com/flower-stock-11-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-4-2g-infused-flower-buckshots-wedding-cake t f \N 2025-11-18 03:53:09.265988 2025-11-18 04:19:59.951829 2025-11-18 03:53:09.265988 2025-11-18 05:10:22.927259 112 Wedding CakeDr. Zodiak \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.951829+00 \N +2244 \N \N \N Goldsmith Distillate Syringe | California OrangeGoldsmith ExtractsHybridTHC: 89.35% goldsmith-distillate-syringe-california-orange \N \N \N \N 89.35 \N Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-california-orange t f \N 2025-11-18 03:53:49.534049 2025-11-18 04:21:03.577133 2025-11-18 03:53:49.534049 2025-11-18 05:13:37.709671 112 California OrangeGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.577133+00 \N +2995 \N \N \N KACTUS CHILL dime-kactus-chill-2g \N \N \N HYBRID 88.26 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/eabe872b-f19d-4ff4-af72-94e03b3a3504?customerType=ADULT t f \N 2025-11-18 14:42:08.975191 2025-11-18 14:42:08.975191 2025-11-18 14:42:08.975191 2025-11-18 14:42:08.975191 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.975191+00 \N +2996 \N \N \N PINK LEMON HAZE dime-pink-lemon-haze-2g \N \N \N HYBRID \N \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/e5087dc3-489a-4921-bf0a-02fdbf281e3a?customerType=ADULT t f \N 2025-11-18 14:42:08.977055 2025-11-18 14:42:08.977055 2025-11-18 14:42:08.977055 2025-11-18 14:42:08.977055 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.977055+00 \N +2998 \N \N \N SOUR GRAPES dime-sour-grapes-2g \N \N \N HYBRID 91.49 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/2d7f71f8-dd5d-4916-a693-e0979f8c503e?customerType=ADULT t f \N 2025-11-18 14:42:08.980553 2025-11-18 14:42:08.980553 2025-11-18 14:42:08.980553 2025-11-18 14:42:08.980553 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.980553+00 \N +2999 \N \N \N STRAWBERRY COUGH dime-strawberry-cough-2g \N \N \N SATIVA 94.34 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/48a20f71-baa9-4fb9-bc56-c3af6a35e0e0?customerType=ADULT t f \N 2025-11-18 14:42:08.982202 2025-11-18 14:42:08.982202 2025-11-18 14:42:08.982202 2025-11-18 14:42:08.982202 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.982202+00 \N +3000 \N \N \N WEDDING CAKE dime-wedding-cake-2g \N \N \N HYBRID 87.75 \N DIME \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/73e16376-36fb-4d3c-ae64-09f2e6a19a66?customerType=ADULT t f \N 2025-11-18 14:42:08.983903 2025-11-18 14:42:08.983903 2025-11-18 14:42:08.983903 2025-11-18 14:42:08.983903 149 2G \N \N \N \N \N \N 65.00 65.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.983903+00 \N +3001 \N \N \N GMO . easy-tiger-gmo-5g \N \N \N I/S 80.30 \N EASY TIGER \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/2716f2cf-08aa-470a-bfcb-c621db961842?customerType=ADULT t f \N 2025-11-18 14:42:08.985766 2025-11-18 14:42:08.985766 2025-11-18 14:42:08.985766 2025-11-18 14:42:08.985766 149 5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.985766+00 \N +3002 \N \N \N PETE'S PEACHES . easy-tiger-pete-s-peaches-5g \N \N \N I/S 76.06 \N EASY TIGER \N \N \N https://best.treez.io/onlinemenu/category/cartridge/item/fbf5f6d3-ddb5-4d45-9c3a-7f315fc8cad6?customerType=ADULT t f \N 2025-11-18 14:42:08.987493 2025-11-18 14:42:08.987493 2025-11-18 14:42:08.987493 2025-11-18 14:42:08.987493 149 5G \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.987493+00 \N +3003 \N \N \N AMY & AL'S BROWNIE 100MG amy-al-s-amy-al-s-brownie-100mg \N \N \N HYBRID \N \N AMY & AL'S \N \N \N https://best.treez.io/onlinemenu/category/edible/item/063d5e60-15e4-48c8-8c6e-b90f5e3ef1db?customerType=ADULT t f \N 2025-11-18 14:42:08.989203 2025-11-18 14:42:08.989203 2025-11-18 14:42:08.989203 2025-11-18 14:42:08.989203 149 \N \N \N \N \N \N 15.00 15.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.989203+00 \N +2192 \N \N \N Drip Top-Shelf Flower | Waffle ConedripHybridTHC: 20.53% drip-top-shelf-flower-waffle-cone-20293 \N \N \N \N 20.53 \N drip \N https://images.dutchie.com/c70afecc0c29bb3431f7fa2b010d88fe?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-top-shelf-flower-waffle-cone-20293 t f \N 2025-11-18 03:53:11.787525 2025-11-18 04:20:03.381134 2025-11-18 03:53:11.787525 2025-11-18 05:10:32.261433 112 Waffle Conedrip \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:03.381134+00 \N +2246 \N \N \N Goldsmith Distillate Syringe | Mango KushGoldsmith ExtractsIndica-HybridTHC: 89.14%CBD: 4.32% goldsmith-distillate-syringe-mango-kush-5943 \N \N \N \N 89.14 4.32 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-mango-kush-5943 t f \N 2025-11-18 03:53:49.538315 2025-11-18 04:21:03.583223 2025-11-18 03:53:49.538315 2025-11-18 05:13:45.760253 112 Mango KushGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.583223+00 \N +2247 \N \N \N Goldsmith Distillate Syringe | Sour DieselGoldsmith ExtractsSativa-HybridTHC: 90.05% goldsmith-distillate-syringe-sour-diesel \N \N \N \N 90.05 \N Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-sour-diesel t f \N 2025-11-18 03:53:49.540133 2025-11-18 04:21:03.586004 2025-11-18 03:53:49.540133 2025-11-18 05:13:48.711242 112 Sour DieselGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.586004+00 \N +2249 \N \N \N Goldsmith | 3-Pack Iced Out Infused Pre-Roll | Blueberry HeadbandGoldsmith ExtractsHybridTHC: 35.52% goldsmith-3-pack-iced-out-infused-pre-roll-blueberry-headband \N \N \N \N 35.52 \N Goldsmith Extracts \N https://images.dutchie.com/6373195102c06cdfc99ebed314d85d7a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-3-pack-iced-out-infused-pre-roll-blueberry-headband t f \N 2025-11-18 03:53:49.544517 2025-11-18 04:21:03.591334 2025-11-18 03:53:49.544517 2025-11-18 05:13:54.697594 112 Blueberry HeadbandGoldsmith Extracts \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.591334+00 \N +3011 \N \N \N RSO CHOCOLATE BAR COOKIES & CREAM 100MG sublime-rso-chocolate-bar-cookies-cream-100mg \N \N \N HYBRID \N \N SUBLIME \N \N \N https://best.treez.io/onlinemenu/category/edible/item/4cc495fc-9d5b-432f-8cd7-f472d1a6663c?customerType=ADULT t f \N 2025-11-18 14:42:09.003491 2025-11-18 14:42:09.003491 2025-11-18 14:42:09.003491 2025-11-18 14:42:09.003491 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.003491+00 \N +3012 \N \N \N RSO DARK CHOCOLATE BAR 100MG sublime-rso-dark-chocolate-bar-100mg \N \N \N HYBRID \N \N SUBLIME \N \N \N https://best.treez.io/onlinemenu/category/edible/item/a8a06f69-c479-41db-8fe5-80f3396f9e0c?customerType=ADULT t f \N 2025-11-18 14:42:09.005021 2025-11-18 14:42:09.005021 2025-11-18 14:42:09.005021 2025-11-18 14:42:09.005021 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.005021+00 \N +2094 \N \N \N Connected Flower Jar | AmbroziaConnected CannabisHybridTHC: 23.54% connected-flower-jar-ambrozia-63682 \N \N \N \N 23.54 \N Connected Cannabis \N https://images.dutchie.com/25d6b8dc773d230fd421abca00402810?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-ambrozia-63682 t f \N 2025-11-18 03:52:02.323126 2025-11-18 04:18:48.516044 2025-11-18 03:52:02.323126 2025-11-18 05:04:45.121964 112 AmbroziaConnected Cannabis \N \N \N {} {} {} 120.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.516044+00 \N +2095 \N \N \N Connected Flower Jar | GelonadeConnected CannabisSativa-HybridTHC: 20.65%Special Offer connected-flower-jar-gelonade \N \N \N \N 20.65 \N Connected Cannabis \N https://images.dutchie.com/39fe71006a6d64088d447ffb8db9cdf0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-gelonade t f \N 2025-11-18 03:52:02.326158 2025-11-18 04:18:48.517994 2025-11-18 03:52:02.326158 2025-11-18 05:04:48.260035 112 GelonadeConnected Cannabis \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.517994+00 \N +3013 \N \N \N BLACK CHERRY VANILLA 100MG 420-to-yuma-black-cherry-vanilla-100mg \N \N \N HYBRID \N \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/edible/item/acc9800d-a27c-4743-a1e4-c2ae5cc21e76?customerType=ADULT t f \N 2025-11-18 14:42:09.006723 2025-11-18 14:42:09.006723 2025-11-18 14:42:09.006723 2025-11-18 14:42:09.006723 149 \N \N \N \N \N \N 14.00 14.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.006723+00 \N +3014 \N \N \N BLOOD ORANGE RASBERRY APPLE 100MG 420-to-yuma-blood-orange-rasberry-apple-100mg \N \N \N HYBRID \N \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/edible/item/3fc2a1ab-7f18-4645-ae0e-03c92bdf1ecc?customerType=ADULT t f \N 2025-11-18 14:42:09.008237 2025-11-18 14:42:09.008237 2025-11-18 14:42:09.008237 2025-11-18 14:42:09.008237 149 \N \N \N \N \N \N 14.00 14.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.008237+00 \N +3015 \N \N \N WILDBERRY LEMONADE 100MG 420-to-yuma-wildberry-lemonade-100mg \N \N \N HYBRID \N \N 420 TO YUMA \N \N \N https://best.treez.io/onlinemenu/category/edible/item/4bc93950-66a7-4ac8-981e-50af7cdb7cc9?customerType=ADULT t f \N 2025-11-18 14:42:09.009958 2025-11-18 14:42:09.009958 2025-11-18 14:42:09.009958 2025-11-18 14:42:09.009958 149 \N \N \N \N \N \N 14.00 14.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.009958+00 \N +2251 \N \N \N Green Dot Labs Flower Jar | Cherry Lime SodaGreen Dot LabsHybridTHC: 23.72%CBD: 0.05% green-dot-labs-flower-jar-cherry-lime-soda \N \N \N \N 23.72 0.05 Green Dot Labs \N https://images.dutchie.com/47744bf7f3d0b88bdf58927cd15c9ca4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-flower-jar-cherry-lime-soda t f \N 2025-11-18 03:53:56.084317 2025-11-18 04:21:11.211526 2025-11-18 03:53:56.084317 2025-11-18 05:14:02.314877 112 Cherry Lime SodaGreen Dot Labs \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:11.211526+00 \N +3016 \N \N \N BLOOD ORANGE 100 MG ROSIN baked-bros-blood-orange-100-mg-rosin \N \N \N HYBRID \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/c182e9ec-67cf-4d60-a377-f0d2f193532f?customerType=ADULT t f \N 2025-11-18 14:42:09.011598 2025-11-18 14:42:09.011598 2025-11-18 14:42:09.011598 2025-11-18 14:42:09.011598 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.011598+00 \N +2275 \N \N \N Halo | Chronic Health | 100mg 1:1 Sleep Well TinctureHalo InfusionsTHC: 0.25%CBD: 0.28% halo-chronic-health-100mg-1-1-sleep-well-tincture \N \N \N \N 0.25 0.28 Halo Infusions \N https://images.dutchie.com/Edibles.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-100mg-1-1-sleep-well-tincture t f \N 2025-11-18 03:54:25.289871 2025-11-18 04:21:46.833236 2025-11-18 03:54:25.289871 2025-11-18 05:15:29.736337 112 100mg 1:1 Sleep Well TinctureHalo Infusions \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.833236+00 \N +3008 \N \N \N POTPOTS DARK CHOCOLATE 100MG lion-labs-potpots-dark-chocolate-100mg \N \N \N HYBRID \N \N LION LABS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/94662e28-a17c-4152-9ac2-9947e60bd291?customerType=ADULT t f \N 2025-11-18 14:42:08.998172 2025-11-18 14:42:08.998172 2025-11-18 14:42:08.998172 2025-11-18 14:42:08.998172 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.998172+00 \N +2277 \N \N \N Halo | Chronic Health | 1400mg THC Pain Relief Ointment 4ozHalo InfusionsTHCTHC: 1481.26 mgCBD: 10.5 mg halo-chronic-health-1400mg-thc-pain-relief-ointment-4oz \N \N \N \N \N \N Halo Infusions \N https://images.dutchie.com/f2043abcd0e9f6cdb1311558d1fabe6e?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-1400mg-thc-pain-relief-ointment-4oz t f \N 2025-11-18 03:54:25.294129 2025-11-18 04:21:46.83948 2025-11-18 03:54:25.294129 2025-11-18 05:15:35.885195 112 1400mg \N \N \N {} {} {} 80.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.83948+00 \N +3009 \N \N \N POTPOTS MILK CHOCOLATE 100MG lion-labs-potpots-milk-chocolate-100mg \N \N \N HYBRID \N \N LION LABS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/bd84d9cb-6ecb-45f7-8846-6c6794208e1e?customerType=ADULT t f \N 2025-11-18 14:42:08.999908 2025-11-18 14:42:08.999908 2025-11-18 14:42:08.999908 2025-11-18 14:42:08.999908 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:08.999908+00 \N +3017 \N \N \N HAPPY GUMMIES 100MG POMEGRANATE NECTAR baked-bros-happy-gummies-100mg-pomegranate-nectar \N \N \N SATIVA \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/f6f2063e-b86d-43b5-b9f6-b1a66198080f?customerType=ADULT t f \N 2025-11-18 14:42:09.013292 2025-11-18 14:42:09.013292 2025-11-18 14:42:09.013292 2025-11-18 14:42:09.013292 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.013292+00 \N +3018 \N \N \N SLEEPY GUMMIES 100MG BLACKBERRY ACAI baked-bros-sleepy-gummies-100mg-blackberry-acai \N \N \N INDICA \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/fdab5df5-9b56-42d3-afa9-af079f353e55?customerType=ADULT t f \N 2025-11-18 14:42:09.014998 2025-11-18 14:42:09.014998 2025-11-18 14:42:09.014998 2025-11-18 14:42:09.014998 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.014998+00 \N +2465 \N \N \N SIP Elixir | 100mg Nano Beverage + Caffeine | Blue RazzSipTHC: 0.21%CBD: 0.01% sip-elixir-100mg-nano-beverage-caffeine-blue-razz \N \N \N \N 0.21 0.01 Sip \N https://images.dutchie.com/cc6e7f88514347996843d2d1970a60e8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-caffeine-blue-razz t f \N 2025-11-18 03:58:31.098189 2025-11-18 04:25:51.866604 2025-11-18 03:58:31.098189 2025-11-18 05:26:21.425128 112 Blue RazzSip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.866604+00 \N +3019 \N \N \N UNWIND HUCKLEBERRY PUNCH 100MG 2:2:1:1 baked-bros-unwind-huckleberry-punch-100mg-2-2-1-1 \N \N \N HYBRID \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/d3d640c7-5623-48de-9b43-61f76cdd3f0e?customerType=ADULT t f \N 2025-11-18 14:42:09.016806 2025-11-18 14:42:09.016806 2025-11-18 14:42:09.016806 2025-11-18 14:42:09.016806 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.016806+00 \N +2323 \N \N \N Keef Classic Soda Orange Kush XTREME | 100mgKeefHybridTHC: 0.03% keef-classic-soda-orange-kush-xtreme-100mg-85098 \N \N \N \N 0.03 \N Keef \N https://images.dutchie.com/f7bd1de192f552b32e0f3221fbe36d21?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-orange-kush-xtreme-100mg-85098 t f \N 2025-11-18 03:55:31.866693 2025-11-18 04:22:52.98282 2025-11-18 03:55:31.866693 2025-11-18 05:18:20.984832 112 100mgKeef \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.98282+00 \N +2326 \N \N \N Keef Classic Soda Original Cola | 10mgKeefHybridTHC: 10.02 mg keef-classic-soda-original-cola-10mg-80418 \N \N \N \N \N \N Keef \N https://images.dutchie.com/9ab2ebd6c02efc77a816a786386f5847?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-original-cola-10mg-80418 t f \N 2025-11-18 03:55:31.874249 2025-11-18 04:22:52.991895 2025-11-18 03:55:31.874249 2025-11-18 05:18:30.350726 112 10mgKeef \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.991895+00 \N +2328 \N \N \N Keef Classic Soda Pineapple X-Press | 10mgKeefHybridTHC: 9.24 mg keef-classic-soda-pineapple-x-press-10mg-31786 \N \N \N \N \N \N Keef \N https://images.dutchie.com/7c04c644fc349c00c982456ad6553d7f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-pineapple-x-press-10mg-31786 t f \N 2025-11-18 03:55:31.878653 2025-11-18 04:22:52.997961 2025-11-18 03:55:31.878653 2025-11-18 05:18:36.513317 112 10mgKeef \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.997961+00 \N +2329 \N \N \N Cookie Blend Diamond-Infused Joint (1g)LeafersIndica-HybridTHC: 42.59% cookie-blend-diamond-infused-joint-1g \N \N \N \N 42.59 \N Leafers \N https://images.dutchie.com/9f67af30140844fce7ffc83bf5f41af1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cookie-blend-diamond-infused-joint-1g t f \N 2025-11-18 03:55:34.548844 2025-11-18 04:22:59.099279 2025-11-18 03:55:34.548844 2025-11-18 05:18:39.646966 112 \N \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:59.099279+00 \N +2331 \N \N \N Exotic Blend Diamond-Infused Joint (1g)LeafersHybridTHC: 49.76% exotic-blend-diamond-infused-joint-1g \N \N \N \N 49.76 \N Leafers \N https://images.dutchie.com/2c246426f3b7e3cc94424bb452bf362d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/exotic-blend-diamond-infused-joint-1g t f \N 2025-11-18 03:55:34.560596 2025-11-18 04:22:59.10864 2025-11-18 03:55:34.560596 2025-11-18 05:18:45.7676 112 \N \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:59.10864+00 \N +2332 \N \N \N Fruit Blend Diamond-Infused Joint (1g)LeafersSativa-HybridTHC: 46.83% fruit-blend-diamond-infused-joint-1g \N \N \N \N 46.83 \N Leafers \N https://images.dutchie.com/eb25f626ab3898978a401dd8f3cbd8ad?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/fruit-blend-diamond-infused-joint-1g t f \N 2025-11-18 03:55:34.563313 2025-11-18 04:22:59.111172 2025-11-18 03:55:34.563313 2025-11-18 05:18:50.824129 112 \N \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:59.111172+00 \N +2334 \N \N \N Sativa Blend Diamond-Infused Joint (1g)LeafersSativaTHC: 47.45% sativa-blend-diamond-infused-joint-1g \N \N \N Sativa 47.45 \N Leafers \N https://images.dutchie.com/2faa9c54d2eb04561335bea8db8c399c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sativa-blend-diamond-infused-joint-1g t f \N 2025-11-18 03:55:34.56926 2025-11-18 04:22:59.115393 2025-11-18 03:55:34.56926 2025-11-18 05:18:56.763994 112 \N \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:59.115393+00 \N +2336 \N \N \N Legends Doinks | 2-Pack x 1g Pre-roll | Fire CrotchLegendsHybridTHC: 20.51%CBD: 0.03% legends-doinks-2-pack-x-1g-pre-roll-fire-crotch \N \N \N \N 20.51 0.03 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-doinks-2-pack-x-1g-pre-roll-fire-crotch t f \N 2025-11-18 03:55:36.528657 2025-11-18 04:23:01.342338 2025-11-18 03:55:36.528657 2025-11-18 05:19:02.817583 112 Fire CrotchLegends \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.342338+00 \N +2337 \N \N \N Legends Doinks | 2-Pack x 1g Pre-roll | Oil TankerLegendsIndica-HybridTHC: 21.1%CBD: 0.03% legends-doinks-2-pack-x-1g-pre-roll-oil-tanker \N \N \N \N 21.10 0.03 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-doinks-2-pack-x-1g-pre-roll-oil-tanker t f \N 2025-11-18 03:55:36.530924 2025-11-18 04:23:01.344559 2025-11-18 03:55:36.530924 2025-11-18 05:19:09.729928 112 Oil TankerLegends \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.344559+00 \N +2339 \N \N \N Legends Flower Jar | Chem D WreckerLegendsIndica-HybridTHC: 28.83%CBD: 0.04%Special Offer legends-flower-jar-chem-d-wrecker \N \N \N \N 28.83 0.04 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-chem-d-wrecker t f \N 2025-11-18 03:55:36.535678 2025-11-18 04:23:01.348247 2025-11-18 03:55:36.535678 2025-11-18 05:19:15.773326 112 Chem D WreckerLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.348247+00 \N +2340 \N \N \N Legends Flower Jar | Chin CheckLegendsTHC: 27.43%CBD: 0.04%Special Offer legends-flower-jar-chin-check \N \N \N \N 27.43 0.04 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-chin-check t f \N 2025-11-18 03:55:36.537952 2025-11-18 04:23:01.350202 2025-11-18 03:55:36.537952 2025-11-18 05:19:18.754579 112 Chin CheckLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.350202+00 \N +2342 \N \N \N Legends Flower Jar | Honey BananaLegendsIndica-HybridTHC: 24.59%CBD: 0.04%Special Offer legends-flower-jar-honey-banana \N \N \N \N 24.59 0.04 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-honey-banana t f \N 2025-11-18 03:55:36.542614 2025-11-18 04:23:01.354262 2025-11-18 03:55:36.542614 2025-11-18 05:19:27.230952 112 Honey BananaLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.354262+00 \N +3020 \N \N \N VERY BERRY HASH ROSIN 100MG baked-bros-very-berry-hash-rosin-100mg \N \N \N HYBRID \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/40d15e15-c7d1-4ced-8a83-0149bdc52118?customerType=ADULT t f \N 2025-11-18 14:42:09.018393 2025-11-18 14:42:09.018393 2025-11-18 14:42:09.018393 2025-11-18 14:42:09.018393 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.018393+00 \N +2346 \N \N \N Legends Flower Jar | Sunset SherbLegendsIndica-HybridTHC: 24.8%CBD: 0.04%Special Offer legends-flower-jar-sunset-sherb \N \N \N \N 24.80 0.04 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-sunset-sherb t f \N 2025-11-18 03:55:36.550688 2025-11-18 04:23:01.36147 2025-11-18 03:55:36.550688 2025-11-18 05:19:39.406012 112 Sunset SherbLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.36147+00 \N +2350 \N \N \N Lunch Box Hash Rosin Caviar Pre Roll | Cherry TropicsLunch BoxHybridTHC: 72.88%CBD: 0.14% lunch-box-hash-rosin-caviar-pre-roll-cherry-tropics \N \N \N \N 72.88 0.14 Lunch Box \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-hash-rosin-caviar-pre-roll-cherry-tropics t f \N 2025-11-18 03:55:48.830445 2025-11-18 04:23:26.308887 2025-11-18 03:55:48.830445 2025-11-18 05:19:52.233366 112 Cherry TropicsLunch Box \N \N \N {} {} {} 65.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:26.308887+00 \N +2353 \N \N \N Mac Pharms Distillate AIO | Apple MintzMac PharmsTHC: 87.32%CBD: 0.79%Special Offer mac-pharms-distillate-aio-apple-mintz \N \N \N \N 87.32 0.79 Mac Pharms \N https://images.dutchie.com/800d3faf9e8381e57a1db37415f72173?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-distillate-aio-apple-mintz t f \N 2025-11-18 03:56:07.585272 2025-11-18 04:23:28.788618 2025-11-18 03:56:07.585272 2025-11-18 05:20:05.967137 112 Apple MintzMac Pharms \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:28.788618+00 \N +2354 \N \N \N Mac Pharms Distillate AIO | Bubblegum PoppersMac PharmsTHC: 88.07%CBD: 0.8%Special Offer mac-pharms-distillate-aio-bubblegum-poppers \N \N \N \N 88.07 0.80 Mac Pharms \N https://images.dutchie.com/800d3faf9e8381e57a1db37415f72173?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-distillate-aio-bubblegum-poppers t f \N 2025-11-18 03:56:07.599661 2025-11-18 04:23:28.796653 2025-11-18 03:56:07.599661 2025-11-18 05:20:09.134125 112 Bubblegum PoppersMac Pharms \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:28.796653+00 \N +2356 \N \N \N Mac Pharms Live Rosin AIO | Baby Z'sMac PharmsTHC: 84.77% mac-pharms-live-rosin-aio-baby-z-s \N \N \N \N 84.77 \N Mac Pharms \N https://images.dutchie.com/800d3faf9e8381e57a1db37415f72173?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-live-rosin-aio-baby-z-s t f \N 2025-11-18 03:56:07.607728 2025-11-18 04:23:28.803696 2025-11-18 03:56:07.607728 2025-11-18 05:20:15.168894 112 Baby Z'sMac Pharms \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:28.803696+00 \N +2357 \N \N \N Mac Pharms Live Rosin AIO | Blue RizzMac PharmsTHC: 80.59% mac-pharms-live-rosin-aio-blue-rizz \N \N \N \N 80.59 \N Mac Pharms \N https://images.dutchie.com/800d3faf9e8381e57a1db37415f72173?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-live-rosin-aio-blue-rizz t f \N 2025-11-18 03:56:07.611367 2025-11-18 04:23:28.806782 2025-11-18 03:56:07.611367 2025-11-18 05:20:18.182356 112 Blue RizzMac Pharms \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:28.806782+00 \N +2359 \N \N \N Mac Pharms Live Rosin AIO | Strawberry GagMac PharmsTHC: 81.23% mac-pharms-live-rosin-aio-strawberry-gag \N \N \N \N 81.23 \N Mac Pharms \N https://images.dutchie.com/800d3faf9e8381e57a1db37415f72173?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-live-rosin-aio-strawberry-gag t f \N 2025-11-18 03:56:07.619945 2025-11-18 04:23:28.813595 2025-11-18 03:56:07.619945 2025-11-18 05:20:25.923068 112 Strawberry GagMac Pharms \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:28.813595+00 \N +2361 \N \N \N Mad Terp Labs Batter | Chester CopperpotMad Terp LabsTHC: 87.98%Special Offer mad-terp-labs-batter-chester-copperpot \N \N \N \N 87.98 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-batter-chester-copperpot t f \N 2025-11-18 03:56:10.16419 2025-11-18 04:23:34.788238 2025-11-18 03:56:10.16419 2025-11-18 05:20:34.644721 112 Chester CopperpotMad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.788238+00 \N +2362 \N \N \N Mad Terp Labs Batter | Jungle PieMad Terp LabsHybridTHC: 84.37%Special Offer mad-terp-labs-batter-jungle-pie \N \N \N \N 84.37 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-batter-jungle-pie t f \N 2025-11-18 03:56:10.166616 2025-11-18 04:23:34.791022 2025-11-18 03:56:10.166616 2025-11-18 05:20:37.764244 112 Jungle PieMad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.791022+00 \N +2364 \N \N \N Mad Terp Labs Batter | Pink BandaidMad Terp LabsIndica-HybridTHC: 83.82%Special Offer mad-terp-labs-batter-pink-bandaid \N \N \N \N 83.82 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-batter-pink-bandaid t f \N 2025-11-18 03:56:10.171486 2025-11-18 04:23:34.796564 2025-11-18 03:56:10.171486 2025-11-18 05:20:43.751386 112 Pink BandaidMad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.796564+00 \N +2366 \N \N \N Mad Terp Labs Cured Applesauce | RosenthalMad Terp LabsHybridTHC: 83.71%Special Offer mad-terp-labs-cured-applesauce-rosenthal \N \N \N \N 83.71 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-cured-applesauce-rosenthal t f \N 2025-11-18 03:56:10.176834 2025-11-18 04:23:34.802194 2025-11-18 03:56:10.176834 2025-11-18 05:20:49.748271 112 RosenthalMad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.802194+00 \N +2367 \N \N \N Mad Terp Labs Cured Suger | SD OGMad Terp LabsIndica-HybridTHC: 79.33%Special Offer mad-terp-labs-cured-suger-sd-og \N \N \N \N 79.33 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-cured-suger-sd-og t f \N 2025-11-18 03:56:10.179208 2025-11-18 04:23:34.804759 2025-11-18 03:56:10.179208 2025-11-18 05:20:52.704849 112 SD OGMad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.804759+00 \N +3021 \N \N \N BRIX STRAWBERRY WATERMELON 100MG baked-bros-brix-strawberry-watermelon-100mg \N \N \N HYBRID \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/37f2bcba-7043-4c79-b361-344fffe0b240?customerType=ADULT t f \N 2025-11-18 14:42:09.019952 2025-11-18 14:42:09.019952 2025-11-18 14:42:09.019952 2025-11-18 14:42:09.019952 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.019952+00 \N +3022 \N \N \N BRIX BLUEBERRY POMEGRANATE 100MG baked-bros-brix-blueberry-pomegranate-100mg \N \N \N HYBRID \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/48429725-ef59-44a0-a8fe-8d91b2e87240?customerType=ADULT t f \N 2025-11-18 14:42:09.021803 2025-11-18 14:42:09.021803 2025-11-18 14:42:09.021803 2025-11-18 14:42:09.021803 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.021803+00 \N +3023 \N \N \N BRIX BLACKBERRY RASPBERRY 100MG baked-bros-brix-blackberry-raspberry-100mg \N \N \N HYBRID \N \N BAKED BROS \N \N \N https://best.treez.io/onlinemenu/category/edible/item/ac02586c-4a46-4e70-a850-e70f46f1eec4?customerType=ADULT t f \N 2025-11-18 14:42:09.023663 2025-11-18 14:42:09.023663 2025-11-18 14:42:09.023663 2025-11-18 14:42:09.023663 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.023663+00 \N +3024 \N \N \N CAMINO MANGO 1:1 100MG camino-camino-mango-1-1-100mg \N \N \N HYBRID \N \N CAMINO \N \N \N https://best.treez.io/onlinemenu/category/edible/item/1d21105a-8e82-4590-ad36-ef3fc7944b51?customerType=ADULT t f \N 2025-11-18 14:42:09.025547 2025-11-18 14:42:09.025547 2025-11-18 14:42:09.025547 2025-11-18 14:42:09.025547 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.025547+00 \N +3025 \N \N \N CAMINO PINEAPPLE HABANERO 100MG camino-camino-pineapple-habanero-100mg \N \N \N S/I \N \N CAMINO \N \N \N https://best.treez.io/onlinemenu/category/edible/item/e1ed08d7-e48e-4844-99d0-d6f484675aef?customerType=ADULT t f \N 2025-11-18 14:42:09.02783 2025-11-18 14:42:09.02783 2025-11-18 14:42:09.02783 2025-11-18 14:42:09.02783 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.02783+00 \N +2374 \N \N \N Mellow Vibes | 100mg Rosin Jelly | WatermelonMellow VibesTHC: 0.83% mellow-vibes-100mg-rosin-jelly-watermelon \N \N \N \N 0.83 \N Mellow Vibes \N https://images.dutchie.com/37198f2341f880c41d76401a05bec469?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mellow-vibes-100mg-rosin-jelly-watermelon t f \N 2025-11-18 03:56:18.910216 2025-11-18 04:23:53.271215 2025-11-18 03:56:18.910216 2025-11-18 05:21:15.803807 112 WatermelonMellow Vibes \N \N \N {} {} {} 14.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:53.271215+00 \N +2375 \N \N \N MFUSED Fire Liquid Diamonds AIO | Cereal MilkMfusedTHC: 92.49%CBD: 0.16% mfused-fire-liquid-diamonds-aio-cereal-milk \N \N \N \N 92.49 0.16 Mfused \N https://images.dutchie.com/be37ba32a5a5759712971de399ea8efc?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-cereal-milk t f \N 2025-11-18 03:56:24.485021 2025-11-18 04:24:01.944368 2025-11-18 03:56:24.485021 2025-11-18 05:21:18.80673 112 Cereal MilkMfused \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.944368+00 \N +3042 \N \N \N WATERMELON INDICA pure-everest-watermelon-indica \N \N \N INDICA \N \N PURE EVEREST \N \N \N https://best.treez.io/onlinemenu/category/edible/item/b74b52bb-491b-4601-b3fd-4bf65cb934b6?customerType=ADULT t f \N 2025-11-18 14:42:09.059721 2025-11-18 14:42:09.059721 2025-11-18 14:42:09.059721 2025-11-18 14:42:09.059721 149 \N \N \N \N \N \N 12.00 12.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.059721+00 \N +2377 \N \N \N MFUSED Fire Liquid Diamonds AIO | Granddaddy PurpleMfusedTHC: 90.89%CBD: 0.17% mfused-fire-liquid-diamonds-aio-granddaddy-purple \N \N \N \N 90.89 0.17 Mfused \N https://images.dutchie.com/6f20bb26b19cf8a838b3950fc0f2923b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-granddaddy-purple t f \N 2025-11-18 03:56:24.498809 2025-11-18 04:24:01.953911 2025-11-18 03:56:24.498809 2025-11-18 05:21:27.944538 112 Granddaddy PurpleMfused \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.953911+00 \N +2379 \N \N \N MFUSED Loud Liquid Diamonds AIO | Acapulco GoldMfusedTHC: 90.84%CBD: 0.15% mfused-loud-liquid-diamonds-aio-acapulco-gold \N \N \N \N 90.84 0.15 Mfused \N https://images.dutchie.com/cbacfd76d19c5210cea83bac9f145cba?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-acapulco-gold t f \N 2025-11-18 03:56:24.505216 2025-11-18 04:24:01.958096 2025-11-18 03:56:24.505216 2025-11-18 05:21:34.217777 112 Acapulco GoldMfused \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.958096+00 \N +2380 \N \N \N MFUSED Loud Liquid Diamonds AIO | Notorious THCMfusedTHCTHC: 92.43%CBD: 0.15% mfused-loud-liquid-diamonds-aio-notorious-thc \N \N \N \N 92.43 0.15 Mfused \N https://images.dutchie.com/e6731dc62664b7c28923fad286aa1334?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-notorious-thc t f \N 2025-11-18 03:56:24.508872 2025-11-18 04:24:01.960205 2025-11-18 03:56:24.508872 2025-11-18 05:21:37.286459 112 Notorious \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.960205+00 \N +2382 \N \N \N MFUSED Twisted Liquid Diamonds AIO | Baja BlazedMfusedTHC: 93.51%CBD: 0.16% mfused-twisted-liquid-diamonds-aio-baja-blazed \N \N \N \N 93.51 0.16 Mfused \N https://images.dutchie.com/1ce771f9703214e4d9cf3061f262e9e2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-baja-blazed t f \N 2025-11-18 03:56:24.515592 2025-11-18 04:24:01.963903 2025-11-18 03:56:24.515592 2025-11-18 05:21:45.308384 112 Baja BlazedMfused \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.963903+00 \N +2384 \N \N \N MFUSED Twisted Liquid Diamonds AIO | Rainbow CloudMfusedTHC: 93.51%CBD: 0.16% mfused-twisted-liquid-diamonds-aio-rainbow-cloud \N \N \N \N 93.51 0.16 Mfused \N https://images.dutchie.com/b500ff8e685a6f59f8b5fcabd6d03d1c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-rainbow-cloud t f \N 2025-11-18 03:56:24.522318 2025-11-18 04:24:01.967318 2025-11-18 03:56:24.522318 2025-11-18 05:21:51.49438 112 Rainbow CloudMfused \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.967318+00 \N +2385 \N \N \N MFUSED Twisted Liquid Diamonds AIO | Tropical TrippinMfusedTHC: 97.6% mfused-twisted-liquid-diamonds-aio-tropical-trippin \N \N \N \N 97.60 \N Mfused \N https://images.dutchie.com/0d624a79890892a704af2208d5fbfb24?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-tropical-trippin t f \N 2025-11-18 03:56:24.525292 2025-11-18 04:24:01.969338 2025-11-18 03:56:24.525292 2025-11-18 05:21:54.508784 112 Tropical TrippinMfused \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.969338+00 \N +2387 \N \N \N MFUSED Vibes THC:CBDV:CBG AIO | Bounce BackMfusedTHCTHC: 59.05%CBD: 0.17% mfused-vibes-thc-cbdv-cbg-aio-bounce-back \N \N \N \N 59.05 0.17 Mfused \N https://images.dutchie.com/0e01987d0d301e7158c22929310417d9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-vibes-thc-cbdv-cbg-aio-bounce-back t f \N 2025-11-18 03:56:24.531242 2025-11-18 04:24:01.97337 2025-11-18 03:56:24.531242 2025-11-18 05:22:01.208448 112 Bounce BackMfused \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.97337+00 \N +2389 \N \N \N Mfused | 5-Pack x 0.5g Infused Pre-roll Fatty | Lemon LoopzMfusedTHC: 35.04% mfused-5-pack-x-0-5g-infused-pre-roll-fatty-lemon-loopz \N \N \N \N 35.04 \N Mfused \N https://images.dutchie.com/616c5d2bba7573343ac61b5bb22cb24a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-5-pack-x-0-5g-infused-pre-roll-fatty-lemon-loopz t f \N 2025-11-18 03:56:24.537065 2025-11-18 04:24:01.977181 2025-11-18 03:56:24.537065 2025-11-18 05:22:07.404482 112 Lemon LoopzMfused \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.977181+00 \N +3026 \N \N \N GUAVA ROSIN 100MG GUMMY good-tide-guava-rosin-100mg-gummy \N \N \N HYBRID \N \N GOOD TIDE \N \N \N https://best.treez.io/onlinemenu/category/edible/item/f3fe66be-de38-4339-89ba-20651c9b5fb7?customerType=ADULT t f \N 2025-11-18 14:42:09.030058 2025-11-18 14:42:09.030058 2025-11-18 14:42:09.030058 2025-11-18 14:42:09.030058 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.030058+00 \N +3027 \N \N \N MANGO ROSIN 100MG GUMMY good-tide-mango-rosin-100mg-gummy \N \N \N INDICA \N \N GOOD TIDE \N \N \N https://best.treez.io/onlinemenu/category/edible/item/af80c53f-bbb6-422f-b193-b2aa4dc1c1ce?customerType=ADULT t f \N 2025-11-18 14:42:09.032011 2025-11-18 14:42:09.032011 2025-11-18 14:42:09.032011 2025-11-18 14:42:09.032011 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.032011+00 \N +3028 \N \N \N PASSION FRUIT 1:1:1 THC/CBD/CBN 100 MG good-tide-passion-fruit-1-1-1-thc-cbd-cbn-100-mg \N \N \N INDICA \N \N GOOD TIDE \N \N \N https://best.treez.io/onlinemenu/category/edible/item/72cc40c8-cea7-4145-ae59-517a163adce1?customerType=ADULT t f \N 2025-11-18 14:42:09.033705 2025-11-18 14:42:09.033705 2025-11-18 14:42:09.033705 2025-11-18 14:42:09.033705 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.033705+00 \N +3029 \N \N \N DRAGON PUNCH PEARLS ROSIN 100MG gron-dragon-punch-pearls-rosin-100mg \N \N \N INDICA \N \N GRON \N \N \N https://best.treez.io/onlinemenu/category/edible/item/79fbae2d-b9f5-4bd6-b793-b9ca1f97a6f8?customerType=ADULT t f \N 2025-11-18 14:42:09.035632 2025-11-18 14:42:09.035632 2025-11-18 14:42:09.035632 2025-11-18 14:42:09.035632 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.035632+00 \N +3030 \N \N \N ORANGE YUZU PEARLS ROSIN 100MG gron-orange-yuzu-pearls-rosin-100mg \N \N \N SATIVA \N \N GRON \N \N \N https://best.treez.io/onlinemenu/category/edible/item/f30117a0-23f3-4d3b-9019-4071d813c8fc?customerType=ADULT t f \N 2025-11-18 14:42:09.037359 2025-11-18 14:42:09.037359 2025-11-18 14:42:09.037359 2025-11-18 14:42:09.037359 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.037359+00 \N +3031 \N \N \N BLACKBERRY LEMONADE PEARLS 1:1:1 CBN/CBD/THC SLEEPY 100MG gron-blackberry-lemonade-pearls-1-1-1-cbn-cbd-thc-sleepy-100mg \N \N \N INDICA \N \N GRON \N \N \N https://best.treez.io/onlinemenu/category/edible/item/0abc7464-0c05-4b1d-ae1c-7267a695a96f?customerType=ADULT t f \N 2025-11-18 14:42:09.039013 2025-11-18 14:42:09.039013 2025-11-18 14:42:09.039013 2025-11-18 14:42:09.039013 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.039013+00 \N +3032 \N \N \N POMEGRANATE PEARLS 4:1 THC/CBD 100MG gron-pomegranate-pearls-4-1-thc-cbd-100mg \N \N \N HYBRID \N \N GRON \N \N \N https://best.treez.io/onlinemenu/category/edible/item/ad88538f-2e4d-4cef-bedb-a265f8d40831?customerType=ADULT t f \N 2025-11-18 14:42:09.041018 2025-11-18 14:42:09.041018 2025-11-18 14:42:09.041018 2025-11-18 14:42:09.041018 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.041018+00 \N +3033 \N \N \N TANGELO PEARLS 2:1:1 THC/CBC/CBG 100MG gron-tangelo-pearls-2-1-1-thc-cbc-cbg-100mg \N \N \N S/I \N \N GRON \N \N \N https://best.treez.io/onlinemenu/category/edible/item/295f42b7-e26f-40f6-b35b-7de008881e02?customerType=ADULT t f \N 2025-11-18 14:42:09.043201 2025-11-18 14:42:09.043201 2025-11-18 14:42:09.043201 2025-11-18 14:42:09.043201 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.043201+00 \N +2468 \N \N \N SIP Elixir | 100mg Nano Beverage | Electric LemonSipHybridTHC: 116.76 mg sip-elixir-100mg-nano-beverage-electric-lemon \N \N \N \N \N \N Sip \N https://images.dutchie.com/f44635fe73f8e3b07fe9e89931dedf3b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-electric-lemon t f \N 2025-11-18 03:58:31.111615 2025-11-18 04:25:51.88166 2025-11-18 03:58:31.111615 2025-11-18 05:26:34.587753 112 Electric LemonSip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.88166+00 \N +3034 \N \N \N THE FRUITS SATIVA 300MG ogeez-the-fruits-sativa-300mg \N \N \N SATIVA \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/d2296cb0-4314-4793-ba21-0ddd1b746679?customerType=ADULT t f \N 2025-11-18 14:42:09.045359 2025-11-18 14:42:09.045359 2025-11-18 14:42:09.045359 2025-11-18 14:42:09.045359 149 \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.045359+00 \N +2407 \N \N \N Ogeez | 100mg Gummy | Sugar Free Tropical SativaOGEEZSativaTHC: 0.18% ogeez-100mg-gummy-sugar-free-tropical-sativa \N \N \N \N 0.18 \N OGEEZ \N https://images.dutchie.com/a5e8f1f3db3e5ad4121659f2e6d2d36f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-sugar-free-tropical-sativa t f \N 2025-11-18 03:56:47.825991 2025-11-18 04:24:12.884013 2025-11-18 03:56:47.825991 2025-11-18 05:23:14.736145 112 Sugar Free Tropical \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:12.884013+00 \N +2409 \N \N \N Ogeez | 100mg/50mg THC/CBN MEGA DOSE 1: ummy | BIG Sleep EditionOGEEZTHCTHC: 0.64% ogeez-100mg-50mg-thc-cbn-mega-dose-1-ummy-big-sleep-edition \N \N \N \N 0.64 \N OGEEZ \N https://images.dutchie.com/e41e8ad2b5fd036e83febd4a94381afe?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-50mg-thc-cbn-mega-dose-1-ummy-big-sleep-edition t f \N 2025-11-18 03:56:47.831505 2025-11-18 04:24:12.888514 2025-11-18 03:56:47.831505 2025-11-18 05:23:20.734226 112 100mg/50mg \N \N \N {} {} {} 9.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:12.888514+00 \N +2410 \N \N \N Ogeez | 100mg Gummy | The Creams Mellow IndicaOGeez!IndicaTHC: 0.18% ogeez-100mg-gummy-the-creams-mellow-indica \N \N \N \N 0.18 \N OGeez! \N https://images.dutchie.com/e5a278c367f37de32913669bdfd8ba6c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-creams-mellow-indica t f \N 2025-11-18 03:56:54.69275 2025-11-18 04:24:28.881945 2025-11-18 03:56:54.69275 2025-11-18 05:23:28.577636 112 The Creams Mellow \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:28.881945+00 \N +2412 \N \N \N Ogeez | 100mg Gummy | The Fruits Mellow IndicaOGeez!IndicaTHC: 0.19% ogeez-100mg-gummy-the-fruits-mellow-indica \N \N \N \N 0.19 \N OGeez! \N https://images.dutchie.com/a5e8f1f3db3e5ad4121659f2e6d2d36f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-fruits-mellow-indica t f \N 2025-11-18 03:56:54.699425 2025-11-18 04:24:28.885937 2025-11-18 03:56:54.699425 2025-11-18 05:23:37.24356 112 The Fruits Mellow \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:28.885937+00 \N +2414 \N \N \N Ogeez | 100mg RSO Gummy | Peg's Raspberry OrangeOGeez!THC: 0.18% ogeez-100mg-rso-gummy-peg-s-raspberry-orange \N \N \N \N 0.18 \N OGeez! \N https://images.dutchie.com/15054e6d330060499144dfbbdb68df41?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-rso-gummy-peg-s-raspberry-orange t f \N 2025-11-18 03:56:54.704083 2025-11-18 04:24:28.889987 2025-11-18 03:56:54.704083 2025-11-18 05:23:43.400114 112 Peg's Raspberry OrangeOGeez! \N \N \N {} {} {} 19.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:28.889987+00 \N +2415 \N \N \N Ogeez | 100mg Rosin Gummy | Vegan Sweet ClementineOGeez!THC: 0.15% ogeez-100mg-rosin-gummy-vegan-sweet-clementine \N \N \N \N 0.15 \N OGeez! \N https://images.dutchie.com/15054e6d330060499144dfbbdb68df41?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-rosin-gummy-vegan-sweet-clementine t f \N 2025-11-18 03:56:54.706049 2025-11-18 04:24:28.892321 2025-11-18 03:56:54.706049 2025-11-18 05:23:46.474655 112 Vegan Sweet ClementineOGeez! \N \N \N {} {} {} 24.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:28.892321+00 \N +2417 \N \N \N On the Rocks Live Rosin Badder | Vanilla MintsOn The RocksTHC: 77.62% on-the-rocks-live-rosin-badder-vanilla-mints \N \N \N \N 77.62 \N On The Rocks \N https://images.dutchie.com/a16eb1a5bed10aa20d345b73220f9088?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/on-the-rocks-live-rosin-badder-vanilla-mints t f \N 2025-11-18 03:57:00.142818 2025-11-18 04:24:38.930387 2025-11-18 03:57:00.142818 2025-11-18 05:23:52.625205 112 Vanilla MintsOn The Rocks \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:38.930387+00 \N +3035 \N \N \N PEG'S RASPBERRY ORANGE RSO 100MG ogeez-peg-s-raspberry-orange-rso-100mg \N \N \N HYBRID \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/60bdb53d-cae3-41c0-99d9-287ca8c8aa9a?customerType=ADULT t f \N 2025-11-18 14:42:09.047137 2025-11-18 14:42:09.047137 2025-11-18 14:42:09.047137 2025-11-18 14:42:09.047137 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.047137+00 \N +2419 \N \N \N Papa's Herb Distillate AIO Vapes | GDPPapa's HerbTHC: 86.86%CBD: 1.12%Special Offer papa-s-herb-distillate-aio-vapes-gdp \N \N \N \N 86.86 1.12 Papa's Herb \N https://images.dutchie.com/f124ed439100c0b60fd7ba16e0f707eb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-gdp t f \N 2025-11-18 03:57:18.793792 2025-11-18 04:24:40.479174 2025-11-18 03:57:18.793792 2025-11-18 05:24:30.179902 112 GDPPapa's Herb \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:40.479174+00 \N +3060 \N \N \N CHEM D io-extracts-chem-d-1g \N \N \N INDICA 74.00 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/470d9852-3267-4d52-ae12-353bd2713aad?customerType=ADULT t f \N 2025-11-18 14:42:09.090798 2025-11-18 14:42:09.090798 2025-11-18 14:42:09.090798 2025-11-18 14:42:09.090798 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.090798+00 \N +2420 \N \N \N Papa's Herb Distillate AIO Vapes | Lemon Cherry GelatoPapa's HerbTHC: 85.37%CBD: 0.88%Special Offer papa-s-herb-distillate-aio-vapes-lemon-cherry-gelato \N \N \N \N 85.37 0.88 Papa's Herb \N https://images.dutchie.com/f124ed439100c0b60fd7ba16e0f707eb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-lemon-cherry-gelato t f \N 2025-11-18 03:57:18.796175 2025-11-18 04:24:40.481652 2025-11-18 03:57:18.796175 2025-11-18 05:24:05.940684 112 Lemon Cherry GelatoPapa's Herb \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:40.481652+00 \N +2422 \N \N \N Papa's Herb Distillate AIO Vapes | Watermelon ZPapa's HerbTHC: 86.95%CBD: 1.1%Special Offer papa-s-herb-distillate-aio-vapes-watermelon-z \N \N \N \N 86.95 1.10 Papa's Herb \N https://images.dutchie.com/f124ed439100c0b60fd7ba16e0f707eb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-watermelon-z t f \N 2025-11-18 03:57:18.801604 2025-11-18 04:24:40.486156 2025-11-18 03:57:18.801604 2025-11-18 05:24:11.946315 112 Watermelon ZPapa's Herb \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:40.486156+00 \N +3036 \N \N \N PINK LEMONADE PEG'S SUMMER RSO 100MG ogeez-pink-lemonade-peg-s-summer-rso-100mg \N \N \N SATIVA \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/98513a62-3cda-4516-aed8-d4b4e74e1a60?customerType=ADULT t f \N 2025-11-18 14:42:09.049108 2025-11-18 14:42:09.049108 2025-11-18 14:42:09.049108 2025-11-18 14:42:09.049108 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.049108+00 \N +3037 \N \N \N TROPICAL SUGAR FREE 100MG ogeez-tropical-sugar-free-100mg \N \N \N INDICA \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/ed0349ef-53c1-4985-b358-9e586604d7ee?customerType=ADULT t f \N 2025-11-18 14:42:09.050944 2025-11-18 14:42:09.050944 2025-11-18 14:42:09.050944 2025-11-18 14:42:09.050944 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.050944+00 \N +3038 \N \N \N THE FRUITS INDICA 100MG ogeez-the-fruits-indica-100mg \N \N \N INDICA \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/0e9f25c7-56f0-44b3-b6c4-b782fc7445cb?customerType=ADULT t f \N 2025-11-18 14:42:09.052795 2025-11-18 14:42:09.052795 2025-11-18 14:42:09.052795 2025-11-18 14:42:09.052795 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.052795+00 \N +3039 \N \N \N THE CREAMS SATIVA 100MG ogeez-the-creams-sativa-100mg \N \N \N SATIVA \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/6f773648-51a7-438a-8ad4-b2ac883ab602?customerType=ADULT t f \N 2025-11-18 14:42:09.054768 2025-11-18 14:42:09.054768 2025-11-18 14:42:09.054768 2025-11-18 14:42:09.054768 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.054768+00 \N +3040 \N \N \N THE CREAMS INDICA100MG ogeez-the-creams-indica100mg \N \N \N INDICA \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/87cc0f1b-f6b8-4753-9617-fba7d11b2922?customerType=ADULT t f \N 2025-11-18 14:42:09.056408 2025-11-18 14:42:09.056408 2025-11-18 14:42:09.056408 2025-11-18 14:42:09.056408 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.056408+00 \N +3041 \N \N \N THE FRUITS SATIVA 100MG ogeez-the-fruits-sativa-100mg \N \N \N SATIVA \N \N OGEEZ \N \N \N https://best.treez.io/onlinemenu/category/edible/item/8866e839-ef8b-4023-86c0-0513b007ea77?customerType=ADULT t f \N 2025-11-18 14:42:09.057993 2025-11-18 14:42:09.057993 2025-11-18 14:42:09.057993 2025-11-18 14:42:09.057993 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.057993+00 \N +2426 \N \N \N Sauce Essentials Distillate AIO | OG Grandaddy PurpleSauceIndicaTHC: 79.86%CBD: 0.18%Special Offer sauce-essentials-distillate-aio-og-grandaddy-purple \N \N \N \N 79.86 0.18 Sauce \N https://images.dutchie.com/c77b489368a42ca2900bcb5010180722?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-distillate-aio-og-grandaddy-purple t f \N 2025-11-18 03:57:23.369162 2025-11-18 04:24:49.080039 2025-11-18 03:57:23.369162 2025-11-18 05:24:21.421305 112 OG Grandaddy PurpleSauce \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.080039+00 \N +2428 \N \N \N Sauce Essentials Distillate AIO | Super Sour DieselSauceSativaTHC: 84.35%CBD: 0.13%Special Offer sauce-essentials-distillate-aio-super-sour-diesel \N \N \N \N 84.35 0.13 Sauce \N https://images.dutchie.com/82c887c1881dd442fd800e84dbda61bc?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-distillate-aio-super-sour-diesel t f \N 2025-11-18 03:57:23.378977 2025-11-18 04:24:49.090099 2025-11-18 03:57:23.378977 2025-11-18 05:24:25.350994 112 Super Sour DieselSauce \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.090099+00 \N +2429 \N \N \N Sauce Essentials Distillate AIO | White BlueberrySauceTHC: 85.64%CBD: 0.22%Special Offer sauce-essentials-distillate-aio-white-blueberry \N \N \N \N 85.64 0.22 Sauce \N https://images.dutchie.com/1a266152d9116f259a7aa0d02bf6b453?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-distillate-aio-white-blueberry t f \N 2025-11-18 03:57:23.381451 2025-11-18 04:24:49.092671 2025-11-18 03:57:23.381451 2025-11-18 05:24:33.785179 112 White BlueberrySauce \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.092671+00 \N +2431 \N \N \N Sauce Essentials Live Resin AIO | Apple FritterSauceHybridTHC: 83.63% sauce-essentials-live-resin-aio-apple-fritter-11924 \N \N \N \N 83.63 \N Sauce \N https://images.dutchie.com/818474e7cec0168cd9ceb0ada767266b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-live-resin-aio-apple-fritter-11924 t f \N 2025-11-18 03:57:23.386633 2025-11-18 04:24:49.098097 2025-11-18 03:57:23.386633 2025-11-18 05:24:39.771093 112 Apple FritterSauce \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.098097+00 \N +3043 \N \N \N BLOOD ORANGE 100MG CBC wyld-blood-orange-100mg-cbc \N \N \N SATIVA \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/72ff8809-a0ef-4c9c-9d3f-4f7b2f1d663f?customerType=ADULT t f \N 2025-11-18 14:42:09.061465 2025-11-18 14:42:09.061465 2025-11-18 14:42:09.061465 2025-11-18 14:42:09.061465 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.061465+00 \N +2433 \N \N \N Sauce Essentials Live Resin AIO | Sour Blue DreamSauceSativa-HybridTHC: 78.92%CBD: 0.19% sauce-essentials-live-resin-aio-sour-blue-dream \N \N \N \N 78.92 0.19 Sauce \N https://images.dutchie.com/818474e7cec0168cd9ceb0ada767266b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-live-resin-aio-sour-blue-dream t f \N 2025-11-18 03:57:23.390743 2025-11-18 04:24:49.104167 2025-11-18 03:57:23.390743 2025-11-18 05:25:00.219519 112 Sour Blue DreamSauce \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.104167+00 \N +2434 \N \N \N Sauce Essentials Live Resin AIO | Strawberry CoughSauceSativa-HybridTHC: 79.5% sauce-essentials-live-resin-aio-strawberry-cough \N \N \N \N 79.50 \N Sauce \N https://images.dutchie.com/2a40a4065e13e4cdf403eb0bbdaa4b0f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-live-resin-aio-strawberry-cough t f \N 2025-11-18 03:57:23.392861 2025-11-18 04:24:49.106763 2025-11-18 03:57:23.392861 2025-11-18 05:24:53.187766 112 Strawberry CoughSauce \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.106763+00 \N +2436 \N \N \N Savvy | Guap | 100mg RSO Gummy 1:2 CBN:THC | Melon CrashSavvyTHCTHC: 0.82% savvy-guap-100mg-rso-gummy-1-2-cbn-thc-melon-crash \N \N \N \N 0.82 \N Savvy \N https://images.dutchie.com/6c16042846c4a294181ed33b1305daef?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/savvy-guap-100mg-rso-gummy-1-2-cbn-thc-melon-crash t f \N 2025-11-18 03:57:30.559225 2025-11-18 04:25:04.60697 2025-11-18 03:57:30.559225 2025-11-18 05:24:59.23553 112 100mg RSO Gummy 1:2 CBN: \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.60697+00 \N +2437 \N \N \N Savvy | Guap | 100mg RSO Gummy | Berry DripSavvyTHC: 0.86% savvy-guap-100mg-rso-gummy-berry-drip \N \N \N \N 0.86 \N Savvy \N https://images.dutchie.com/ab776cfc2212e17a56cd7fed471e32be?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/savvy-guap-100mg-rso-gummy-berry-drip t f \N 2025-11-18 03:57:30.561456 2025-11-18 04:25:04.609451 2025-11-18 03:57:30.561456 2025-11-18 05:25:04.695464 112 Berry DripSavvy \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.609451+00 \N +2439 \N \N \N Strut - Fresh Berry [2g]SavvyIndicaTHC: 88.44%CBD: 0.28% strut-fresh-berry-2g \N \N \N \N 88.44 0.28 Savvy \N https://images.dutchie.com/0df7dfa85a65e19b455a2655dc3006c4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/strut-fresh-berry-2g t f \N 2025-11-18 03:57:30.566168 2025-11-18 04:25:04.614565 2025-11-18 03:57:30.566168 2025-11-18 05:25:11.017672 112 \N \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.614565+00 \N +2441 \N \N \N Strut - Rainbow Sherbet [2g]SavvyIndicaTHC: 89.95%CBD: 0.23% strut-rainbow-sherbet-2g \N \N \N \N 89.95 0.23 Savvy \N https://images.dutchie.com/0df7dfa85a65e19b455a2655dc3006c4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/strut-rainbow-sherbet-2g t f \N 2025-11-18 03:57:30.570852 2025-11-18 04:25:04.61846 2025-11-18 03:57:30.570852 2025-11-18 05:25:18.615591 112 \N \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.61846+00 \N +2442 \N \N \N Strut - Summer Melon [2g]SavvySativaTHC: 87.8%CBD: 0.22% strut-summer-melon-2g \N \N \N \N 87.80 0.22 Savvy \N https://images.dutchie.com/eb9bb6c1751a424f15f0c1071417d11c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/strut-summer-melon-2g t f \N 2025-11-18 03:57:30.572807 2025-11-18 04:25:04.620463 2025-11-18 03:57:30.572807 2025-11-18 05:25:21.5913 112 \N \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:04.620463+00 \N +3044 \N \N \N ELDERBERRY THC-100MG CBN-50MG wyld-elderberry-thc-100mg-cbn-50mg \N \N \N INDICA \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/4e8a59a4-119b-435d-9f9b-f8678b1815ee?customerType=ADULT t f \N 2025-11-18 14:42:09.06336 2025-11-18 14:42:09.06336 2025-11-18 14:42:09.06336 2025-11-18 14:42:09.06336 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.06336+00 \N +3045 \N \N \N PEAR 1:1 THC/CBG wyld-pear-1-1-thc-cbg \N \N \N HYBRID \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/06bd978f-74a9-49e4-ab88-6d7cf84a2377?customerType=ADULT t f \N 2025-11-18 14:42:09.065184 2025-11-18 14:42:09.065184 2025-11-18 14:42:09.065184 2025-11-18 14:42:09.065184 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.065184+00 \N +2471 \N \N \N SIP Elixir | 100mg Nano Beverage | WatermelonSipHybridTHC: 104.57 mg sip-elixir-100mg-nano-beverage-watermelon \N \N \N \N \N \N Sip \N https://images.dutchie.com/40686cadb568297f39b0b3b0bdaaa9af?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-watermelon t f \N 2025-11-18 03:58:31.117844 2025-11-18 04:25:51.889554 2025-11-18 03:58:31.117844 2025-11-18 05:26:43.508427 112 WatermelonSip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.889554+00 \N +3046 \N \N \N POMEGRANATE 1:1 THC/CBD wyld-pomegranate-1-1-thc-cbd \N \N \N HYBRID \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/8f58c7df-3798-4692-b969-ba814f7465f6?customerType=ADULT t f \N 2025-11-18 14:42:09.067008 2025-11-18 14:42:09.067008 2025-11-18 14:42:09.067008 2025-11-18 14:42:09.067008 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.067008+00 \N +3055 \N \N \N KR TWERPZ ROPE PEACH 100MG korova-kr-twerpz-rope-peach-100mg \N \N \N HYBRID \N \N KOROVA \N \N \N https://best.treez.io/onlinemenu/category/edible/item/ed7d5a0d-30eb-4062-839c-03683dbf5b2d?customerType=ADULT t f \N 2025-11-18 14:42:09.082608 2025-11-18 14:42:09.082608 2025-11-18 14:42:09.082608 2025-11-18 14:42:09.082608 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.082608+00 \N +2450 \N \N \N Select Essential Cart | Lemon Cheesecake select-essential-cart-lemon-cheesecake With Select Essentials, you don't need to choose between the strains you love and quality oil. Essentials delivers a high potency oil with exceptional flavor and a wide variety of your favorite strains. Available in 1g the variety of strains you love. \N \N Sativa 83.18 0.18 Select \N https://s3-us-west-2.amazonaws.com/dutchie-images/a50b81806eb105e5e2ade0c6ebe5fce0 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/select-essential-cart-lemon-cheesecake t f \N 2025-11-18 03:57:55.444569 2025-11-18 05:08:07.854836 2025-11-18 03:57:55.444569 2025-11-18 05:08:07.854836 112 \N \N \N \N {} {} {} 35.00 \N 5 5 left in stock \N \N in_stock \N 2025-11-18 05:08:07.854836+00 \N +3049 \N \N \N PEACH 2:1 wyld-peach-2-1 \N \N \N HYBRID \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/de54661e-6b78-409f-aa86-31de70cc6a60?customerType=ADULT t f \N 2025-11-18 14:42:09.072092 2025-11-18 14:42:09.072092 2025-11-18 14:42:09.072092 2025-11-18 14:42:09.072092 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.072092+00 \N +3050 \N \N \N SOUR APPLE wyld-sour-apple \N \N \N SATIVA \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/18bf917d-cdef-4879-ace4-938e91748046?customerType=ADULT t f \N 2025-11-18 14:42:09.073836 2025-11-18 14:42:09.073836 2025-11-18 14:42:09.073836 2025-11-18 14:42:09.073836 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.073836+00 \N +2458 \N \N \N Session Live Resin AIO | Mother's MilkSession Cannabis Co.SativaTHC: 74.8%CBD: 0.16%Special Offer session-live-resin-aio-mother-s-milk \N \N \N \N 74.80 0.16 Session Cannabis Co. \N https://images.dutchie.com/2103e7652bba4a1491f9bad87be2fb3c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/session-live-resin-aio-mother-s-milk t f \N 2025-11-18 03:57:59.090143 2025-11-18 04:25:25.340759 2025-11-18 03:57:59.090143 2025-11-18 05:25:55.476301 112 Mother's MilkSession Cannabis Co. \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:25.340759+00 \N +2459 \N \N \N Shango Flower Jar | Orion's BeltShangoIndica-HybridTHC: 24.83% shango-flower-jar-orion-s-belt \N \N \N \N 24.83 \N Shango \N https://images.dutchie.com/9ef8598b2407303193a147317cba3cf3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/shango-flower-jar-orion-s-belt t f \N 2025-11-18 03:58:06.160337 2025-11-18 04:25:40.378828 2025-11-18 03:58:06.160337 2025-11-18 05:26:02.246515 112 Orion's BeltShango \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:40.378828+00 \N +2461 \N \N \N Shango Flower Jar | Strawberry ZkillatoShangoIndica-HybridTHC: 24.19% shango-flower-jar-strawberry-zkillato \N \N \N \N 24.19 \N Shango \N https://images.dutchie.com/9c34cb99e6e329ac13c39480c10499b0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/shango-flower-jar-strawberry-zkillato t f \N 2025-11-18 03:58:06.171002 2025-11-18 04:25:40.386683 2025-11-18 03:58:06.171002 2025-11-18 05:26:08.0677 112 Strawberry ZkillatoShango \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:40.386683+00 \N +2463 \N \N \N Simply Twisted | 2-Pack x (1g) Pre-Rolls | Cactus BreathSimply TwistedIndicaTHC: 23.86%Special Offer simply-twisted-2-pack-x-1g-pre-rolls-cactus-breath \N \N \N \N 23.86 \N Simply Twisted \N https://images.dutchie.com/5d8fb0ed4703238fa13c44b487971102?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/simply-twisted-2-pack-x-1g-pre-rolls-cactus-breath t f \N 2025-11-18 03:58:11.406481 2025-11-18 04:25:51.422695 2025-11-18 03:58:11.406481 2025-11-18 05:26:14.070519 112 Cactus BreathSimply Twisted \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.422695+00 \N +2464 \N \N \N Simply Twisted | 2-Pack x (1g) Pre-Rolls | HeadbandSimply TwistedHybridTHC: 31.16%Special Offer simply-twisted-2-pack-x-1g-pre-rolls-headband-69529 \N \N \N \N 31.16 \N Simply Twisted \N https://images.dutchie.com/5d8fb0ed4703238fa13c44b487971102?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/simply-twisted-2-pack-x-1g-pre-rolls-headband-69529 t f \N 2025-11-18 03:58:11.409226 2025-11-18 04:25:51.425854 2025-11-18 03:58:11.409226 2025-11-18 05:26:17.052874 112 HeadbandSimply Twisted \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.425854+00 \N +3051 \N \N \N SOUR CHERRY THC-100MG wyld-sour-cherry-thc-100mg \N \N \N INDICA \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/2e38f04d-b35b-4030-b7c5-1a85f7fcd006?customerType=ADULT t f \N 2025-11-18 14:42:09.075845 2025-11-18 14:42:09.075845 2025-11-18 14:42:09.075845 2025-11-18 14:42:09.075845 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.075845+00 \N +2466 \N \N \N SIP Elixir | 100mg Nano Beverage + Caffeine | Citrus SparkSipTHC: 0.21% sip-elixir-100mg-nano-beverage-caffeine-citrus-spark \N \N \N \N 0.21 \N Sip \N https://images.dutchie.com/cc6e7f88514347996843d2d1970a60e8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-caffeine-citrus-spark t f \N 2025-11-18 03:58:31.106021 2025-11-18 04:25:51.875594 2025-11-18 03:58:31.106021 2025-11-18 05:26:26.661795 112 Citrus SparkSip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.875594+00 \N +3104 \N \N \N CANNABOMBZ 1500MG RELIEF ROLL-ON io-extracts-cannabombz-1500mg-relief-roll-on \N \N \N HYBRID \N \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/topical/item/e63c7bc7-e441-47de-b73c-7a95f56b584b?customerType=ADULT t f \N 2025-11-18 14:42:09.171008 2025-11-18 14:42:09.171008 2025-11-18 14:42:09.171008 2025-11-18 14:42:09.171008 149 \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.171008+00 \N +2467 \N \N \N SIP Elixir | 100mg Nano Beverage + Caffeine | XpressoSipTHC: 95.16 mg sip-elixir-100mg-nano-beverage-caffeine-xpresso \N \N \N \N \N \N Sip \N https://images.dutchie.com/cc6e7f88514347996843d2d1970a60e8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-caffeine-xpresso t f \N 2025-11-18 03:58:31.109378 2025-11-18 04:25:51.87916 2025-11-18 03:58:31.109378 2025-11-18 05:26:29.679221 112 XpressoSip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.87916+00 \N +2469 \N \N \N SIP Elixir | 100mg Nano Beverage | HurricaneSipTHC: 0.19% sip-elixir-100mg-nano-beverage-hurricane \N \N \N \N 0.19 \N Sip \N https://images.dutchie.com/26d0ace1ae7a8ba3213a076a12724449?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-hurricane t f \N 2025-11-18 03:58:31.113626 2025-11-18 04:25:51.884205 2025-11-18 03:58:31.113626 2025-11-18 05:26:37.56452 112 HurricaneSip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.884205+00 \N +2470 \N \N \N SIP Elixir | 100mg Nano Beverage | Tropical CrushSipTHC: 108.83 mg sip-elixir-100mg-nano-beverage-tropical-crush \N \N \N \N \N \N Sip \N https://images.dutchie.com/cc6e7f88514347996843d2d1970a60e8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sip-elixir-100mg-nano-beverage-tropical-crush t f \N 2025-11-18 03:58:31.115785 2025-11-18 04:25:51.88677 2025-11-18 03:58:31.115785 2025-11-18 05:26:40.60579 112 Tropical CrushSip \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:51.88677+00 \N +3047 \N \N \N HUCKLEBERRY wyld-huckleberry \N \N \N HYBRID \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/8c4b6b3a-9bf9-403a-b419-1a77f0e70844?customerType=ADULT t f \N 2025-11-18 14:42:09.068775 2025-11-18 14:42:09.068775 2025-11-18 14:42:09.068775 2025-11-18 14:42:09.068775 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.068775+00 \N +3052 \N \N \N STRAWBERRY CBD-20MG:THC-1MG wyld-strawberry-cbd-20mg-thc-1mg \N \N \N HYBRID \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/38e6e428-21b8-4776-8c9d-412272ae576b?customerType=ADULT t f \N 2025-11-18 14:42:09.077524 2025-11-18 14:42:09.077524 2025-11-18 14:42:09.077524 2025-11-18 14:42:09.077524 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.077524+00 \N +3053 \N \N \N WYLD RASBERRY 100MG wyld-wyld-rasberry-100mg \N \N \N SATIVA \N \N WYLD \N \N \N https://best.treez.io/onlinemenu/category/edible/item/7703cf87-e403-4e26-9a54-5c5059764eda?customerType=ADULT t f \N 2025-11-18 14:42:09.079279 2025-11-18 14:42:09.079279 2025-11-18 14:42:09.079279 2025-11-18 14:42:09.079279 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.079279+00 \N +3054 \N \N \N KR TWERPZ ROPE GRAPE 100MG korova-kr-twerpz-rope-grape-100mg \N \N \N INDICA \N \N KOROVA \N \N \N https://best.treez.io/onlinemenu/category/edible/item/4e4469ba-2241-4d68-97d4-c4429361fc8c?customerType=ADULT t f \N 2025-11-18 14:42:09.080916 2025-11-18 14:42:09.080916 2025-11-18 14:42:09.080916 2025-11-18 14:42:09.080916 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.080916+00 \N +2474 \N \N \N Smokiez | 100mg Sour Gummy | 1:1 THC to CBD | TropicalSmokiez EdiblesTHCTHC: 0.17% smokiez-100mg-sour-gummy-1-1-thc-to-cbd-tropical \N \N \N \N 0.17 \N Smokiez Edibles \N https://images.dutchie.com/53ccfa75be037467feda2fc37b1606e1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-1-1-thc-to-cbd-tropical t f \N 2025-11-18 03:58:33.152093 2025-11-18 04:25:58.253364 2025-11-18 03:58:33.152093 2025-11-18 05:26:56.166399 112 1:1 \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:58.253364+00 \N +2475 \N \N \N Smokiez | 100mg Sour Gummy | Single | Blue RaspberrySmokiez EdiblesTHC: 93.15 mgCBD: 0.2 mgSpecial Offer smokiez-100mg-sour-gummy-single-blue-raspberry \N \N \N \N \N \N Smokiez Edibles \N https://images.dutchie.com/1dd0d3a1d42a8f7f5ce571ab2b59e4bc?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-single-blue-raspberry t f \N 2025-11-18 03:58:33.160431 2025-11-18 04:25:58.259365 2025-11-18 03:58:33.160431 2025-11-18 05:26:59.685559 112 Blue RaspberrySmokiez Edibles \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:58.259365+00 \N +2477 \N \N \N Smokiez | 100mg Sour Gummy | Single | WatermelonSmokiez EdiblesTHC: 95.95 mgSpecial Offer smokiez-100mg-sour-gummy-single-watermelon \N \N \N \N \N \N Smokiez Edibles \N https://images.dutchie.com/7f10f3df749834adff00ead178221e82?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sour-gummy-single-watermelon t f \N 2025-11-18 03:58:33.164962 2025-11-18 04:25:58.263624 2025-11-18 03:58:33.164962 2025-11-18 05:27:07.601893 112 WatermelonSmokiez Edibles \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:58.263624+00 \N +2479 \N \N \N Smokiez | 100mg Sweet Gummy | Single | PeachSmokiez EdiblesTHC: 0.43%Special Offer smokiez-100mg-sweet-gummy-single-peach \N \N \N \N 0.43 \N Smokiez Edibles \N https://images.dutchie.com/5994f2b8fc0290e663cd2ad6d45b6da6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/smokiez-100mg-sweet-gummy-single-peach t f \N 2025-11-18 03:58:33.170067 2025-11-18 04:25:58.267665 2025-11-18 03:58:33.170067 2025-11-18 05:27:13.684506 112 PeachSmokiez Edibles \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:58.267665+00 \N +2480 \N \N \N Sonoran Roots Pre-Roll | Sticky CakeSonoran RootsHybridTHC: 17.72% sonoran-roots-pre-roll-sticky-cake \N \N \N \N 17.72 \N Sonoran Roots \N https://images.dutchie.com/988e8d61dfd2be10c36536374bfb7f96?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sonoran-roots-pre-roll-sticky-cake t f \N 2025-11-18 03:58:34.848164 2025-11-18 04:26:01.134352 2025-11-18 03:58:34.848164 2025-11-18 05:27:14.465293 112 Sticky CakeSonoran Roots \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:01.134352+00 \N +2482 \N \N \N Space Rocks Infused Flower | Apple FRTRSpace RocksHybridTHC: 45.99% space-rocks-infused-flower-apple-frtr \N \N \N \N 45.99 \N Space Rocks \N https://images.dutchie.com/143dbd241e177058c61ca37c7c87cef4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-infused-flower-apple-frtr t f \N 2025-11-18 03:58:41.845095 2025-11-18 04:26:16.000056 2025-11-18 03:58:41.845095 2025-11-18 05:27:23.096956 112 Apple FRTRSpace Rocks \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.000056+00 \N +3056 \N \N \N RSO HARD CANDY FRUIT PUNCH 100MG sublime-rso-hard-candy-fruit-punch-100mg \N \N \N HYBRID \N \N SUBLIME \N \N \N https://best.treez.io/onlinemenu/category/edible/item/f3349cd0-af10-4801-9b0a-a229cc4112d3?customerType=ADULT t f \N 2025-11-18 14:42:09.084215 2025-11-18 14:42:09.084215 2025-11-18 14:42:09.084215 2025-11-18 14:42:09.084215 149 \N \N \N \N \N \N 22.00 22.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.084215+00 \N +3057 \N \N \N HONEY BOMB INDICA 1:1:1 100MG/10PK honey-bomb-honey-bomb-indica-1-1-1-100mg-10pk \N \N \N INDICA \N \N HONEY BOMB \N \N \N https://best.treez.io/onlinemenu/category/edible/item/00fd8544-355f-4783-a958-05eec957f17a?customerType=ADULT t f \N 2025-11-18 14:42:09.085744 2025-11-18 14:42:09.085744 2025-11-18 14:42:09.085744 2025-11-18 14:42:09.085744 149 \N \N \N \N \N \N 26.00 26.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.085744+00 \N +3058 \N \N \N HONEY BOMB HYBRID 3:1 100MG/10PK honey-bomb-honey-bomb-hybrid-3-1-100mg-10pk \N \N \N HYBRID \N \N HONEY BOMB \N \N \N https://best.treez.io/onlinemenu/category/edible/item/85e4304d-2a82-4f51-86bc-70304d843e41?customerType=ADULT t f \N 2025-11-18 14:42:09.087672 2025-11-18 14:42:09.087672 2025-11-18 14:42:09.087672 2025-11-18 14:42:09.087672 149 \N \N \N \N \N \N 26.00 26.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.087672+00 \N +3059 \N \N \N HONEY BOMB SATIVA 2:1 100MG/10PK honey-bomb-honey-bomb-sativa-2-1-100mg-10pk \N \N \N SATIVA \N \N HONEY BOMB \N \N \N https://best.treez.io/onlinemenu/category/edible/item/bb2c9225-7488-4336-a9ba-f34d8f1a8691?customerType=ADULT t f \N 2025-11-18 14:42:09.089292 2025-11-18 14:42:09.089292 2025-11-18 14:42:09.089292 2025-11-18 14:42:09.089292 149 \N \N \N \N \N \N 26.00 26.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.089292+00 \N +2491 \N \N \N Sticky Saguaro Flower Mylar | BlueberrySticky SaguaroIndica-HybridTHC: 22.75%Special Offer sticky-saguaro-flower-mylar-blueberry \N \N \N \N 22.75 \N Sticky Saguaro \N https://images.dutchie.com/3f51f5dcfd96cbe02670e8cbf5c16072?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-flower-mylar-blueberry t f \N 2025-11-18 03:58:47.55618 2025-11-18 04:26:27.086154 2025-11-18 03:58:47.55618 2025-11-18 05:27:57.118393 112 BlueberrySticky Saguaro \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.086154+00 \N +2493 \N \N \N Sticky Saguaro Flower Mylar | GluejitsuSticky SaguaroIndica-HybridTHC: 22.4%Special Offer sticky-saguaro-flower-mylar-gluejitsu \N \N \N \N 22.40 \N Sticky Saguaro \N https://images.dutchie.com/0e5415e3462832d106c0d74aa4afd81d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-flower-mylar-gluejitsu t f \N 2025-11-18 03:58:47.567496 2025-11-18 04:26:27.098297 2025-11-18 03:58:47.567496 2025-11-18 05:28:06.716518 112 GluejitsuSticky Saguaro \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.098297+00 \N +3061 \N \N \N DONNY BURGER io-extracts-donny-burger-1g \N \N \N I/S 80.00 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/d2c2617b-5e6f-4243-8076-3be329f832b9?customerType=ADULT t f \N 2025-11-18 14:42:09.092753 2025-11-18 14:42:09.092753 2025-11-18 14:42:09.092753 2025-11-18 14:42:09.092753 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.092753+00 \N +3062 \N \N \N GREAM X TWIN PEAKS io-extracts-gream-x-twin-peaks-1g \N \N \N HYBRID 81.72 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/b7379e50-a1d5-4ddd-a007-e1fe2ade1161?customerType=ADULT t f \N 2025-11-18 14:42:09.095149 2025-11-18 14:42:09.095149 2025-11-18 14:42:09.095149 2025-11-18 14:42:09.095149 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.095149+00 \N +3063 \N \N \N STRAWBERRY MANGO HAZE io-extracts-strawberry-mango-haze-1g \N \N \N S/I 80.39 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/2ca79742-2c7f-47d4-8535-1aea2646a7f7?customerType=ADULT t f \N 2025-11-18 14:42:09.096906 2025-11-18 14:42:09.096906 2025-11-18 14:42:09.096906 2025-11-18 14:42:09.096906 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.096906+00 \N +3064 \N \N \N MEDELLIN X EL GUANTE io-extracts-medellin-x-el-guante-1g \N \N \N I/S 76.83 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/3339770c-ea33-46a8-83d8-d7f1b07e58ed?customerType=ADULT t f \N 2025-11-18 14:42:09.098684 2025-11-18 14:42:09.098684 2025-11-18 14:42:09.098684 2025-11-18 14:42:09.098684 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.098684+00 \N +3065 \N \N \N ALIEN INVASION achieve-alien-invasion-1g \N \N \N I/S 60.20 \N ACHIEVE \N \N \N https://best.treez.io/onlinemenu/category/extract/item/737043b7-ccf1-4680-add9-998edcf6f3da?customerType=ADULT t f \N 2025-11-18 14:42:09.100517 2025-11-18 14:42:09.100517 2025-11-18 14:42:09.100517 2025-11-18 14:42:09.100517 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.100517+00 \N +3067 \N \N \N ORION'S BANANA achieve-orion-s-banana-1g \N \N \N I/S 57.29 \N ACHIEVE \N \N \N https://best.treez.io/onlinemenu/category/extract/item/145122e1-7ef5-41fb-b41c-d35f636d8343?customerType=ADULT t f \N 2025-11-18 14:42:09.104218 2025-11-18 14:42:09.104218 2025-11-18 14:42:09.104218 2025-11-18 14:42:09.104218 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.104218+00 \N +3085 \N \N \N TRUFFLE BUTTER PR (2 X . best-truffle-butter-pr-2-x-5g \N \N \N I/S 30.28 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/0b690a54-6ddb-4d67-ac9e-3aa0b2d942f8?customerType=ADULT t f \N 2025-11-18 14:42:09.137873 2025-11-18 14:42:09.137873 2025-11-18 14:42:09.137873 2025-11-18 14:42:09.137873 149 5G \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.137873+00 \N +2497 \N \N \N Sticky Saguaro Sticky Disty Cartridge | DurbanSticky SaguaroTHC: 92.55% sticky-saguaro-sticky-disty-cartridge-durban \N \N \N \N 92.55 \N Sticky Saguaro \N https://images.dutchie.com/8409ca80ac9f401cb5b7952e13807570?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-sticky-disty-cartridge-durban t f \N 2025-11-18 03:58:47.578233 2025-11-18 04:26:27.110413 2025-11-18 03:58:47.578233 2025-11-18 05:28:19.11025 112 DurbanSticky Saguaro \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.110413+00 \N +3068 \N \N \N DULCE DE UVA easy-tiger-dulce-de-uva-1g \N \N \N I/S 70.02 \N EASY TIGER \N \N \N https://best.treez.io/onlinemenu/category/extract/item/6aa34cd0-764b-42f6-b9c3-19e0daf66b97?customerType=ADULT t f \N 2025-11-18 14:42:09.105845 2025-11-18 14:42:09.105845 2025-11-18 14:42:09.105845 2025-11-18 14:42:09.105845 149 1G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.105845+00 \N +3069 \N \N \N SUB- Z easy-tiger-sub-z-1g \N \N \N HYBRID 74.34 \N EASY TIGER \N \N \N https://best.treez.io/onlinemenu/category/extract/item/ce3ad1c7-4792-4fe1-b355-3aefed184eac?customerType=ADULT t f \N 2025-11-18 14:42:09.107552 2025-11-18 14:42:09.107552 2025-11-18 14:42:09.107552 2025-11-18 14:42:09.107552 149 1G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.107552+00 \N +3070 \N \N \N A5 WAYGU green-dot-labs-a5-waygu-1g \N \N \N HYBRID 72.97 \N GREEN DOT LABS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/82c7707c-7e27-40db-a968-a69b573b43b4?customerType=ADULT t f \N 2025-11-18 14:42:09.109354 2025-11-18 14:42:09.109354 2025-11-18 14:42:09.109354 2025-11-18 14:42:09.109354 149 1G \N \N \N \N \N \N 70.00 70.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.109354+00 \N +3071 \N \N \N FINAL BOSS green-dot-labs-final-boss-1g \N \N \N HYBRID 76.72 \N GREEN DOT LABS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/dde0c4e8-5c35-459a-8977-f8a65b87c359?customerType=ADULT t f \N 2025-11-18 14:42:09.111523 2025-11-18 14:42:09.111523 2025-11-18 14:42:09.111523 2025-11-18 14:42:09.111523 149 1G \N \N \N \N \N \N 70.00 70.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.111523+00 \N +3072 \N \N \N LEMON GRINDER green-dot-labs-lemon-grinder-1g \N \N \N HYBRID 76.67 \N GREEN DOT LABS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/6865edd6-7b26-421e-9ae9-e219e7b97573?customerType=ADULT t f \N 2025-11-18 14:42:09.113228 2025-11-18 14:42:09.113228 2025-11-18 14:42:09.113228 2025-11-18 14:42:09.113228 149 1G \N \N \N \N \N \N 70.00 70.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.113228+00 \N +3073 \N \N \N CHEMDAWG hash-factory-chemdawg-1g \N \N \N HYBRID 70.13 \N HASH FACTORY \N \N \N https://best.treez.io/onlinemenu/category/extract/item/0f4e9282-d29f-4d03-b1ad-c531fe00fee6?customerType=ADULT t f \N 2025-11-18 14:42:09.11486 2025-11-18 14:42:09.11486 2025-11-18 14:42:09.11486 2025-11-18 14:42:09.11486 149 1G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.11486+00 \N +3074 \N \N \N MOROCCAN PEACHES hash-factory-moroccan-peaches-1g \N \N \N HYBRID 76.66 \N HASH FACTORY \N \N \N https://best.treez.io/onlinemenu/category/extract/item/c305eadd-4148-459d-8ae7-412d89e77e7a?customerType=ADULT t f \N 2025-11-18 14:42:09.11668 2025-11-18 14:42:09.11668 2025-11-18 14:42:09.11668 2025-11-18 14:42:09.11668 149 1G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.11668+00 \N +3075 \N \N \N WEDDING PIE hash-factory-wedding-pie-1g \N \N \N I/S 77.36 \N HASH FACTORY \N \N \N https://best.treez.io/onlinemenu/category/extract/item/2ae9fb75-04fb-48a0-9094-e5ccd3fd60a9?customerType=ADULT t f \N 2025-11-18 14:42:09.11825 2025-11-18 14:42:09.11825 2025-11-18 14:42:09.11825 2025-11-18 14:42:09.11825 149 1G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.11825+00 \N +3076 \N \N \N BAKED ALASKA shango-baked-alaska-1g \N \N \N INDICA 73.84 \N SHANGO \N \N \N https://best.treez.io/onlinemenu/category/extract/item/7629db51-dfcb-427d-88a2-ec64c5d1e641?customerType=ADULT t f \N 2025-11-18 14:42:09.120026 2025-11-18 14:42:09.120026 2025-11-18 14:42:09.120026 2025-11-18 14:42:09.120026 149 1G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.120026+00 \N +2511 \N \N \N Stiiizy Distillate Pod | Apple FritterSTIIIZYTHC: 85.44%CBD: 0.12% stiiizy-distillate-pod-apple-fritter-9456 \N \N \N \N 85.44 0.12 STIIIZY \N https://images.dutchie.com/d721b15318961fa24d5595ac22c7325b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-distillate-pod-apple-fritter-9456 t f \N 2025-11-18 03:59:06.698391 2025-11-18 04:26:27.665029 2025-11-18 03:59:06.698391 2025-11-18 05:29:09.694765 112 Apple FritterSTIIIZY \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.665029+00 \N +2512 \N \N \N Stiiizy Distillate Pod | Birthday CakeSTIIIZYTHC: 83.07%CBD: 0.14% stiiizy-distillate-pod-birthday-cake \N \N \N \N 83.07 0.14 STIIIZY \N https://images.dutchie.com/d721b15318961fa24d5595ac22c7325b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-distillate-pod-birthday-cake t f \N 2025-11-18 03:59:06.708471 2025-11-18 04:26:27.673754 2025-11-18 03:59:06.708471 2025-11-18 05:29:12.803616 112 Birthday CakeSTIIIZY \N \N \N {} {} {} 23.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.673754+00 \N +3077 \N \N \N DEVIL DRIVER shango-devil-driver-1g \N \N \N SATIVA 77.59 \N SHANGO \N \N \N https://best.treez.io/onlinemenu/category/extract/item/785af548-bb62-40ce-84c9-8261b9890950?customerType=ADULT t f \N 2025-11-18 14:42:09.12163 2025-11-18 14:42:09.12163 2025-11-18 14:42:09.12163 2025-11-18 14:42:09.12163 149 1G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.12163+00 \N +2514 \N \N \N Stiiizy Live Resin Liquid Diamond Pod | Purple HazeSTIIIZYTHC: 82.8%CBD: 0.72% stiiizy-live-resin-liquid-diamond-pod-purple-haze \N \N \N \N 82.80 0.72 STIIIZY \N https://images.dutchie.com/eb8376c1a07f5a55ac35d8a7983b372a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-live-resin-liquid-diamond-pod-purple-haze t f \N 2025-11-18 03:59:06.714157 2025-11-18 04:26:27.681411 2025-11-18 03:59:06.714157 2025-11-18 05:29:19.004066 112 Purple HazeSTIIIZY \N \N \N {} {} {} 27.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.681411+00 \N +3078 \N \N \N DURBAN POISON aeriz-durban-poison-1g \N \N \N SATIVA 76.16 \N AERIZ \N \N \N https://best.treez.io/onlinemenu/category/extract/item/765ee1b2-bcbe-46fb-9380-74cb152732a7?customerType=ADULT t f \N 2025-11-18 14:42:09.1232 2025-11-18 14:42:09.1232 2025-11-18 14:42:09.1232 2025-11-18 14:42:09.1232 149 1G \N \N \N \N \N \N 41.00 41.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.1232+00 \N +3105 \N \N \N VIP MEMBERSHIP best-vip-membership \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/9d3f8ff5-2c18-4cf8-af89-e905b3b310af?customerType=ADULT t f \N 2025-11-18 14:42:09.172724 2025-11-18 14:42:09.172724 2025-11-18 14:42:09.172724 2025-11-18 14:42:09.172724 149 \N \N \N \N \N \N 99.00 99.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.172724+00 \N +2516 \N \N \N The Healing Alchemist Distillate Cart | GMO CookiesThe Healing AlchemistTHC: 78.64%CBD: 0.19% the-healing-alchemist-distillate-cart-gmo-cookies \N \N \N \N 78.64 0.19 The Healing Alchemist \N https://images.dutchie.com/fb04fabb7f97c4da61c94f75924080d2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-healing-alchemist-distillate-cart-gmo-cookies t f \N 2025-11-18 03:59:13.479649 2025-11-18 04:26:36.880698 2025-11-18 03:59:13.479649 2025-11-18 05:29:49.660403 112 GMO CookiesThe Healing Alchemist \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:36.880698+00 \N +3079 \N \N \N CANNA RSO cannabrands-canna-rso-1g \N \N \N HYBRID 75.88 \N CANNABRANDS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/8a7669db-e238-4004-ad72-b8b78bc98e69?customerType=ADULT t f \N 2025-11-18 14:42:09.124906 2025-11-18 14:42:09.124906 2025-11-18 14:42:09.124906 2025-11-18 14:42:09.124906 149 1G \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.124906+00 \N +3080 \N \N \N BLACK MAPLE SUGAR io-extracts-black-maple-sugar-1g \N \N \N I/S 74.09 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/c8389fc6-9e0e-4d6a-a571-78a6a83d8f69?customerType=ADULT t f \N 2025-11-18 14:42:09.12681 2025-11-18 14:42:09.12681 2025-11-18 14:42:09.12681 2025-11-18 14:42:09.12681 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.12681+00 \N +3081 \N \N \N TROPICAL CANDY io-extracts-tropical-candy-1g \N \N \N HYBRID 80.02 \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/extract/item/d0bf872f-2c78-465a-9d23-d655cc28958b?customerType=ADULT t f \N 2025-11-18 14:42:09.128684 2025-11-18 14:42:09.128684 2025-11-18 14:42:09.128684 2025-11-18 14:42:09.128684 149 1G \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.128684+00 \N +3082 \N \N \N DARK WEB PR alien-labs-dark-web-pr-1g \N \N \N HYBRID 18.64 \N ALIEN LABS \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/31eaa6d4-44b1-46ca-ae14-51b7c7ca150e?customerType=ADULT t f \N 2025-11-18 14:42:09.131727 2025-11-18 14:42:09.131727 2025-11-18 14:42:09.131727 2025-11-18 14:42:09.131727 149 1G \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.131727+00 \N +3083 \N \N \N HEADBAND PR (2 X . best-headband-pr-2-x-5g \N \N \N HYBRID 31.16 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/e36598bd-b3fe-4380-8713-4f5392f0f418?customerType=ADULT t f \N 2025-11-18 14:42:09.133362 2025-11-18 14:42:09.133362 2025-11-18 14:42:09.133362 2025-11-18 14:42:09.133362 149 5G \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.133362+00 \N +3084 \N \N \N OUTER SPACE PR best-outer-space-pr-1g \N \N \N S/I 28.99 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/18bb997d-a468-4268-8d40-abce08d2dbfd?customerType=ADULT t f \N 2025-11-18 14:42:09.135862 2025-11-18 14:42:09.135862 2025-11-18 14:42:09.135862 2025-11-18 14:42:09.135862 149 1G \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.135862+00 \N +2519 \N \N \N The Healing Alchemist Distillate Cart | Sour DieselThe Healing AlchemistSativa-HybridTHC: 77.1%CBD: 0.18% the-healing-alchemist-distillate-cart-sour-diesel \N \N \N \N 77.10 0.18 The Healing Alchemist \N https://images.dutchie.com/fb04fabb7f97c4da61c94f75924080d2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-healing-alchemist-distillate-cart-sour-diesel t f \N 2025-11-18 03:59:13.49374 2025-11-18 04:26:36.893517 2025-11-18 03:59:13.49374 2025-11-18 05:29:41.603997 112 Sour DieselThe Healing Alchemist \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:36.893517+00 \N +2521 \N \N \N DR Flower Mylar | Lemon Vuitton 36 (PH)The PharmSativa-HybridTHC: 15.43% dr-flower-mylar-lemon-vuitton-36-ph \N \N \N \N 15.43 \N The Pharm \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-lemon-vuitton-36-ph t f \N 2025-11-18 03:59:18.652825 2025-11-18 04:26:51.697887 2025-11-18 03:59:18.652825 2025-11-18 05:29:47.731617 112 Lemon Vuitton 36 (PH)The Pharm \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.697887+00 \N +2522 \N \N \N DR Flower Mylar | Mule Fuel (PH)The PharmIndicaTHC: 29.56%Special Offer dr-flower-mylar-mule-fuel-ph \N \N \N \N 29.56 \N The Pharm \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-mule-fuel-ph t f \N 2025-11-18 03:59:18.661348 2025-11-18 04:26:51.704053 2025-11-18 03:59:18.661348 2025-11-18 05:29:52.649854 112 Mule Fuel (PH)The Pharm \N \N \N {} {} {} 75.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.704053+00 \N +2524 \N \N \N Diamond Dusties | 6pk Live Resin Infused Pre-Roll | EmeraldThe PharmHybridTHC: 37.13% diamond-dusties-6pk-live-resin-infused-pre-roll-emerald \N \N \N \N 37.13 \N The Pharm \N https://images.dutchie.com/3f0f597c0763e74e3b844d7487bd78b3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/diamond-dusties-6pk-live-resin-infused-pre-roll-emerald t f \N 2025-11-18 03:59:18.666535 2025-11-18 04:26:51.709055 2025-11-18 03:59:18.666535 2025-11-18 05:30:00.672169 112 EmeraldThe Pharm \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.709055+00 \N +2525 \N \N \N Diamond Dusties | 6pk Live Resin Infused Pre-Roll | Sapphire IndicaThe PharmIndicaTHC: 35.64% diamond-dusties-6pk-live-resin-infused-pre-roll-sapphire-indica \N \N \N \N 35.64 \N The Pharm \N https://images.dutchie.com/3f0f597c0763e74e3b844d7487bd78b3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/diamond-dusties-6pk-live-resin-infused-pre-roll-sapphire-indica t f \N 2025-11-18 03:59:18.668733 2025-11-18 04:26:51.711239 2025-11-18 03:59:18.668733 2025-11-18 05:30:03.664718 112 Sapphire \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.711239+00 \N +2527 \N \N \N Dusties | 1.3G Infused Pre-Roll | DOUBLE YUMThe PharmTHC: 38.68% dusties-1-3g-infused-pre-roll-double-yum \N \N \N \N 38.68 \N The Pharm \N https://images.dutchie.com/58a799b7691188d3ecdacc1a34de4a82?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-1-3g-infused-pre-roll-double-yum t f \N 2025-11-18 03:59:18.674043 2025-11-18 04:26:51.715319 2025-11-18 03:59:18.674043 2025-11-18 05:30:09.85007 112 DOUBLE YUMThe Pharm \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.715319+00 \N +2528 \N \N \N Dusties | 1.3G Infused Pre-Roll | HorchataThe PharmTHC: 40.33%CBD: 0.22% dusties-1-3g-infused-pre-roll-horchata \N \N \N \N 40.33 0.22 The Pharm \N https://images.dutchie.com/58a799b7691188d3ecdacc1a34de4a82?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-1-3g-infused-pre-roll-horchata t f \N 2025-11-18 03:59:18.676954 2025-11-18 04:26:51.7172 2025-11-18 03:59:18.676954 2025-11-18 05:30:15.725715 112 HorchataThe Pharm \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.7172+00 \N +2530 \N \N \N Dusties | 6pk Infused Pre-Roll | Blueberry MacThe PharmHybridTHC: 42.01% dusties-6pk-infused-pre-roll-blueberry-mac \N \N \N \N 42.01 \N The Pharm \N https://images.dutchie.com/a4bd98acea24a101687b5435a088fbaa?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-6pk-infused-pre-roll-blueberry-mac t f \N 2025-11-18 03:59:18.68267 2025-11-18 04:26:51.721215 2025-11-18 03:59:18.68267 2025-11-18 05:30:25.441492 112 Blueberry MacThe Pharm \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.721215+00 \N +2531 \N \N \N Dusties | 6pk Infused Pre-Roll | Peachy PunchThe PharmIndicaTHC: 37.72% dusties-6pk-infused-pre-roll-peachy-punch \N \N \N \N 37.72 \N The Pharm \N https://images.dutchie.com/a4bd98acea24a101687b5435a088fbaa?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-6pk-infused-pre-roll-peachy-punch t f \N 2025-11-18 03:59:18.684778 2025-11-18 04:26:51.723165 2025-11-18 03:59:18.684778 2025-11-18 05:30:28.491079 112 Peachy PunchThe Pharm \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.723165+00 \N +2533 \N \N \N The Pharm Budder | Blue DreamThe PharmSativa-HybridTHC: 71.11%Special Offer the-pharm-budder-blue-dream \N \N \N \N 71.11 \N The Pharm \N https://images.dutchie.com/14c863f38d86af6f2914e9c65231afca?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-pharm-budder-blue-dream t f \N 2025-11-18 03:59:18.689345 2025-11-18 04:26:51.726888 2025-11-18 03:59:18.689345 2025-11-18 05:30:34.535051 112 Blue DreamThe Pharm \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.726888+00 \N +2535 \N \N \N The Strain Source Flower Mylar | ClementineThe Strain Source (TSS)Sativa-HybridTHC: 19.98%Special Offer the-strain-source-flower-mylar-clementine \N \N \N Sativa 19.98 \N The Strain Source (TSS) \N https://images.dutchie.com/f02460a5277d742a5e286a19ecd5f70f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-strain-source-flower-mylar-clementine t f \N 2025-11-18 03:59:23.227155 2025-11-18 04:27:02.763916 2025-11-18 03:59:23.227155 2025-11-18 05:30:40.471701 112 ClementineThe Strain Source (TSS) \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:02.763916+00 \N +2538 \N \N \N Thunder Bud Infused Pre-Roll | Garlic RTZThunder BudIndica-HybridTHC: 24.77% thunder-bud-infused-pre-roll-garlic-rtz \N \N \N \N 24.77 \N Thunder Bud \N https://images.dutchie.com/41291abe6a2eed2fa5e675a89d736366?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-infused-pre-roll-garlic-rtz t f \N 2025-11-18 03:59:42.451904 2025-11-18 04:27:08.323587 2025-11-18 03:59:42.451904 2025-11-18 05:30:53.83222 112 Garlic RTZThunder Bud \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.323587+00 \N +3087 \N \N \N BISCOTTI connected-biscotti-1g \N \N \N I/S 22.93 \N CONNECTED \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/62ef8e54-d737-4160-8d11-b09c89de47f4?customerType=ADULT t f \N 2025-11-18 14:42:09.141822 2025-11-18 14:42:09.141822 2025-11-18 14:42:09.141822 2025-11-18 14:42:09.141822 149 1G \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.141822+00 \N +3088 \N \N \N NAPOLEON BAYONET GL napoleon-napoleon-bayonet-gl-3g \N \N \N INDICA 29.23 \N NAPOLEON \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/8583095d-b256-492a-a8a6-7162873cb4db?customerType=ADULT t f \N 2025-11-18 14:42:09.14359 2025-11-18 14:42:09.14359 2025-11-18 14:42:09.14359 2025-11-18 14:42:09.14359 149 3G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.14359+00 \N +3089 \N \N \N NAPOLEON DVD napoleon-napoleon-dvd-3g \N \N \N SATIVA 26.60 \N NAPOLEON \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/c05fead5-b860-4593-ba51-19e863373782?customerType=ADULT t f \N 2025-11-18 14:42:09.145161 2025-11-18 14:42:09.145161 2025-11-18 14:42:09.145161 2025-11-18 14:42:09.145161 149 3G \N \N \N \N \N \N 30.00 30.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.145161+00 \N +3090 \N \N \N ANIMAL FACE white-mountain-animal-face-1g \N \N \N S/I 20.45 \N WHITE MOUNTAIN \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/b8e1752c-8465-4384-ac44-f0c1d7ffb7d8?customerType=ADULT t f \N 2025-11-18 14:42:09.146785 2025-11-18 14:42:09.146785 2025-11-18 14:42:09.146785 2025-11-18 14:42:09.146785 149 1G \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.146785+00 \N +2542 \N \N \N Thunder Bud Pre-Roll | GruntzThunder BudIndica-HybridTHC: 20.14%Special Offer thunder-bud-pre-roll-gruntz \N \N \N \N 20.14 \N Thunder Bud \N https://images.dutchie.com/7ccebf8db7dd13f7f2a265847bd15d6b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-gruntz t f \N 2025-11-18 03:59:42.468427 2025-11-18 04:27:08.338383 2025-11-18 03:59:42.468427 2025-11-18 05:31:08.079734 112 GruntzThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.338383+00 \N +3091 \N \N \N CANDY GAS white-mountain-candy-gas-1g \N \N \N HYBRID 27.35 \N WHITE MOUNTAIN \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/4a9b8576-87d5-41fb-86b5-651a089119df?customerType=ADULT t f \N 2025-11-18 14:42:09.148555 2025-11-18 14:42:09.148555 2025-11-18 14:42:09.148555 2025-11-18 14:42:09.148555 149 1G \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.148555+00 \N +3092 \N \N \N DO-SI-DO white-montain-do-si-do-1g \N \N \N I/S 26.86 \N WHITE MONTAIN \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/d6b23373-8be5-4772-b813-491245c7f942?customerType=ADULT t f \N 2025-11-18 14:42:09.150322 2025-11-18 14:42:09.150322 2025-11-18 14:42:09.150322 2025-11-18 14:42:09.150322 149 1G \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.150322+00 \N +2552 \N \N \N Tipsy Turtle | 100mg Candy CubesTipsy TurtleTHC: 0.31%Special Offer tipsy-turtle-100mg-candy-cubes \N \N \N \N 0.31 \N Tipsy Turtle \N https://images.dutchie.com/2098a106963fac2f6ca2c991ce43c0b0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tipsy-turtle-100mg-candy-cubes t f \N 2025-11-18 03:59:44.498353 2025-11-18 04:27:09.460313 2025-11-18 03:59:44.498353 2025-11-18 05:31:44.9947 112 100mg Candy CubesTipsy Turtle \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:09.460313+00 \N +3093 \N \N \N KARAT best-karat-1g \N \N \N HYBRID 46.47 \N BEST \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/9040833c-00e5-437f-b4a2-ca18e1ca25a6?customerType=ADULT t f \N 2025-11-18 14:42:09.151858 2025-11-18 14:42:09.151858 2025-11-18 14:42:09.151858 2025-11-18 14:42:09.151858 149 1G \N \N \N \N \N \N 40.00 40.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.151858+00 \N +2554 \N \N \N Tipsy Turtle | 100mg Chocolate Dipped PretzelsTipsy TurtleTHC: 0.51%Special Offer tipsy-turtle-100mg-chocolate-dipped-pretzels \N \N \N \N 0.51 \N Tipsy Turtle \N https://images.dutchie.com/a06ad9aa0d279468495030839026f0fb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tipsy-turtle-100mg-chocolate-dipped-pretzels t f \N 2025-11-18 03:59:44.509858 2025-11-18 04:27:09.469302 2025-11-18 03:59:44.509858 2025-11-18 05:31:54.103544 112 100mg Chocolate Dipped PretzelsTipsy Turtle \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:09.469302+00 \N +3094 \N \N \N CANDY CAKE + I-95 RESIN ROLL (5 X . green-dot-labs-candy-cake-i-95-resin-roll-5-x-5g \N \N \N HYBRID 40.72 \N GREEN DOT LABS \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/b837a013-f4bf-46e6-a190-3a026fb6dde6?customerType=ADULT t f \N 2025-11-18 14:42:09.153533 2025-11-18 14:42:09.153533 2025-11-18 14:42:09.153533 2025-11-18 14:42:09.153533 149 5G \N \N \N \N \N \N 55.00 55.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.153533+00 \N +2556 \N \N \N Tipsy Turtle | 100mg Sand DollarTipsy TurtleTHC: 2.11%Special Offer tipsy-turtle-100mg-sand-dollar \N \N \N \N 2.11 \N Tipsy Turtle \N https://images.dutchie.com/25f122b0d8f2a1c726ee0fb79bdfe7e6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tipsy-turtle-100mg-sand-dollar t f \N 2025-11-18 03:59:44.514475 2025-11-18 04:27:09.473752 2025-11-18 03:59:44.514475 2025-11-18 05:32:03.356367 112 100mg Sand DollarTipsy Turtle \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:09.473752+00 \N +2557 \N \N \N Tipsy Turtle | 100mg THC 50mg CBD Salted Caramel BrownieTipsy TurtleTHCTHC: 0.28%CBD: 0.14%Special Offer tipsy-turtle-100mg-thc-50mg-cbd-salted-caramel-brownie \N \N \N \N 0.28 0.14 Tipsy Turtle \N https://images.dutchie.com/864f9f4ab5f82110313ed0204c1f82e7?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tipsy-turtle-100mg-thc-50mg-cbd-salted-caramel-brownie t f \N 2025-11-18 03:59:44.516937 2025-11-18 04:27:09.475426 2025-11-18 03:59:44.516937 2025-11-18 05:32:06.967798 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:09.475426+00 \N +3095 \N \N \N CHERRY LIME SODA + RAINBOW BELTS V2 LIVE RESIN ROLL(5 X . green-dot-labs-cherry-lime-soda-rainbow-belts-v2-live-resin-roll-5-x-5g \N \N \N HYBRID 38.41 \N GREEN DOT LABS \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/fc4b5222-123d-4d4f-b361-3fd801afb7c4?customerType=ADULT t f \N 2025-11-18 14:42:09.155299 2025-11-18 14:42:09.155299 2025-11-18 14:42:09.155299 2025-11-18 14:42:09.155299 149 5G \N \N \N \N \N \N 55.00 55.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.155299+00 \N +2560 \N \N \N Tropics Live Hash Rosin AIO | Cherry KushTropicsIndica-HybridTHC: 78.45%CBD: 0.17% tropics-live-hash-rosin-aio-cherry-kush \N \N \N \N 78.45 0.17 Tropics \N https://images.dutchie.com/656fc5532954c7711531e79430063f6f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tropics-live-hash-rosin-aio-cherry-kush t f \N 2025-11-18 03:59:54.485838 2025-11-18 04:27:27.520208 2025-11-18 03:59:54.485838 2025-11-18 05:32:16.033237 112 Cherry KushTropics \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:27.520208+00 \N +3106 \N \N \N 4 PIECE ALUMINUM ROTARY GRINDER 63MM best-4-piece-aluminum-rotary-grinder-63mm \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/6c1da7cf-afa5-416a-92ab-57c356e769cb?customerType=ADULT t f \N 2025-11-18 14:42:09.174305 2025-11-18 14:42:09.174305 2025-11-18 14:42:09.174305 2025-11-18 14:42:09.174305 149 \N \N \N \N \N \N 60.00 60.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.174305+00 \N +2561 \N \N \N Tropics Live Hash Rosin AIO | GarlottiTropicsIndica-HybridTHC: 77.09%CBD: 0.13% tropics-live-hash-rosin-aio-garlotti \N \N \N \N 77.09 0.13 Tropics \N https://images.dutchie.com/a4e57559103938667fdb6e257271fe26?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tropics-live-hash-rosin-aio-garlotti t f \N 2025-11-18 03:59:54.494592 2025-11-18 04:27:27.528368 2025-11-18 03:59:54.494592 2025-11-18 05:32:19.083847 112 GarlottiTropics \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:27.528368+00 \N +3096 \N \N \N PINK FROOT + PINK FROOT LIVE RESIN ROLL (5 X . green-dot-labs-pink-froot-pink-froot-live-resin-roll-5-x-5g \N \N \N HYBRID 34.59 \N GREEN DOT LABS \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/5fceaa5e-7f90-4e4d-b763-c480fc04cae9?customerType=ADULT t f \N 2025-11-18 14:42:09.156993 2025-11-18 14:42:09.156993 2025-11-18 14:42:09.156993 2025-11-18 14:42:09.156993 149 5G \N \N \N \N \N \N 55.00 55.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.156993+00 \N +2563 \N \N \N Tropics Live Hash Rosin AIO | Polar BreathTropicsIndica-HybridTHC: 81.65%CBD: 0.15% tropics-live-hash-rosin-aio-polar-breath \N \N \N \N 81.65 0.15 Tropics \N https://images.dutchie.com/a4e57559103938667fdb6e257271fe26?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tropics-live-hash-rosin-aio-polar-breath t f \N 2025-11-18 03:59:54.503402 2025-11-18 04:27:27.533307 2025-11-18 03:59:54.503402 2025-11-18 05:32:27.865239 112 Polar BreathTropics \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:27.533307+00 \N +3097 \N \N \N CHERRYZONA X ORIGINAL GLUE lunchbox-cherryzona-x-original-glue-2-5g \N \N \N HYBRID 35.51 \N LUNCHBOX \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/c9e909bd-5a97-4c48-b9af-e540eb1b2d77?customerType=ADULT t f \N 2025-11-18 14:42:09.158581 2025-11-18 14:42:09.158581 2025-11-18 14:42:09.158581 2025-11-18 14:42:09.158581 149 2.5G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.158581+00 \N +2622 \N \N \N Wyld | 100mg CBD 50mg THC | PeachWyldTHCTHC: 0.13%CBD: 0.27% wyld-100mg-cbd-50mg-thc-peach \N \N \N \N 0.13 0.27 Wyld \N https://images.dutchie.com/4904cc55e5f83a36db53a14addd9f2e6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-100mg-cbd-50mg-thc-peach t f \N 2025-11-18 04:00:34.942262 2025-11-18 04:28:14.289695 2025-11-18 04:00:34.942262 2025-11-18 05:36:10.595016 112 100mg \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.289695+00 \N +3098 \N \N \N LEMON SQUIRT lunchbox-lemon-squirt-2-5g \N \N \N HYBRID 31.04 \N LUNCHBOX \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/d9bf995c-59a2-4d6f-8d1b-52acfec72e23?customerType=ADULT t f \N 2025-11-18 14:42:09.160475 2025-11-18 14:42:09.160475 2025-11-18 14:42:09.160475 2025-11-18 14:42:09.160475 149 2.5G \N \N \N \N \N \N 50.00 50.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.160475+00 \N +2566 \N \N \N Tru Infusion Flower Mylar | GrapechataTRU InfusionHybridTHC: 23.07% tru-infusion-flower-mylar-grapechata \N \N \N \N 23.07 \N TRU Infusion \N https://images.dutchie.com/c326a71404cf81817828dc7f2f768699?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-flower-mylar-grapechata t f \N 2025-11-18 03:59:59.216351 2025-11-18 04:27:38.500811 2025-11-18 03:59:59.216351 2025-11-18 05:32:37.928073 112 GrapechataTRU Infusion \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.500811+00 \N +2568 \N \N \N Tru Infusion Liquid Diamond AIO | Sour MangoTRU InfusionTHC: 85.69%CBD: 0.21% tru-infusion-liquid-diamond-aio-sour-mango \N \N \N \N 85.69 0.21 TRU Infusion \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-liquid-diamond-aio-sour-mango t f \N 2025-11-18 03:59:59.227925 2025-11-18 04:27:38.509313 2025-11-18 03:59:59.227925 2025-11-18 05:32:43.955952 112 Sour MangoTRU Infusion \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.509313+00 \N +3101 \N \N \N MO MEAT twisties-mo-meat-1-25g \N \N \N INDICA 44.60 \N TWISTIES \N \N \N https://best.treez.io/onlinemenu/category/preroll/item/b410bf7c-6a4a-4453-b5e0-4309eb6bde16?customerType=ADULT t f \N 2025-11-18 14:42:09.165637 2025-11-18 14:42:09.165637 2025-11-18 14:42:09.165637 2025-11-18 14:42:09.165637 149 1.25G \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.165637+00 \N +2570 \N \N \N Tru Infusion Live Resin AIO | Ice Box PieTRU InfusionIndica-HybridTHC: 80.12%CBD: 0.16% tru-infusion-live-resin-aio-ice-box-pie \N \N \N \N 80.12 0.16 TRU Infusion \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-aio-ice-box-pie t f \N 2025-11-18 03:59:59.232522 2025-11-18 04:27:38.513672 2025-11-18 03:59:59.232522 2025-11-18 05:32:55.013825 112 Ice Box PieTRU Infusion \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.513672+00 \N +2572 \N \N \N Tru Infusion | 100mg Gummies | PeachTRU InfusionTHC: 0.2% tru-infusion-100mg-gummies-peach \N \N \N \N 0.20 \N TRU Infusion \N https://images.dutchie.com/7e1190f489ab697dd9fc55d6c8a04c22?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-100mg-gummies-peach t f \N 2025-11-18 03:59:59.237237 2025-11-18 04:27:38.519463 2025-11-18 03:59:59.237237 2025-11-18 05:33:01.317955 112 PeachTRU Infusion \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.519463+00 \N +2573 \N \N \N Tru Infusion | Live Resin Batter | GMOZKTRU InfusionHybridTHC: 78.26%CBD: 0.14% tru-infusion-live-resin-batter-gmozk \N \N \N \N 78.26 0.14 TRU Infusion \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-batter-gmozk t f \N 2025-11-18 03:59:59.239669 2025-11-18 04:27:38.521944 2025-11-18 03:59:59.239669 2025-11-18 05:33:04.572267 112 GMOZKTRU Infusion \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.521944+00 \N +2575 \N \N \N Tru Infusion | Live Resin Batter | Silver ProjectTRU InfusionHybridTHC: 77.23%CBD: 0.15% tru-infusion-live-resin-batter-silver-project \N \N \N \N 77.23 0.15 TRU Infusion \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-batter-silver-project t f \N 2025-11-18 03:59:59.243942 2025-11-18 04:27:38.525771 2025-11-18 03:59:59.243942 2025-11-18 05:33:12.829094 112 Silver ProjectTRU Infusion \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.525771+00 \N +2576 \N \N \N Varz Flower Jar | Colonel CrasherVarzHybridTHC: 21.05%Special Offer varz-flower-jar-colonel-crasher \N \N \N \N 21.05 \N Varz \N https://images.dutchie.com/e85dd12dd5854a4952a3c686fb8e8e35?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-flower-jar-colonel-crasher t f \N 2025-11-18 04:00:18.610819 2025-11-18 04:27:43.990381 2025-11-18 04:00:18.610819 2025-11-18 05:33:12.492316 112 Colonel CrasherVarz \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:43.990381+00 \N +3102 \N \N \N CANNABOMBZ- 2500 MG THC SALVE io-extracts-cannabombz-2500-mg-thc-salve \N \N \N HYBRID \N \N IO EXTRACTS \N \N \N https://best.treez.io/onlinemenu/category/topical/item/cf8deb87-e45c-4865-8753-2e339e737fc4?customerType=ADULT t f \N 2025-11-18 14:42:09.167235 2025-11-18 14:42:09.167235 2025-11-18 14:42:09.167235 2025-11-18 14:42:09.167235 149 \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.167235+00 \N +3103 \N \N \N GRUV INTIMATE OIL 600MG lush-gruv-intimate-oil-600mg \N \N \N HYBRID \N \N LUSH \N \N \N https://best.treez.io/onlinemenu/category/topical/item/84201a68-9081-45ba-a81e-7b3b38cd3b4f?customerType=ADULT t f \N 2025-11-18 14:42:09.169164 2025-11-18 14:42:09.169164 2025-11-18 14:42:09.169164 2025-11-18 14:42:09.169164 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.169164+00 \N +3150 \N \N \N ORANGE CHILI PIPE g2-orange-chili-pipe \N \N \N \N \N \N G2 \N \N \N https://best.treez.io/onlinemenu/category/merch/item/2c594f5b-df84-42d0-9f88-85597c5a5cc1?customerType=ADULT t f \N 2025-11-18 14:42:09.253969 2025-11-18 14:42:09.253969 2025-11-18 14:42:09.253969 2025-11-18 14:42:09.253969 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.253969+00 \N +3107 \N \N \N 4 PIECE ALUMINUM ROTARY GRINDER 55MM best-4-piece-aluminum-rotary-grinder-55mm \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/34b2859e-6620-492e-8157-134f70d90eea?customerType=ADULT t f \N 2025-11-18 14:42:09.176084 2025-11-18 14:42:09.176084 2025-11-18 14:42:09.176084 2025-11-18 14:42:09.176084 149 \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.176084+00 \N +3108 \N \N \N GLASS TRAY LARGE best-glass-tray-large \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/c117150b-a97f-425c-8d99-d4091326d917?customerType=ADULT t f \N 2025-11-18 14:42:09.178347 2025-11-18 14:42:09.178347 2025-11-18 14:42:09.178347 2025-11-18 14:42:09.178347 149 \N \N \N \N \N \N 23.00 23.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.178347+00 \N +3109 \N \N \N ROLL & GO MEDIUM best-roll-go-medium \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/466383f7-ca07-48e5-9673-9202b1fcd714?customerType=ADULT t f \N 2025-11-18 14:42:09.180211 2025-11-18 14:42:09.180211 2025-11-18 14:42:09.180211 2025-11-18 14:42:09.180211 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.180211+00 \N +3110 \N \N \N GLASS TRAY MEDIUM best-glass-tray-medium \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/71427d30-ab9e-4d8e-9494-4f102dceb327?customerType=ADULT t f \N 2025-11-18 14:42:09.182086 2025-11-18 14:42:09.182086 2025-11-18 14:42:09.182086 2025-11-18 14:42:09.182086 149 \N \N \N \N \N \N 17.00 17.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.182086+00 \N +3111 \N \N \N SYNDICASE 2.0 best-syndicase-2-0 \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/3f1d7ab1-1c63-4a09-ba91-95018bb23bd3?customerType=ADULT t f \N 2025-11-18 14:42:09.183953 2025-11-18 14:42:09.183953 2025-11-18 14:42:09.183953 2025-11-18 14:42:09.183953 149 \N \N \N \N \N \N 12.00 12.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.183953+00 \N +3112 \N \N \N GLASS ASHTRAY SMALL best-glass-ashtray-small \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/08ff02ef-2977-471f-9bc0-77e661303043?customerType=ADULT t f \N 2025-11-18 14:42:09.185863 2025-11-18 14:42:09.185863 2025-11-18 14:42:09.185863 2025-11-18 14:42:09.185863 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.185863+00 \N +3113 \N \N \N GRINDER CARD #1 best-grinder-card-1 \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/18b66a1b-ba7d-4c28-bfec-95533aa5b72b?customerType=ADULT t f \N 2025-11-18 14:42:09.187801 2025-11-18 14:42:09.187801 2025-11-18 14:42:09.187801 2025-11-18 14:42:09.187801 149 \N \N \N \N \N \N 8.00 8.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.187801+00 \N +3114 \N \N \N GRINDER CARD #2 best-grinder-card-2 \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/7c4b5434-8a38-494c-86f2-2703d497952b?customerType=ADULT t f \N 2025-11-18 14:42:09.189647 2025-11-18 14:42:09.189647 2025-11-18 14:42:09.189647 2025-11-18 14:42:09.189647 149 \N \N \N \N \N \N 8.00 8.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.189647+00 \N +3115 \N \N \N ASHTRAY debowler-ashtray \N \N \N \N \N \N DEBOWLER \N \N \N https://best.treez.io/onlinemenu/category/merch/item/600761d3-22ca-4e56-8c6e-ab8eff9add89?customerType=ADULT t f \N 2025-11-18 14:42:09.191433 2025-11-18 14:42:09.191433 2025-11-18 14:42:09.191433 2025-11-18 14:42:09.191433 149 \N \N \N \N \N \N 9.00 9.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.191433+00 \N +3116 \N \N \N OIL LAMP / ASHTRAY oil-lamp-ashtray \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/815df46b-6bec-4b14-82ca-dbb4a093d515?customerType=ADULT t f \N 2025-11-18 14:42:09.193117 2025-11-18 14:42:09.193117 2025-11-18 14:42:09.193117 2025-11-18 14:42:09.193117 149 \N \N \N \N \N \N 300.00 300.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.193117+00 \N +3144 \N \N \N MINI ISLAND BASH CIGARILLOS 3PK swisher-mini-island-bash-cigarillos-3pk \N \N \N \N \N \N SWISHER \N \N \N https://best.treez.io/onlinemenu/category/merch/item/d1e85906-9fa8-441a-bfd4-8b4e44c65f10?customerType=ADULT t f \N 2025-11-18 14:42:09.242549 2025-11-18 14:42:09.242549 2025-11-18 14:42:09.242549 2025-11-18 14:42:09.242549 149 \N \N \N \N \N \N 4.00 4.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.242549+00 \N +2598 \N \N \N Vortex Flower Jar | BananacondaVortexHybridTHC: 19.63%CBD: 0.01% vortex-flower-jar-bananaconda-19050 \N \N \N \N 19.63 0.01 Vortex \N https://images.dutchie.com/6c5eb5820e5d8952de6b8d03ceb1c221?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-bananaconda-19050 t f \N 2025-11-18 04:00:20.21399 2025-11-18 04:27:45.188691 2025-11-18 04:00:20.21399 2025-11-18 05:34:43.437204 112 BananacondaVortex \N \N \N {} {} {} 100.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.188691+00 \N +2607 \N \N \N Wana | 100mg Gummies 1:1:1 | Passion Fruit PineappleWanaTHC: 0.23%CBD: 0.22% wana-100mg-gummies-1-1-1-passion-fruit-pineapple \N \N \N \N 0.23 0.22 Wana \N https://images.dutchie.com/0fbd54c9200721979a9db939645cef1c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-gummies-1-1-1-passion-fruit-pineapple t f \N 2025-11-18 04:00:24.801039 2025-11-18 04:27:48.227559 2025-11-18 04:00:24.801039 2025-11-18 05:35:14.665688 112 Passion Fruit PineappleWana \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.227559+00 \N +2608 \N \N \N Wana | 100mg Gummies 2:1 | Exotic YuzuWanaTHC: 0.22%CBD: 0.5% wana-100mg-gummies-2-1-exotic-yuzu \N \N \N \N 0.22 0.50 Wana \N https://images.dutchie.com/13ba1f2f35e41597e7031db04cb05d3d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-gummies-2-1-exotic-yuzu t f \N 2025-11-18 04:00:24.808611 2025-11-18 04:27:48.234977 2025-11-18 04:00:24.808611 2025-11-18 05:35:26.058469 112 Exotic YuzuWana \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.234977+00 \N +2451 \N \N \N Select Essential Cart | Sour TangieSelectHybridTHC: 86.95%CBD: 0.21% select-essential-cart-sour-tangie-24664 \N \N \N \N \N \N Select \N \N \N /embedded-menu/AZ-Deeply-Rooted/product/select-essential-cart-sour-tangie-24664 t f \N 2025-11-18 03:57:55.446597 2025-11-18 03:57:55.446597 2025-11-18 03:57:55.446597 2025-11-18 19:45:18.571361 112 \N \N \N \N {} {} {} \N \N \N \N \N \N in_stock \N 2025-11-18 03:57:55.446597+00 \N +2610 \N \N \N Wana | 100mg Gummies | Tropical TrioWanaSativaTHC: 0.21% wana-100mg-gummies-tropical-trio \N \N \N \N 0.21 \N Wana \N https://images.dutchie.com/dfe15d3d42c7438f32a07773dd72ffd8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-gummies-tropical-trio t f \N 2025-11-18 04:00:24.813193 2025-11-18 04:27:48.239736 2025-11-18 04:00:24.813193 2025-11-18 05:35:26.233126 112 Tropical TrioWana \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.239736+00 \N +2612 \N \N \N Wana | 100mg QS Gummies 1:1 | Strawberry MargaritaWanaTHC: 0.21%CBD: 0.24% wana-100mg-qs-gummies-1-1-strawberry-margarita \N \N \N \N 0.21 0.24 Wana \N https://images.dutchie.com/62bf0b4dc6be9d375baeff49dd3a4ddc?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-qs-gummies-1-1-strawberry-margarita t f \N 2025-11-18 04:00:24.817535 2025-11-18 04:27:48.243948 2025-11-18 04:00:24.817535 2025-11-18 05:35:38.516378 112 Strawberry MargaritaWana \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.243948+00 \N +2614 \N \N \N Wana | 100mg QS Gummies | Island PunchWanaTHC: 0.24% wana-100mg-qs-gummies-island-punch \N \N \N \N 0.24 \N Wana \N https://images.dutchie.com/0231cc85415728e74b63bcdd4626ee53?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-qs-gummies-island-punch t f \N 2025-11-18 04:00:24.821313 2025-11-18 04:27:48.248134 2025-11-18 04:00:24.821313 2025-11-18 05:35:44.874339 112 Island PunchWana \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.248134+00 \N +2615 \N \N \N Wana | 100mg QS Gummies | LimoncelloWanaTHC: 0.22% wana-100mg-qs-gummies-limoncello \N \N \N \N 0.22 \N Wana \N https://images.dutchie.com/a8ed428013fd65247ebaed5194887f59?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-qs-gummies-limoncello t f \N 2025-11-18 04:00:24.823436 2025-11-18 04:27:48.250269 2025-11-18 04:00:24.823436 2025-11-18 05:35:47.932824 112 LimoncelloWana \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.250269+00 \N +3151 \N \N \N 4" ORANGE PIPE g2-4-orange-pipe \N \N \N \N \N \N G2 \N \N \N https://best.treez.io/onlinemenu/category/merch/item/15cb03ab-ffa0-45a6-88dd-5e3bfe1236fc?customerType=ADULT t f \N 2025-11-18 14:42:09.255706 2025-11-18 14:42:09.255706 2025-11-18 14:42:09.255706 2025-11-18 14:42:09.255706 149 \N \N \N \N \N \N 5.00 5.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.255706+00 \N +2618 \N \N \N Wana | Quick Relief | 1:1:1 50mg CBC: 50mg CBG :50mg THC | Cherry ColaWanaTHCTHC: 0.12% wana-quick-relief-1-1-1-50mg-cbc-50mg-cbg-50mg-thc-cherry-cola \N \N \N \N 0.12 \N Wana \N https://images.dutchie.com/76ea622e53f1b0765925a08cdcad58f3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-quick-relief-1-1-1-50mg-cbc-50mg-cbg-50mg-thc-cherry-cola t f \N 2025-11-18 04:00:24.829714 2025-11-18 04:27:48.256782 2025-11-18 04:00:24.829714 2025-11-18 05:35:57.621482 112 1:1:1 50mg CBC: 50mg CBG :50mg \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.256782+00 \N +2619 \N \N \N Wana | Stay Asleep Gummies 20:5:5:10 CBD:CBN:CBG:THC | Dream BerryWanaTHCTHC: 0.22%CBD: 0.39% wana-stay-asleep-gummies-20-5-5-10-cbd-cbn-cbg-thc-dream-berry \N \N \N \N 0.22 0.39 Wana \N https://images.dutchie.com/3e44f8c4812a88ba07df1606949ae1ae?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-stay-asleep-gummies-20-5-5-10-cbd-cbn-cbg-thc-dream-berry t f \N 2025-11-18 04:00:24.832442 2025-11-18 04:27:48.258692 2025-11-18 04:00:24.832442 2025-11-18 05:36:01.444053 112 Stay Asleep Gummies 20:5:5:10 \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.258692+00 \N +2620 \N \N \N Wizard Trees Flower Jar | Lime LightWizard TreesSativa-HybridTHC: 24.14% wizard-trees-flower-jar-lime-light \N \N \N \N 24.14 \N Wizard Trees \N https://images.dutchie.com/a4ea88a117c0205448e0a5dbecf0c5bb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wizard-trees-flower-jar-lime-light t f \N 2025-11-18 04:00:30.133667 2025-11-18 04:28:03.159838 2025-11-18 04:00:30.133667 2025-11-18 05:36:04.574548 112 Lime LightWizard Trees \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:03.159838+00 \N +3145 \N \N \N REGULAR CIGARILLO POUCH 2PK swisher-regular-cigarillo-pouch-2pk \N \N \N \N \N \N SWISHER \N \N \N https://best.treez.io/onlinemenu/category/merch/item/ed8874b8-7ff2-4bfe-8033-1244a32e3402?customerType=ADULT t f \N 2025-11-18 14:42:09.24468 2025-11-18 14:42:09.24468 2025-11-18 14:42:09.24468 2025-11-18 14:42:09.24468 149 \N \N \N \N \N \N 3.00 3.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.24468+00 \N +2067 \N \N \N DR Flower Mylar | London Fog (ASY)AsylumIndica-HybridTHC: 29.07% dr-flower-mylar-london-fog-asy \N \N \N \N 29.07 \N Asylum \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-london-fog-asy t f \N 2025-11-18 03:16:27.275158 2025-11-18 04:16:43.486231 2025-11-18 03:16:27.275158 2025-11-18 05:03:06.065576 112 London Fog (ASY)Asylum \N \N \N {} {} {} 110.00 \N \N \N \N \N in_stock \N 2025-11-18 04:16:43.486231+00 \N +3119 \N \N \N SMALL JEWEL TIPS small-jewel-tips \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/2674e1e2-8fae-46cd-bc38-0ce58bc028fe?customerType=ADULT t f \N 2025-11-18 14:42:09.198383 2025-11-18 14:42:09.198383 2025-11-18 14:42:09.198383 2025-11-18 14:42:09.198383 149 \N \N \N \N \N \N 5.00 5.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.198383+00 \N +3120 \N \N \N HEMPWICK 15.5FT hempwick-15-5ft \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/cb096a27-0028-4897-9e0d-5aff7b90f86c?customerType=ADULT t f \N 2025-11-18 14:42:09.199955 2025-11-18 14:42:09.199955 2025-11-18 14:42:09.199955 2025-11-18 14:42:09.199955 149 \N \N \N \N \N \N 4.00 4.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.199955+00 \N +2069 \N \N \N Canamo Live Resin Cart | Mean MugCanamo ConcentratesHybridTHC: 76.15%CBD: 0.15% canamo-live-resin-cart-mean-mug \N \N \N \N 76.15 0.15 Canamo Concentrates \N https://images.dutchie.com/56c4848a32d0620cd9fadd1daac33604?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-mean-mug t f \N 2025-11-18 03:51:25.801951 2025-11-18 04:17:55.606977 2025-11-18 03:51:25.801951 2025-11-18 05:03:12.25503 112 Mean MugCanamo Concentrates \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.606977+00 \N +2071 \N \N \N Canamo Live Resin Cart | Sour OctangCanamo ConcentratesSativa-HybridTHC: 78.65%CBD: 0.15% canamo-live-resin-cart-sour-octang \N \N \N \N 78.65 0.15 Canamo Concentrates \N https://images.dutchie.com/56c4848a32d0620cd9fadd1daac33604?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-sour-octang t f \N 2025-11-18 03:51:25.80667 2025-11-18 04:17:55.613234 2025-11-18 03:51:25.80667 2025-11-18 05:03:18.431169 112 Sour OctangCanamo Concentrates \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.613234+00 \N +2073 \N \N \N Canamo Shatter | Grateful BreathCanamo ConcentratesIndica-HybridTHC: 78.79%CBD: 0.14%Special Offer canamo-shatter-grateful-breath \N \N \N \N 78.79 0.14 Canamo Concentrates \N https://images.dutchie.com/c81ef2ff9b2e6afafe5a46cbbde24775?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-grateful-breath t f \N 2025-11-18 03:51:25.811334 2025-11-18 04:17:55.619317 2025-11-18 03:51:25.811334 2025-11-18 05:03:29.497559 112 Grateful BreathCanamo Concentrates \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.619317+00 \N +2076 \N \N \N Canamo Shatter | Rotten RozayCanamo ConcentratesHybridTHC: 74%CBD: 0.11%Special Offer canamo-shatter-rotten-rozay \N \N \N \N 74.00 0.11 Canamo Concentrates \N https://images.dutchie.com/c81ef2ff9b2e6afafe5a46cbbde24775?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-rotten-rozay t f \N 2025-11-18 03:51:25.818148 2025-11-18 04:17:55.629116 2025-11-18 03:51:25.818148 2025-11-18 05:03:38.49548 112 Rotten RozayCanamo Concentrates \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:17:55.629116+00 \N +2081 \N \N \N Chill Pill | 100mg THC 10pk | RESINTIMEChill PillTHCTHC: 3.19% chill-pill-100mg-thc-10pk-resintime \N \N \N \N 3.19 \N Chill Pill \N https://images.dutchie.com/ea7289d2272736e9d5360efcebe1ec4d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-resintime t f \N 2025-11-18 03:51:38.540008 2025-11-18 04:18:16.28816 2025-11-18 03:51:38.540008 2025-11-18 05:03:53.336487 112 100mg \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.28816+00 \N +2093 \N \N \N Connected Cured Resin Cart | Silver SpoonConnected CannabisSativa-HybridTHC: 74.88% connected-cured-resin-cart-silver-spoon \N \N \N \N 74.88 \N Connected Cannabis \N https://images.dutchie.com/9a5a2ae45ddaad6b8a6bdf2b34975d3a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-cured-resin-cart-silver-spoon t f \N 2025-11-18 03:52:02.32092 2025-11-18 04:18:48.513953 2025-11-18 03:52:02.32092 2025-11-18 05:04:42.03436 112 Silver SpoonConnected Cannabis \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.513953+00 \N +2096 \N \N \N Connected Flower Jar | Ghost OGConnected CannabisIndica-HybridTHC: 24.82%Special Offer connected-flower-jar-ghost-og \N \N \N \N 24.82 \N Connected Cannabis \N https://images.dutchie.com/cb9ecdbe35dc15a9224cd8e3e0c35bbd?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-flower-jar-ghost-og t f \N 2025-11-18 03:52:02.32848 2025-11-18 04:18:48.52082 2025-11-18 03:52:02.32848 2025-11-18 05:04:51.417492 112 Ghost OGConnected Cannabis \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:48.52082+00 \N +3152 \N \N \N ASSORTED FUNNEL BOWL 14MM assorted-funnel-bowl-14mm \N \N \N \N \N \N \N \N \N https://best.treez.io/onlinemenu/category/merch/item/af65eb42-a208-40f7-b1cb-2a96ce924306?customerType=ADULT t f \N 2025-11-18 14:42:09.257441 2025-11-18 14:42:09.257441 2025-11-18 14:42:09.257441 2025-11-18 14:42:09.257441 149 \N \N \N \N \N \N 2.20 2.20 \N \N \N \N in_stock \N 2025-11-18 14:42:09.257441+00 \N +2098 \N \N \N Cure Injoy Distillate AIO | JealousyCure InjoyTHC: 85.73%CBD: 0.23%Special Offer cure-injoy-distillate-aio-jealousy \N \N \N \N 85.73 0.23 Cure Injoy \N https://images.dutchie.com/41c1250b0448e11c291915d59d715368?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-jealousy t f \N 2025-11-18 03:52:14.329809 2025-11-18 04:19:07.174225 2025-11-18 03:52:14.329809 2025-11-18 05:04:57.584197 112 JealousyCure Injoy \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:07.174225+00 \N +2101 \N \N \N Cure Injoy Distillate AIO | RS11Cure InjoyTHC: 83.02%CBD: 0.2%Special Offer cure-injoy-distillate-aio-rs11 \N \N \N \N 83.02 0.20 Cure Injoy \N https://images.dutchie.com/7aafb525b7c2ead0594a4400c5a79ec7?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-rs11 t f \N 2025-11-18 03:52:14.337069 2025-11-18 04:19:07.180682 2025-11-18 03:52:14.337069 2025-11-18 05:05:14.399087 112 RS11Cure Injoy \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:07.180682+00 \N +2104 \N \N \N DR Flower Mylar | AK 1995(PH)Deeply RootedSativa-HybridTHC: 21.02%Special Offer dr-flower-mylar-ak-1995-ph \N \N \N \N 21.02 \N Deeply Rooted \N https://images.dutchie.com/a571bfaf180a86ea8c5b863c7988807e?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-ak-1995-ph t f \N 2025-11-18 03:52:35.805755 2025-11-18 04:19:24.137852 2025-11-18 03:52:35.805755 2025-11-18 05:05:23.383774 112 AK 1995(PH)Deeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.137852+00 \N +2106 \N \N \N DR Flower Mylar | Black MapleDeeply RootedHybridTHC: 25.67%Special Offer dr-flower-mylar-black-maple-8834 \N \N \N \N 25.67 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-black-maple-8834 t f \N 2025-11-18 03:52:35.812171 2025-11-18 04:19:24.142011 2025-11-18 03:52:35.812171 2025-11-18 05:05:29.486179 112 Black MapleDeeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.142011+00 \N +3146 \N \N \N WHITE GRAPE CIGARILLO POUCH 2PK swisher-white-grape-cigarillo-pouch-2pk \N \N \N \N \N \N SWISHER \N \N \N https://best.treez.io/onlinemenu/category/merch/item/1c250e9e-d681-4144-ac50-715e21bf1b0a?customerType=ADULT t f \N 2025-11-18 14:42:09.246526 2025-11-18 14:42:09.246526 2025-11-18 14:42:09.246526 2025-11-18 14:42:09.246526 149 \N \N \N \N \N \N 3.00 3.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.246526+00 \N +3147 \N \N \N 5" ETCHED HEAVY ORANGE PIPE g2-5-etched-heavy-orange-pipe \N \N \N \N \N \N G2 \N \N \N https://best.treez.io/onlinemenu/category/merch/item/2d3c9112-af13-4c4e-962a-f0657be7282b?customerType=ADULT t f \N 2025-11-18 14:42:09.248371 2025-11-18 14:42:09.248371 2025-11-18 14:42:09.248371 2025-11-18 14:42:09.248371 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.248371+00 \N +3148 \N \N \N 5" HEAVY GLASS PIPE g2-5-heavy-glass-pipe \N \N \N \N \N \N G2 \N \N \N https://best.treez.io/onlinemenu/category/merch/item/b58bd772-6f5f-4cba-afd2-d03662310707?customerType=ADULT t f \N 2025-11-18 14:42:09.250193 2025-11-18 14:42:09.250193 2025-11-18 14:42:09.250193 2025-11-18 14:42:09.250193 149 \N \N \N \N \N \N 20.00 20.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.250193+00 \N +3149 \N \N \N 5" BLACK GOLF BALL PIPE g2-5-black-golf-ball-pipe \N \N \N \N \N \N G2 \N \N \N https://best.treez.io/onlinemenu/category/merch/item/18f680e4-5368-46b6-905d-404e1fc8e6fb?customerType=ADULT t f \N 2025-11-18 14:42:09.252121 2025-11-18 14:42:09.252121 2025-11-18 14:42:09.252121 2025-11-18 14:42:09.252121 149 \N \N \N \N \N \N 14.00 14.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.252121+00 \N +2320 \N \N \N Keef Classic Soda Blue Razz | 10mgKeefHybridTHC: 9.55 mg keef-classic-soda-blue-razz-10mg-37618 \N \N \N \N \N \N Keef \N https://images.dutchie.com/20f0a5ac292f6526177c127d3bc65e37?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-blue-razz-10mg-37618 t f \N 2025-11-18 03:55:31.858875 2025-11-18 04:22:52.971644 2025-11-18 03:55:31.858875 2025-11-18 05:18:01.136021 112 10mgKeef \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.971644+00 \N +3154 \N \N \N PEACEMAKER CLASSIC CONES 3PK raw-peacemaker-classic-cones-3pk \N \N \N \N \N \N RAW \N \N \N https://best.treez.io/onlinemenu/category/merch/item/8b5daeeb-e335-4088-b274-f244923a5a1d?customerType=ADULT t f \N 2025-11-18 14:42:09.261025 2025-11-18 14:42:09.261025 2025-11-18 14:42:09.261025 2025-11-18 14:42:09.261025 149 \N \N \N \N \N \N 5.00 5.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.261025+00 \N +3155 \N \N \N BEST DISPENSARY NAVY (POCKET LOGO)T SHIRT best-best-dispensary-navy-pocket-logo-t-shirt \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/279373e5-1814-4701-80c0-65b8c64a5079?customerType=ADULT t f \N 2025-11-18 14:42:09.262779 2025-11-18 14:42:09.262779 2025-11-18 14:42:09.262779 2025-11-18 14:42:09.262779 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.262779+00 \N +3156 \N \N \N BEST DISPENSARY WHITE (CHEST LOGO ) T SHIRT best-best-dispensary-white-chest-logo-t-shirt \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/984c28b2-8afe-42e8-8909-d601e60161eb?customerType=ADULT t f \N 2025-11-18 14:42:09.264545 2025-11-18 14:42:09.264545 2025-11-18 14:42:09.264545 2025-11-18 14:42:09.264545 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.264545+00 \N +3157 \N \N \N BEST DISPENSARY NAVY (CHEST LOGO ) T SHIRT best-best-dispensary-navy-chest-logo-t-shirt \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/a28b92a3-8e18-44e7-b342-6cefe2f59fc1?customerType=ADULT t f \N 2025-11-18 14:42:09.266339 2025-11-18 14:42:09.266339 2025-11-18 14:42:09.266339 2025-11-18 14:42:09.266339 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.266339+00 \N +3158 \N \N \N WOMENS BEST DISPENSARY NAVY (CROWN LOGO ) T SHIRT best-womens-best-dispensary-navy-crown-logo-t-shirt \N \N \N \N \N \N BEST \N \N \N https://best.treez.io/onlinemenu/category/merch/item/e719d6e9-4e47-4f34-85d1-7290b4033872?customerType=ADULT t f \N 2025-11-18 14:42:09.268008 2025-11-18 14:42:09.268008 2025-11-18 14:42:09.268008 2025-11-18 14:42:09.268008 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.268008+00 \N +3159 \N \N \N PUFFCO PROXY BLACK puffco-puffco-proxy-black \N \N \N \N \N \N PUFFCO \N \N \N https://best.treez.io/onlinemenu/category/merch/item/59a08056-d479-45bf-be2e-aec02e4fcfbc?customerType=ADULT t f \N 2025-11-18 14:42:09.269749 2025-11-18 14:42:09.269749 2025-11-18 14:42:09.269749 2025-11-18 14:42:09.269749 149 \N \N \N \N \N \N 255.00 255.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.269749+00 \N +3160 \N \N \N CITRUS SPARK 100MG sip-citrus-spark-100mg \N \N \N HYBRID \N \N SIP \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/47b66199-996a-4c8f-86b7-ac34d70eaf95?customerType=ADULT t f \N 2025-11-18 14:42:09.271442 2025-11-18 14:42:09.271442 2025-11-18 14:42:09.271442 2025-11-18 14:42:09.271442 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.271442+00 \N +3161 \N \N \N DREAMBERRY 100MG sip-dreamberry-100mg \N \N \N HYBRID \N \N SIP \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/63454733-1d1c-499f-8971-4e9021cc7146?customerType=ADULT t f \N 2025-11-18 14:42:09.273123 2025-11-18 14:42:09.273123 2025-11-18 14:42:09.273123 2025-11-18 14:42:09.273123 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.273123+00 \N +3162 \N \N \N SIP HURRICANE 100MG sip-sip-hurricane-100mg \N \N \N HYBRID \N \N SIP \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/cd3112ad-557c-4900-89b4-42fcabe8c402?customerType=ADULT t f \N 2025-11-18 14:42:09.2749 2025-11-18 14:42:09.2749 2025-11-18 14:42:09.2749 2025-11-18 14:42:09.2749 149 \N \N \N \N \N \N 10.00 10.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.2749+00 \N +3163 \N \N \N BUBBA KUSH ROOT BEER 100MG keef-bubba-kush-root-beer-100mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/94902f7a-29a7-4847-b809-201acc359582?customerType=ADULT t f \N 2025-11-18 14:42:09.276643 2025-11-18 14:42:09.276643 2025-11-18 14:42:09.276643 2025-11-18 14:42:09.276643 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.276643+00 \N +3164 \N \N \N C.R.E.A.M 100MG keef-c-r-e-a-m-100mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/24ad27b0-e3fc-46ca-8300-bb8c6ec3f775?customerType=ADULT t f \N 2025-11-18 14:42:09.278305 2025-11-18 14:42:09.278305 2025-11-18 14:42:09.278305 2025-11-18 14:42:09.278305 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.278305+00 \N +2146 \N \N \N Dermafreeze | 600mg Roll-On TopicalDermafreezeTHC: 114.76 mgCBD: 423.9 mg dermafreeze-600mg-roll-on-topical \N \N \N \N \N \N Dermafreeze \N https://images.dutchie.com/0aefd2567defbfc59d51f528329498b2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-600mg-roll-on-topical t f \N 2025-11-18 03:52:38.019528 2025-11-18 04:19:27.554109 2025-11-18 03:52:38.019528 2025-11-18 05:07:51.449224 112 600mg Roll-On TopicalDermafreeze \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:27.554109+00 \N +2151 \N \N \N Dime Distillate AIO | Blackberry OGDime IndustriesTHC: 94.8%CBD: 0.15% dime-distillate-aio-blackberry-og \N \N \N \N 94.80 0.15 Dime Industries \N https://images.dutchie.com/04842a79b29a70e3d7edf8acdddcfa62?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-blackberry-og t f \N 2025-11-18 03:52:43.833245 2025-11-18 04:19:43.676045 2025-11-18 03:52:43.833245 2025-11-18 05:08:09.614 112 Blackberry OGDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.676045+00 \N +2153 \N \N \N Dime Distillate AIO | Cactus ChillDime IndustriesTHC: 90.49%CBD: 0.21% dime-distillate-aio-cactus-chill \N \N \N \N 90.49 0.21 Dime Industries \N https://images.dutchie.com/7ac91c37b384f9b3f2ce6616dcab7de2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-cactus-chill t f \N 2025-11-18 03:52:43.83786 2025-11-18 04:19:43.680576 2025-11-18 03:52:43.83786 2025-11-18 05:08:15.913428 112 Cactus ChillDime Industries \N \N \N {} {} {} 60.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.680576+00 \N +2156 \N \N \N Dime Distillate AIO | Pink RoseDime IndustriesTHC: 92.24%CBD: 0.24% dime-distillate-aio-pink-rose-25111 \N \N \N \N 92.24 0.24 Dime Industries \N https://images.dutchie.com/4e03b21077df453b0df49cc134f8fb8c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-pink-rose-25111 t f \N 2025-11-18 03:52:43.844448 2025-11-18 04:19:43.686905 2025-11-18 03:52:43.844448 2025-11-18 05:08:29.184788 112 Pink RoseDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.686905+00 \N +2158 \N \N \N Dime Distillate AIO | Tropical KiwiDime IndustriesTHC: 94.75%CBD: 0.18% dime-distillate-aio-tropical-kiwi-45270 \N \N \N \N 94.75 0.18 Dime Industries \N https://images.dutchie.com/412c818f2729b5a349d2a0bc50625619?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-aio-tropical-kiwi-45270 t f \N 2025-11-18 03:52:43.848402 2025-11-18 04:19:43.691366 2025-11-18 03:52:43.848402 2025-11-18 05:08:37.22074 112 Tropical KiwiDime Industries \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.691366+00 \N +2201 \N \N \N Micro Drops | 100mg Daytime THC TinctureDrip OilsTHCTHC: 0.39% micro-drops-100mg-daytime-thc-tincture \N \N \N \N 0.39 \N Drip Oils \N https://images.dutchie.com/185e708cdf4bf83dde4beca51671eaee?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/micro-drops-100mg-daytime-thc-tincture t f \N 2025-11-18 03:53:13.823699 2025-11-18 04:20:19.276019 2025-11-18 03:53:13.823699 2025-11-18 05:11:01.785949 112 100mg Daytime \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.276019+00 \N +2161 \N \N \N Dime Distillate Cart | Blackberry OGDime IndustriesTHC: 95.15%CBD: 0.25% dime-distillate-cart-blackberry-og \N \N \N \N 95.15 0.25 Dime Industries \N https://images.dutchie.com/03efb5383e9bff7d2cdbe1eda2e9fb98?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-blackberry-og t f \N 2025-11-18 03:52:43.854676 2025-11-18 04:19:43.697352 2025-11-18 03:52:43.854676 2025-11-18 05:08:47.059672 112 Blackberry OGDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.697352+00 \N +2163 \N \N \N Dime Distillate Cart | Forbidden AppleDime IndustriesTHC: 92.97%CBD: 0.21% dime-distillate-cart-forbidden-apple \N \N \N \N 92.97 0.21 Dime Industries \N https://images.dutchie.com/03efb5383e9bff7d2cdbe1eda2e9fb98?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-forbidden-apple t f \N 2025-11-18 03:52:43.859249 2025-11-18 04:19:43.701443 2025-11-18 03:52:43.859249 2025-11-18 05:08:53.04683 112 Forbidden AppleDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.701443+00 \N +2166 \N \N \N Dime Distillate Cart | Pina ColadaDime IndustriesTHC: 92.32%CBD: 0.26% dime-distillate-cart-pina-colada \N \N \N \N 92.32 0.26 Dime Industries \N https://images.dutchie.com/03efb5383e9bff7d2cdbe1eda2e9fb98?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-pina-colada t f \N 2025-11-18 03:52:43.866796 2025-11-18 04:19:43.708341 2025-11-18 03:52:43.866796 2025-11-18 05:09:02.583427 112 Pina ColadaDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.708341+00 \N +2168 \N \N \N Dime Distillate Cart | Sour GrapeDime IndustriesTHC: 93.54%CBD: 0.2% dime-distillate-cart-sour-grape \N \N \N \N 93.54 0.20 Dime Industries \N https://images.dutchie.com/03efb5383e9bff7d2cdbe1eda2e9fb98?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-distillate-cart-sour-grape t f \N 2025-11-18 03:52:43.871768 2025-11-18 04:19:43.712875 2025-11-18 03:52:43.871768 2025-11-18 05:09:10.683774 112 Sour GrapeDime Industries \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.712875+00 \N +2170 \N \N \N Dime Live Resin AIO | Dime OGDime IndustriesIndicaTHC: 75.59%CBD: 0.19% dime-live-resin-aio-dime-og \N \N \N \N 75.59 0.19 Dime Industries \N https://images.dutchie.com/e5c7e5a84d2f6690e98336c1012e8fb9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dime-live-resin-aio-dime-og t f \N 2025-11-18 03:52:43.876208 2025-11-18 04:19:43.71694 2025-11-18 03:52:43.876208 2025-11-18 05:09:16.970198 112 Dime OGDime Industries \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:43.71694+00 \N +2175 \N \N \N Doja Flower Jar | Lime SickleDOJASativaTHC: 24.7% doja-flower-jar-lime-sickle \N \N \N \N 24.70 \N DOJA \N https://images.dutchie.com/06591953507a9e789dd163e21dfe8299?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/doja-flower-jar-lime-sickle t f \N 2025-11-18 03:52:50.258858 2025-11-18 04:19:52.220752 2025-11-18 03:52:50.258858 2025-11-18 05:09:33.086376 112 Lime SickleDOJA \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:52.220752+00 \N +2177 \N \N \N Dr. Zodiak Distillate Moonrock Astro Pod | Bobby BlueDr. ZodiakTHC: 92.68%CBD: 3.16%Special Offer dr-zodiak-distillate-moonrock-astro-pod-bobby-blue \N \N \N \N 92.68 3.16 Dr. Zodiak \N https://images.dutchie.com/7d506d1cee1cfc231471d65e31261faf?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-distillate-moonrock-astro-pod-bobby-blue t f \N 2025-11-18 03:53:09.234517 2025-11-18 04:19:59.917958 2025-11-18 03:53:09.234517 2025-11-18 05:09:39.239154 112 Bobby BlueDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.917958+00 \N +2322 \N \N \N Keef Classic Soda Mr. Puffer XTREME | 100mgKeefHybridTHC: 0.03% keef-classic-soda-mr-puffer-xtreme-100mg-78461 \N \N \N \N 0.03 \N Keef \N https://images.dutchie.com/05132bde2eca56e3bf8b851752536be3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-mr-puffer-xtreme-100mg-78461 t f \N 2025-11-18 03:55:31.864106 2025-11-18 04:22:52.979025 2025-11-18 03:55:31.864106 2025-11-18 05:18:13.000895 112 100mgKeef \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.979025+00 \N +3165 \N \N \N Classic Soda - Purple Passion keef-classic-soda-purple-passion \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/da0ac51d-15b0-4ada-83af-e5adb9f41ed3?customerType=ADULT t f \N 2025-11-18 14:42:09.27994 2025-11-18 14:42:09.27994 2025-11-18 14:42:09.27994 2025-11-18 14:42:09.27994 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.27994+00 \N +3166 \N \N \N HIGH OCTANE 100MG keef-high-octane-100mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/9d543edd-4df0-492e-b3ec-2f2d371c2472?customerType=ADULT t f \N 2025-11-18 14:42:09.281564 2025-11-18 14:42:09.281564 2025-11-18 14:42:09.281564 2025-11-18 14:42:09.281564 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.281564+00 \N +3167 \N \N \N KEEF - BLUE RAZZ 100MG keef-keef-blue-razz-100mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/e8bafb91-9485-4fd8-ac73-ff54d93e6595?customerType=ADULT t f \N 2025-11-18 14:42:09.283754 2025-11-18 14:42:09.283754 2025-11-18 14:42:09.283754 2025-11-18 14:42:09.283754 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.283754+00 \N +3168 \N \N \N MR. PUFFER 100MG keef-mr-puffer-100mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/6dc290a2-bca3-42c9-a0f6-528c92fbac9f?customerType=ADULT t f \N 2025-11-18 14:42:09.285741 2025-11-18 14:42:09.285741 2025-11-18 14:42:09.285741 2025-11-18 14:42:09.285741 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.285741+00 \N +2191 \N \N \N Drip Top-Shelf Flower | MelonattidripSativaTHC: 26.67% drip-top-shelf-flower-melonatti \N \N \N \N 26.67 \N drip \N https://images.dutchie.com/c70afecc0c29bb3431f7fa2b010d88fe?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-top-shelf-flower-melonatti t f \N 2025-11-18 03:53:11.784897 2025-11-18 04:20:03.377534 2025-11-18 03:53:11.784897 2025-11-18 05:10:28.994052 112 Melonattidrip \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:03.377534+00 \N +2194 \N \N \N Distillate AIO | Orange Cream FizzDrip OilsHybridTHC: 92.65%CBD: 0.87%Special Offer distillate-aio-orange-cream-fizz \N \N \N \N 92.65 0.87 Drip Oils \N https://images.dutchie.com/e6984e674633394c8b0c5ea7bb128911?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-orange-cream-fizz t f \N 2025-11-18 03:53:13.808032 2025-11-18 04:20:19.260567 2025-11-18 03:53:13.808032 2025-11-18 05:10:38.443366 112 Orange Cream FizzDrip Oils \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.260567+00 \N +2196 \N \N \N Distillate AIO | Strawberry WatermelonDrip OilsHybridTHC: 86.4%CBD: 1.45%Special Offer distillate-aio-strawberry-watermelon \N \N \N \N 86.40 1.45 Drip Oils \N https://images.dutchie.com/f25cef0e48df6c59797b26ee9dfc9f06?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-strawberry-watermelon t f \N 2025-11-18 03:53:13.812669 2025-11-18 04:20:19.26539 2025-11-18 03:53:13.812669 2025-11-18 05:10:44.39252 112 Strawberry WatermelonDrip Oils \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.26539+00 \N +2199 \N \N \N High CBN | 600mg CBN Hemp TinctureDrip Oils high-cbn-600mg-cbn-hemp-tincture \N \N \N \N \N \N Drip Oils \N https://images.dutchie.com/49283ea64968ca90b41eb6aac9d95e35?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-cbn-600mg-cbn-hemp-tincture t f \N 2025-11-18 03:53:13.819672 2025-11-18 04:20:19.272005 2025-11-18 03:53:13.819672 2025-11-18 05:10:55.776216 112 600mg CBN Hemp TinctureDrip Oils \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.272005+00 \N +2204 \N \N \N Easy Tiger Live Hash Rosin | GarlicaneEasy TigerIndica-HybridTHC: 76.11%CBD: 0.13% easy-tiger-live-hash-rosin-garlicane \N \N \N \N 76.11 0.13 Easy Tiger \N https://images.dutchie.com/7dd476bc64e835f7ed875802381d8bb6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/easy-tiger-live-hash-rosin-garlicane t f \N 2025-11-18 03:53:20.428917 2025-11-18 04:20:27.900194 2025-11-18 03:53:20.428917 2025-11-18 05:11:11.260852 112 GarlicaneEasy Tiger \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:27.900194+00 \N +2207 \N \N \N Easy Tiger Live Rosin AIO | Sub-ZEasy TigerHybridTHC: 77.81% easy-tiger-live-rosin-aio-sub-z \N \N \N \N 77.81 \N Easy Tiger \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/easy-tiger-live-rosin-aio-sub-z t f \N 2025-11-18 03:53:20.43628 2025-11-18 04:20:27.909183 2025-11-18 03:53:20.43628 2025-11-18 05:11:22.557064 112 Sub-ZEasy Tiger \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:27.909183+00 \N +2216 \N \N \N Elevate Flower Mylar | White PoisonElevateSativaTHC: 22.93%Special Offer elevate-flower-mylar-white-poison \N \N \N \N 22.93 \N Elevate \N https://images.dutchie.com/1a2fc2529fe59217be8df21519a22226?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/elevate-flower-mylar-white-poison t f \N 2025-11-18 03:53:25.90139 2025-11-18 04:20:35.627387 2025-11-18 03:53:25.90139 2025-11-18 05:11:52.720234 112 White PoisonElevate \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:35.627387+00 \N +2218 \N \N \N Sublime Pre-Roll | Marshmallow OGFeel SublimeHybridTHC: 22.93%Special Offer sublime-pre-roll-marshmallow-og \N \N \N \N 22.93 \N Feel Sublime \N https://images.dutchie.com/293448dc4415cdaca6ddb3287562e3d1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sublime-pre-roll-marshmallow-og t f \N 2025-11-18 03:53:44.875073 2025-11-18 04:20:38.980229 2025-11-18 03:53:44.875073 2025-11-18 05:12:03.572632 112 Marshmallow OGFeel Sublime \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:38.980229+00 \N +2220 \N \N \N Sublime | 100mg Strawberry Crumb BitesFeel SublimeTHC: 0.22% sublime-100mg-strawberry-crumb-bites \N \N \N \N 0.22 \N Feel Sublime \N https://images.dutchie.com/ecd0a5d144e2f128485f78a9e7ac01e8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sublime-100mg-strawberry-crumb-bites t f \N 2025-11-18 03:53:44.880298 2025-11-18 04:20:38.984872 2025-11-18 03:53:44.880298 2025-11-18 05:12:09.742404 112 100mg Strawberry Crumb BitesFeel Sublime \N \N \N {} {} {} 16.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:38.984872+00 \N +2221 \N \N \N Gelato Baller Brush | Cherry PieGelatoHybridTHC: 46.3%CBD: 1.47% gelato-baller-brush-cherry-pie \N \N \N \N 46.30 1.47 Gelato \N https://images.dutchie.com/b027905743bde1811fd25072e5a75d13?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/gelato-baller-brush-cherry-pie t f \N 2025-11-18 03:53:47.533466 2025-11-18 04:20:54.896438 2025-11-18 03:53:47.533466 2025-11-18 05:12:13.370208 112 Cherry PieGelato \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:54.896438+00 \N +3169 \N \N \N ORANGE KUSH 100MG keef-orange-kush-100mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/ebbb4c37-cf76-4159-b154-a2aec8841de7?customerType=ADULT t f \N 2025-11-18 14:42:09.287472 2025-11-18 14:42:09.287472 2025-11-18 14:42:09.287472 2025-11-18 14:42:09.287472 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.287472+00 \N +3170 \N \N \N ORIGINAL COLA 100MG keef-original-cola-100mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/cab71806-5370-4044-b04f-c357fe26d0a4?customerType=ADULT t f \N 2025-11-18 14:42:09.289188 2025-11-18 14:42:09.289188 2025-11-18 14:42:09.289188 2025-11-18 14:42:09.289188 149 \N \N \N \N \N \N 18.00 18.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.289188+00 \N +3171 \N \N \N 10MG BLUE RASPBERRY keef-10mg-blue-raspberry \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/c1c3fbb3-122b-4f76-b3de-447f31abb1cd?customerType=ADULT t f \N 2025-11-18 14:42:09.290926 2025-11-18 14:42:09.290926 2025-11-18 14:42:09.290926 2025-11-18 14:42:09.290926 149 \N \N \N \N \N \N 7.00 7.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.290926+00 \N +3172 \N \N \N 10MG BUBBA KUSH ROOT BEER keef-10mg-bubba-kush-root-beer \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/f8d34fe1-f163-491f-acc2-f305de12b3d5?customerType=ADULT t f \N 2025-11-18 14:42:09.292891 2025-11-18 14:42:09.292891 2025-11-18 14:42:09.292891 2025-11-18 14:42:09.292891 149 \N \N \N \N \N \N 7.00 7.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.292891+00 \N +3173 \N \N \N MANGO SPARKLING H2O 1:1 10MG keef-mango-sparkling-h2o-1-1-10mg \N \N \N HYBRID \N \N KEEF \N \N \N https://best.treez.io/onlinemenu/category/beverage/item/4a5373a7-f64b-46c5-8c17-4c7fe401abc7?customerType=ADULT t f \N 2025-11-18 14:42:09.294795 2025-11-18 14:42:09.294795 2025-11-18 14:42:09.294795 2025-11-18 14:42:09.294795 149 \N \N \N \N \N \N 7.00 7.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.294795+00 \N +3174 \N \N \N CHILL PILL ANYTIME 20X5MG/ 100MG life-is-chill-chill-pill-anytime-20x5mg-100mg \N \N \N HYBRID \N \N LIFE IS CHILL \N \N \N https://best.treez.io/onlinemenu/category/pill/item/00f47ef9-b41a-46be-bdce-31506ed1889f?customerType=ADULT t f \N 2025-11-18 14:42:09.296145 2025-11-18 14:42:09.296145 2025-11-18 14:42:09.296145 2025-11-18 14:42:09.296145 149 \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.296145+00 \N +3175 \N \N \N CHILL PILL NIGHTTIME 20X5MG/100MG life-is-chill-chill-pill-nighttime-20x5mg-100mg \N \N \N INDICA \N \N LIFE IS CHILL \N \N \N https://best.treez.io/onlinemenu/category/pill/item/9a274ab7-1df3-488e-afac-26f9318f167e?customerType=ADULT t f \N 2025-11-18 14:42:09.297517 2025-11-18 14:42:09.297517 2025-11-18 14:42:09.297517 2025-11-18 14:42:09.297517 149 \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.297517+00 \N +3176 \N \N \N CHILL PILL ANYTIME 10X10MG/100MG life-is-chill-chill-pill-anytime-10x10mg-100mg \N \N \N HYBRID \N \N LIFE IS CHILL \N \N \N https://best.treez.io/onlinemenu/category/pill/item/9f9cbb2f-79a2-4222-a5c2-6d36416bc9a6?customerType=ADULT t f \N 2025-11-18 14:42:09.298899 2025-11-18 14:42:09.298899 2025-11-18 14:42:09.298899 2025-11-18 14:42:09.298899 149 \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.298899+00 \N +3177 \N \N \N CHILL PILL DAYTIME 10X10MG/100MG life-is-chill-chill-pill-daytime-10x10mg-100mg \N \N \N SATIVA \N \N LIFE IS CHILL \N \N \N https://best.treez.io/onlinemenu/category/pill/item/c84c4655-ec18-41b5-822b-f34ec39262a2?customerType=ADULT t f \N 2025-11-18 14:42:09.300176 2025-11-18 14:42:09.300176 2025-11-18 14:42:09.300176 2025-11-18 14:42:09.300176 149 \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.300176+00 \N +3178 \N \N \N CHILL PILL NIGHTTIME 10X10MG/100MG life-is-chill-chill-pill-nighttime-10x10mg-100mg \N \N \N INDICA \N \N LIFE IS CHILL \N \N \N https://best.treez.io/onlinemenu/category/pill/item/e6c1842b-2e49-4e63-aed7-9b9bad001c2d?customerType=ADULT t f \N 2025-11-18 14:42:09.301537 2025-11-18 14:42:09.301537 2025-11-18 14:42:09.301537 2025-11-18 14:42:09.301537 149 \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.301537+00 \N +2228 \N \N \N Goldsmith Distillate Cartridge | Forbidden FruitGoldsmith ExtractsTHC: 90.02%CBD: 4.4% goldsmith-distillate-cartridge-forbidden-fruit \N \N \N \N 90.02 4.40 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-forbidden-fruit t f \N 2025-11-18 03:53:49.494779 2025-11-18 04:21:03.531194 2025-11-18 03:53:49.494779 2025-11-18 05:12:41.927316 112 Forbidden FruitGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.531194+00 \N +2230 \N \N \N Goldsmith Distillate Cartridge | Ice Cream CakeGoldsmith ExtractsTHC: 89.23%CBD: 4.5% goldsmith-distillate-cartridge-ice-cream-cake \N \N \N \N 89.23 4.50 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-ice-cream-cake t f \N 2025-11-18 03:53:49.500576 2025-11-18 04:21:03.537229 2025-11-18 03:53:49.500576 2025-11-18 05:12:47.881267 112 Ice Cream CakeGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.537229+00 \N +2233 \N \N \N Goldsmith Distillate Cartridge | Mango KushGoldsmith ExtractsTHC: 89.14%CBD: 4.3% goldsmith-distillate-cartridge-mango-kush \N \N \N \N 89.14 4.30 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-mango-kush t f \N 2025-11-18 03:53:49.506383 2025-11-18 04:21:03.544665 2025-11-18 03:53:49.506383 2025-11-18 05:12:58.369075 112 Mango KushGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.544665+00 \N +2236 \N \N \N Goldsmith Distillate Cartridge | Pina ColadaGoldsmith ExtractsTHC: 92.53% goldsmith-distillate-cartridge-pina-colada \N \N \N \N 92.53 \N Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-pina-colada t f \N 2025-11-18 03:53:49.514451 2025-11-18 04:21:03.552604 2025-11-18 03:53:49.514451 2025-11-18 05:13:12.515228 112 Pina ColadaGoldsmith Extracts \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.552604+00 \N +2238 \N \N \N Goldsmith Distillate Cartridge | Purple PunchGoldsmith ExtractsTHC: 88.4%CBD: 4.08%Special Offer goldsmith-distillate-cartridge-purple-punch-23759 \N \N \N \N 88.40 4.08 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-purple-punch-23759 t f \N 2025-11-18 03:53:49.519809 2025-11-18 04:21:03.558831 2025-11-18 03:53:49.519809 2025-11-18 05:13:18.775406 112 Purple PunchGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.558831+00 \N +2241 \N \N \N Goldsmith Distillate Cartridge | WatermelonGoldsmith ExtractsTHC: 88.99%Special Offer goldsmith-distillate-cartridge-watermelon \N \N \N \N 88.99 \N Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-cartridge-watermelon t f \N 2025-11-18 03:53:49.526811 2025-11-18 04:21:03.568442 2025-11-18 03:53:49.526811 2025-11-18 05:13:28.587982 112 WatermelonGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.568442+00 \N +2243 \N \N \N Goldsmith Distillate Syringe | Blackberry KushGoldsmith ExtractsIndica-HybridTHC: 89.42%CBD: 4.12% goldsmith-distillate-syringe-blackberry-kush \N \N \N \N 89.42 4.12 Goldsmith Extracts \N https://images.dutchie.com/6373195102c06cdfc99ebed314d85d7a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-blackberry-kush t f \N 2025-11-18 03:53:49.531894 2025-11-18 04:21:03.574124 2025-11-18 03:53:49.531894 2025-11-18 05:13:34.755434 112 Blackberry KushGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.574124+00 \N +2245 \N \N \N Goldsmith Distillate Syringe | Forbidden FruitGoldsmith ExtractsIndica-HybridTHC: 90.02%CBD: 4.4% goldsmith-distillate-syringe-forbidden-fruit-37103 \N \N \N \N 90.02 4.40 Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-forbidden-fruit-37103 t f \N 2025-11-18 03:53:49.536327 2025-11-18 04:21:03.580335 2025-11-18 03:53:49.536327 2025-11-18 05:13:42.693007 112 Forbidden FruitGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.580335+00 \N +2248 \N \N \N Goldsmith Distillate Syringe | Sour TangieGoldsmith ExtractsSativaTHC: 88.67% goldsmith-distillate-syringe-sour-tangie-33013 \N \N \N \N 88.67 \N Goldsmith Extracts \N https://images.dutchie.com/93437665ae5723a4dde932dc5816cae1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-distillate-syringe-sour-tangie-33013 t f \N 2025-11-18 03:53:49.54228 2025-11-18 04:21:03.588854 2025-11-18 03:53:49.54228 2025-11-18 05:13:51.701191 112 Sour TangieGoldsmith Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.588854+00 \N +2250 \N \N \N Goldsmith | 3-Pack Iced Out Infused Pre-Roll | UrsulaGoldsmith ExtractsIndicaTHC: 38.15% goldsmith-3-pack-iced-out-infused-pre-roll-ursula \N \N \N \N 38.15 \N Goldsmith Extracts \N https://images.dutchie.com/6373195102c06cdfc99ebed314d85d7a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/goldsmith-3-pack-iced-out-infused-pre-roll-ursula t f \N 2025-11-18 03:53:49.546944 2025-11-18 04:21:03.59414 2025-11-18 03:53:49.546944 2025-11-18 05:14:16.562409 112 UrsulaGoldsmith Extracts \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:03.59414+00 \N +2252 \N \N \N Green Dot Labs Live Rosin AIO | Candy CakeGreen Dot LabsIndica-HybridTHC: 76.17% green-dot-labs-live-rosin-aio-candy-cake \N \N \N \N 76.17 \N Green Dot Labs \N https://images.dutchie.com/f4ae94d7254274e30fe88b70e6ed45d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/green-dot-labs-live-rosin-aio-candy-cake t f \N 2025-11-18 03:53:56.094482 2025-11-18 04:21:11.21984 2025-11-18 03:53:56.094482 2025-11-18 05:14:05.630334 112 Candy CakeGreen Dot Labs \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:11.21984+00 \N +2325 \N \N \N Keef Classic Soda Original Cola XTREME | 100mgKeefHybridTHC: 0.03% keef-classic-soda-original-cola-xtreme-100mg-7852 \N \N \N \N 0.03 \N Keef \N https://images.dutchie.com/848f8a8e5ff130da9da502657bc82d09?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/keef-classic-soda-original-cola-xtreme-100mg-7852 t f \N 2025-11-18 03:55:31.872094 2025-11-18 04:22:52.98895 2025-11-18 03:55:31.872094 2025-11-18 05:18:27.317868 112 100mgKeef \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:52.98895+00 \N +3179 \N \N \N CHILL PILL DAYTIME 20X5MG/100MG life-is-chill-chill-pill-daytime-20x5mg-100mg \N \N \N SATIVA \N \N LIFE IS CHILL \N \N \N https://best.treez.io/onlinemenu/category/pill/item/ede3eef6-c7b9-4a0d-855a-336b6c3d3aed?customerType=ADULT t f \N 2025-11-18 14:42:09.303203 2025-11-18 14:42:09.303203 2025-11-18 14:42:09.303203 2025-11-18 14:42:09.303203 149 \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.303203+00 \N +2253 \N \N \N Grow Sciences Live Hash Rosin Sauce | Tropical CherryGrow SciencesSativa-HybridTHC: 78.83% grow-sciences-live-hash-rosin-sauce-tropical-cherry \N \N \N \N 78.83 \N Grow Sciences \N https://images.dutchie.com/36f2f3463d7361ce86b1f00bdca66720?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-live-hash-rosin-sauce-tropical-cherry t f \N 2025-11-18 03:54:01.835716 2025-11-18 04:21:14.564286 2025-11-18 03:54:01.835716 2025-11-18 05:14:08.760855 112 Tropical CherryGrow Sciences \N \N \N {} {} {} 70.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.564286+00 \N +2255 \N \N \N Grow Sciences Live Resin Cartridge | Uva x StrawguavaGrow SciencesTHC: 69.98% grow-sciences-live-resin-cartridge-uva-x-strawguava \N \N \N \N 69.98 \N Grow Sciences \N https://images.dutchie.com/9fccfa91147d41b6618ce14641429168?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-live-resin-cartridge-uva-x-strawguava t f \N 2025-11-18 03:54:01.851425 2025-11-18 04:21:14.573611 2025-11-18 03:54:01.851425 2025-11-18 05:14:19.559508 112 Uva x StrawguavaGrow Sciences \N \N \N {} {} {} 33.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.573611+00 \N +2258 \N \N \N Grow Sciences | 100mg Rosin Fruit Chew | Pink GrapefruitGrow SciencesTHC: 0.25%Special Offer grow-sciences-100mg-rosin-fruit-chew-pink-grapefruit \N \N \N \N 0.25 \N Grow Sciences \N https://images.dutchie.com/3cc63a5b28c9bc8743537d63c0ab6d42?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-100mg-rosin-fruit-chew-pink-grapefruit t f \N 2025-11-18 03:54:01.858393 2025-11-18 04:21:14.5791 2025-11-18 03:54:01.858393 2025-11-18 05:14:28.738762 112 Pink GrapefruitGrow Sciences \N \N \N {} {} {} 16.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.5791+00 \N +2261 \N \N \N Grow Sciences | 3.7g Flower Jar | Pineapple FruzGrow SciencesIndica-HybridTHC: 21% grow-sciences-3-7g-flower-jar-pineapple-fruz \N \N \N \N 21.00 \N Grow Sciences \N https://images.dutchie.com/00d05364e10d01765ebb95bc214b5254?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-pineapple-fruz t f \N 2025-11-18 03:54:01.867067 2025-11-18 04:21:14.584976 2025-11-18 03:54:01.867067 2025-11-18 05:14:37.761246 112 Pineapple FruzGrow Sciences \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.584976+00 \N +2263 \N \N \N Grow Sciences | 3.7g Flower Jar | Z CubedGrow SciencesHybridTHC: 21.59% grow-sciences-3-7g-flower-jar-z-cubed \N \N \N \N 21.59 \N Grow Sciences \N https://images.dutchie.com/00d05364e10d01765ebb95bc214b5254?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/grow-sciences-3-7g-flower-jar-z-cubed t f \N 2025-11-18 03:54:01.872431 2025-11-18 04:21:14.588574 2025-11-18 03:54:01.872431 2025-11-18 05:14:44.845889 112 Z CubedGrow Sciences \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:14.588574+00 \N +2266 \N \N \N Milk Chocolate High-Dose Mini BarGrönTHCTAC: 100 mgTHC: 1.39%Special Offer milk-chocolate-high-dose-mini-bar-36665 \N \N \N \N 1.39 \N Grön \N https://images.dutchie.com/6741730735c170652646067197579aa3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/milk-chocolate-high-dose-mini-bar-36665 t f \N 2025-11-18 03:54:20.460538 2025-11-18 04:21:30.503788 2025-11-18 03:54:20.460538 2025-11-18 05:14:55.848009 112 \N \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:30.503788+00 \N +2269 \N \N \N THC Raspberry Lemonade Pearls - SativaGrönSativaTAC: 100 mgTHC: 0.27% thc-raspberry-lemonade-pearls-sativa-21478 \N \N \N \N 0.27 \N Grön \N https://images.dutchie.com/26309c7223a5302499a51bc51e42ad22?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thc-raspberry-lemonade-pearls-sativa-21478 t f \N 2025-11-18 03:54:20.467489 2025-11-18 04:21:30.509206 2025-11-18 03:54:20.467489 2025-11-18 05:15:06.54274 112 \N \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:30.509206+00 \N +2274 \N \N \N Halo | Canna Confections | 100mg Vanilla Caramels | SativaHalo InfusionsSativaTHC: 0.08% halo-canna-confections-100mg-vanilla-caramels-sativa \N \N \N \N 0.08 \N Halo Infusions \N https://images.dutchie.com/ee63fa78aaf6d3eb0fe987c53ee42ea9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-canna-confections-100mg-vanilla-caramels-sativa t f \N 2025-11-18 03:54:25.287755 2025-11-18 04:21:46.829445 2025-11-18 03:54:25.287755 2025-11-18 05:15:26.512747 112 \N \N \N {} {} {} 16.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.829445+00 \N +2283 \N \N \N Canna Brands Full Spectrum RSO | Canna RSOHash FactoryTHC: 60.35%CBD: 0.15% canna-brands-full-spectrum-rso-canna-rso \N \N \N \N 60.35 0.15 Hash Factory \N https://images.dutchie.com/69e418fc9afcb3a4db11a1b9e2ca2fa9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canna-brands-full-spectrum-rso-canna-rso t f \N 2025-11-18 03:54:31.730888 2025-11-18 04:21:50.133848 2025-11-18 03:54:31.730888 2025-11-18 05:15:54.214359 112 Canna RSOHash Factory \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:50.133848+00 \N +3180 \N \N \N SUNO RSO TABLETS 100MG lion-labs-suno-rso-tablets-100mg \N \N \N INDICA \N \N LION LABS \N \N \N https://best.treez.io/onlinemenu/category/pill/item/05d927d9-d618-441c-8f5a-8a59647cbba4?customerType=ADULT t f \N 2025-11-18 14:42:09.304977 2025-11-18 14:42:09.304977 2025-11-18 14:42:09.304977 2025-11-18 14:42:09.304977 149 \N \N \N \N \N \N 16.00 16.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.304977+00 \N +3181 \N \N \N VETCBD MOBILITY SOFT CHEWS 150MG 30CT vetcbd-mobility-soft-chews-150mg-30-ct-vetcbd-mobility-soft-chews-150mg-30ct \N \N \N \N \N \N VETCBD MOBILITY SOFT CHEWS 150MG 30 CT \N \N \N https://best.treez.io/onlinemenu/category/cbd/item/b32210bd-9112-4150-9f91-fec53e09f5f4?customerType=ADULT t f \N 2025-11-18 14:42:09.306694 2025-11-18 14:42:09.306694 2025-11-18 14:42:09.306694 2025-11-18 14:42:09.306694 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.306694+00 \N +3182 \N \N \N VETCBD FULL SPECTRUM TINCTURE 500MG vetcbd-full-spectrum-tincture-500mg-vetcbd-full-spectrum-tincture-500mg \N \N \N \N \N \N VETCBD FULL SPECTRUM TINCTURE 500MG \N \N \N https://best.treez.io/onlinemenu/category/cbd/item/70b5fcb4-78ae-4555-a507-ee999b6a9586?customerType=ADULT t f \N 2025-11-18 14:42:09.308042 2025-11-18 14:42:09.308042 2025-11-18 14:42:09.308042 2025-11-18 14:42:09.308042 149 \N \N \N \N \N \N 45.00 45.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.308042+00 \N +3183 \N \N \N VETCBD FULL SPECTRUM TINCTURE 250MG vetcbd-vetcbd-full-spectrum-tincture-250mg \N \N \N \N \N \N VETCBD \N \N \N https://best.treez.io/onlinemenu/category/cbd/item/75dc8397-017f-4302-b0a4-c6361082aba6?customerType=ADULT t f \N 2025-11-18 14:42:09.30974 2025-11-18 14:42:09.30974 2025-11-18 14:42:09.30974 2025-11-18 14:42:09.30974 149 \N \N \N \N \N \N 31.00 31.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.30974+00 \N +3184 \N \N \N GLYCERIN TINCTURE 1:1 100 THC/CBD halo-infusions-glycerin-tincture-1-1-100-thc-cbd \N \N \N HYBRID \N \N HALO INFUSIONS \N \N \N https://best.treez.io/onlinemenu/category/tincture/item/15b1645c-da53-4b0e-9658-6df0ac75be71?customerType=ADULT t f \N 2025-11-18 14:42:09.311404 2025-11-18 14:42:09.311404 2025-11-18 14:42:09.311404 2025-11-18 14:42:09.311404 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.311404+00 \N +3185 \N \N \N SLEEP WELL TINCTURE 1:1 THC/CBD halo-infusions-sleep-well-tincture-1-1-thc-cbd \N \N \N INDICA \N \N HALO INFUSIONS \N \N \N https://best.treez.io/onlinemenu/category/tincture/item/b9ab0a01-07eb-45a8-8589-8c6fb914fcad?customerType=ADULT t f \N 2025-11-18 14:42:09.313152 2025-11-18 14:42:09.313152 2025-11-18 14:42:09.313152 2025-11-18 14:42:09.313152 149 \N \N \N \N \N \N 25.00 25.00 \N \N \N \N in_stock \N 2025-11-18 14:42:09.313152+00 \N +2290 \N \N \N HighMart Flower Mylar | SonoraHighMartHybridTHC: 27.3%CBD: 0.16% highmart-flower-mylar-sonora-57334 \N \N \N \N 27.30 0.16 HighMart \N https://images.dutchie.com/ad46cabf9b60f8979cd0b9a812fda497?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/highmart-flower-mylar-sonora-57334 t f \N 2025-11-18 03:54:58.828144 2025-11-18 04:22:22.505621 2025-11-18 03:54:58.828144 2025-11-18 05:16:17.969586 112 SonoraHighMart \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:22.505621+00 \N +2291 \N \N \N Hot Rod | Infused Pre-Roll-Pony PizzazzHot RodSativaTHC: 49.77% hot-rod-infused-pre-roll-pony-pizzazz-83458 \N \N \N \N 49.77 \N Hot Rod \N https://images.dutchie.com/7615a399e1b9b0cdfce9305e4d48f8a9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/hot-rod-infused-pre-roll-pony-pizzazz-83458 t f \N 2025-11-18 03:55:00.914909 2025-11-18 04:22:25.6473 2025-11-18 03:55:00.914909 2025-11-18 05:16:21.011936 112 Infused Pre-Roll-Pony PizzazzHot Rod \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:25.6473+00 \N +2292 \N \N \N IO Extracts Cured Badder | Star QueenI.O. ExtractsIndica-HybridTHC: 72.75%CBD: 0.13% io-extracts-cured-badder-star-queen \N \N \N \N 72.75 0.13 I.O. Extracts \N https://images.dutchie.com/4d0ecac0b350e7d9d278546f76e63a14?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-cured-badder-star-queen t f \N 2025-11-18 03:55:07.404912 2025-11-18 04:22:41.765928 2025-11-18 03:55:07.404912 2025-11-18 05:16:23.914945 112 Star QueenI.O. Extracts \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.765928+00 \N +2553 \N \N \N Tipsy Turtle | 100mg Caramel NipsTipsy TurtleSpecial Offer tipsy-turtle-100mg-caramel-nips \N \N \N \N \N \N Tipsy Turtle \N https://images.dutchie.com/2098a106963fac2f6ca2c991ce43c0b0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tipsy-turtle-100mg-caramel-nips t f \N 2025-11-18 03:59:44.50712 2025-11-18 04:27:09.466966 2025-11-18 03:59:44.50712 2025-11-18 05:31:51.076005 112 100mg Caramel NipsTipsy TurtleSpecial Offer \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:09.466966+00 \N +2294 \N \N \N IO Extracts Cured Batter | Glitter BombI.O. ExtractsIndica-HybridTHC: 75.54%CBD: 0.47% io-extracts-cured-batter-glitter-bomb \N \N \N \N 75.54 0.47 I.O. Extracts \N https://images.dutchie.com/c7a805bcc9a56c1c311ea81102fe103f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-cured-batter-glitter-bomb t f \N 2025-11-18 03:55:07.415493 2025-11-18 04:22:41.774819 2025-11-18 03:55:07.415493 2025-11-18 05:16:33.869352 112 Glitter BombI.O. Extracts \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.774819+00 \N +2297 \N \N \N IO Extracts Live Hash Rosin AIO Vape | MedellinI.O. ExtractsHybridTHC: 80.09% io-extracts-live-hash-rosin-aio-vape-medellin \N \N \N \N 80.09 \N I.O. Extracts \N https://images.dutchie.com/8230140f9fbb210900567af9fd820b33?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-hash-rosin-aio-vape-medellin t f \N 2025-11-18 03:55:07.421903 2025-11-18 04:22:41.78166 2025-11-18 03:55:07.421903 2025-11-18 05:16:43.251139 112 MedellinI.O. Extracts \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.78166+00 \N +2299 \N \N \N IO Extracts Live Hash Rosin AIO Vape | PressureI.O. ExtractsHybridTHC: 73.34%CBD: 0.16% io-extracts-live-hash-rosin-aio-vape-pressure \N \N \N \N 73.34 0.16 I.O. Extracts \N https://images.dutchie.com/8230140f9fbb210900567af9fd820b33?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-hash-rosin-aio-vape-pressure t f \N 2025-11-18 03:55:07.426337 2025-11-18 04:22:41.785738 2025-11-18 03:55:07.426337 2025-11-18 05:16:49.442369 112 PressureI.O. Extracts \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.785738+00 \N +2302 \N \N \N IO Extracts Live Resin Badder | Blue 22I.O. ExtractsIndica-HybridTHC: 72.61%CBD: 0.13% io-extracts-live-resin-badder-blue-22 \N \N \N \N 72.61 0.13 I.O. Extracts \N https://images.dutchie.com/4d0ecac0b350e7d9d278546f76e63a14?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-resin-badder-blue-22 t f \N 2025-11-18 03:55:07.431595 2025-11-18 04:22:41.792252 2025-11-18 03:55:07.431595 2025-11-18 05:16:58.392898 112 Blue 22I.O. Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.792252+00 \N +2305 \N \N \N IO Extracts Live Resin Badder | MACI.O. ExtractsIndica-HybridTHC: 73.75%CBD: 0.17% io-extracts-live-resin-badder-mac \N \N \N \N 73.75 0.17 I.O. Extracts \N https://images.dutchie.com/4d0ecac0b350e7d9d278546f76e63a14?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-resin-badder-mac t f \N 2025-11-18 03:55:07.436895 2025-11-18 04:22:41.798092 2025-11-18 03:55:07.436895 2025-11-18 05:17:10.600339 112 MACI.O. Extracts \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.798092+00 \N +2308 \N \N \N IO Extracts Live Resin Cart | PineappleI.O. ExtractsHybridTHC: 75.04%CBD: 0.13% io-extracts-live-resin-cart-pineapple-53337 \N \N \N \N 75.04 0.13 I.O. Extracts \N https://images.dutchie.com/5efaa0e504137ee1fbb6c234ef403332?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/io-extracts-live-resin-cart-pineapple-53337 t f \N 2025-11-18 03:55:07.442245 2025-11-18 04:22:41.803258 2025-11-18 03:55:07.442245 2025-11-18 05:17:19.692158 112 PineappleI.O. Extracts \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:41.803258+00 \N +2309 \N \N \N Jeeter Diamond Cartridge | Double RainbowJeeterTHC: 88.92%CBD: 0.14% jeeter-diamond-cartridge-double-rainbow \N \N \N \N 88.92 0.14 Jeeter \N https://images.dutchie.com/5839831350deb9d0962afd3fe6608530?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-diamond-cartridge-double-rainbow t f \N 2025-11-18 03:55:13.055106 2025-11-18 04:22:50.677993 2025-11-18 03:55:13.055106 2025-11-18 05:17:22.68477 112 Double RainbowJeeter \N \N \N {} {} {} 44.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.677993+00 \N +2311 \N \N \N Jeeter | 1.3g Solventless Live Rosin Infused Baby Cannon | 9lb HammerJeeterTHC: 41.44% jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-9lb-hammer \N \N \N \N 41.44 \N Jeeter \N https://images.dutchie.com/2b4eabb8eba95fde81e65448b7ce7801?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-1-3g-solventless-live-rosin-infused-baby-cannon-9lb-hammer t f \N 2025-11-18 03:55:13.065138 2025-11-18 04:22:50.68663 2025-11-18 03:55:13.065138 2025-11-18 05:17:39.789487 112 9lb HammerJeeter \N \N \N {} {} {} 36.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.68663+00 \N +2314 \N \N \N Jeeter | 3-Pack x 0.5g Live Resin Infused Pre-Roll | #1 StunnaJeeterTHC: 40.44% jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-1-stunna \N \N \N \N 40.44 \N Jeeter \N https://images.dutchie.com/35915bcb0fb5c00733c688efea7d2466?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-1-stunna t f \N 2025-11-18 03:55:13.071652 2025-11-18 04:22:50.69319 2025-11-18 03:55:13.071652 2025-11-18 05:17:42.893165 112 #1 StunnaJeeter \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.69319+00 \N +2316 \N \N \N Jeeter | 3-Pack x 0.5g Live Resin Infused Pre-Roll | The WhiteJeeterTHC: 38.36%CBD: 0.31% jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-the-white \N \N \N \N 38.36 0.31 Jeeter \N https://images.dutchie.com/35915bcb0fb5c00733c688efea7d2466?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-3-pack-x-0-5g-live-resin-infused-pre-roll-the-white t f \N 2025-11-18 03:55:13.076043 2025-11-18 04:22:50.697708 2025-11-18 03:55:13.076043 2025-11-18 05:17:49.064628 112 The WhiteJeeter \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:50.697708+00 \N +2330 \N \N \N Exotic Blend Diamond-Infused (3-pack of .5g joints)LeafersHybridTHC: 46.74% exotic-blend-diamond-infused-3-pack-of-5g-joints \N \N \N \N 46.74 \N Leafers \N https://images.dutchie.com/47f1121c6e60af860e76e1d8e8f3daaf?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/exotic-blend-diamond-infused-3-pack-of-5g-joints t f \N 2025-11-18 03:55:34.557673 2025-11-18 04:22:59.105736 2025-11-18 03:55:34.557673 2025-11-18 05:18:42.79724 112 \N \N \N \N {} {} {} 32.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:59.105736+00 \N +2333 \N \N \N Indica Blend Diamond-Infused Joint (1g)LeafersIndicaTHC: 49.53% indica-blend-diamond-infused-joint-1g \N \N \N Indica 49.53 \N Leafers \N https://images.dutchie.com/6e24a602718f5ee89cfb7a709b964d6c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/indica-blend-diamond-infused-joint-1g t f \N 2025-11-18 03:55:34.566058 2025-11-18 04:22:59.113439 2025-11-18 03:55:34.566058 2025-11-18 05:18:53.802146 112 \N \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:59.113439+00 \N +2335 \N \N \N Legends Doinks | 2-Pack x 1g Pre-roll | Chin CheckLegendsTHC: 27.43%CBD: 0.04% legends-doinks-2-pack-x-1g-pre-roll-chin-check \N \N \N \N 27.43 0.04 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-doinks-2-pack-x-1g-pre-roll-chin-check t f \N 2025-11-18 03:55:36.520824 2025-11-18 04:23:01.336346 2025-11-18 03:55:36.520824 2025-11-18 05:18:59.787222 112 Chin CheckLegends \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.336346+00 \N +2338 \N \N \N Legends Doinks | 2-Pack x 1g Pre-roll | Papaya PowerLegendsIndica-HybridTHC: 20.1%CBD: 0.03% legends-doinks-2-pack-x-1g-pre-roll-papaya-power \N \N \N \N 20.10 0.03 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-doinks-2-pack-x-1g-pre-roll-papaya-power t f \N 2025-11-18 03:55:36.53334 2025-11-18 04:23:01.346296 2025-11-18 03:55:36.53334 2025-11-18 05:19:12.760773 112 Papaya PowerLegends \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.346296+00 \N +2341 \N \N \N Legends Flower Jar | Fire CrotchLegendsHybridTHC: 20.51%CBD: 0.03%Special Offer legends-flower-jar-fire-crotch \N \N \N \N 20.51 0.03 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-fire-crotch t f \N 2025-11-18 03:55:36.540449 2025-11-18 04:23:01.35227 2025-11-18 03:55:36.540449 2025-11-18 05:19:22.076298 112 Fire CrotchLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.35227+00 \N +2344 \N \N \N Legends Flower Jar | Oil TankerLegendsIndica-HybridTHC: 21.1%CBD: 0.03%Special Offer legends-flower-jar-oil-tanker \N \N \N \N 21.10 0.03 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-oil-tanker t f \N 2025-11-18 03:55:36.546534 2025-11-18 04:23:01.358128 2025-11-18 03:55:36.546534 2025-11-18 05:19:33.211183 112 Oil TankerLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.358128+00 \N +2345 \N \N \N Legends Flower Jar | Papaya PowerLegendsIndica-HybridTHC: 20.1%CBD: 0.03%Special Offer legends-flower-jar-papaya-power \N \N \N \N 20.10 0.03 Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-papaya-power t f \N 2025-11-18 03:55:36.548719 2025-11-18 04:23:01.359823 2025-11-18 03:55:36.548719 2025-11-18 05:19:36.303282 112 Papaya PowerLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.359823+00 \N +2347 \N \N \N Legends Flower Jar | Z KushLegendsTHC: 23.66%Special Offer legends-flower-jar-z-kush \N \N \N \N 23.66 \N Legends \N https://images.dutchie.com/9733b8fa3ac8973cad56c8e2c2e99dab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/legends-flower-jar-z-kush t f \N 2025-11-18 03:55:36.552567 2025-11-18 04:23:01.363083 2025-11-18 03:55:36.552567 2025-11-18 05:19:42.508746 112 Z KushLegends \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:01.363083+00 \N +2424 \N \N \N Preferred Gardens Flower Jar | OG MarkerPreferred GardensIndicaTHC: 23.24% preferred-gardens-flower-jar-og-marker \N \N \N \N \N \N Preferred Gardens \N \N \N /embedded-menu/AZ-Deeply-Rooted/product/preferred-gardens-flower-jar-og-marker t f \N 2025-11-18 03:57:21.775533 2025-11-18 03:57:21.775533 2025-11-18 03:57:21.775533 2025-11-18 19:45:18.067124 112 \N \N \N \N {} {} {} \N \N \N \N \N \N in_stock \N 2025-11-18 03:57:21.775533+00 \N +2355 \N \N \N Mac Pharms Distillate AIO | Papaya X Louie BagzMac PharmsTHC: 87.71%CBD: 0.84%Special Offer mac-pharms-distillate-aio-papaya-x-louie-bagz-33138 \N \N \N \N 87.71 0.84 Mac Pharms \N https://images.dutchie.com/800d3faf9e8381e57a1db37415f72173?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-distillate-aio-papaya-x-louie-bagz-33138 t f \N 2025-11-18 03:56:07.604139 2025-11-18 04:23:28.800394 2025-11-18 03:56:07.604139 2025-11-18 05:20:12.173529 112 Papaya X Louie BagzMac Pharms \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:28.800394+00 \N +2358 \N \N \N Mac Pharms Live Rosin AIO | Poppabear's Purp OGMac PharmsTHC: 80.94% mac-pharms-live-rosin-aio-poppabear-s-purp-og \N \N \N \N 80.94 \N Mac Pharms \N https://images.dutchie.com/800d3faf9e8381e57a1db37415f72173?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mac-pharms-live-rosin-aio-poppabear-s-purp-og t f \N 2025-11-18 03:56:07.616065 2025-11-18 04:23:28.810064 2025-11-18 03:56:07.616065 2025-11-18 05:20:21.175025 112 Poppabear's Purp OGMac Pharms \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:28.810064+00 \N +2360 \N \N \N Mad Terp Labs Batter | Banana SnifferMad Terp LabsTHC: 83.21%Special Offer mad-terp-labs-batter-banana-sniffer \N \N \N \N 83.21 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-batter-banana-sniffer t f \N 2025-11-18 03:56:10.155381 2025-11-18 04:23:34.779266 2025-11-18 03:56:10.155381 2025-11-18 05:20:28.929912 112 Banana SnifferMad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.779266+00 \N +2363 \N \N \N Mad Terp Labs Batter | MAC V2Mad Terp LabsHybridTHC: 67.72%Special Offer mad-terp-labs-batter-mac-v2 \N \N \N \N 67.72 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-batter-mac-v2 t f \N 2025-11-18 03:56:10.168956 2025-11-18 04:23:34.79343 2025-11-18 03:56:10.168956 2025-11-18 05:20:40.791992 112 MAC V2Mad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.79343+00 \N +2365 \N \N \N Mad Terp Labs Batter | Watermelon TourmalineMad Terp LabsHybridTHC: 78.8%Special Offer mad-terp-labs-batter-watermelon-tourmaline \N \N \N \N 78.80 \N Mad Terp Labs \N https://images.dutchie.com/2292e5f9c44ed72cc4f427edfcb28df5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mad-terp-labs-batter-watermelon-tourmaline t f \N 2025-11-18 03:56:10.174337 2025-11-18 04:23:34.799463 2025-11-18 03:56:10.174337 2025-11-18 05:20:46.730183 112 Watermelon TourmalineMad Terp Labs \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:34.799463+00 \N +2370 \N \N \N Made Flower Jar | High BeamMadeHybridTHC: 29.7% made-flower-jar-high-beam \N \N \N \N 29.70 \N Made \N https://images.dutchie.com/5679e7de7be9a9e80cbd932536740bef?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/made-flower-jar-high-beam t f \N 2025-11-18 03:56:12.29942 2025-11-18 04:23:37.303629 2025-11-18 03:56:12.29942 2025-11-18 05:21:01.753837 112 High BeamMade \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:37.303629+00 \N +2372 \N \N \N Mellow Vibes | 100mg Jelly | Mango GuavaMellow VibesTHC: 96.62 mg mellow-vibes-100mg-jelly-mango-guava \N \N \N \N \N \N Mellow Vibes \N https://images.dutchie.com/e1e6feac5244b4a25302289dcb8090f2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mellow-vibes-100mg-jelly-mango-guava t f \N 2025-11-18 03:56:18.899981 2025-11-18 04:23:53.262416 2025-11-18 03:56:18.899981 2025-11-18 05:21:09.808237 112 Mango GuavaMellow Vibes \N \N \N {} {} {} 7.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:53.262416+00 \N +2373 \N \N \N Mellow Vibes | 100mg Rosin Jelly | Prickly PearMellow VibesTHC: 0.82% mellow-vibes-100mg-rosin-jelly-prickly-pear \N \N \N \N 0.82 \N Mellow Vibes \N https://images.dutchie.com/d2e1c60974396da2504d0b3798fd352f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mellow-vibes-100mg-rosin-jelly-prickly-pear t f \N 2025-11-18 03:56:18.907561 2025-11-18 04:23:53.26895 2025-11-18 03:56:18.907561 2025-11-18 05:21:12.849467 112 Prickly PearMellow Vibes \N \N \N {} {} {} 14.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:53.26895+00 \N +2376 \N \N \N MFUSED Fire Liquid Diamonds AIO | ChocolopeMfusedTHC: 93.55%CBD: 0.21% mfused-fire-liquid-diamonds-aio-chocolope \N \N \N \N 93.55 0.21 Mfused \N https://images.dutchie.com/0975ff47066a4b178833a9eaac2e2487?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-chocolope t f \N 2025-11-18 03:56:24.495358 2025-11-18 04:24:01.95168 2025-11-18 03:56:24.495358 2025-11-18 05:21:21.773314 112 ChocolopeMfused \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.95168+00 \N +2378 \N \N \N MFUSED Fire Liquid Diamonds AIO | Tropical Sapphire KushMfusedTHC: 93.51%CBD: 0.18% mfused-fire-liquid-diamonds-aio-tropical-sapphire-kush \N \N \N \N 93.51 0.18 Mfused \N https://images.dutchie.com/ff29c3acf8d872e59a884e2611120ad9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-fire-liquid-diamonds-aio-tropical-sapphire-kush t f \N 2025-11-18 03:56:24.502084 2025-11-18 04:24:01.956126 2025-11-18 03:56:24.502084 2025-11-18 05:21:31.008838 112 Tropical Sapphire KushMfused \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.956126+00 \N +2381 \N \N \N MFUSED Loud Liquid Diamonds AIO | Pacific PunchMfusedTHC: 94.31%CBD: 2.04% mfused-loud-liquid-diamonds-aio-pacific-punch \N \N \N \N 94.31 2.04 Mfused \N https://images.dutchie.com/056fc137959603537d3145c4642d2e3d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-loud-liquid-diamonds-aio-pacific-punch t f \N 2025-11-18 03:56:24.512476 2025-11-18 04:24:01.962201 2025-11-18 03:56:24.512476 2025-11-18 05:22:19.830771 112 Pacific PunchMfused \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.962201+00 \N +2383 \N \N \N MFUSED Twisted Liquid Diamonds AIO | Lemon LoopzMfusedTHC: 96.26% mfused-twisted-liquid-diamonds-aio-lemon-loopz \N \N \N \N 96.26 \N Mfused \N https://images.dutchie.com/b165d9aca79c88aa7c4747e9bce07d35?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-twisted-liquid-diamonds-aio-lemon-loopz t f \N 2025-11-18 03:56:24.519297 2025-11-18 04:24:01.965602 2025-11-18 03:56:24.519297 2025-11-18 05:21:48.310128 112 Lemon LoopzMfused \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.965602+00 \N +2386 \N \N \N MFUSED Vibes THC:CBD:CBC AIO | Genius ModeMfusedTHCTHC: 57.28%CBD: 14.23% mfused-vibes-thc-cbd-cbc-aio-genius-mode \N \N \N \N 57.28 14.23 Mfused \N https://images.dutchie.com/18de1deafcdac08baa460085157c2010?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-vibes-thc-cbd-cbc-aio-genius-mode t f \N 2025-11-18 03:56:24.528076 2025-11-18 04:24:01.97136 2025-11-18 03:56:24.528076 2025-11-18 05:21:58.035489 112 Genius ModeMfused \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.97136+00 \N +2388 \N \N \N Mfused | 5-Pack x 0.5g Infused Pre-roll Fatty | Galactic GrapeMfusedTHC: 36.21% mfused-5-pack-x-0-5g-infused-pre-roll-fatty-galactic-grape \N \N \N \N 36.21 \N Mfused \N https://images.dutchie.com/2e94b9729103b6f7eb010eff505c3e6a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mfused-5-pack-x-0-5g-infused-pre-roll-fatty-galactic-grape t f \N 2025-11-18 03:56:24.533989 2025-11-18 04:24:01.975305 2025-11-18 03:56:24.533989 2025-11-18 05:22:04.271441 112 Galactic GrapeMfused \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:01.975305+00 \N +2404 \N \N \N NWD Flower Mylar | Strawnana MACNWDIndica-HybridTHC: 24.22% nwd-flower-mylar-strawnana-mac \N \N \N \N 24.22 \N NWD \N https://images.dutchie.com/flower-stock-11-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/nwd-flower-mylar-strawnana-mac t f \N 2025-11-18 03:56:45.758862 2025-11-18 04:24:10.49152 2025-11-18 03:56:45.758862 2025-11-18 05:23:05.546532 112 Strawnana MACNWD \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:10.49152+00 \N +2411 \N \N \N Ogeez | 100mg Gummy | The Creams Sunny SativaOGeez!SativaTHC: 0.18% ogeez-100mg-gummy-the-creams-sunny-sativa \N \N \N \N 0.18 \N OGeez! \N https://images.dutchie.com/40af316d13b801c7b856db8f4ac6c509?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-creams-sunny-sativa t f \N 2025-11-18 03:56:54.696661 2025-11-18 04:24:28.884053 2025-11-18 03:56:54.696661 2025-11-18 05:23:57.439336 112 The Creams Sunny \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:28.884053+00 \N +2413 \N \N \N Ogeez | 100mg Gummy | The Fruits Sunny SativaOGeez!SativaTHC: 0.18% ogeez-100mg-gummy-the-fruits-sunny-sativa \N \N \N \N 0.18 \N OGeez! \N https://images.dutchie.com/40af316d13b801c7b856db8f4ac6c509?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-the-fruits-sunny-sativa t f \N 2025-11-18 03:56:54.701555 2025-11-18 04:24:28.88794 2025-11-18 03:56:54.701555 2025-11-18 05:23:40.473749 112 The Fruits Sunny \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:28.88794+00 \N +2416 \N \N \N Ogeez | 100mg THC: 100mg CBD 1: ummy | Happy Balance Strawberries and CreaOGeez!THCTHC: 0.19%CBD: 0.17% ogeez-100mg-thc-100mg-cbd-1-ummy-happy-balance-strawberries-and-crea \N \N \N \N 0.19 0.17 OGeez! \N https://images.dutchie.com/40af316d13b801c7b856db8f4ac6c509?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-thc-100mg-cbd-1-ummy-happy-balance-strawberries-and-crea t f \N 2025-11-18 03:56:54.708531 2025-11-18 04:24:28.894546 2025-11-18 03:56:54.708531 2025-11-18 05:23:49.626306 112 100mg \N \N \N {} {} {} 17.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:28.894546+00 \N +2418 \N \N \N Papa's Herb Distillate AIO Vapes | Blue DreamPapa's HerbTHC: 86.76%CBD: 1.18%Special Offer papa-s-herb-distillate-aio-vapes-blue-dream \N \N \N \N 86.76 1.18 Papa's Herb \N https://images.dutchie.com/f124ed439100c0b60fd7ba16e0f707eb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-blue-dream t f \N 2025-11-18 03:57:18.785417 2025-11-18 04:24:40.472396 2025-11-18 03:57:18.785417 2025-11-18 05:23:55.557023 112 Blue DreamPapa's Herb \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:40.472396+00 \N +2421 \N \N \N Papa's Herb Distillate AIO Vapes | Starwalker OGPapa's HerbTHC: 87.32%CBD: 0.85%Special Offer papa-s-herb-distillate-aio-vapes-starwalker-og \N \N \N \N 87.32 0.85 Papa's Herb \N https://images.dutchie.com/f124ed439100c0b60fd7ba16e0f707eb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-starwalker-og t f \N 2025-11-18 03:57:18.799307 2025-11-18 04:24:40.484076 2025-11-18 03:57:18.799307 2025-11-18 05:24:08.882822 112 Starwalker OGPapa's Herb \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:40.484076+00 \N +2423 \N \N \N Papa's Herb Distillate AIO Vapes | Wedding CakePapa's HerbTHC: 86.31%CBD: 1.11%Special Offer papa-s-herb-distillate-aio-vapes-wedding-cake \N \N \N \N 86.31 1.11 Papa's Herb \N https://images.dutchie.com/f124ed439100c0b60fd7ba16e0f707eb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/papa-s-herb-distillate-aio-vapes-wedding-cake t f \N 2025-11-18 03:57:18.80376 2025-11-18 04:24:40.488106 2025-11-18 03:57:18.80376 2025-11-18 05:24:14.940196 112 Wedding CakePapa's Herb \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:40.488106+00 \N +2635 \N \N \N Preferred Gardens Flower Jar | OG MarkerPreferred GardensTHC: 30.36% preferred-gardens-flower-jar-og-marker-75537 \N \N \N \N 30.36 \N Preferred Gardens \N https://images.dutchie.com/311c29a141adf60d31cf02c470736a01?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/preferred-gardens-flower-jar-og-marker-75537 t f \N 2025-11-18 04:24:46.531322 2025-11-18 04:24:46.531322 2025-11-18 04:24:46.531322 2025-11-18 05:37:00.55389 112 OG MarkerPreferred Gardens \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:46.531322+00 \N +2425 \N \N \N Preferred Gardens Flower Jar | Perm LeePreferred GardensIndica-HybridTHC: 28.19% preferred-gardens-flower-jar-perm-lee \N \N \N \N 28.19 \N Preferred Gardens \N https://images.dutchie.com/311c29a141adf60d31cf02c470736a01?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/preferred-gardens-flower-jar-perm-lee t f \N 2025-11-18 03:57:21.78328 2025-11-18 04:24:46.540336 2025-11-18 03:57:21.78328 2025-11-18 05:24:18.437517 112 Perm LeePreferred Gardens \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:46.540336+00 \N +2427 \N \N \N Sauce Essentials Distillate AIO | Skywalker OGSauceIndica-HybridTHC: 81.88%Special Offer sauce-essentials-distillate-aio-skywalker-og \N \N \N \N 81.88 \N Sauce \N https://images.dutchie.com/c77b489368a42ca2900bcb5010180722?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-distillate-aio-skywalker-og t f \N 2025-11-18 03:57:23.37659 2025-11-18 04:24:49.087281 2025-11-18 03:57:23.37659 2025-11-18 05:24:24.380295 112 Skywalker OGSauce \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.087281+00 \N +2430 \N \N \N Sauce Essentials Live Resin AIO | Aloha ExpressSauceTHC: 76.91% sauce-essentials-live-resin-aio-aloha-express \N \N \N \N 76.91 \N Sauce \N https://images.dutchie.com/818474e7cec0168cd9ceb0ada767266b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-live-resin-aio-aloha-express t f \N 2025-11-18 03:57:23.383858 2025-11-18 04:24:49.095218 2025-11-18 03:57:23.383858 2025-11-18 05:24:36.780842 112 Aloha ExpressSauce \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.095218+00 \N +2432 \N \N \N Sauce Essentials Live Resin AIO | GelatoSauceIndica-HybridTHC: 94.44% sauce-essentials-live-resin-aio-gelato \N \N \N \N 94.44 \N Sauce \N https://images.dutchie.com/0c86705dc0df241931b579c7d6c0b424?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-live-resin-aio-gelato t f \N 2025-11-18 03:57:23.388868 2025-11-18 04:24:49.100945 2025-11-18 03:57:23.388868 2025-11-18 05:24:42.894358 112 GelatoSauce \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:49.100945+00 \N +2513 \N \N \N Stiiizy Distillate Pod | Magic MelonSTIIIZYTHC: 90.35% - 92.11%CBD: 0.15% stiiizy-distillate-pod-magic-melon \N \N \N \N 90.35 0.15 STIIIZY \N https://images.dutchie.com/d721b15318961fa24d5595ac22c7325b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/stiiizy-distillate-pod-magic-melon t f \N 2025-11-18 03:59:06.711332 2025-11-18 04:26:27.676525 2025-11-18 03:59:06.711332 2025-11-18 05:29:16.045973 112 Magic MelonSTIIIZY \N \N \N {} {} {} 23.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.676525+00 \N +2515 \N \N \N Sublime Pre-Roll | 1g Single-Royal Blue DieselSublime BrandsHybridTHC: 25.84%Special Offer sublime-pre-roll-1g-single-royal-blue-diesel-25548 \N \N \N \N 25.84 \N Sublime Brands \N https://images.dutchie.com/6ccb5a49f883094f7b74a0b2dc515fc1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sublime-pre-roll-1g-single-royal-blue-diesel-25548 t f \N 2025-11-18 03:59:08.76762 2025-11-18 04:26:33.845558 2025-11-18 03:59:08.76762 2025-11-18 05:29:23.186992 112 1g Single-Royal Blue DieselSublime Brands \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:33.845558+00 \N +2517 \N \N \N The Healing Alchemist Distillate Cart | Platinum PunchThe Healing AlchemistHybridTHC: 84.8%CBD: 0.21% the-healing-alchemist-distillate-cart-platinum-punch \N \N \N \N 84.80 0.21 The Healing Alchemist \N https://images.dutchie.com/fb04fabb7f97c4da61c94f75924080d2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-healing-alchemist-distillate-cart-platinum-punch t f \N 2025-11-18 03:59:13.487566 2025-11-18 04:26:36.888462 2025-11-18 03:59:13.487566 2025-11-18 05:29:35.501546 112 Platinum PunchThe Healing Alchemist \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:36.888462+00 \N +2518 \N \N \N The Healing Alchemist Distillate Cart | Raspberry LemnonadeThe Healing AlchemistHybridTHC: 75.9%CBD: 0.15% the-healing-alchemist-distillate-cart-raspberry-lemnonade \N \N \N \N 75.90 0.15 The Healing Alchemist \N https://images.dutchie.com/fb04fabb7f97c4da61c94f75924080d2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-healing-alchemist-distillate-cart-raspberry-lemnonade t f \N 2025-11-18 03:59:13.490839 2025-11-18 04:26:36.891031 2025-11-18 03:59:13.490839 2025-11-18 05:29:38.665884 112 Raspberry LemnonadeThe Healing Alchemist \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:36.891031+00 \N +2520 \N \N \N The Healing Alchemist Distillate Cart | Watermelon LimeThe Healing AlchemistIndica-HybridTHC: 77.48%CBD: 0.18% the-healing-alchemist-distillate-cart-watermelon-lime \N \N \N \N 77.48 0.18 The Healing Alchemist \N https://images.dutchie.com/fb04fabb7f97c4da61c94f75924080d2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-healing-alchemist-distillate-cart-watermelon-lime t f \N 2025-11-18 03:59:13.496464 2025-11-18 04:26:36.895753 2025-11-18 03:59:13.496464 2025-11-18 05:29:44.746946 112 Watermelon LimeThe Healing Alchemist \N \N \N {} {} {} 22.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:36.895753+00 \N +2609 \N \N \N Wana | 100mg Gummies | Berry PatchWanaTHC: 0.23% wana-100mg-gummies-berry-patch \N \N \N \N 0.23 \N Wana \N https://images.dutchie.com/a351c60df8c4cff47d6a752e32a4adcd?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-gummies-berry-patch t f \N 2025-11-18 04:00:24.811031 2025-11-18 04:27:48.237084 2025-11-18 04:00:24.811031 2025-11-18 05:35:28.896234 112 Berry PatchWana \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.237084+00 \N +2523 \N \N \N Diamond Dusties | 1.3G Live Resin Infused Pre-Roll | Ruby SativaThe PharmHybridTHC: 41.43% diamond-dusties-1-3g-live-resin-infused-pre-roll-ruby-sativa-87808 \N \N \N \N 41.43 \N The Pharm \N https://images.dutchie.com/225d9667a1a1778ceba1be2c6ebaaa18?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/diamond-dusties-1-3g-live-resin-infused-pre-roll-ruby-sativa-87808 t f \N 2025-11-18 03:59:18.66409 2025-11-18 04:26:51.706642 2025-11-18 03:59:18.66409 2025-11-18 05:29:57.645764 112 Ruby \N \N \N {} {} {} 24.00 18.00 \N \N \N \N in_stock \N 2025-11-18 04:26:51.706642+00 \N +2526 \N \N \N Dusties | 1.3G Infused Pre-Roll | Blueberry MacThe PharmHybridTHC: 39.11%CBD: 0.36% dusties-1-3g-infused-pre-roll-blueberry-mac \N \N \N \N 39.11 0.36 The Pharm \N https://images.dutchie.com/58a799b7691188d3ecdacc1a34de4a82?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-1-3g-infused-pre-roll-blueberry-mac t f \N 2025-11-18 03:59:18.67138 2025-11-18 04:26:51.713407 2025-11-18 03:59:18.67138 2025-11-18 05:30:06.696464 112 Blueberry MacThe Pharm \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.713407+00 \N +2529 \N \N \N Dusties | 1.3G Infused Pre-Roll | Watermelon AKThe PharmSativaTHC: 38.19%CBD: 0.6% dusties-1-3g-infused-pre-roll-watermelon-ak \N \N \N \N 38.19 0.60 The Pharm \N https://images.dutchie.com/58a799b7691188d3ecdacc1a34de4a82?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dusties-1-3g-infused-pre-roll-watermelon-ak t f \N 2025-11-18 03:59:18.680052 2025-11-18 04:26:51.71921 2025-11-18 03:59:18.680052 2025-11-18 05:30:22.408604 112 Watermelon AKThe Pharm \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.71921+00 \N +2532 \N \N \N The Pharm Badder | Sour LeopardThe PharmHybridTHC: 70.33%CBD: 0.12%Special Offer the-pharm-badder-sour-leopard \N \N \N \N 70.33 0.12 The Pharm \N https://images.dutchie.com/14c863f38d86af6f2914e9c65231afca?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-pharm-badder-sour-leopard t f \N 2025-11-18 03:59:18.687152 2025-11-18 04:26:51.725158 2025-11-18 03:59:18.687152 2025-11-18 05:30:31.488347 112 Sour LeopardThe Pharm \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.725158+00 \N +2534 \N \N \N The Pharm Budder | G6 OG x CCThe PharmTHC: 68.11%CBD: 0.28%Special Offer the-pharm-budder-g6-og-x-cc \N \N \N \N 68.11 0.28 The Pharm \N https://images.dutchie.com/14c863f38d86af6f2914e9c65231afca?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-pharm-budder-g6-og-x-cc t f \N 2025-11-18 03:59:18.691284 2025-11-18 04:26:51.728878 2025-11-18 03:59:18.691284 2025-11-18 05:30:37.424155 112 G6 OG x CCThe Pharm \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:51.728878+00 \N +2539 \N \N \N Thunder Bud Pre-Roll | Cake FaceThunder BudHybridTHC: 28.29%Special Offer thunder-bud-pre-roll-cake-face \N \N \N \N 28.29 \N Thunder Bud \N https://images.dutchie.com/d92119bbc513fb51ca454fed4103574c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-cake-face t f \N 2025-11-18 03:59:42.461127 2025-11-18 04:27:08.330217 2025-11-18 03:59:42.461127 2025-11-18 05:30:56.857057 112 Cake FaceThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.330217+00 \N +2449 \N \N \N Select Essential Briq AIO | Lychee DreamSelectIndicaTHC: 87.65%CBD: 0.16% select-essential-briq-aio-lychee-dream-20553 \N \N \N \N \N \N Select \N \N \N /embedded-menu/AZ-Deeply-Rooted/product/select-essential-briq-aio-lychee-dream-20553 t f \N 2025-11-18 03:57:55.442522 2025-11-18 03:57:55.442522 2025-11-18 03:57:55.442522 2025-11-18 19:45:18.572007 112 \N \N \N \N {} {} {} \N \N \N \N \N \N in_stock \N 2025-11-18 03:57:55.442522+00 \N +2611 \N \N \N Wana | 100mg Gummies | WatermelonWanaTHC: 0.22% wana-100mg-gummies-watermelon \N \N \N \N 0.22 \N Wana \N https://images.dutchie.com/215b8976f24f35371abae16ca6b66e63?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-gummies-watermelon t f \N 2025-11-18 04:00:24.815631 2025-11-18 04:27:48.241761 2025-11-18 04:00:24.815631 2025-11-18 05:35:31.855767 112 WatermelonWana \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.241761+00 \N +2555 \N \N \N Tipsy Turtle | 100mg Glazed PecansTipsy TurtleTHC: 0.24%Special Offer tipsy-turtle-100mg-glazed-pecans \N \N \N \N 0.24 \N Tipsy Turtle \N https://images.dutchie.com/25f122b0d8f2a1c726ee0fb79bdfe7e6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tipsy-turtle-100mg-glazed-pecans t f \N 2025-11-18 03:59:44.512245 2025-11-18 04:27:09.471764 2025-11-18 03:59:44.512245 2025-11-18 05:32:00.27419 112 100mg Glazed PecansTipsy Turtle \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:09.471764+00 \N +2558 \N \N \N Tipsy Turtle | 100mg Turtle Tracks Snack MixTipsy TurtleTHC: 0.48%Special Offer tipsy-turtle-100mg-turtle-tracks-snack-mix \N \N \N \N 0.48 \N Tipsy Turtle \N https://images.dutchie.com/a38de9f2f2a51a22e8053c922eefe72c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tipsy-turtle-100mg-turtle-tracks-snack-mix t f \N 2025-11-18 03:59:44.519672 2025-11-18 04:27:09.477521 2025-11-18 03:59:44.519672 2025-11-18 05:32:10.040116 112 100mg Turtle Tracks Snack MixTipsy Turtle \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:09.477521+00 \N +2559 \N \N \N Trip Live Rosin Cart | SpritzerTripHybridTHC: 77.14%CBD: 0.17% trip-live-rosin-cart-spritzer \N \N \N \N 77.14 0.17 Trip \N https://images.dutchie.com/d15188654d3b3b0b24bf44177606e259?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/trip-live-rosin-cart-spritzer t f \N 2025-11-18 03:59:49.096213 2025-11-18 04:27:12.515595 2025-11-18 03:59:49.096213 2025-11-18 05:32:13.007969 112 SpritzerTrip \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:12.515595+00 \N +2562 \N \N \N Tropics Live Hash Rosin AIO | Mint SmashTropicsIndica-HybridTHC: 81.23% tropics-live-hash-rosin-aio-mint-smash \N \N \N \N 81.23 \N Tropics \N https://images.dutchie.com/a4e57559103938667fdb6e257271fe26?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tropics-live-hash-rosin-aio-mint-smash t f \N 2025-11-18 03:59:54.498562 2025-11-18 04:27:27.530757 2025-11-18 03:59:54.498562 2025-11-18 05:32:24.834973 112 Mint SmashTropics \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:27.530757+00 \N +2564 \N \N \N Tropics Live Hash Rosin AIO | Royal RTZTropicsIndicaTHC: 76.51%CBD: 0.24% tropics-live-hash-rosin-aio-royal-rtz \N \N \N \N 76.51 0.24 Tropics \N https://images.dutchie.com/a4e57559103938667fdb6e257271fe26?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tropics-live-hash-rosin-aio-royal-rtz t f \N 2025-11-18 03:59:54.506851 2025-11-18 04:27:27.535512 2025-11-18 03:59:54.506851 2025-11-18 05:32:31.746558 112 Royal RTZTropics \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:27.535512+00 \N +2565 \N \N \N Tropics Vape | .5g Solventless Live Hash Rosin Disposable-Trop CherryTropicsSativa-HybridTHC: 77.19%CBD: 0.16% tropics-vape-5g-solventless-live-hash-rosin-disposable-trop-cherry-36382 \N \N \N \N 77.19 0.16 Tropics \N https://images.dutchie.com/f251e0c0be3096f568ec94ea02c5c9fb?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tropics-vape-5g-solventless-live-hash-rosin-disposable-trop-cherry-36382 t f \N 2025-11-18 03:59:54.509369 2025-11-18 04:27:27.537862 2025-11-18 03:59:54.509369 2025-11-18 05:32:34.914093 112 .5g Solventless Live Hash Rosin Disposable-Trop CherryTropics \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:27.537862+00 \N +2567 \N \N \N Tru Infusion Flower Mylar | Gush MintsTRU InfusionIndica-HybridTHC: 20.09% tru-infusion-flower-mylar-gush-mints \N \N \N \N 20.09 \N TRU Infusion \N https://images.dutchie.com/635e346130d44828785eee62deaa0cd1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-flower-mylar-gush-mints t f \N 2025-11-18 03:59:59.225143 2025-11-18 04:27:38.50695 2025-11-18 03:59:59.225143 2025-11-18 05:32:40.951541 112 Gush MintsTRU Infusion \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.50695+00 \N +2569 \N \N \N Tru Infusion Live Resin AIO | GMOZKTRU InfusionHybridTHC: 79.17%CBD: 0.13% tru-infusion-live-resin-aio-gmozk \N \N \N \N 79.17 0.13 TRU Infusion \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-aio-gmozk t f \N 2025-11-18 03:59:59.230242 2025-11-18 04:27:38.511362 2025-11-18 03:59:59.230242 2025-11-18 05:32:51.942038 112 GMOZKTRU Infusion \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.511362+00 \N +2571 \N \N \N Tru Infusion Live Resin AIO | Lemon Cherry GelatoTRU InfusionIndica-HybridTHC: 81.59%CBD: 0.15% tru-infusion-live-resin-aio-lemon-cherry-gelato \N \N \N \N 81.59 0.15 TRU Infusion \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-aio-lemon-cherry-gelato t f \N 2025-11-18 03:59:59.234543 2025-11-18 04:27:38.516442 2025-11-18 03:59:59.234543 2025-11-18 05:32:58.245838 112 Lemon Cherry GelatoTRU Infusion \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.516442+00 \N +2574 \N \N \N Tru Infusion | Live Resin Batter | Ice Box PieTRU InfusionIndica-HybridTHC: 79.73%CBD: 0.15% tru-infusion-live-resin-batter-ice-box-pie \N \N \N \N 79.73 0.15 TRU Infusion \N https://images.dutchie.com/aa33e735e1dbc702862affb772d2038f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/tru-infusion-live-resin-batter-ice-box-pie t f \N 2025-11-18 03:59:59.241558 2025-11-18 04:27:38.523931 2025-11-18 03:59:59.241558 2025-11-18 05:33:21.765214 112 Ice Box PieTRU Infusion \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:38.523931+00 \N +3257 1 58 deeply-rooted-az-concentrates-1764475146326-42 Canamo Live Resin Cart | Sour OctangCanamo ConcentratesSativa-HybridTHC: 78.65%CBD: 0.15% canamo-live-resin-cart-sour-octangcanamo-concentratessativa-hybridthc-78-65-cbd-0-15-1764475146383-d1io6t \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-sour-octang f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3258 1 58 deeply-rooted-az-concentrates-1764475146326-43 Canamo Live Resin Cart | Two By FourCanamo ConcentratesTHC: 75.33%CBD: 0.15% canamo-live-resin-cart-two-by-fourcanamo-concentratesthc-75-33-cbd-0-15-1764475146384-8x3og6 \N 18.75 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-live-resin-cart-two-by-four f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 5g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +2597 \N \N \N Vortex Flower Jar | BananacondaVortexHybridTHC: 19.63%CBD: 0.01%Special Offer vortex-flower-jar-bananaconda \N \N \N \N 19.63 0.01 Vortex \N https://images.dutchie.com/6c5eb5820e5d8952de6b8d03ceb1c221?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-bananaconda t f \N 2025-11-18 04:00:20.204504 2025-11-18 04:27:45.179931 2025-11-18 04:00:20.204504 2025-11-18 05:34:40.302879 112 BananacondaVortex \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.179931+00 \N +2599 \N \N \N Vortex Flower Jar | First Class GasVortexIndica-HybridTHC: 27.6%Special Offer vortex-flower-jar-first-class-gas \N \N \N \N 27.60 \N Vortex \N https://images.dutchie.com/813762fcd4d3e3cf862b66576d70b0f6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-first-class-gas t f \N 2025-11-18 04:00:20.216627 2025-11-18 04:27:45.191537 2025-11-18 04:00:20.216627 2025-11-18 05:34:46.495902 112 First Class GasVortex \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.191537+00 \N +2601 \N \N \N Vortex Flower Jar | Hash BurgerVortexIndica-HybridTHC: 25.21%Special Offer vortex-flower-jar-hash-burger \N \N \N \N 25.21 \N Vortex \N https://images.dutchie.com/6c5eb5820e5d8952de6b8d03ceb1c221?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-hash-burger t f \N 2025-11-18 04:00:20.221173 2025-11-18 04:27:45.198407 2025-11-18 04:00:20.221173 2025-11-18 05:34:52.783 112 Hash BurgerVortex \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.198407+00 \N +2604 \N \N \N Vortex Flower Jar | Pink RTZVortexHybridTHC: 22.17% vortex-flower-jar-pink-rtz \N \N \N \N 22.17 \N Vortex \N https://images.dutchie.com/6c5eb5820e5d8952de6b8d03ceb1c221?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-pink-rtz t f \N 2025-11-18 04:00:20.227877 2025-11-18 04:27:45.2069 2025-11-18 04:00:20.227877 2025-11-18 05:35:01.26821 112 Pink RTZVortex \N \N \N {} {} {} 100.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.2069+00 \N +2606 \N \N \N Vortex Flower Jar | Yellow ZushiVortexIndica-HybridTHC: 25%CBD: 0.07% vortex-flower-jar-yellow-zushi-16567 \N \N \N \N 25.00 0.07 Vortex \N https://images.dutchie.com/6c5eb5820e5d8952de6b8d03ceb1c221?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-yellow-zushi-16567 t f \N 2025-11-18 04:00:20.233567 2025-11-18 04:27:45.213007 2025-11-18 04:00:20.233567 2025-11-18 05:35:10.581893 112 Yellow ZushiVortex \N \N \N {} {} {} 100.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.213007+00 \N +3320 1 59 deeply-rooted-az-edibles-1764475244783-0 100mg MEGA DOSE 1: ummyOGeez!THC: 0.69%CBD: 0.63% 100mg-mega-dose-1-ummyogeez-thc-0-69-cbd-0-63-1764475244789-1gfqwm \N 5.40 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/100mg-mega-dose-1-ummy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +2613 \N \N \N Wana | 100mg QS Gummies | Arizona SunriseWanaTHC: 0.21% wana-100mg-qs-gummies-arizona-sunrise \N \N \N \N 0.21 \N Wana \N https://images.dutchie.com/4e198208b7cc4a73efeecbee9ada72e3?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-qs-gummies-arizona-sunrise t f \N 2025-11-18 04:00:24.819476 2025-11-18 04:27:48.246042 2025-11-18 04:00:24.819476 2025-11-18 05:35:41.669104 112 Arizona SunriseWana \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.246042+00 \N +2616 \N \N \N Wana | 100mg QS Gummies | Peach BelliniWanaTHC: 0.22% wana-100mg-qs-gummies-peach-bellini \N \N \N \N 0.22 \N Wana \N https://images.dutchie.com/e25182293f4dadf69677cf6c84e70305?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wana-100mg-qs-gummies-peach-bellini t f \N 2025-11-18 04:00:24.825514 2025-11-18 04:27:48.252498 2025-11-18 04:00:24.825514 2025-11-18 05:35:48.856869 112 Peach BelliniWana \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:48.252498+00 \N +2621 \N \N \N Wizard trees Pre-roll 2 Pack | Tea TimeWizard TreesHybridTHC: 28.23% wizard-trees-pre-roll-2-pack-tea-time \N \N \N \N 28.23 \N Wizard Trees \N https://images.dutchie.com/51ada64b86f54f0b77416142776354fd?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wizard-trees-pre-roll-2-pack-tea-time t f \N 2025-11-18 04:00:30.142288 2025-11-18 04:28:03.168569 2025-11-18 04:00:30.142288 2025-11-18 05:36:07.565134 112 Tea TimeWizard Trees \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:03.168569+00 \N +2623 \N \N \N Wyld | 100mg THC Gummies | HuckleberryWyldTHCTHC: 0.28% wyld-100mg-thc-gummies-huckleberry-73171 \N \N \N \N 0.28 \N Wyld \N https://images.dutchie.com/82ff65950399e06d0839ad9ad8beaef9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-100mg-thc-gummies-huckleberry-73171 t f \N 2025-11-18 04:00:34.949695 2025-11-18 04:28:14.297057 2025-11-18 04:00:34.949695 2025-11-18 05:36:13.548561 112 100mg \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.297057+00 \N +2625 \N \N \N Wyld | 1:1 THC/CBC Gummies | Blood OrangeWyldTHCTHC: 0.26% wyld-1-1-thc-cbc-gummies-blood-orange-89919 \N \N \N \N 0.26 \N Wyld \N https://images.dutchie.com/a11175af3885260afa70b07d1a281087?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-1-1-thc-cbc-gummies-blood-orange-89919 t f \N 2025-11-18 04:00:34.954495 2025-11-18 04:28:14.301491 2025-11-18 04:00:34.954495 2025-11-18 05:36:23.271794 112 1:1 \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.301491+00 \N +2628 \N \N \N Wyld | 1:1 THC/CBG Gummies | PearWyldTHCTHC: 0.27% wyld-1-1-thc-cbg-gummies-pear \N \N \N \N 0.27 \N Wyld \N https://images.dutchie.com/e032ee2c071e8460d64f8315555089c0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-1-1-thc-cbg-gummies-pear t f \N 2025-11-18 04:00:34.961064 2025-11-18 04:28:14.308734 2025-11-18 04:00:34.961064 2025-11-18 05:36:32.983512 112 1:1 \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.308734+00 \N +2630 \N \N \N Wyld | One 100mg THC Gummies | Raspberry LimeWyldTHCTHC: 0.54%CBD: 0.53% wyld-one-100mg-thc-gummies-raspberry-lime \N \N \N \N 0.54 0.53 Wyld \N https://images.dutchie.com/e8d2c713bf852e9ca2e8fdf34cfe05df?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/wyld-one-100mg-thc-gummies-raspberry-lime t f \N 2025-11-18 04:00:34.966085 2025-11-18 04:28:14.312705 2025-11-18 04:00:34.966085 2025-11-18 05:36:39.169999 112 One 100mg \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:14.312705+00 \N +3262 1 58 deeply-rooted-az-concentrates-1764475146326-47 Canamo Shatter | Rotten RozayCanamo ConcentratesHybridTHC: 74%CBD: 0.11% canamo-shatter-rotten-rozaycanamo-concentrateshybridthc-74-cbd-0-11-1764475146389-441q5n \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/canamo-shatter-rotten-rozay f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3263 1 58 deeply-rooted-az-concentrates-1764475146326-48 Connected Live Resin Cart | ChromeConnected CannabisHybridTHC: 74.31%Special Offer connected-live-resin-cart-chromeconnected-cannabishybridthc-74-31-special-offer-1764475146390-yyu4a0 \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-live-resin-cart-chrome f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3264 1 58 deeply-rooted-az-concentrates-1764475146326-49 Connected Live Resin Cart | Gelato 41Connected CannabisIndica-HybridTHC: 75.02%CBD: 0.1%Special Offer connected-live-resin-cart-gelato-41connected-cannabisindica-hybridthc-75-02-cbd-0-1-special-offer-1764475146391-fycg1x \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-live-resin-cart-gelato-41 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3265 1 58 deeply-rooted-az-concentrates-1764475146326-50 Connected Live Resin Cart | Jack of DiamondsConnected CannabisHybridTHC: 79.6%Special Offer connected-live-resin-cart-jack-of-diamondsconnected-cannabishybridthc-79-6-special-offer-1764475146392-heraxt \N 45.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/connected-live-resin-cart-jack-of-diamonds f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3266 1 58 deeply-rooted-az-concentrates-1764475146326-51 DR Cured Resin Batter | Fam 95 (AZOL)Deeply RootedIndica-HybridTHC: 61.51%Special Offer dr-cured-resin-batter-fam-95-azol-deeply-rootedindica-hybridthc-61-51-special-offer-1764475146394-sop5h4 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-cured-resin-batter-fam-95-azol f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3267 1 58 deeply-rooted-az-concentrates-1764475146326-52 DR Cured Resin Sauce | GMO (AZOL)Deeply RootedIndicaTHC: 64.2%CBD: 0.27%Special Offer dr-cured-resin-sauce-gmo-azol-deeply-rootedindicathc-64-2-cbd-0-27-special-offer-1764475146395-g3v4cu \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-cured-resin-sauce-gmo-azol f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +3268 1 58 deeply-rooted-az-concentrates-1764475146326-53 DR Cured Sugar | Vice City (AZOL)Deeply RootedHybridTHC: 69.86%Special Offer dr-cured-sugar-vice-city-azol-deeply-rootedhybridthc-69-86-special-offer-1764475146396-dyxvj7 \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-cured-sugar-vice-city-azol f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 2025-11-30 03:59:06.326065 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 03:59:06.326065+00 \N +2077 \N \N \N Cannabish Flower Jar | Grande GuavaCannabishSativa-HybridTHC: 24.74% cannabish-flower-jar-grande-guava \N \N \N \N 24.74 \N Cannabish \N https://images.dutchie.com/24449820687562bd0b8fb93801585c51?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cannabish-flower-jar-grande-guava t f \N 2025-11-18 03:51:32.166876 2025-11-18 04:18:12.888104 2025-11-18 03:51:32.166876 2025-11-18 05:03:58.125177 112 Grande GuavaCannabish \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:12.888104+00 \N +2078 \N \N \N Chill Pill | 100mg THC 10pk | ANYTIMEChill PillTHCTHC: 3.08% chill-pill-100mg-thc-10pk-anytime \N \N \N \N 3.08 \N Chill Pill \N https://images.dutchie.com/409000ee80084c696b16a68054661372?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-anytime t f \N 2025-11-18 03:51:38.525871 2025-11-18 04:18:16.274758 2025-11-18 03:51:38.525871 2025-11-18 05:03:44.430905 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.274758+00 \N +2079 \N \N \N Chill Pill | 100mg THC 10pk | DAYTIMEChill PillTHCTHC: 3.25% chill-pill-100mg-thc-10pk-daytime \N \N \N \N 3.25 \N Chill Pill \N https://images.dutchie.com/397fdb99300828093083269c471413a8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-daytime t f \N 2025-11-18 03:51:38.534711 2025-11-18 04:18:16.283147 2025-11-18 03:51:38.534711 2025-11-18 05:03:47.40045 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.283147+00 \N +2080 \N \N \N Chill Pill | 100mg THC 10pk | FLEXTIMEChill PillTHCTHC: 3.04% chill-pill-100mg-thc-10pk-flextime \N \N \N \N 3.04 \N Chill Pill \N https://images.dutchie.com/a3219f2f1c5ef3c089767b25452e4248?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-10pk-flextime t f \N 2025-11-18 03:51:38.537275 2025-11-18 04:18:16.28599 2025-11-18 03:51:38.537275 2025-11-18 05:03:50.360333 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.28599+00 \N +2082 \N \N \N Chill Pill | 100mg THC 20pk | ANYTIMEChill PillTHCTHC: 1.54% chill-pill-100mg-thc-20pk-anytime \N \N \N \N 1.54 \N Chill Pill \N https://images.dutchie.com/409000ee80084c696b16a68054661372?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-anytime t f \N 2025-11-18 03:51:38.542765 2025-11-18 04:18:16.290624 2025-11-18 03:51:38.542765 2025-11-18 05:04:01.136219 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.290624+00 \N +2083 \N \N \N Chill Pill | 100mg THC 20pk | DAYTIMEChill PillTHCTHC: 1.46% chill-pill-100mg-thc-20pk-daytime \N \N \N \N 1.46 \N Chill Pill \N https://images.dutchie.com/397fdb99300828093083269c471413a8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-daytime t f \N 2025-11-18 03:51:38.545113 2025-11-18 04:18:16.293329 2025-11-18 03:51:38.545113 2025-11-18 05:04:04.107806 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.293329+00 \N +2085 \N \N \N Chill Pill | 100mg THC 20pk | NIGHTTIMEChill PillTHCTHC: 3.26% chill-pill-100mg-thc-20pk-nighttime \N \N \N \N 3.26 \N Chill Pill \N https://images.dutchie.com/b2068a9967f5b1e4e65f5485e1eef134?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-nighttime t f \N 2025-11-18 03:51:38.549799 2025-11-18 04:18:16.297733 2025-11-18 03:51:38.549799 2025-11-18 05:04:10.150823 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.297733+00 \N +2086 \N \N \N Chill Pill | 100mg THC | Chill DropsChill PillTHCTHC: 97.32 mgCBD: 1.12 mg chill-pill-100mg-thc-chill-drops \N \N \N \N \N \N Chill Pill \N https://images.dutchie.com/a0dfa203e7c0dcf1fa04ddf2c1dd0eb1?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-chill-drops t f \N 2025-11-18 03:51:38.552015 2025-11-18 04:18:16.299724 2025-11-18 03:51:38.552015 2025-11-18 05:04:13.176081 112 100mg \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.299724+00 \N +2084 \N \N \N Chill Pill | 100mg THC 20pk | LIFETIMEChill PillTHCTHC: 3.68%CBD: 3.44% chill-pill-100mg-thc-20pk-lifetime \N \N \N \N 3.68 3.44 Chill Pill \N https://images.dutchie.com/68973f78f277703faaa2fd8fac634e6b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-lifetime t f \N 2025-11-18 03:51:38.547559 2025-11-18 04:18:16.295705 2025-11-18 03:51:38.547559 2025-11-18 05:04:07.071825 112 100mg \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:18:16.295705+00 \N +2097 \N \N \N Cure Injoy Distillate AIO | Guava LavaCure InjoyTHC: 84.5%CBD: 0.2%Special Offer cure-injoy-distillate-aio-guava-lava \N \N \N \N 84.50 0.20 Cure Injoy \N https://images.dutchie.com/9a9e274482227e1a44a9631415b2c5de?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-guava-lava t f \N 2025-11-18 03:52:14.321383 2025-11-18 04:19:07.167855 2025-11-18 03:52:14.321383 2025-11-18 05:04:54.536598 112 Guava LavaCure Injoy \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:07.167855+00 \N +2099 \N \N \N Cure Injoy Distillate AIO | LimenesiaCure InjoyTHC: 86.46%CBD: 0.21%Special Offer cure-injoy-distillate-aio-limenesia \N \N \N \N 86.46 0.21 Cure Injoy \N https://images.dutchie.com/a36f6c0b4ed899a6de1170adb046aab8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-limenesia t f \N 2025-11-18 03:52:14.332423 2025-11-18 04:19:07.176425 2025-11-18 03:52:14.332423 2025-11-18 05:05:00.617946 112 LimenesiaCure Injoy \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:07.176425+00 \N +2100 \N \N \N Cure Injoy Distillate AIO | Papaya KushCure InjoyTHC: 85.31%CBD: 0.2%Special Offer cure-injoy-distillate-aio-papaya-kush \N \N \N \N 85.31 0.20 Cure Injoy \N https://images.dutchie.com/659f79045e2a58dc0171f56763a3b564?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/cure-injoy-distillate-aio-papaya-kush t f \N 2025-11-18 03:52:14.334917 2025-11-18 04:19:07.178391 2025-11-18 03:52:14.334917 2025-11-18 05:05:03.64709 112 Papaya KushCure Injoy \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:07.178391+00 \N +2105 \N \N \N DR Flower Mylar | AngelicaDeeply RootedIndica-HybridTHC: 31.66%Special Offer dr-flower-mylar-angelica \N \N \N \N 31.66 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-angelica t f \N 2025-11-18 03:52:35.809167 2025-11-18 04:19:24.140133 2025-11-18 03:52:35.809167 2025-11-18 05:05:26.384746 112 AngelicaDeeply Rooted \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.140133+00 \N +2108 \N \N \N DR Flower Mylar | Chemistry #24Deeply RootedHybridTHC: 19.23%Special Offer dr-flower-mylar-chemistry-24-96614 \N \N \N \N 19.23 \N Deeply Rooted \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-chemistry-24-96614 t f \N 2025-11-18 03:52:35.818262 2025-11-18 04:19:24.149612 2025-11-18 03:52:35.818262 2025-11-18 05:05:36.42057 112 Chemistry #24Deeply Rooted \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.149612+00 \N +2112 \N \N \N DR Flower Mylar | ICC X Biker OG (ASY)Deeply RootedIndica-HybridTHC: 22.99% dr-flower-mylar-icc-x-biker-og-asy-4260 \N \N \N \N 22.99 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-icc-x-biker-og-asy-4260 t f \N 2025-11-18 03:52:35.83198 2025-11-18 04:19:24.156861 2025-11-18 03:52:35.83198 2025-11-18 05:05:53.077059 112 ICC X Biker OG (ASY)Deeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.156861+00 \N +2114 \N \N \N DR Flower Mylar | Jealousy Mintz (ARZ)Deeply RootedIndica-HybridTHC: 29.25%Special Offer dr-flower-mylar-jealousy-mintz-arz \N \N \N \N 29.25 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-jealousy-mintz-arz t f \N 2025-11-18 03:52:35.839033 2025-11-18 04:19:24.161457 2025-11-18 03:52:35.839033 2025-11-18 05:05:59.114544 112 Jealousy Mintz (ARZ)Deeply Rooted \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.161457+00 \N +2115 \N \N \N DR Flower Mylar | LSD (ASY)Deeply RootedIndica-HybridTHC: 25.91% dr-flower-mylar-lsd-asy-81792 \N \N \N \N 25.91 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-lsd-asy-81792 t f \N 2025-11-18 03:52:35.843095 2025-11-18 04:19:24.163483 2025-11-18 03:52:35.843095 2025-11-18 05:06:02.134795 112 LSD (ASY)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.163483+00 \N +2117 \N \N \N DR Flower Mylar | Leopard Head (Pharm)Deeply RootedTHC: 17.8% dr-flower-mylar-leopard-head-pharm \N \N \N \N 17.80 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-leopard-head-pharm t f \N 2025-11-18 03:52:35.850221 2025-11-18 04:19:24.167342 2025-11-18 03:52:35.850221 2025-11-18 05:06:08.374523 112 Leopard Head (Pharm)Deeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.167342+00 \N +3321 1 59 deeply-rooted-az-edibles-1764475244783-1 10:1 Tart Cherry - CBN/THC - NightlyGrönTHCTHC: 0.08%CBD: 0.01% 10-1-tart-cherry-cbn-thc-nightlygr-nthcthc-0-08-cbd-0-01-1764475244792-bzcrn1 \N 14.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/10-1-tart-cherry-cbn-thc-nightly-31379 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3565 1 56 deeply-rooted-az-pre-rolls-1764475447223-45 Jeeter Quad Infused Pre-Rolls | Bubba GJeeterTHC: 35.52% jeeter-quad-infused-pre-rolls-bubba-gjeeterthc-35-52-1764475447293-dhoowl \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-quad-infused-pre-rolls-bubba-g f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +2118 \N \N \N DR Flower Mylar | Mac 1 (ASY)Deeply RootedIndica-HybridTHC: 28.28% dr-flower-mylar-mac-1-asy-91912 \N \N \N \N 28.28 \N Deeply Rooted \N https://images.dutchie.com/flower-stock-3-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-mac-1-asy-91912 t f \N 2025-11-18 03:52:35.853212 2025-11-18 04:19:24.169214 2025-11-18 03:52:35.853212 2025-11-18 05:06:11.420837 112 Mac 1 (ASY)Deeply Rooted \N \N \N {} {} {} 110.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.169214+00 \N +2120 \N \N \N DR Flower Mylar | Ronny Burger (Living Soil)Deeply RootedIndicaTHC: 21.67% dr-flower-mylar-ronny-burger-living-soil \N \N \N \N 21.67 \N Deeply Rooted \N https://images.dutchie.com/flower-stock-13-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-ronny-burger-living-soil t f \N 2025-11-18 03:52:35.859819 2025-11-18 04:19:24.172759 2025-11-18 03:52:35.859819 2025-11-18 05:06:19.73667 112 Ronny Burger (Living Soil)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.172759+00 \N +2122 \N \N \N DR Flower Mylar | SuperBoof (Sueno)Deeply RootedHybridTHC: 29.55% dr-flower-mylar-superboof-sueno-81974 \N \N \N \N 29.55 \N Deeply Rooted \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-superboof-sueno-81974 t f \N 2025-11-18 03:52:35.866422 2025-11-18 04:19:24.1766 2025-11-18 03:52:35.866422 2025-11-18 05:06:25.709579 112 SuperBoof (Sueno)Deeply Rooted \N \N \N {} {} {} 110.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.1766+00 \N +2124 \N \N \N DR Flower Pop Top | Government Oasis (PH)Deeply RootedHybridTHC: 19.94%Special Offer dr-flower-pop-top-government-oasis-ph \N \N \N \N 19.94 \N Deeply Rooted \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-pop-top-government-oasis-ph t f \N 2025-11-18 03:52:35.873689 2025-11-18 04:19:24.180496 2025-11-18 03:52:35.873689 2025-11-18 05:06:31.866111 112 Government Oasis (PH)Deeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.180496+00 \N +2126 \N \N \N DR Live Hash Rosin | Arctic Gummies (ET)Deeply RootedSativa-HybridTHC: 73.23%CBD: 0.16% dr-live-hash-rosin-arctic-gummies-et \N \N \N \N 73.23 0.16 Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-arctic-gummies-et t f \N 2025-11-18 03:52:35.881015 2025-11-18 04:19:24.184624 2025-11-18 03:52:35.881015 2025-11-18 05:06:38.350334 112 Arctic Gummies (ET)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.184624+00 \N +2578 \N \N \N Varz Flower Jar | PaveVarzHybridTHC: 27.6%Special Offer varz-flower-jar-pave \N \N \N \N 27.60 \N Varz \N https://images.dutchie.com/e85dd12dd5854a4952a3c686fb8e8e35?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-flower-jar-pave t f \N 2025-11-18 04:00:18.621847 2025-11-18 04:27:44.002137 2025-11-18 04:00:18.621847 2025-11-18 05:33:25.352285 112 PaveVarz \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.002137+00 \N +2127 \N \N \N DR Live Hash Rosin | Dark Rainbow (ENVY)Deeply RootedHybridTHC: 77.34% dr-live-hash-rosin-dark-rainbow-envy \N \N \N \N 77.34 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-dark-rainbow-envy t f \N 2025-11-18 03:52:35.884806 2025-11-18 04:19:24.186749 2025-11-18 03:52:35.884806 2025-11-18 05:06:41.315507 112 Dark Rainbow (ENVY)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.186749+00 \N +2129 \N \N \N DR Live Hash Rosin | Gruntz (ENVY)Deeply RootedIndica-HybridTHC: 73.01% dr-live-hash-rosin-gruntz-envy \N \N \N \N 73.01 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-gruntz-envy t f \N 2025-11-18 03:52:35.892391 2025-11-18 04:19:24.190932 2025-11-18 03:52:35.892391 2025-11-18 05:06:49.359927 112 Gruntz (ENVY)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.190932+00 \N +2131 \N \N \N DR Live Hash Rosin | Monsoon (ET)Deeply RootedIndica-HybridTHC: 69.01%CBD: 0.14% dr-live-hash-rosin-monsoon-et \N \N \N \N 69.01 0.14 Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-monsoon-et t f \N 2025-11-18 03:52:35.900407 2025-11-18 04:19:24.195369 2025-11-18 03:52:35.900407 2025-11-18 05:06:55.585119 112 Monsoon (ET)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.195369+00 \N +2132 \N \N \N DR Live Hash Rosin | Oishii (ENVY)Deeply RootedIndica-HybridTHC: 77.66% dr-live-hash-rosin-oishii-envy \N \N \N \N 77.66 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-oishii-envy t f \N 2025-11-18 03:52:35.90438 2025-11-18 04:19:24.197181 2025-11-18 03:52:35.90438 2025-11-18 05:06:58.570051 112 Oishii (ENVY)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.197181+00 \N +2136 \N \N \N DR Live Hash Rosin | Super Buff CherryDeeply RootedHybridTHC: 73.54% dr-live-hash-rosin-super-buff-cherry \N \N \N \N 73.54 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-super-buff-cherry t f \N 2025-11-18 03:52:35.919669 2025-11-18 04:19:24.20548 2025-11-18 03:52:35.919669 2025-11-18 05:07:26.842452 112 Super Buff CherryDeeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.20548+00 \N +2137 \N \N \N DR Live Hash Rosin | White RontzDeeply RootedIndica-HybridTHC: 77.79% dr-live-hash-rosin-white-rontz \N \N \N \N 77.79 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-white-rontz t f \N 2025-11-18 03:52:35.923625 2025-11-18 04:19:24.207877 2025-11-18 03:52:35.923625 2025-11-18 05:07:19.024937 112 White RontzDeeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.207877+00 \N +2139 \N \N \N DR Live Resin Batter | Lemon MeringueDeeply RootedSativa-HybridTHC: 53.86%Special Offer dr-live-resin-batter-lemon-meringue \N \N \N \N 53.86 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-batter-lemon-meringue t f \N 2025-11-18 03:52:35.930904 2025-11-18 04:19:24.211963 2025-11-18 03:52:35.930904 2025-11-18 05:07:29.841604 112 Lemon MeringueDeeply Rooted \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.211963+00 \N +2141 \N \N \N DR Live Resin Sauce | Forbidden RTZDeeply RootedIndica-HybridTHC: 70.43%Special Offer dr-live-resin-sauce-forbidden-rtz \N \N \N \N 70.43 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-sauce-forbidden-rtz t f \N 2025-11-18 03:52:35.938307 2025-11-18 04:19:24.21604 2025-11-18 03:52:35.938307 2025-11-18 05:07:35.82152 112 Forbidden RTZDeeply Rooted \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.21604+00 \N +2142 \N \N \N DR Live Resin Sauce | Neon SunshineDeeply RootedHybridTHC: 72.7% dr-live-resin-sauce-neon-sunshine \N \N \N \N 72.70 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-sauce-neon-sunshine t f \N 2025-11-18 03:52:35.941872 2025-11-18 04:19:24.218113 2025-11-18 03:52:35.941872 2025-11-18 05:07:38.96471 112 Neon SunshineDeeply Rooted \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.218113+00 \N +2144 \N \N \N DR Live Resin | Grape GasolineDeeply RootedIndica-HybridTHC: 73.83%Special Offer dr-live-resin-grape-gasoline \N \N \N \N 73.83 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-grape-gasoline t f \N 2025-11-18 03:52:35.94898 2025-11-18 04:19:24.222506 2025-11-18 03:52:35.94898 2025-11-18 05:07:45.389048 112 Grape GasolineDeeply Rooted \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.222506+00 \N +2634 \N \N \N DR Flower Mylar | Black MapleDeeply RootedHybridTHC: 25.67%Special Offer dr-flower-mylar-black-maple \N \N \N \N 25.67 \N Deeply Rooted \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-black-maple t f \N 2025-11-18 04:19:24.143978 2025-11-18 04:19:24.143978 2025-11-18 04:19:24.143978 2025-11-18 05:36:57.497345 112 Black MapleDeeply Rooted \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.143978+00 \N +2111 \N \N \N DR Flower Mylar | Hot Lava (TRU)Deeply RootedHybridTHC: 30.68%Special Offer dr-flower-mylar-hot-lava-tru-29978 \N \N \N \N 30.68 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-hot-lava-tru-29978 t f \N 2025-11-18 03:52:35.828463 2025-11-18 04:19:24.155119 2025-11-18 03:52:35.828463 2025-11-18 05:05:49.943263 112 Hot Lava (TRU)Deeply Rooted \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.155119+00 \N +2147 \N \N \N Dermafreeze | 600mg TopicalDermafreezeTHC: 97 mgCBD: 527 mg dermafreeze-600mg-topical \N \N \N \N \N \N Dermafreeze \N https://images.dutchie.com/0aefd2567defbfc59d51f528329498b2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-600mg-topical t f \N 2025-11-18 03:52:38.028583 2025-11-18 04:19:27.561986 2025-11-18 03:52:38.028583 2025-11-18 05:07:54.458656 112 600mg TopicalDermafreeze \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:27.561986+00 \N +2113 \N \N \N DR Flower Mylar | ICC x Biker OG (ASY)Deeply RootedIndica-HybridTHC: 22.99% dr-flower-mylar-icc-x-biker-og-asy \N \N \N \N 22.99 \N Deeply Rooted \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-icc-x-biker-og-asy t f \N 2025-11-18 03:52:35.835471 2025-11-18 04:19:24.159584 2025-11-18 03:52:35.835471 2025-11-18 05:05:56.126693 112 ICC x Biker OG (ASY)Deeply Rooted \N \N \N {} {} {} 75.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.159584+00 \N +2116 \N \N \N DR Flower Mylar | Lemon Cherry RTZ (ASY)Deeply RootedHybridTHC: 24.37%CBD: 0.03% dr-flower-mylar-lemon-cherry-rtz-asy-80506 \N \N \N \N 24.37 0.03 Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-lemon-cherry-rtz-asy-80506 t f \N 2025-11-18 03:52:35.846689 2025-11-18 04:19:24.1651 2025-11-18 03:52:35.846689 2025-11-18 05:06:05.310266 112 Lemon Cherry RTZ (ASY)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.1651+00 \N +2119 \N \N \N DR Flower Mylar | Mule Fuel (PH)Deeply RootedIndicaTHC: 25.78%Special Offer dr-flower-mylar-mule-fuel-ph-61668 \N \N \N \N 25.78 \N Deeply Rooted \N https://images.dutchie.com/7aa675bad14b3e6ce80ff54ee12abe50?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-mule-fuel-ph-61668 t f \N 2025-11-18 03:52:35.856546 2025-11-18 04:19:24.171105 2025-11-18 03:52:35.856546 2025-11-18 05:06:16.771849 112 Mule Fuel (PH)Deeply Rooted \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.171105+00 \N +2121 \N \N \N DR Flower Mylar | Royal Cake (PH)Deeply RootedHybridTHC: 25.37%Special Offer dr-flower-mylar-royal-cake-ph \N \N \N \N 25.37 \N Deeply Rooted \N https://images.dutchie.com/c87e2f9d86b17ca67b31d0e275d77f6d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-flower-mylar-royal-cake-ph t f \N 2025-11-18 03:52:35.863119 2025-11-18 04:19:24.174594 2025-11-18 03:52:35.863119 2025-11-18 05:06:22.713387 112 Royal Cake (PH)Deeply Rooted \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.174594+00 \N +2128 \N \N \N DR Live Hash Rosin | Fire and Rain (ET)Deeply RootedHybridTHC: 72.52%CBD: 0.13% dr-live-hash-rosin-fire-and-rain-et \N \N \N \N 72.52 0.13 Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-fire-and-rain-et t f \N 2025-11-18 03:52:35.888927 2025-11-18 04:19:24.188806 2025-11-18 03:52:35.888927 2025-11-18 05:06:44.290349 112 Fire and Rain (ET)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.188806+00 \N +3368 1 59 deeply-rooted-az-edibles-1764475244783-48 High CBN | 600mg CBN Hemp TinctureDrip Oils high-cbn-600mg-cbn-hemp-tincturedrip-oils-1764475244849-5yii5c \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-cbn-600mg-cbn-hemp-tincture f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 600mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3566 1 56 deeply-rooted-az-pre-rolls-1764475447223-46 Jeeter Quad Infused Pre-Rolls | Fire OGJeeterTHC: 39.39% jeeter-quad-infused-pre-rolls-fire-ogjeeterthc-39-39-1764475447294-2vtku6 \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/jeeter-quad-infused-pre-rolls-fire-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 2025-11-30 04:04:07.224235 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:04:07.224235+00 \N +2130 \N \N \N DR Live Hash Rosin | Jelly Cake (ENVY)Deeply RootedIndicaTHC: 78.11% dr-live-hash-rosin-jelly-cake-envy \N \N \N \N 78.11 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-jelly-cake-envy t f \N 2025-11-18 03:52:35.896258 2025-11-18 04:19:24.193266 2025-11-18 03:52:35.896258 2025-11-18 05:06:52.425908 112 Jelly Cake (ENVY)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.193266+00 \N +2133 \N \N \N DR Live Hash Rosin | P Di Sole (ET)Deeply RootedSativaTHC: 80.06%CBD: 0.16% dr-live-hash-rosin-p-di-sole-et \N \N \N \N 80.06 0.16 Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-p-di-sole-et t f \N 2025-11-18 03:52:35.908145 2025-11-18 04:19:24.198931 2025-11-18 03:52:35.908145 2025-11-18 05:07:01.560679 112 P Di Sole (ET)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.198931+00 \N +2135 \N \N \N DR Live Hash Rosin | Rainbow PP (ET)Deeply RootedSativa-HybridTHC: 71.92%CBD: 0.14% dr-live-hash-rosin-rainbow-pp-et \N \N \N \N 71.92 0.14 Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-rainbow-pp-et t f \N 2025-11-18 03:52:35.915677 2025-11-18 04:19:24.202905 2025-11-18 03:52:35.915677 2025-11-18 05:07:08.855062 112 Rainbow PP (ET)Deeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.202905+00 \N +2138 \N \N \N DR Live Hash Rosin | Winter SunsetDeeply RootedSativa-HybridTHC: 69.69%CBD: 0.14% dr-live-hash-rosin-winter-sunset \N \N \N \N 69.69 0.14 Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-hash-rosin-winter-sunset t f \N 2025-11-18 03:52:35.927196 2025-11-18 04:19:24.209695 2025-11-18 03:52:35.927196 2025-11-18 05:07:22.063176 112 Winter SunsetDeeply Rooted \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.209695+00 \N +2140 \N \N \N DR Live Resin Batter | Hash BurgerDeeply RootedIndica-HybridTHC: 65.55% dr-live-resin-batter-hash-burger \N \N \N \N 65.55 \N Deeply Rooted \N https://images.dutchie.com/concentrates-stock-liveresin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-batter-hash-burger t f \N 2025-11-18 03:52:35.934358 2025-11-18 04:19:24.213801 2025-11-18 03:52:35.934358 2025-11-18 05:07:32.766148 112 Hash BurgerDeeply Rooted \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.213801+00 \N +2143 \N \N \N DR Live Resin | F-Berries (Fokai)Deeply RootedIndica-HybridTHC: 72.68% dr-live-resin-f-berries-fokai \N \N \N \N 72.68 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-f-berries-fokai t f \N 2025-11-18 03:52:35.945588 2025-11-18 04:19:24.220309 2025-11-18 03:52:35.945588 2025-11-18 05:07:42.214538 112 F-Berries (Fokai)Deeply Rooted \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.220309+00 \N +2145 \N \N \N DR Live Resin | Lemon Cherry Kush (Fokai)Deeply RootedIndica-HybridTHC: 58.58%Special Offer dr-live-resin-lemon-cherry-kush-fokai \N \N \N \N 58.58 \N Deeply Rooted \N https://images.dutchie.com/4ee3c9deac4050e3f4f55f8a68b762d5?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-live-resin-lemon-cherry-kush-fokai t f \N 2025-11-18 03:52:35.952844 2025-11-18 04:19:24.224413 2025-11-18 03:52:35.952844 2025-11-18 05:07:48.471515 112 Lemon Cherry Kush (Fokai)Deeply Rooted \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:24.224413+00 \N +2149 \N \N \N Dermafreeze | Dermaheat | 600mg TopicalDermafreezeTHC: 103 mgCBD: 499 mg dermafreeze-dermaheat-600mg-topical \N \N \N \N \N \N Dermafreeze \N https://images.dutchie.com/64134efcdb0ed00ffccaf738fa4f1387?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-dermaheat-600mg-topical t f \N 2025-11-18 03:52:38.034443 2025-11-18 04:19:27.567539 2025-11-18 03:52:38.034443 2025-11-18 05:08:02.692479 112 600mg TopicalDermafreeze \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:27.567539+00 \N +2148 \N \N \N Dermafreeze | Dermaheat | 600mg Roll-On TopicalDermafreezeTHC: 93.69 mgCBD: 461.42 mg dermafreeze-dermaheat-600mg-roll-on-topical \N \N \N \N \N \N Dermafreeze \N https://images.dutchie.com/64134efcdb0ed00ffccaf738fa4f1387?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dermafreeze-dermaheat-600mg-roll-on-topical t f \N 2025-11-18 03:52:38.031472 2025-11-18 04:19:27.564691 2025-11-18 03:52:38.031472 2025-11-18 05:07:59.488428 112 600mg Roll-On TopicalDermafreeze \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:27.564691+00 \N +2179 \N \N \N Dr. Zodiak Distillate Moonrock Astro Pod | Lynwood LemonadeDr. ZodiakTHC: 92.32%CBD: 3.17%Special Offer dr-zodiak-distillate-moonrock-astro-pod-lynwood-lemonade \N \N \N \N 92.32 3.17 Dr. Zodiak \N https://images.dutchie.com/1b22ef900e4dfd3957cfa5c92745279b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-distillate-moonrock-astro-pod-lynwood-lemonade t f \N 2025-11-18 03:53:09.244455 2025-11-18 04:19:59.928038 2025-11-18 03:53:09.244455 2025-11-18 05:09:47.242568 112 Lynwood LemonadeDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.928038+00 \N +2487 \N \N \N Space Rocks Space Dust | ZSpace RocksIndica-HybridTHC: 87.07% space-rocks-space-dust-z \N \N \N \N 87.07 \N Space Rocks \N https://images.dutchie.com/dfab07981f495b2b792d7cd52342b6dd?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-space-dust-z t f \N 2025-11-18 03:58:41.859083 2025-11-18 04:26:16.010593 2025-11-18 03:58:41.859083 2025-11-18 05:27:43.204141 112 ZSpace Rocks \N \N \N {} {} {} 25.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.010593+00 \N +2182 \N \N \N Dr. Zodiak Hash Hole | Blue Sherb x Rainbow PPDr. ZodiakTHC: 33.1% dr-zodiak-hash-hole-blue-sherb-x-rainbow-pp \N \N \N \N 33.10 \N Dr. Zodiak \N https://images.dutchie.com/flower-stock-2-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-hash-hole-blue-sherb-x-rainbow-pp t f \N 2025-11-18 03:53:09.250813 2025-11-18 04:19:59.936019 2025-11-18 03:53:09.250813 2025-11-18 05:09:56.562168 112 Blue Sherb x Rainbow PPDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.936019+00 \N +2185 \N \N \N Dr. Zodiak Infused Flower Snowballz | Lion HeartDr. ZodiakTHC: 56.51%Special Offer dr-zodiak-infused-flower-snowballz-lion-heart \N \N \N \N 56.51 \N Dr. Zodiak \N https://images.dutchie.com/flower-stock-10-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/dr-zodiak-infused-flower-snowballz-lion-heart t f \N 2025-11-18 03:53:09.257033 2025-11-18 04:19:59.943077 2025-11-18 03:53:09.257033 2025-11-18 05:10:05.914026 112 Lion HeartDr. Zodiak \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:19:59.943077+00 \N +2193 \N \N \N Distillate AIO | Kiwi BurstDrip OilsIndica-HybridTHC: 88.76%CBD: 1.5%Special Offer distillate-aio-kiwi-burst \N \N \N \N 88.76 1.50 Drip Oils \N https://images.dutchie.com/f25cef0e48df6c59797b26ee9dfc9f06?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-kiwi-burst t f \N 2025-11-18 03:53:13.803779 2025-11-18 04:20:19.258125 2025-11-18 03:53:13.803779 2025-11-18 05:10:35.382568 112 Kiwi BurstDrip Oils \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.258125+00 \N +2195 \N \N \N Distillate AIO | Pink GuavaDrip OilsHybridTHC: 92.21%CBD: 0.24% distillate-aio-pink-guava \N \N \N \N 92.21 0.24 Drip Oils \N https://images.dutchie.com/06575e0da9a61dae2e7f50198c71e2d9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/distillate-aio-pink-guava t f \N 2025-11-18 03:53:13.810286 2025-11-18 04:20:19.262771 2025-11-18 03:53:13.810286 2025-11-18 05:10:41.428478 112 Pink GuavaDrip Oils \N \N \N {} {} {} 55.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.262771+00 \N +2197 \N \N \N Drip 1:1 THC/CBD Full Spectrum | RSODrip OilsTHCCBD: 57.53% drip-1-1-thc-cbd-full-spectrum-rso-62891 \N \N \N \N \N 57.53 Drip Oils \N https://images.dutchie.com/b35f20658f20e879d6d87aeb9c09ab78?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-1-1-thc-cbd-full-spectrum-rso-62891 t f \N 2025-11-18 03:53:13.815098 2025-11-18 04:20:19.267673 2025-11-18 03:53:13.815098 2025-11-18 05:10:49.460856 112 RSODrip Oils \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.267673+00 \N +2198 \N \N \N Drip 1:1 THC/CBD Salve 1ozDrip OilsTHCTHC: 1.82%CBD: 1.72% drip-1-1-thc-cbd-salve-1oz \N \N \N \N 1.82 1.72 Drip Oils \N https://images.dutchie.com/add0ab4dd77e0f595327f8f951216a1f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/drip-1-1-thc-cbd-salve-1oz t f \N 2025-11-18 03:53:13.817214 2025-11-18 04:20:19.269637 2025-11-18 03:53:13.817214 2025-11-18 05:10:52.745951 112 \N \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.269637+00 \N +2200 \N \N \N Live Rosin Batter C.C. | Cherry PalomaDrip OilsHybridTHC: 72.95%CBD: 0.14% live-rosin-batter-c-c-cherry-paloma \N \N \N \N 72.95 0.14 Drip Oils \N https://images.dutchie.com/c9e883ec023a1339adc48a75813a036d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/live-rosin-batter-c-c-cherry-paloma t f \N 2025-11-18 03:53:13.821737 2025-11-18 04:20:19.273996 2025-11-18 03:53:13.821737 2025-11-18 05:10:58.820328 112 Cherry PalomaDrip Oils \N \N \N {} {} {} 60.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.273996+00 \N +2202 \N \N \N Micro Drops | 2:1 200/100mg CBN/THC Night Time TinctureDrip OilsTHCTHC: 0.36% micro-drops-2-1-200-100mg-cbn-thc-night-time-tincture \N \N \N \N 0.36 \N Drip Oils \N https://images.dutchie.com/c9965893754530fd3b1a3e4e6abc8f67?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/micro-drops-2-1-200-100mg-cbn-thc-night-time-tincture t f \N 2025-11-18 03:53:13.825518 2025-11-18 04:20:19.27814 2025-11-18 03:53:13.825518 2025-11-18 05:11:04.782068 112 2:1 200/100mg CBN/ \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:19.27814+00 \N +2208 \N \N \N Easy Tiger Live Rosin AIO | Super Buff CherryEasy TigerHybridTHC: 76.95%CBD: 0.16% easy-tiger-live-rosin-aio-super-buff-cherry \N \N \N \N 76.95 0.16 Easy Tiger \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/easy-tiger-live-rosin-aio-super-buff-cherry t f \N 2025-11-18 03:53:20.438239 2025-11-18 04:20:27.911728 2025-11-18 03:53:20.438239 2025-11-18 05:11:25.55313 112 Super Buff CherryEasy Tiger \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:20:27.911728+00 \N +2271 \N \N \N Halo Cannabis Flower Mylar | L.A BakerHalo CannabisIndica-HybridTHC: 28.4% halo-cannabis-flower-mylar-l-a-baker \N \N \N \N 28.40 \N Halo Cannabis \N https://images.dutchie.com/5eadde1d53a4ac2fd12ae6e0d0e524e8?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-cannabis-flower-mylar-l-a-baker t f \N 2025-11-18 03:54:23.226811 2025-11-18 04:21:39.174112 2025-11-18 03:54:23.226811 2025-11-18 05:15:15.168979 112 L.A BakerHalo Cannabis \N \N \N {} {} {} 40.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:39.174112+00 \N +2279 \N \N \N Halo | Chronic Health | 350mg Pain Relief OintmentHalo InfusionsTHC: 0.59% halo-chronic-health-350mg-pain-relief-ointment \N \N \N \N 0.59 \N Halo Infusions \N https://images.dutchie.com/topicals-stock-lotion-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-350mg-pain-relief-ointment t f \N 2025-11-18 03:54:25.298996 2025-11-18 04:21:46.844364 2025-11-18 03:54:25.298996 2025-11-18 05:15:42.046478 112 350mg Pain Relief OintmentHalo Infusions \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.844364+00 \N +2281 \N \N \N Halo | Chronic Health | 50mg 1:1 Pain Relief Roll-OnHalo InfusionsTHC: 0.17%CBD: 0.16% halo-chronic-health-50mg-1-1-pain-relief-roll-on \N \N \N \N 0.17 0.16 Halo Infusions \N https://images.dutchie.com/topicals-stock-lotion-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-50mg-1-1-pain-relief-roll-on t f \N 2025-11-18 03:54:25.303091 2025-11-18 04:21:46.849575 2025-11-18 03:54:25.303091 2025-11-18 05:15:48.086344 112 50mg 1:1 Pain Relief Roll-OnHalo Infusions \N \N \N {} {} {} 19.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.849575+00 \N +2393 \N \N \N Mr. Honey Budder | Cherry PieMr. HoneyIndica-HybridTHC: 78.81%Special Offer mr-honey-budder-cherry-pie \N \N \N \N 78.81 \N Mr. Honey \N https://images.dutchie.com/dce6707bb97afa925f0678f27e40f63c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-budder-cherry-pie t f \N 2025-11-18 03:56:43.190403 2025-11-18 04:24:04.713464 2025-11-18 03:56:43.190403 2025-11-18 05:22:29.855895 112 Cherry PieMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.713464+00 \N +2282 \N \N \N Halo | Chronic Health | 90mg Pain Relief Ointment 0.5ozHalo InfusionsTHC: 0.59% halo-chronic-health-90mg-pain-relief-ointment-0-5oz \N \N \N \N 0.59 \N Halo Infusions \N https://images.dutchie.com/4cadf422f3faf851dd05bdf1a9884808?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-90mg-pain-relief-ointment-0-5oz t f \N 2025-11-18 03:54:25.304909 2025-11-18 04:21:46.852309 2025-11-18 03:54:25.304909 2025-11-18 05:15:51.208746 112 90mg Pain Relief Ointment 0.5ozHalo Infusions \N \N \N {} {} {} 9.50 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.852309+00 \N +2273 \N \N \N Halo | Canna Confections | 100mg Vanilla Caramels | IndicaHalo InfusionsIndicaTHC: 0.07% halo-canna-confections-100mg-vanilla-caramels-indica \N \N \N \N 0.07 \N Halo Infusions \N https://images.dutchie.com/ee63fa78aaf6d3eb0fe987c53ee42ea9?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-canna-confections-100mg-vanilla-caramels-indica t f \N 2025-11-18 03:54:25.28483 2025-11-18 04:21:46.825614 2025-11-18 03:54:25.28483 2025-11-18 05:15:21.364291 112 \N \N \N {} {} {} 16.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.825614+00 \N +2276 \N \N \N Halo | Chronic Health | 100mg Pain Relief LotionHalo InfusionsTHC: 0.2% halo-chronic-health-100mg-pain-relief-lotion \N \N \N \N 0.20 \N Halo Infusions \N https://images.dutchie.com/ea4b7f40f8c537b352bd7559eec4bf75?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-100mg-pain-relief-lotion t f \N 2025-11-18 03:54:25.29187 2025-11-18 04:21:46.836462 2025-11-18 03:54:25.29187 2025-11-18 05:15:32.814321 112 100mg Pain Relief LotionHalo Infusions \N \N \N {} {} {} 14.00 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.836462+00 \N +2280 \N \N \N Halo | Chronic Health | 45/45mg 1:1 THC:CBD Pain Relief Ointment 0.5ozHalo InfusionsTHCTHC: 0.31%CBD: 0.31% halo-chronic-health-45-45mg-1-1-thc-cbd-pain-relief-ointment-0-5oz \N \N \N \N 0.31 0.31 Halo Infusions \N https://images.dutchie.com/topicals-stock-lotion-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/halo-chronic-health-45-45mg-1-1-thc-cbd-pain-relief-ointment-0-5oz t f \N 2025-11-18 03:54:25.30096 2025-11-18 04:21:46.8469 2025-11-18 03:54:25.30096 2025-11-18 05:15:45.051899 112 45/45mg 1:1 \N \N \N {} {} {} 9.50 \N \N \N \N \N in_stock \N 2025-11-18 04:21:46.8469+00 \N +2284 \N \N \N High Rollin Cannabis Flower Jar | Peach PieHigh Rollin CannabisIndica-HybridTHC: 22.64%CBD: 0.03% high-rollin-cannabis-flower-jar-peach-pie \N \N \N \N 22.64 0.03 High Rollin Cannabis \N https://images.dutchie.com/flower-stock-10-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-rollin-cannabis-flower-jar-peach-pie t f \N 2025-11-18 03:54:37.442939 2025-11-18 04:22:06.170438 2025-11-18 03:54:37.442939 2025-11-18 05:15:59.325165 112 Peach PieHigh Rollin Cannabis \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:06.170438+00 \N +2285 \N \N \N High West Farms Diamond Infused Pre-Rolls | Citrus KushHigh West FarmsIndica-HybridTHC: 41.72% high-west-farms-diamond-infused-pre-rolls-citrus-kush \N \N \N \N 41.72 \N High West Farms \N https://images.dutchie.com/b4c5f0554d1ca9d24a09763bf4f68030?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/high-west-farms-diamond-infused-pre-rolls-citrus-kush t f \N 2025-11-18 03:54:56.115602 2025-11-18 04:22:15.019514 2025-11-18 03:54:56.115602 2025-11-18 05:16:02.307894 112 Citrus KushHigh West Farms \N \N \N {} {} {} 12.00 \N \N \N \N \N in_stock \N 2025-11-18 04:22:15.019514+00 \N +2397 \N \N \N Mr. Honey Shatter | Banger SorbetMr. HoneyHybridTHC: 72.83%Special Offer mr-honey-shatter-banger-sorbet \N \N \N \N 72.83 \N Mr. Honey \N https://images.dutchie.com/fb9678e4efdf02c8928c9e47a169605c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-shatter-banger-sorbet t f \N 2025-11-18 03:56:43.199735 2025-11-18 04:24:04.722553 2025-11-18 03:56:43.199735 2025-11-18 05:22:42.181805 112 Banger SorbetMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.722553+00 \N +2348 \N \N \N Lost Dutchmen Flower Mylar | Scoops of ChemLost DutchmenIndicaTHC: 18.98%CBD: 0.04%Special Offer lost-dutchmen-flower-mylar-scoops-of-chem \N \N \N \N 18.98 0.04 Lost Dutchmen \N https://images.dutchie.com/54acccf5f2b4e078ede6839463b74714?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lost-dutchmen-flower-mylar-scoops-of-chem t f \N 2025-11-18 03:55:43.171159 2025-11-18 04:23:17.510351 2025-11-18 03:55:43.171159 2025-11-18 05:20:00.032468 112 Scoops of ChemLost Dutchmen \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:17.510351+00 \N +2351 \N \N \N Lunch Box Hash Rosin Caviar Pre Roll | Off White StrawberryLunch BoxHybridTHC: 71.55%CBD: 0.14%Special Offer lunch-box-hash-rosin-caviar-pre-roll-off-white-strawberry \N \N \N \N 71.55 0.14 Lunch Box \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-hash-rosin-caviar-pre-roll-off-white-strawberry t f \N 2025-11-18 03:55:48.832743 2025-11-18 04:23:26.312954 2025-11-18 03:55:48.832743 2025-11-18 05:19:55.205528 112 Off White StrawberryLunch Box \N \N \N {} {} {} 65.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:26.312954+00 \N +2349 \N \N \N Lunch Box Hash Rosin Caviar Pre Roll | Banana CreamLunch BoxSativa-HybridTHC: 75.2%CBD: 0.13% lunch-box-hash-rosin-caviar-pre-roll-banana-cream-6057 \N \N \N \N 75.20 0.13 Lunch Box \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-hash-rosin-caviar-pre-roll-banana-cream-6057 t f \N 2025-11-18 03:55:48.822791 2025-11-18 04:23:26.296716 2025-11-18 03:55:48.822791 2025-11-18 05:19:49.047023 112 Banana CreamLunch Box \N \N \N {} {} {} 65.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:26.296716+00 \N +2352 \N \N \N Lunch Box Live Hash Rosin AIO | Banana SquirtLunch BoxSativa-HybridTHC: 73.58%CBD: 0.12%Special Offer lunch-box-live-hash-rosin-aio-banana-squirt \N \N \N \N 73.58 0.12 Lunch Box \N https://images.dutchie.com/concentrates-stock-rosin-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/lunch-box-live-hash-rosin-aio-banana-squirt t f \N 2025-11-18 03:55:48.835294 2025-11-18 04:23:26.315978 2025-11-18 03:55:48.835294 2025-11-18 05:20:03.001863 112 Banana SquirtLunch Box \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:23:26.315978+00 \N +2390 \N \N \N Mr. Honey Budder | BCC x JealousyMr. HoneyHybridTHC: 78.72%CBD: 0.23%Special Offer mr-honey-budder-bcc-x-jealousy \N \N \N \N 78.72 0.23 Mr. Honey \N https://images.dutchie.com/dce6707bb97afa925f0678f27e40f63c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-budder-bcc-x-jealousy t f \N 2025-11-18 03:56:43.177353 2025-11-18 04:24:04.7017 2025-11-18 03:56:43.177353 2025-11-18 05:22:10.476737 112 BCC x JealousyMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.7017+00 \N +2392 \N \N \N Mr. Honey Budder | Blue DreamMr. HoneySativa-HybridTHC: 71.11%Special Offer mr-honey-budder-blue-dream \N \N \N \N 71.11 \N Mr. Honey \N https://images.dutchie.com/dce6707bb97afa925f0678f27e40f63c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-budder-blue-dream t f \N 2025-11-18 03:56:43.188018 2025-11-18 04:24:04.711167 2025-11-18 03:56:43.188018 2025-11-18 05:22:26.286553 112 Blue DreamMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.711167+00 \N +2395 \N \N \N Mr. Honey Cured Sugar | Ricky SpanishMr. HoneySativa-HybridTHC: 83.01%Special Offer mr-honey-cured-sugar-ricky-spanish \N \N \N \N 83.01 \N Mr. Honey \N https://images.dutchie.com/dce6707bb97afa925f0678f27e40f63c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-cured-sugar-ricky-spanish t f \N 2025-11-18 03:56:43.195597 2025-11-18 04:24:04.718316 2025-11-18 03:56:43.195597 2025-11-18 05:22:36.204774 112 Ricky SpanishMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.718316+00 \N +2398 \N \N \N Mr. Honey Shatter | MacMr. HoneyIndica-HybridTHC: 79.9%Special Offer mr-honey-shatter-mac \N \N \N \N 79.90 \N Mr. Honey \N https://images.dutchie.com/fb9678e4efdf02c8928c9e47a169605c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-shatter-mac t f \N 2025-11-18 03:56:43.202046 2025-11-18 04:24:04.724619 2025-11-18 03:56:43.202046 2025-11-18 05:22:45.197815 112 MacMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.724619+00 \N +2399 \N \N \N Mr. Honey Shatter | MimosaMr. HoneySativaTHC: 85.07%Special Offer mr-honey-shatter-mimosa \N \N \N \N 85.07 \N Mr. Honey \N https://images.dutchie.com/fb9678e4efdf02c8928c9e47a169605c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-shatter-mimosa t f \N 2025-11-18 03:56:43.204938 2025-11-18 04:24:04.726347 2025-11-18 03:56:43.204938 2025-11-18 05:22:48.333195 112 MimosaMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.726347+00 \N +2401 \N \N \N Mr. Honey Shatter | Yodi SodaMr. HoneyIndicaTHC: 83.08%Special Offer mr-honey-shatter-yodi-soda \N \N \N \N 83.08 \N Mr. Honey \N https://images.dutchie.com/fb9678e4efdf02c8928c9e47a169605c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-shatter-yodi-soda t f \N 2025-11-18 03:56:43.209905 2025-11-18 04:24:04.729931 2025-11-18 03:56:43.209905 2025-11-18 05:22:56.491791 112 Yodi SodaMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.729931+00 \N +2402 \N \N \N Mr. Honey Shatter | ZedbandMr. HoneyHybridTHC: 78.51%Special Offer mr-honey-shatter-zedband \N \N \N \N 78.51 \N Mr. Honey \N https://images.dutchie.com/fb9678e4efdf02c8928c9e47a169605c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-shatter-zedband t f \N 2025-11-18 03:56:43.211831 2025-11-18 04:24:04.731693 2025-11-18 03:56:43.211831 2025-11-18 05:22:59.466371 112 ZedbandMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.731693+00 \N +2394 \N \N \N Mr. Honey Budder | Ricky SpanishMr. HoneySativa-HybridTHC: 82.27%CBD: 0.12%Special Offer mr-honey-budder-ricky-spanish \N \N \N \N 82.27 0.12 Mr. Honey \N https://images.dutchie.com/dce6707bb97afa925f0678f27e40f63c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-budder-ricky-spanish t f \N 2025-11-18 03:56:43.192942 2025-11-18 04:24:04.715764 2025-11-18 03:56:43.192942 2025-11-18 05:22:33.24323 112 Ricky SpanishMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.715764+00 \N +2396 \N \N \N Mr. Honey Live Resin Budder | MCCMr. HoneyHybridTHC: 72.91%CBD: 0.18% mr-honey-live-resin-budder-mcc \N \N \N \N 72.91 0.18 Mr. Honey \N https://images.dutchie.com/a25beaa42b45e2c65ad9f131db12b71a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-live-resin-budder-mcc t f \N 2025-11-18 03:56:43.19763 2025-11-18 04:24:04.720141 2025-11-18 03:56:43.19763 2025-11-18 05:22:39.214029 112 MCCMr. Honey \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.720141+00 \N +2400 \N \N \N Mr. Honey Shatter | Retro RootzMr. HoneyHybridTHC: 70.56%Special Offer mr-honey-shatter-retro-rootz \N \N \N \N 70.56 \N Mr. Honey \N https://images.dutchie.com/fb9678e4efdf02c8928c9e47a169605c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/mr-honey-shatter-retro-rootz t f \N 2025-11-18 03:56:43.207725 2025-11-18 04:24:04.728188 2025-11-18 03:56:43.207725 2025-11-18 05:22:53.471168 112 Retro RootzMr. Honey \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:04.728188+00 \N +2406 \N \N \N Ogeez | 100mg Gummy | Sugar Free Tropical IndicaOGEEZIndicaTHC: 0.19% ogeez-100mg-gummy-sugar-free-tropical-indica \N \N \N \N 0.19 \N OGEEZ \N https://images.dutchie.com/a5e8f1f3db3e5ad4121659f2e6d2d36f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-gummy-sugar-free-tropical-indica t f \N 2025-11-18 03:56:47.817266 2025-11-18 04:24:12.878031 2025-11-18 03:56:47.817266 2025-11-18 05:23:11.754433 112 Sugar Free Tropical \N \N \N {} {} {} 15.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:12.878031+00 \N +2408 \N \N \N Ogeez | 100mg Rosin Gummy | Vegan Sweet LemonOGEEZTHC: 0.19% ogeez-100mg-rosin-gummy-vegan-sweet-lemon \N \N \N \N 0.19 \N OGEEZ \N https://images.dutchie.com/15054e6d330060499144dfbbdb68df41?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/ogeez-100mg-rosin-gummy-vegan-sweet-lemon t f \N 2025-11-18 03:56:47.828843 2025-11-18 04:24:12.886403 2025-11-18 03:56:47.828843 2025-11-18 05:23:25.546182 112 Vegan Sweet LemonOGEEZ \N \N \N {} {} {} 24.00 \N \N \N \N \N in_stock \N 2025-11-18 04:24:12.886403+00 \N +2453 \N \N \N Session Live Resin AIO | Cherry PunchSessionHybridTHC: 74.25%CBD: 0.1%Special Offer session-live-resin-aio-cherry-punch \N \N \N \N 74.25 0.10 Session \N https://images.dutchie.com/2103e7652bba4a1491f9bad87be2fb3c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/session-live-resin-aio-cherry-punch t f \N 2025-11-18 03:57:57.457185 2025-11-18 04:25:22.226011 2025-11-18 03:57:57.457185 2025-11-18 05:25:34.86038 112 Cherry PunchSession \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:22.226011+00 \N +2454 \N \N \N Session Live Resin AIO | FatsoSessionIndicaTHC: 76.04%CBD: 0.1%Special Offer session-live-resin-aio-fatso \N \N \N \N 76.04 0.10 Session \N https://images.dutchie.com/2103e7652bba4a1491f9bad87be2fb3c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/session-live-resin-aio-fatso t f \N 2025-11-18 03:57:57.460355 2025-11-18 04:25:22.228989 2025-11-18 03:57:57.460355 2025-11-18 05:25:43.334871 112 FatsoSession \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:22.228989+00 \N +2456 \N \N \N Session Live Resin AIO | Pineapple DonutSessionSativa-HybridTHC: 79.16%CBD: 0.15%Special Offer session-live-resin-aio-pineapple-donut \N \N \N \N 79.16 0.15 Session \N https://images.dutchie.com/2103e7652bba4a1491f9bad87be2fb3c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/session-live-resin-aio-pineapple-donut t f \N 2025-11-18 03:57:57.466388 2025-11-18 04:25:22.234648 2025-11-18 03:57:57.466388 2025-11-18 05:25:56.490646 112 Pineapple DonutSession \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:25:22.234648+00 \N +2489 \N \N \N Space Rocks | Space Rocketz Infused Pre-Roll | Grape CakeSpace RocksIndica-HybridTHC: 54.47% space-rocks-space-rocketz-infused-pre-roll-grape-cake \N \N \N \N 54.47 \N Space Rocks \N https://images.dutchie.com/3cd11b47077dc9d0b2e3ddd22ad1157c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-space-rocketz-infused-pre-roll-grape-cake t f \N 2025-11-18 03:58:41.863738 2025-11-18 04:26:16.014122 2025-11-18 03:58:41.863738 2025-11-18 05:27:49.135879 112 Grape CakeSpace Rocks \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.014122+00 \N +3647 1 3 deeply-rooted-az-specials-1764475522731-27 Accessories | Stiiizy Pro Battery | BlackSTIIIZY accessories-stiiizy-pro-battery-blackstiiizy-1764475522775-3l3kti \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/accessories-stiiizy-pro-battery-black f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N \N \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +2485 \N \N \N Space Rocks Infused Flower | Glitter BombSpace RocksIndica-HybridTHC: 42.72% space-rocks-infused-flower-glitter-bomb \N \N \N \N 42.72 \N Space Rocks \N https://images.dutchie.com/flower-stock-9-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-infused-flower-glitter-bomb t f \N 2025-11-18 03:58:41.854246 2025-11-18 04:26:16.006673 2025-11-18 03:58:41.854246 2025-11-18 05:27:33.731492 112 Glitter BombSpace Rocks \N \N \N {} {} {} 60.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.006673+00 \N +2486 \N \N \N Space Rocks Infused Flower | Yellow ZlushiSpace RocksIndica-HybridTHC: 46.21% space-rocks-infused-flower-yellow-zlushi \N \N \N \N 46.21 \N Space Rocks \N https://images.dutchie.com/143dbd241e177058c61ca37c7c87cef4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-infused-flower-yellow-zlushi t f \N 2025-11-18 03:58:41.856891 2025-11-18 04:26:16.008572 2025-11-18 03:58:41.856891 2025-11-18 05:27:35.42785 112 Yellow ZlushiSpace Rocks \N \N \N {} {} {} 60.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.008572+00 \N +2490 \N \N \N Space Rocks | Space Rocketz Infused Pre-Roll | LuciliciousSpace RocksIndica-HybridTHC: 44.1% space-rocks-space-rocketz-infused-pre-roll-lucilicious \N \N \N \N 44.10 \N Space Rocks \N https://images.dutchie.com/3cd11b47077dc9d0b2e3ddd22ad1157c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/space-rocks-space-rocketz-infused-pre-roll-lucilicious t f \N 2025-11-18 03:58:41.866122 2025-11-18 04:26:16.016021 2025-11-18 03:58:41.866122 2025-11-18 05:27:54.163048 112 LuciliciousSpace Rocks \N \N \N {} {} {} 18.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:16.016021+00 \N +2499 \N \N \N Sticky Saguaro Sticky Disty Cartridge | The Blue OneSticky SaguaroTHC: 91.17% sticky-saguaro-sticky-disty-cartridge-the-blue-one \N \N \N \N 91.17 \N Sticky Saguaro \N https://images.dutchie.com/8409ca80ac9f401cb5b7952e13807570?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-sticky-disty-cartridge-the-blue-one t f \N 2025-11-18 03:58:47.583946 2025-11-18 04:26:27.116398 2025-11-18 03:58:47.583946 2025-11-18 05:28:25.069903 112 The Blue OneSticky Saguaro \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.116398+00 \N +2500 \N \N \N Sticky Saguaro StickyDisty Cartridge | Purple UrkleSticky SaguaroTHC: 89.25% sticky-saguaro-stickydisty-cartridge-purple-urkle \N \N \N \N 89.25 \N Sticky Saguaro \N https://images.dutchie.com/vaporizer-stock-1-v1.jpg?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-stickydisty-cartridge-purple-urkle t f \N 2025-11-18 03:58:47.586417 2025-11-18 04:26:27.119605 2025-11-18 03:58:47.586417 2025-11-18 05:28:33.43422 112 Purple UrkleSticky Saguaro \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.119605+00 \N +2502 \N \N \N Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | MandarinSticky SaguaroTHC: 107.06 mgCBD: 2.6 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-mandarin \N \N \N \N \N \N Sticky Saguaro \N https://images.dutchie.com/1df5cd3fcfee45ccdfabc533003a9c4d?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-mandarin t f \N 2025-11-18 03:58:47.591472 2025-11-18 04:26:27.125725 2025-11-18 03:58:47.591472 2025-11-18 05:28:39.419884 112 MandarinSticky Saguaro \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.125725+00 \N +2503 \N \N \N Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | MangoSticky SaguaroTHC: 94.27 mgCBD: 2.36 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-mango \N \N \N \N \N \N Sticky Saguaro \N https://images.dutchie.com/f4c7eca4401528d0acdb6a46123ac3d6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-mango t f \N 2025-11-18 03:58:47.59407 2025-11-18 04:26:27.130358 2025-11-18 03:58:47.59407 2025-11-18 05:28:42.44963 112 MangoSticky Saguaro \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.130358+00 \N +2505 \N \N \N Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | BlueberrySticky SaguaroIndica-HybridTHC: 22.75% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-blueberry \N \N \N \N 22.75 \N Sticky Saguaro \N https://images.dutchie.com/4a52de9cd902c6f1383f2768cc7637ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-blueberry t f \N 2025-11-18 03:58:47.599168 2025-11-18 04:26:27.136933 2025-11-18 03:58:47.599168 2025-11-18 05:28:48.600507 112 BlueberrySticky Saguaro \N \N \N {} {} {} 8.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.136933+00 \N +2507 \N \N \N Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | PineappleSticky SaguaroHybridTHC: 25.56% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-pineapple \N \N \N \N 25.56 \N Sticky Saguaro \N https://images.dutchie.com/4a52de9cd902c6f1383f2768cc7637ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-pineapple t f \N 2025-11-18 03:58:47.604194 2025-11-18 04:26:27.143719 2025-11-18 03:58:47.604194 2025-11-18 05:28:54.1316 112 PineappleSticky Saguaro \N \N \N {} {} {} 8.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.143719+00 \N +2508 \N \N \N Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | ShibuiSticky SaguaroSativa-HybridTHC: 20.39% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-shibui \N \N \N \N 20.39 \N Sticky Saguaro \N https://images.dutchie.com/4a52de9cd902c6f1383f2768cc7637ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-shibui t f \N 2025-11-18 03:58:47.607348 2025-11-18 04:26:27.146462 2025-11-18 03:58:47.607348 2025-11-18 05:28:58.256979 112 ShibuiSticky Saguaro \N \N \N {} {} {} 8.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.146462+00 \N +2510 \N \N \N Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | TropkickSticky SaguaroIndica-HybridTHC: 23.44% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-tropkick \N \N \N \N 23.44 \N Sticky Saguaro \N https://images.dutchie.com/4a52de9cd902c6f1383f2768cc7637ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-tropkick t f \N 2025-11-18 03:58:47.613266 2025-11-18 04:26:27.153501 2025-11-18 03:58:47.613266 2025-11-18 05:29:06.47349 112 TropkickSticky Saguaro \N \N \N {} {} {} 8.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.153501+00 \N +2498 \N \N \N Sticky Saguaro Sticky Disty Cartridge | Pure OGSticky SaguaroTHC: 91.05% sticky-saguaro-sticky-disty-cartridge-pure-og \N \N \N \N 91.05 \N Sticky Saguaro \N https://images.dutchie.com/8409ca80ac9f401cb5b7952e13807570?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-sticky-disty-cartridge-pure-og t f \N 2025-11-18 03:58:47.58112 2025-11-18 04:26:27.113373 2025-11-18 03:58:47.58112 2025-11-18 05:28:22.085624 112 Pure OGSticky Saguaro \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.113373+00 \N +2501 \N \N \N Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | CherrySticky SaguaroTHC: 111.27 mgCBD: 2.75 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-cherry \N \N \N \N \N \N Sticky Saguaro \N https://images.dutchie.com/e2958413918295971dd8cb92b7f9dfd2?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-cherry t f \N 2025-11-18 03:58:47.588955 2025-11-18 04:26:27.122287 2025-11-18 03:58:47.588955 2025-11-18 05:28:36.417943 112 CherrySticky Saguaro \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.122287+00 \N +2504 \N \N \N Sticky Saguaro | 100mg Sticky Sweet Gummy | Single | The Blue OneSticky SaguaroTHC: 104.7 mgCBD: 2.35 mg sticky-saguaro-100mg-sticky-sweet-gummy-single-the-blue-one \N \N \N \N \N \N Sticky Saguaro \N https://images.dutchie.com/b20330c45f0e68b6254a3811fa6c958a?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-100mg-sticky-sweet-gummy-single-the-blue-one t f \N 2025-11-18 03:58:47.596529 2025-11-18 04:26:27.13357 2025-11-18 03:58:47.596529 2025-11-18 05:28:45.386657 112 The Blue OneSticky Saguaro \N \N \N {} {} {} 10.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.13357+00 \N +2506 \N \N \N Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | GluejitsuSticky SaguaroIndica-HybridTHC: 22.4% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-gluejitsu \N \N \N \N 22.40 \N Sticky Saguaro \N https://images.dutchie.com/4a52de9cd902c6f1383f2768cc7637ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-gluejitsu t f \N 2025-11-18 03:58:47.601555 2025-11-18 04:26:27.140366 2025-11-18 03:58:47.601555 2025-11-18 05:28:51.743404 112 GluejitsuSticky Saguaro \N \N \N {} {} {} 8.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.140366+00 \N +2509 \N \N \N Sticky Saguaro | 2-Pack x (1g) Lil Stickys Pre-Rolls | Sour MacSticky SaguaroHybridTHC: 15.16% sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-sour-mac \N \N \N \N 15.16 \N Sticky Saguaro \N https://images.dutchie.com/4a52de9cd902c6f1383f2768cc7637ab?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sticky-saguaro-2-pack-x-1g-lil-stickys-pre-rolls-sour-mac t f \N 2025-11-18 03:58:47.610482 2025-11-18 04:26:27.14937 2025-11-18 03:58:47.610482 2025-11-18 05:29:03.491897 112 Sour MacSticky Saguaro \N \N \N {} {} {} 8.00 \N \N \N \N \N in_stock \N 2025-11-18 04:26:27.14937+00 \N +2537 \N \N \N The Strain Source Flower Mylar | SlurricaneThe Strain Source (TSS)IndicaTHC: 18.48%Special Offer the-strain-source-flower-mylar-slurricane \N \N \N \N 18.48 \N The Strain Source (TSS) \N https://images.dutchie.com/f02460a5277d742a5e286a19ecd5f70f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-strain-source-flower-mylar-slurricane t f \N 2025-11-18 03:59:23.237168 2025-11-18 04:27:02.772946 2025-11-18 03:59:23.237168 2025-11-18 05:30:50.938465 112 SlurricaneThe Strain Source (TSS) \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:02.772946+00 \N +2536 \N \N \N The Strain Source Flower Mylar | Lemon Cherry GelatoThe Strain Source (TSS)Indica-HybridTHC: 20.07%Special Offer the-strain-source-flower-mylar-lemon-cherry-gelato \N \N \N Indica 20.07 \N The Strain Source (TSS) \N https://images.dutchie.com/f02460a5277d742a5e286a19ecd5f70f?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/the-strain-source-flower-mylar-lemon-cherry-gelato t f \N 2025-11-18 03:59:23.234385 2025-11-18 04:27:02.76989 2025-11-18 03:59:23.234385 2025-11-18 05:30:47.899531 112 Lemon Cherry GelatoThe Strain Source (TSS) \N \N \N {} {} {} 20.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:02.76989+00 \N +2544 \N \N \N Thunder Bud Pre-Roll | IllemonatiThunder BudIndica-HybridTHC: 20.53%Special Offer thunder-bud-pre-roll-illemonati \N \N \N \N 20.53 \N Thunder Bud \N https://images.dutchie.com/bba9a10f15dfa8990c2066116f652423?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-illemonati t f \N 2025-11-18 03:59:42.472965 2025-11-18 04:27:08.34263 2025-11-18 03:59:42.472965 2025-11-18 05:31:15.715966 112 IllemonatiThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.34263+00 \N +2545 \N \N \N Thunder Bud Pre-Roll | MacaroonsThunder BudIndica-HybridTHC: 24.2%Special Offer thunder-bud-pre-roll-macaroons \N \N \N \N 24.20 \N Thunder Bud \N https://images.dutchie.com/7ccebf8db7dd13f7f2a265847bd15d6b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-macaroons t f \N 2025-11-18 03:59:42.475456 2025-11-18 04:27:08.344514 2025-11-18 03:59:42.475456 2025-11-18 05:31:18.637743 112 MacaroonsThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.344514+00 \N +2547 \N \N \N Thunder Bud Pre-Roll | Pink GorillaThunder BudTHC: 23.87%Special Offer thunder-bud-pre-roll-pink-gorilla \N \N \N \N 23.87 \N Thunder Bud \N https://images.dutchie.com/d92119bbc513fb51ca454fed4103574c?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-pink-gorilla t f \N 2025-11-18 03:59:42.480279 2025-11-18 04:27:08.349735 2025-11-18 03:59:42.480279 2025-11-18 05:31:32.75113 112 Pink GorillaThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.349735+00 \N +2549 \N \N \N Thunder Bud Pre-Roll | The OneThunder BudHybridTHC: 21.78%Special Offer thunder-bud-pre-roll-the-one \N \N \N \N 21.78 \N Thunder Bud \N https://images.dutchie.com/bba9a10f15dfa8990c2066116f652423?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-the-one t f \N 2025-11-18 03:59:42.484922 2025-11-18 04:27:08.355141 2025-11-18 03:59:42.484922 2025-11-18 05:31:35.80085 112 The OneThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.355141+00 \N +2550 \N \N \N Thunder Bud Pre-Roll | Violet MeadowsThunder BudIndica-HybridTHC: 23.19%Special Offer thunder-bud-pre-roll-violet-meadows \N \N \N \N 23.19 \N Thunder Bud \N https://images.dutchie.com/bba9a10f15dfa8990c2066116f652423?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-violet-meadows t f \N 2025-11-18 03:59:42.487419 2025-11-18 04:27:08.357619 2025-11-18 03:59:42.487419 2025-11-18 05:31:38.981378 112 Violet MeadowsThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.357619+00 \N +2540 \N \N \N Thunder Bud Pre-Roll | Delicata CookiesThunder BudIndica-HybridTHC: 29.77%Special Offer thunder-bud-pre-roll-delicata-cookies \N \N \N \N 29.77 \N Thunder Bud \N https://images.dutchie.com/bba9a10f15dfa8990c2066116f652423?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-delicata-cookies t f \N 2025-11-18 03:59:42.463642 2025-11-18 04:27:08.332685 2025-11-18 03:59:42.463642 2025-11-18 05:31:00.041878 112 Delicata CookiesThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.332685+00 \N +3768 1 57 deeply-rooted-az-vaporizers-1764475641905-85 Red PlumDime IndustriesIndicaTHC: 93.03%CBD: 0.18% red-plumdime-industriesindicathc-93-03-cbd-0-18-1764475642007-anius5 \N 37.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/red-plum f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 2g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +2543 \N \N \N Thunder Bud Pre-Roll | HeadbangerThunder BudSativaTHC: 21.72%Special Offer thunder-bud-pre-roll-headbanger \N \N \N \N 21.72 \N Thunder Bud \N https://images.dutchie.com/7ccebf8db7dd13f7f2a265847bd15d6b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-headbanger t f \N 2025-11-18 03:59:42.470805 2025-11-18 04:27:08.34062 2025-11-18 03:59:42.470805 2025-11-18 05:31:12.650444 112 HeadbangerThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.34062+00 \N +2546 \N \N \N Thunder Bud Pre-Roll | Magic BlastThunder BudHybridTHC: 24.93%Special Offer thunder-bud-pre-roll-magic-blast \N \N \N \N 24.93 \N Thunder Bud \N https://images.dutchie.com/7ccebf8db7dd13f7f2a265847bd15d6b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-magic-blast t f \N 2025-11-18 03:59:42.477764 2025-11-18 04:27:08.346646 2025-11-18 03:59:42.477764 2025-11-18 05:31:22.051127 112 Magic BlastThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.346646+00 \N +2548 \N \N \N Thunder Bud Pre-Roll | Singapore SlingThunder BudSativa-HybridTHC: 25.95%CBD: 0.14%Special Offer thunder-bud-pre-roll-singapore-sling \N \N \N \N 25.95 0.14 Thunder Bud \N https://images.dutchie.com/bba9a10f15dfa8990c2066116f652423?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-singapore-sling t f \N 2025-11-18 03:59:42.48267 2025-11-18 04:27:08.352352 2025-11-18 03:59:42.48267 2025-11-18 05:31:28.02315 112 Singapore SlingThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.352352+00 \N +2551 \N \N \N Thunder Bud Pre-Roll | Walkin-N-LAThunder BudHybridTHC: 26.17%Special Offer thunder-bud-pre-roll-walkin-n-la \N \N \N \N 26.17 \N Thunder Bud \N https://images.dutchie.com/7ccebf8db7dd13f7f2a265847bd15d6b?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/thunder-bud-pre-roll-walkin-n-la t f \N 2025-11-18 03:59:42.490162 2025-11-18 04:27:08.360097 2025-11-18 03:59:42.490162 2025-11-18 05:31:41.993298 112 Walkin-N-LAThunder Bud \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:08.360097+00 \N +2594 \N \N \N Varz Pre-Roll | Punch BreathVarzHybridTHC: 20.86%Special Offer varz-pre-roll-punch-breath \N \N \N \N 20.86 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-punch-breath t f \N 2025-11-18 04:00:18.660726 2025-11-18 04:27:44.047957 2025-11-18 04:00:18.660726 2025-11-18 05:34:26.021522 112 Punch BreathVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.047957+00 \N +2596 \N \N \N Varz Pre-Roll | Super Lemon HazeVarzSativa-HybridTHC: 19.55%Special Offer varz-pre-roll-super-lemon-haze \N \N \N \N 19.55 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-super-lemon-haze t f \N 2025-11-18 04:00:18.665425 2025-11-18 04:27:44.052555 2025-11-18 04:00:18.665425 2025-11-18 05:34:33.545767 112 Super Lemon HazeVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.052555+00 \N +2580 \N \N \N Varz Live Resin | Agent RS CandyVarzTHC: 81.59%CBD: 0.12%Special Offer varz-live-resin-agent-rs-candy \N \N \N \N 81.59 0.12 Varz \N https://images.dutchie.com/e1b7e36079d08553d4c339815671e026?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-live-resin-agent-rs-candy t f \N 2025-11-18 04:00:18.627752 2025-11-18 04:27:44.00857 2025-11-18 04:00:18.627752 2025-11-18 05:33:31.294005 112 Agent RS CandyVarz \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.00857+00 \N +2581 \N \N \N Varz Live Resin | Michelin ManVarzTHC: 75.8%CBD: 0.16%Special Offer varz-live-resin-michelin-man \N \N \N \N 75.80 0.16 Varz \N https://images.dutchie.com/e1b7e36079d08553d4c339815671e026?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-live-resin-michelin-man t f \N 2025-11-18 04:00:18.629972 2025-11-18 04:27:44.011793 2025-11-18 04:00:18.629972 2025-11-18 05:33:34.310299 112 Michelin ManVarz \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.011793+00 \N +2583 \N \N \N Varz Live Resin | Pineapple FruzVarzIndica-HybridTHC: 79.54%CBD: 0.12%Special Offer varz-live-resin-pineapple-fruz \N \N \N \N 79.54 0.12 Varz \N https://images.dutchie.com/e1b7e36079d08553d4c339815671e026?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-live-resin-pineapple-fruz t f \N 2025-11-18 04:00:18.634437 2025-11-18 04:27:44.018518 2025-11-18 04:00:18.634437 2025-11-18 05:33:46.7093 112 Pineapple FruzVarz \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.018518+00 \N +2585 \N \N \N Varz Live Resin | Punch Breath OGVarzHybridTHC: 78.58%CBD: 0.15%Special Offer varz-live-resin-punch-breath-og \N \N \N \N 78.58 0.15 Varz \N https://images.dutchie.com/e1b7e36079d08553d4c339815671e026?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-live-resin-punch-breath-og t f \N 2025-11-18 04:00:18.638796 2025-11-18 04:27:44.023433 2025-11-18 04:00:18.638796 2025-11-18 05:33:53.849527 112 Punch Breath OGVarz \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.023433+00 \N +2586 \N \N \N Varz Live Resin | Super BoofVarzHybridTHC: 79.48%CBD: 0.1%Special Offer varz-live-resin-super-boof \N \N \N \N 79.48 0.10 Varz \N https://images.dutchie.com/e1b7e36079d08553d4c339815671e026?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-live-resin-super-boof t f \N 2025-11-18 04:00:18.640959 2025-11-18 04:27:44.026955 2025-11-18 04:00:18.640959 2025-11-18 05:33:56.842363 112 Super BoofVarz \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.026955+00 \N +2588 \N \N \N Varz Pre-Roll | Biscotti ChunksVarzIndica-HybridTHC: 30.15%CBD: 0.04%Special Offer varz-pre-roll-biscotti-chunks \N \N \N \N 30.15 0.04 Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-biscotti-chunks t f \N 2025-11-18 04:00:18.645609 2025-11-18 04:27:44.032388 2025-11-18 04:00:18.645609 2025-11-18 05:34:02.818104 112 Biscotti ChunksVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.032388+00 \N +2579 \N \N \N Varz Flower Jar | Red VelvetVarzIndica-HybridTHC: 21.48%Special Offer varz-flower-jar-red-velvet \N \N \N \N 21.48 \N Varz \N https://images.dutchie.com/e85dd12dd5854a4952a3c686fb8e8e35?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-flower-jar-red-velvet t f \N 2025-11-18 04:00:18.625078 2025-11-18 04:27:44.005529 2025-11-18 04:00:18.625078 2025-11-18 05:33:46.707424 112 Red VelvetVarz \N \N \N {} {} {} 50.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.005529+00 \N +2582 \N \N \N Varz Live Resin | Pezpaya PowerVarzTHC: 81.59%CBD: 0.01%Special Offer varz-live-resin-pezpaya-power \N \N \N \N 81.59 0.01 Varz \N https://images.dutchie.com/e1b7e36079d08553d4c339815671e026?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-live-resin-pezpaya-power t f \N 2025-11-18 04:00:18.632083 2025-11-18 04:27:44.01535 2025-11-18 04:00:18.632083 2025-11-18 05:33:37.257725 112 Pezpaya PowerVarz \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.01535+00 \N +2584 \N \N \N Varz Live Resin | Pineapple Upside Down CakeVarzSativaTHC: 71.7%CBD: 0.13%Special Offer varz-live-resin-pineapple-upside-down-cake \N \N \N \N 71.70 0.13 Varz \N https://images.dutchie.com/e1b7e36079d08553d4c339815671e026?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-live-resin-pineapple-upside-down-cake t f \N 2025-11-18 04:00:18.636704 2025-11-18 04:27:44.021143 2025-11-18 04:00:18.636704 2025-11-18 05:33:49.780635 112 Pineapple Upside Down CakeVarz \N \N \N {} {} {} 30.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.021143+00 \N +2587 \N \N \N Varz Pre-Roll | Agent OrangeVarzHybridTHC: 15.44%Special Offer varz-pre-roll-agent-orange \N \N \N \N 15.44 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-agent-orange t f \N 2025-11-18 04:00:18.643347 2025-11-18 04:27:44.029803 2025-11-18 04:00:18.643347 2025-11-18 05:33:59.843372 112 Agent OrangeVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.029803+00 \N +2589 \N \N \N Varz Pre-Roll | Black IceVarzIndica-HybridTHC: 24.88%Special Offer varz-pre-roll-black-ice \N \N \N \N 24.88 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-black-ice t f \N 2025-11-18 04:00:18.648434 2025-11-18 04:27:44.034883 2025-11-18 04:00:18.648434 2025-11-18 05:34:04.58049 112 Black IceVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.034883+00 \N +2590 \N \N \N Varz Pre-Roll | Candy FumezVarzHybridTHC: 23.73%Special Offer varz-pre-roll-candy-fumez \N \N \N \N 23.73 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-candy-fumez t f \N 2025-11-18 04:00:18.650796 2025-11-18 04:27:44.037478 2025-11-18 04:00:18.650796 2025-11-18 05:34:11.055859 112 Candy FumezVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.037478+00 \N +2592 \N \N \N Varz Pre-Roll | Gorilla DoshaVarzIndicaTHC: 23.25%Special Offer varz-pre-roll-gorilla-dosha \N \N \N \N 23.25 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-gorilla-dosha t f \N 2025-11-18 04:00:18.655281 2025-11-18 04:27:44.043087 2025-11-18 04:00:18.655281 2025-11-18 05:34:20.133194 112 Gorilla DoshaVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.043087+00 \N +2595 \N \N \N Varz Pre-Roll | Red VelvetVarzIndica-HybridTHC: 21.46%Special Offer varz-pre-roll-red-velvet \N \N \N \N 21.46 \N Varz \N https://images.dutchie.com/65c8cb0e0fd41d51c568d12a491547f0?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/varz-pre-roll-red-velvet t f \N 2025-11-18 04:00:18.663124 2025-11-18 04:27:44.05017 2025-11-18 04:00:18.663124 2025-11-18 05:34:29.170177 112 Red VelvetVarz \N \N \N {} {} {} 6.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:44.05017+00 \N +2600 \N \N \N Vortex Flower Jar | First Class GasVortexIndica-HybridTHC: 27.6% vortex-flower-jar-first-class-gas-85166 \N \N \N \N 27.60 \N Vortex \N https://images.dutchie.com/813762fcd4d3e3cf862b66576d70b0f6?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-first-class-gas-85166 t f \N 2025-11-18 04:00:20.218936 2025-11-18 04:27:45.194697 2025-11-18 04:00:20.218936 2025-11-18 05:34:49.663423 112 First Class GasVortex \N \N \N {} {} {} 100.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.194697+00 \N +2602 \N \N \N Vortex Flower Jar | Hash BurgerVortexIndica-HybridTHC: 25.21% vortex-flower-jar-hash-burger-29536 \N \N \N \N 25.21 \N Vortex \N https://images.dutchie.com/6c5eb5820e5d8952de6b8d03ceb1c221?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-hash-burger-29536 t f \N 2025-11-18 04:00:20.223487 2025-11-18 04:27:45.201377 2025-11-18 04:00:20.223487 2025-11-18 05:34:56.009881 112 Hash BurgerVortex \N \N \N {} {} {} 100.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.201377+00 \N +2603 \N \N \N Vortex Flower Jar | Pink RTZVortexHybridTHC: 22.17%Special Offer vortex-flower-jar-pink-rtz-91555 \N \N \N \N 22.17 \N Vortex \N https://images.dutchie.com/73db4837d153fe3ff893e7722878f3c4?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-pink-rtz-91555 t f \N 2025-11-18 04:00:20.225558 2025-11-18 04:27:45.204117 2025-11-18 04:00:20.225558 2025-11-18 05:35:00.805647 112 Pink RTZVortex \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.204117+00 \N +2605 \N \N \N Vortex Flower Jar | The OneVortexHybridTHC: 21.31%Special Offer vortex-flower-jar-the-one \N \N \N \N 21.31 \N Vortex \N https://images.dutchie.com/6c5eb5820e5d8952de6b8d03ceb1c221?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/vortex-flower-jar-the-one t f \N 2025-11-18 04:00:20.230852 2025-11-18 04:27:45.209814 2025-11-18 04:00:20.230852 2025-11-18 05:35:07.404352 112 The OneVortex \N \N \N {} {} {} 45.00 \N \N \N \N \N in_stock \N 2025-11-18 04:27:45.209814+00 \N +2633 \N \N \N Yam Yams THCa Diamond Dust | Snow StromYam YamsIndicaTHC: 86.34% yam-yams-thca-diamond-dust-snow-strom \N \N \N \N 86.34 \N Yam Yams \N https://images.dutchie.com/5f0527723ca6725075cce2d4f6119317?auto=format%2Ccompress&cs=srgb&fit=max&fill=solid&fillColor=%23fff&ixlib=react-9.8.1&h=104&w=104 \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/yam-yams-thca-diamond-dust-snow-strom t f \N 2025-11-18 04:00:54.712297 2025-11-18 04:28:19.674576 2025-11-18 04:00:54.712297 2025-11-18 05:36:54.362437 112 Snow StromYam Yams \N \N \N {} {} {} 35.00 \N \N \N \N \N in_stock \N 2025-11-18 04:28:19.674576+00 \N +3340 1 59 deeply-rooted-az-edibles-1764475244783-20 Chill Pill | 100mg THC 20pk | LIFETIMEChill PillTHCTHC: 3.68%CBD: 3.44% chill-pill-100mg-thc-20pk-lifetimechill-pillthcthc-3-68-cbd-3-44-1764475244817-p4jpaz \N 11.25 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/chill-pill-100mg-thc-20pk-lifetime f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3322 1 59 deeply-rooted-az-edibles-1764475244783-2 1:1 Blackberry MEGA - CBN/THC - Sleepy IndicaGrönTHCTHC: 0.47% 1-1-blackberry-mega-cbn-thc-sleepy-indicagr-nthcthc-0-47-1764475244794-gzvxn6 \N 7.70 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-blackberry-mega-cbn-thc-sleepy-indica-78560 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3323 1 59 deeply-rooted-az-edibles-1764475244783-3 1:1 Dark Chocolate Mini Bar - Sleepy Indica - CBN/THCGrönTHCTHC: 1.35% 1-1-dark-chocolate-mini-bar-sleepy-indica-cbn-thcgr-nthcthc-1-35-1764475244795-ewri9c \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-dark-chocolate-mini-bar-sleepy-indica-cbn-thc-71264 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3324 1 59 deeply-rooted-az-edibles-1764475244783-4 1:1 Dark Chocolate Peanut Butter Pips - CBD/THC - HybridGrönTHCTAC: 200 mgTHC: 0.43%CBD: 0.41% 1-1-dark-chocolate-peanut-butter-pips-cbd-thc-hybridgr-nthctac-200-mgthc-0-43-cbd-0-41-1764475244796-09evko \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-dark-chocolate-peanut-butter-pips-cbd-thc-hybrid-45073 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 2025-11-30 04:00:44.783073 \N 200 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:00:44.783073+00 \N +3620 1 3 deeply-rooted-az-specials-1764475522731-0 100mg MEGA DOSE 1: ummyOGeez!THC: 0.69%CBD: 0.63% 100mg-mega-dose-1-ummyogeez-thc-0-69-cbd-0-63-1764475522736-09fixo \N 5.40 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/100mg-mega-dose-1-ummy f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 100mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3621 1 3 deeply-rooted-az-specials-1764475522731-1 10:1 Tart Cherry - CBN/THC - NightlyGrönTHCTHC: 0.08%CBD: 0.01% 10-1-tart-cherry-cbn-thc-nightlygr-nthcthc-0-08-cbd-0-01-1764475522739-inw13k \N 14.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/10-1-tart-cherry-cbn-thc-nightly-31379 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3622 1 3 deeply-rooted-az-specials-1764475522731-2 1:1 Blackberry MEGA - CBN/THC - Sleepy IndicaGrönTHCTHC: 0.47% 1-1-blackberry-mega-cbn-thc-sleepy-indicagr-nthcthc-0-47-1764475522741-r7uepx \N 7.70 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-blackberry-mega-cbn-thc-sleepy-indica-78560 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3623 1 3 deeply-rooted-az-specials-1764475522731-3 1:1 CBD/CBG Sport ICE Muscle Recovery Lotion 2ozDrip OilsCBD: 0.91% 1-1-cbd-cbg-sport-ice-muscle-recovery-lotion-2ozdrip-oilscbd-0-91-1764475522742-55rvfe \N 15.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-cbd-cbg-sport-ice-muscle-recovery-lotion-2oz f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 2oz \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3624 1 3 deeply-rooted-az-specials-1764475522731-4 1:1 Dark Chocolate Mini Bar - Sleepy Indica - CBN/THCGrönTHCTHC: 1.35% 1-1-dark-chocolate-mini-bar-sleepy-indica-cbn-thcgr-nthcthc-1-35-1764475522744-pg1m9t \N 9.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-dark-chocolate-mini-bar-sleepy-indica-cbn-thc-71264 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3625 1 3 deeply-rooted-az-specials-1764475522731-5 1:1 Dark Chocolate Peanut Butter Pips - CBD/THC - HybridGrönTHCTAC: 200 mgTHC: 0.43%CBD: 0.41% 1-1-dark-chocolate-peanut-butter-pips-cbd-thc-hybridgr-nthctac-200-mgthc-0-43-cbd-0-41-1764475522745-775fja \N 12.00 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/1-1-dark-chocolate-peanut-butter-pips-cbd-thc-hybrid-45073 f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 2025-11-30 04:05:22.731246 \N 200 mg \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:05:22.731246+00 \N +3769 1 57 deeply-rooted-az-vaporizers-1764475641905-86 Sauce Essentials Distillate AIO | Skywalker OGSauceIndica-HybridTHC: 81.88% sauce-essentials-distillate-aio-skywalker-ogsauceindica-hybridthc-81-88-1764475642008-c2llk9 \N 22.50 \N \N \N \N \N \N \N \N https://dutchie.com/embedded-menu/AZ-Deeply-Rooted/product/sauce-essentials-distillate-aio-skywalker-og f f {"effects": [], "flavors": [], "lineage": null, "terpenes": [], "allWeights": []} 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 2025-11-30 04:07:21.905042 \N 1g \N \N \N \N \N \N \N \N \N \N \N \N in_stock \N 2025-11-30 04:07:21.905042+00 \N +\. + + +-- +-- Data for Name: proxies; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.proxies (id, host, port, protocol, username, password, active, is_anonymous, last_tested_at, test_result, response_time_ms, created_at, updated_at, failure_count, city, state, country, country_code, location_updated_at) FROM stdin; +86 104.207.32.34 3129 http \N \N t t 2025-11-16 17:48:03.34691 success 360 2025-11-16 17:11:04.82791 2025-11-16 17:48:03.34691 0 Ashburn Virginia United States US 2025-11-17 07:20:46.510568 +41 45.3.35.157 3129 http \N \N t t 2025-11-16 17:47:38.88153 success 550 2025-11-16 17:11:04.768807 2025-11-16 17:47:38.88153 0 Ashburn Virginia United States US 2025-11-17 07:19:37.258324 +8 45.3.35.182 3129 http \N \N t t 2025-11-16 17:47:20.660127 success 346 2025-11-16 17:11:04.718983 2025-11-16 17:47:20.660127 0 Ashburn Virginia United States US 2025-11-17 07:19:29.826766 +53 45.3.37.224 3129 http \N \N t t 2025-11-16 17:47:46.502628 success 353 2025-11-16 17:11:04.784588 2025-11-16 17:47:46.502628 0 Ashburn Virginia United States US 2025-11-17 07:20:39.992877 +44 45.3.49.25 3129 http \N \N t t 2025-11-16 17:47:40.635305 success 491 2025-11-16 17:11:04.772909 2025-11-16 17:47:40.635305 0 Ashburn Virginia United States US 2025-11-17 07:19:37.947337 +17 45.3.50.145 3129 http \N \N t t 2025-11-16 17:47:26.784097 success 349 2025-11-16 17:11:04.73256 2025-11-16 17:47:26.784097 0 Ashburn Virginia United States US 2025-11-17 07:19:31.762931 +62 45.3.51.4 3129 http \N \N t t 2025-11-16 17:47:49.993104 success 354 2025-11-16 17:11:04.796416 2025-11-16 17:47:49.993104 0 Ashburn Virginia United States US 2025-11-17 07:20:42.065252 +87 45.3.62.42 3129 http \N \N t t 2025-11-16 17:48:03.700296 success 349 2025-11-16 17:11:04.829299 2025-11-16 17:48:03.700296 0 Ashburn Virginia United States US 2025-11-17 07:20:46.717872 +38 65.111.10.10 3129 http \N \N t t 2025-11-16 17:47:37.612207 success 340 2025-11-16 17:11:04.764899 2025-11-16 17:47:37.612207 0 Ashburn Virginia United States US 2025-11-17 07:19:36.572166 +14 65.111.11.45 3129 http \N \N t t 2025-11-16 17:47:25.739776 success 409 2025-11-16 17:11:04.727888 2025-11-16 17:47:25.739776 0 Ashburn Virginia United States US 2025-11-17 07:19:31.073302 +79 65.111.13.134 3129 http \N \N t t 2025-11-16 17:47:59.011831 success 783 2025-11-16 17:11:04.819092 2025-11-16 17:47:59.011831 0 Ashburn Virginia United States US 2025-11-17 07:20:44.824585 +57 65.111.13.31 3129 http \N \N t t 2025-11-16 17:47:48.015406 success 345 2025-11-16 17:11:04.789766 2025-11-16 17:47:48.015406 0 Ashburn Virginia United States US 2025-11-17 07:20:40.916551 +77 65.111.14.82 3129 http \N \N t t 2025-11-16 17:47:57.880472 success 888 2025-11-16 17:11:04.816512 2025-11-16 17:47:57.880472 0 Ashburn Virginia United States US 2025-11-17 07:20:44.583748 +24 65.111.15.160 3129 http \N \N t t 2025-11-16 17:47:30.406429 success 392 2025-11-16 17:11:04.743325 2025-11-16 17:47:30.406429 0 Ashburn Virginia United States US 2025-11-17 07:19:33.367744 +48 65.111.3.188 3129 http \N \N t t 2025-11-16 17:47:43.051565 success 619 2025-11-16 17:11:04.778185 2025-11-16 17:47:43.051565 0 Ashburn Virginia United States US 2025-11-17 07:20:38.859931 +66 65.111.4.197 3129 http \N \N t t 2025-11-16 17:47:52.751755 success 1609 2025-11-16 17:11:04.801863 2025-11-16 17:47:52.751755 0 Ashburn Virginia United States US 2025-11-17 07:20:42.983097 +10 65.111.7.164 3129 http \N \N t t 2025-11-16 17:47:22.188074 success 1177 2025-11-16 17:11:04.722122 2025-11-16 17:47:22.188074 0 Ashburn Virginia United States US 2025-11-17 07:19:30.285154 +52 65.111.8.16 3129 http \N \N t t 2025-11-16 17:47:46.145599 success 1668 2025-11-16 17:11:04.783211 2025-11-16 17:47:46.145599 0 Ashburn Virginia United States US 2025-11-17 07:20:39.765527 +990 104.207.46.61 3129 http \N \N t t 2025-11-16 17:23:57.003454 success 343 2025-11-16 17:11:05.955582 2025-11-30 02:05:41.834811 2 Ashburn Virginia United States US \N +112 45.3.34.252 3129 http \N \N t t 2025-11-16 17:48:14.94972 success 343 2025-11-16 17:11:04.863595 2025-11-30 02:05:39.208707 2 Ashburn Virginia United States US 2025-11-17 07:21:52.40383 +136 45.3.49.166 3129 http \N \N t t 2025-11-16 17:48:26.746682 success 368 2025-11-16 17:11:04.89641 2025-11-30 02:05:39.659569 2 Ashburn Virginia United States US 2025-11-17 07:22:57.914657 +94 104.167.19.101 3129 http \N \N t t 2025-11-16 17:48:07.68091 success 405 2025-11-16 17:11:04.839037 2025-11-16 17:48:07.68091 0 Ashburn Virginia United States US 2025-11-17 07:21:48.279507 +127 104.167.19.222 3129 http \N \N t t 2025-11-16 17:48:22.286412 success 343 2025-11-16 17:11:04.884509 2025-11-16 17:48:22.286412 0 Ashburn Virginia United States US 2025-11-17 07:21:55.845961 +166 104.167.25.136 3129 http \N \N t t 2025-11-16 17:48:40.069334 success 360 2025-11-16 17:11:04.939308 2025-11-16 17:48:40.069334 0 Ashburn Virginia United States US 2025-11-17 07:23:04.973768 +152 104.207.38.15 3129 http \N \N t t 2025-11-16 17:48:33.98405 success 338 2025-11-16 17:11:04.91961 2025-11-16 17:48:33.98405 0 Ashburn Virginia United States US 2025-11-17 07:23:01.76494 +131 104.207.38.60 3129 http \N \N t t 2025-11-16 17:48:24.605561 success 357 2025-11-16 17:11:04.889981 2025-11-16 17:48:24.605561 0 Ashburn Virginia United States US 2025-11-17 07:21:56.777604 +169 104.207.39.185 3129 http \N \N t t 2025-11-16 17:48:41.606682 success 697 2025-11-16 17:11:04.943508 2025-11-16 17:48:41.606682 0 Ashburn Virginia United States US 2025-11-17 07:23:05.676162 +128 104.207.40.100 3129 http \N \N t t 2025-11-16 17:48:23.516873 success 1226 2025-11-16 17:11:04.885755 2025-11-16 17:48:23.516873 0 Ashburn Virginia United States US 2025-11-17 07:21:56.075036 +111 104.207.40.104 3129 http \N \N t t 2025-11-16 17:48:14.604006 success 800 2025-11-16 17:11:04.862272 2025-11-16 17:48:14.604006 0 Ashburn Virginia United States US 2025-11-17 07:21:52.174635 +149 104.207.40.107 3129 http \N \N t t 2025-11-16 17:48:32.904437 success 344 2025-11-16 17:11:04.915496 2025-11-16 17:48:32.904437 0 Ashburn Virginia United States US 2025-11-17 07:23:01.077322 +122 104.207.40.30 3129 http \N \N t t 2025-11-16 17:48:20.468932 success 552 2025-11-16 17:11:04.877348 2025-11-16 17:48:20.468932 0 Ashburn Virginia United States US 2025-11-17 07:21:54.698649 +153 193.56.28.221 3129 http \N \N t t 2025-11-16 17:48:34.949703 success 961 2025-11-16 17:11:04.920989 2025-11-16 17:48:34.949703 0 Ashburn Virginia United States US 2025-11-17 07:23:01.994585 +156 209.50.160.128 3129 http \N \N t t 2025-11-16 17:48:36.098833 success 350 2025-11-16 17:11:04.925813 2025-11-16 17:48:36.098833 0 Ashburn Virginia United States US 2025-11-17 07:23:02.682275 +133 209.50.160.229 3129 http \N \N t t 2025-11-16 17:48:25.320039 success 351 2025-11-16 17:11:04.892513 2025-11-16 17:48:25.320039 0 Ashburn Virginia United States US 2025-11-17 07:21:57.241269 +132 209.50.163.13 3129 http \N \N t t 2025-11-16 17:48:24.962363 success 353 2025-11-16 17:11:04.891319 2025-11-16 17:48:24.962363 0 Ashburn Virginia United States US 2025-11-17 07:21:57.008931 +123 209.50.163.249 3129 http \N \N t t 2025-11-16 17:48:20.830779 success 353 2025-11-16 17:11:04.878578 2025-11-16 17:48:20.830779 0 Ashburn Virginia United States US 2025-11-17 07:21:54.926458 +116 209.50.164.151 3129 http \N \N t t 2025-11-16 17:48:18.099968 success 420 2025-11-16 17:11:04.86896 2025-11-16 17:48:18.099968 0 Ashburn Virginia United States US 2025-11-17 07:21:53.320141 +119 209.50.164.175 3129 http \N \N t t 2025-11-16 17:48:19.213404 success 347 2025-11-16 17:11:04.873369 2025-11-16 17:48:19.213404 0 Ashburn Virginia United States US 2025-11-17 07:21:54.008842 +97 209.50.166.160 3129 http \N \N t t 2025-11-16 17:48:08.879178 success 353 2025-11-16 17:11:04.843481 2025-11-16 17:48:08.879178 0 Ashburn Virginia United States US 2025-11-17 07:21:48.96675 +147 209.50.166.46 3129 http \N \N t t 2025-11-16 17:48:32.198998 success 515 2025-11-16 17:11:04.912248 2025-11-16 17:48:32.198998 0 Ashburn Virginia United States US 2025-11-17 07:23:00.78418 +139 209.50.167.71 3129 http \N \N t t 2025-11-16 17:48:28.599441 success 344 2025-11-16 17:11:04.90035 2025-11-16 17:48:28.599441 0 Ashburn Virginia United States US 2025-11-17 07:22:58.663066 +124 209.50.168.220 3129 http \N \N t t 2025-11-16 17:48:21.193698 success 360 2025-11-16 17:11:04.880015 2025-11-16 17:48:21.193698 0 Ashburn Virginia United States US 2025-11-17 07:21:55.156776 +141 209.50.168.223 3129 http \N \N t t 2025-11-16 17:48:29.293028 success 345 2025-11-16 17:11:04.90304 2025-11-16 17:48:29.293028 0 Ashburn Virginia United States US 2025-11-17 07:22:59.132882 +103 209.50.169.185 3129 http \N \N t t 2025-11-16 17:48:11.255525 success 525 2025-11-16 17:11:04.851563 2025-11-16 17:48:11.255525 0 Ashburn Virginia United States US 2025-11-17 07:21:50.359972 +107 216.26.224.177 3129 http \N \N t t 2025-11-16 17:48:12.653528 success 341 2025-11-16 17:11:04.857032 2025-11-16 17:48:12.653528 0 Ashburn Virginia United States US 2025-11-17 07:21:51.261918 +173 216.26.225.146 3129 http \N \N t t 2025-11-16 17:48:44.146408 success 343 2025-11-16 17:11:04.948515 2025-11-16 17:48:44.146408 0 Ashburn Virginia United States US 2025-11-17 07:23:06.582401 +164 216.26.229.185 3129 http \N \N t t 2025-11-16 17:48:38.914175 success 340 2025-11-16 17:11:04.936654 2025-11-16 17:48:38.914175 0 Ashburn Virginia United States US 2025-11-17 07:23:04.516534 +144 216.26.230.211 3129 http \N \N t t 2025-11-16 17:48:30.784475 success 514 2025-11-16 17:11:04.908141 2025-11-16 17:48:30.784475 0 Ashburn Virginia United States US 2025-11-17 07:22:59.935337 +91 216.26.230.32 3129 http \N \N t t 2025-11-16 17:48:06.244927 success 360 2025-11-16 17:11:04.834702 2025-11-16 17:48:06.244927 0 Ashburn Virginia United States US 2025-11-17 07:21:47.650396 +162 216.26.230.70 3129 http \N \N t t 2025-11-16 17:48:38.207872 success 352 2025-11-16 17:11:04.934061 2025-11-16 17:48:38.207872 0 Ashburn Virginia United States US 2025-11-17 07:23:04.056265 +589 216.26.234.224 3129 http \N \N t t 2025-11-16 17:28:31.013116 success 678 2025-11-16 17:11:05.477493 2025-11-16 17:28:31.013116 0 Ashburn Virginia United States US \N +168 216.26.238.136 3129 http \N \N t t 2025-11-16 17:48:40.905918 success 376 2025-11-16 17:11:04.941952 2025-11-16 17:48:40.905918 0 Ashburn Virginia United States US 2025-11-17 07:23:05.436401 +99 216.26.238.149 3129 http \N \N t t 2025-11-16 17:48:09.626552 success 399 2025-11-16 17:11:04.846457 2025-11-16 17:48:09.626552 0 Ashburn Virginia United States US 2025-11-17 07:21:49.425723 +98 45.3.33.150 3129 http \N \N t t 2025-11-16 17:48:09.224539 success 343 2025-11-16 17:11:04.845181 2025-11-16 17:48:09.224539 0 Ashburn Virginia United States US 2025-11-17 07:21:49.196595 +102 45.3.34.140 3129 http \N \N t t 2025-11-16 17:48:10.727307 success 389 2025-11-16 17:11:04.850229 2025-11-16 17:48:10.727307 0 Ashburn Virginia United States US 2025-11-17 07:21:50.163389 +174 45.3.34.219 3129 http \N \N t t 2025-11-16 17:48:44.885052 success 735 2025-11-16 17:11:04.949845 2025-11-16 17:48:44.885052 0 Ashburn Virginia United States US 2025-11-17 07:23:06.811879 +161 45.3.37.131 3129 http \N \N t t 2025-11-16 17:48:37.852312 success 350 2025-11-16 17:11:04.932835 2025-11-16 17:48:37.852312 0 Ashburn Virginia United States US 2025-11-17 07:23:03.828309 +172 45.3.37.7 3129 http \N \N t t 2025-11-16 17:48:43.801073 success 356 2025-11-16 17:11:04.947268 2025-11-16 17:48:43.801073 0 Ashburn Virginia United States US 2025-11-17 07:23:06.351053 +106 45.3.48.43 3129 http \N \N t t 2025-11-16 17:48:12.309116 success 340 2025-11-16 17:11:04.855829 2025-11-16 17:48:12.309116 0 Ashburn Virginia United States US 2025-11-17 07:21:51.031652 +114 45.3.49.17 3129 http \N \N t t 2025-11-16 17:48:17.300479 success 345 2025-11-16 17:11:04.866345 2025-11-16 17:48:17.300479 0 Ashburn Virginia United States US 2025-11-17 07:21:52.866842 +157 45.3.51.51 3129 http \N \N t t 2025-11-16 17:48:36.458468 success 357 2025-11-16 17:11:04.92737 2025-11-16 17:48:36.458468 0 Ashburn Virginia United States US 2025-11-17 07:23:02.911948 +137 65.111.0.20 3129 http \N \N t t 2025-11-16 17:48:27.859545 success 1109 2025-11-16 17:11:04.897694 2025-11-16 17:48:27.859545 0 Ashburn Virginia United States US 2025-11-17 07:22:58.135246 +148 65.111.11.178 3129 http \N \N t t 2025-11-16 17:48:32.557837 success 355 2025-11-16 17:11:04.913876 2025-11-16 17:48:32.557837 0 Ashburn Virginia United States US 2025-11-17 07:23:00.917887 +159 65.111.15.38 3129 http \N \N t t 2025-11-16 17:48:37.155468 success 346 2025-11-16 17:11:04.930202 2025-11-16 17:48:37.155468 0 Ashburn Virginia United States US 2025-11-17 07:23:03.369431 +143 65.111.2.61 3129 http \N \N t t 2025-11-16 17:48:30.267663 success 350 2025-11-16 17:11:04.906123 2025-11-16 17:48:30.267663 0 Ashburn Virginia United States US 2025-11-17 07:22:59.670165 +118 65.111.5.173 3129 http \N \N t t 2025-11-16 17:48:18.86117 success 395 2025-11-16 17:11:04.871995 2025-11-16 17:48:18.86117 0 Ashburn Virginia United States US 2025-11-17 07:21:53.778977 +108 65.111.7.94 3129 http \N \N t t 2025-11-16 17:48:13.006088 success 349 2025-11-16 17:11:04.858182 2025-11-16 17:48:13.006088 0 Ashburn Virginia United States US 2025-11-17 07:21:51.488683 +223 104.207.34.144 3129 http \N \N t t 2025-11-16 17:49:13.620054 success 522 2025-11-16 17:11:05.019384 2025-11-16 17:49:13.620054 0 Ashburn Virginia United States US 2025-11-17 07:24:17.981306 +228 104.207.35.170 3129 http \N \N t t 2025-11-16 17:49:15.424916 success 352 2025-11-16 17:11:05.025826 2025-11-16 17:49:15.424916 0 Ashburn Virginia United States US \N +200 104.207.39.0 3129 http \N \N t t 2025-11-16 17:48:58.91959 success 602 2025-11-16 17:11:04.988312 2025-11-16 17:48:58.91959 0 Ashburn Virginia United States US 2025-11-17 07:24:12.441637 +261 104.207.40.129 3129 http \N \N t t 2025-11-16 17:49:31.725565 success 348 2025-11-16 17:11:05.068978 2025-11-16 17:49:31.725565 0 Ashburn Virginia United States US \N +244 104.207.44.168 3129 http \N \N t t 2025-11-16 17:49:23.687715 success 345 2025-11-16 17:11:05.046804 2025-11-16 17:49:23.687715 0 Ashburn Virginia United States US \N +214 193.56.28.144 3129 http \N \N t t 2025-11-16 17:49:08.211375 success 513 2025-11-16 17:11:05.006634 2025-11-16 17:49:08.211375 0 Ashburn Virginia United States US 2025-11-17 07:24:15.916634 +210 209.50.160.67 3129 http \N \N t t 2025-11-16 17:49:05.664538 success 535 2025-11-16 17:11:05.000833 2025-11-16 17:49:05.664538 0 Ashburn Virginia United States US 2025-11-17 07:24:14.850491 +235 209.50.161.152 3129 http \N \N t t 2025-11-16 17:49:18.05337 success 351 2025-11-16 17:11:05.034693 2025-11-16 17:49:18.05337 0 Ashburn Virginia United States US \N +220 209.50.162.234 3129 http \N \N t t 2025-11-16 17:49:11.692047 success 530 2025-11-16 17:11:05.015409 2025-11-16 17:49:11.692047 0 Ashburn Virginia United States US 2025-11-17 07:24:17.287621 +239 45.3.34.247 3129 http \N \N t t 2025-11-16 17:49:21.679026 success 651 2025-11-16 17:11:05.03991 2025-11-16 17:49:21.679026 0 Ashburn Virginia United States US \N +236 209.50.162.75 3129 http \N \N t t 2025-11-16 17:49:18.413974 success 357 2025-11-16 17:11:05.036002 2025-11-16 17:49:18.413974 0 Ashburn Virginia United States US \N +252 45.3.50.101 3129 http \N \N t t 2025-11-16 17:49:27.323935 success 364 2025-11-16 17:11:05.057567 2025-11-16 17:49:27.323935 0 Ashburn Virginia United States US \N +183 209.50.174.167 3129 http \N \N t t 2025-11-16 17:48:49.413133 success 446 2025-11-16 17:11:04.964612 2025-11-16 17:48:49.413133 0 Ashburn Virginia United States US 2025-11-17 07:24:08.826228 +198 65.111.11.91 3129 http \N \N t t 2025-11-16 17:48:57.623795 success 738 2025-11-16 17:11:04.98562 2025-11-16 17:48:57.623795 0 Ashburn Virginia United States US 2025-11-17 07:24:11.930079 +256 65.111.3.184 3129 http \N \N t t 2025-11-16 17:49:29.380955 success 354 2025-11-16 17:11:05.062824 2025-11-16 17:49:29.380955 0 Ashburn Virginia United States US \N +241 209.50.164.225 3129 http \N \N t t 2025-11-16 17:49:22.37705 success 350 2025-11-16 17:11:05.042519 2025-11-16 17:49:22.37705 0 Ashburn Virginia United States US \N +192 209.50.165.195 3129 http \N \N t t 2025-11-16 17:48:53.00292 success 360 2025-11-16 17:11:04.977597 2025-11-16 17:48:53.00292 0 Ashburn Virginia United States US 2025-11-17 07:24:10.902887 +248 65.111.8.51 3129 http \N \N t t 2025-11-16 17:49:25.784527 success 722 2025-11-16 17:11:05.052015 2025-11-16 17:49:25.784527 0 Ashburn Virginia United States US \N +250 45.3.36.241 3129 http \N \N t t 2025-11-16 17:49:26.567747 success 362 2025-11-16 17:11:05.05484 2025-11-16 17:49:26.567747 0 Ashburn Virginia United States US \N +194 65.111.5.92 3129 http \N \N t t 2025-11-16 17:48:53.720769 success 353 2025-11-16 17:11:04.980358 2025-11-16 17:48:53.720769 0 Ashburn Virginia United States US 2025-11-17 07:24:11.321058 +260 209.50.166.116 3129 http \N \N t t 2025-11-16 17:49:31.375122 success 355 2025-11-16 17:11:05.067831 2025-11-16 17:49:31.375122 0 Ashburn Virginia United States US \N +233 65.111.2.70 3129 http \N \N t t 2025-11-16 17:49:17.350972 success 346 2025-11-16 17:11:05.032017 2025-11-16 17:49:17.350972 0 Ashburn Virginia United States US \N +212 209.50.168.217 3129 http \N \N t t 2025-11-16 17:49:06.818726 success 530 2025-11-16 17:11:05.003876 2025-11-16 17:49:06.818726 0 Ashburn Virginia United States US 2025-11-17 07:24:15.384294 +224 209.50.171.157 3129 http \N \N t t 2025-11-16 17:49:13.966851 success 344 2025-11-16 17:11:05.020896 2025-11-16 17:49:13.966851 0 Ashburn Virginia United States US 2025-11-17 07:24:18.207378 +264 209.50.175.239 3129 http \N \N t t 2025-11-16 17:49:32.794091 success 345 2025-11-16 17:11:05.072622 2025-11-16 17:49:32.794091 0 Ashburn Virginia United States US \N +254 209.50.170.132 3129 http \N \N t t 2025-11-16 17:49:28.054886 success 345 2025-11-16 17:11:05.060251 2025-11-16 17:49:28.054886 0 Ashburn Virginia United States US \N +225 45.3.48.204 3129 http \N \N t t 2025-11-16 17:49:14.315247 success 346 2025-11-16 17:11:05.022218 2025-11-16 17:49:14.315247 0 Ashburn Virginia United States US 2025-11-17 07:24:18.435543 +179 45.3.51.45 3129 http \N \N t t 2025-11-16 17:48:47.932084 success 342 2025-11-16 17:11:04.957221 2025-11-16 17:48:47.932084 0 Ashburn Virginia United States US 2025-11-17 07:23:07.958365 +190 65.111.10.216 3129 http \N \N t t 2025-11-16 17:48:52.295708 success 437 2025-11-16 17:11:04.974499 2025-11-16 17:48:52.295708 0 Ashburn Virginia United States US 2025-11-17 07:24:10.445639 +245 209.50.170.160 3129 http \N \N t t 2025-11-16 17:49:24.04113 success 350 2025-11-16 17:11:05.048088 2025-11-16 17:49:24.04113 0 Ashburn Virginia United States US \N +237 65.111.8.217 3129 http \N \N t t 2025-11-16 17:49:18.770469 success 354 2025-11-16 17:11:05.037308 2025-11-16 17:49:18.770469 0 Ashburn Virginia United States US \N +208 216.26.236.108 3129 http \N \N t t 2025-11-16 17:49:04.510568 success 541 2025-11-16 17:11:04.99832 2025-11-16 17:49:04.510568 0 Ashburn Virginia United States US 2025-11-17 07:24:14.38093 +185 209.50.173.19 3129 http \N \N t t 2025-11-16 17:48:50.421487 success 619 2025-11-16 17:11:04.967537 2025-11-16 17:48:50.421487 0 Ashburn Virginia United States US 2025-11-17 07:24:09.288524 +13 209.50.173.47 3129 http \N \N t t 2025-11-16 17:47:25.327256 success 645 2025-11-16 17:11:04.726372 2025-11-16 17:47:25.327256 0 Ashburn Virginia United States US 2025-11-17 07:19:30.845046 +231 209.50.174.114 3129 http \N \N t t 2025-11-16 17:49:16.49789 success 350 2025-11-16 17:11:05.029473 2025-11-16 17:49:16.49789 0 Ashburn Virginia United States US \N +219 104.207.32.59 3129 http \N \N t t 2025-11-16 17:49:11.157285 success 615 2025-11-16 17:11:05.013901 2025-11-16 17:49:11.157285 0 Ashburn Virginia United States US 2025-11-17 07:24:17.071303 +189 216.26.228.175 3129 http \N \N t t 2025-11-16 17:48:51.855691 success 341 2025-11-16 17:11:04.973089 2025-11-16 17:48:51.855691 0 Ashburn Virginia United States US 2025-11-17 07:24:10.212374 +199 216.26.224.237 3129 http \N \N t t 2025-11-16 17:48:58.313445 success 686 2025-11-16 17:11:04.986837 2025-11-16 17:48:58.313445 0 Ashburn Virginia United States US 2025-11-17 07:24:12.168871 +247 209.50.174.205 3129 http \N \N t t 2025-11-16 17:49:25.058731 success 345 2025-11-16 17:11:05.050635 2025-11-16 17:49:25.058731 0 Ashburn Virginia United States US \N +262 216.26.227.144 3129 http \N \N t t 2025-11-16 17:49:32.082191 success 353 2025-11-16 17:11:05.070197 2025-11-16 17:49:32.082191 0 Ashburn Virginia United States US \N +195 216.26.228.32 3129 http \N \N t t 2025-11-16 17:48:54.087355 success 363 2025-11-16 17:11:04.981656 2025-11-16 17:48:54.087355 0 Ashburn Virginia United States US 2025-11-17 07:24:11.446821 +257 216.26.234.41 3129 http \N \N t t 2025-11-16 17:49:29.756123 success 372 2025-11-16 17:11:05.064074 2025-11-16 17:49:29.756123 0 Ashburn Virginia United States US \N +218 216.26.237.43 3129 http \N \N t t 2025-11-16 17:49:10.54026 success 536 2025-11-16 17:11:05.012483 2025-11-16 17:49:10.54026 0 Ashburn Virginia United States US 2025-11-17 07:24:16.892381 +227 216.26.238.238 3129 http \N \N t t 2025-11-16 17:49:15.069076 success 394 2025-11-16 17:11:05.024584 2025-11-16 17:49:15.069076 0 Ashburn Virginia United States US \N +204 45.3.48.81 3129 http \N \N t t 2025-11-16 17:49:02.107533 success 620 2025-11-16 17:11:04.993305 2025-11-16 17:49:02.107533 0 Ashburn Virginia United States US 2025-11-17 07:24:13.432989 +229 45.3.49.88 3129 http \N \N t t 2025-11-16 17:49:15.771695 success 343 2025-11-16 17:11:05.027018 2025-11-16 17:49:15.771695 0 Ashburn Virginia United States US \N +253 65.111.0.228 3129 http \N \N t t 2025-11-16 17:49:27.704942 success 378 2025-11-16 17:11:05.058894 2025-11-16 17:49:27.704942 0 Ashburn Virginia United States US \N +232 65.111.1.104 3129 http \N \N t t 2025-11-16 17:49:17.00047 success 499 2025-11-16 17:11:05.030733 2025-11-16 17:49:17.00047 0 Ashburn Virginia United States US \N +215 65.111.14.35 3129 http \N \N t t 2025-11-16 17:49:08.83821 success 623 2025-11-16 17:11:05.00801 2025-11-16 17:49:08.83821 0 Ashburn Virginia United States US 2025-11-17 07:24:16.183609 +243 65.111.15.34 3129 http \N \N t t 2025-11-16 17:49:23.338869 success 363 2025-11-16 17:11:05.045335 2025-11-16 17:49:23.338869 0 Ashburn Virginia United States US \N +269 193.56.28.216 3129 http \N \N t t 2025-11-16 17:49:35.727856 success 350 2025-11-16 17:11:05.078873 2025-11-16 17:49:35.727856 0 Ashburn Virginia United States US \N +271 193.56.28.223 3129 http \N \N t t 2025-11-16 17:49:36.50342 success 373 2025-11-16 17:11:05.081327 2025-11-16 17:49:36.50342 0 Ashburn Virginia United States US \N +313 104.207.34.169 3129 http \N \N t t 2025-11-16 17:49:55.243124 success 388 2025-11-16 17:11:05.134127 2025-11-16 17:49:55.243124 0 Ashburn Virginia United States US \N +341 209.50.162.100 3129 http \N \N t t 2025-11-16 17:50:10.824496 success 380 2025-11-16 17:11:05.171138 2025-11-16 17:50:10.824496 0 Ashburn Virginia United States US \N +299 104.207.45.43 3129 http \N \N t t 2025-11-16 17:49:48.217124 success 381 2025-11-16 17:11:05.116934 2025-11-16 17:49:48.217124 0 Ashburn Virginia United States US \N +319 65.111.6.37 3129 http \N \N t t 2025-11-16 17:49:57.389417 success 354 2025-11-16 17:11:05.141393 2025-11-16 17:49:57.389417 0 Ashburn Virginia United States US \N +321 45.3.49.207 3129 http \N \N t t 2025-11-16 17:49:58.099116 success 345 2025-11-16 17:11:05.143732 2025-11-16 17:49:58.099116 0 Ashburn Virginia United States US \N +317 65.111.12.29 3129 http \N \N t t 2025-11-16 17:49:56.677903 success 370 2025-11-16 17:11:05.13904 2025-11-16 17:49:56.677903 0 Ashburn Virginia United States US \N +300 209.50.167.228 3129 http \N \N t t 2025-11-16 17:49:48.561846 success 341 2025-11-16 17:11:05.118114 2025-11-16 17:49:48.561846 0 Ashburn Virginia United States US \N +323 216.26.234.148 3129 http \N \N t t 2025-11-16 17:50:00.06709 success 463 2025-11-16 17:11:05.146299 2025-11-16 17:50:00.06709 0 Ashburn Virginia United States US \N +331 45.3.51.77 3129 http \N \N t t 2025-11-16 17:50:06.410488 success 397 2025-11-16 17:11:05.15687 2025-11-16 17:50:06.410488 0 Ashburn Virginia United States US \N +350 104.207.40.239 3129 http \N \N t t 2025-11-16 17:50:15.232584 success 772 2025-11-16 17:11:05.182816 2025-11-16 17:50:15.232584 0 Ashburn Virginia United States US \N +267 193.56.28.175 3129 http \N \N t t 2025-11-16 17:49:34.918943 success 1381 2025-11-16 17:11:05.076473 2025-11-16 17:49:34.918943 0 Ashburn Virginia United States US \N +275 45.3.39.73 3129 http \N \N t t 2025-11-16 17:49:38.296314 success 733 2025-11-16 17:11:05.086256 2025-11-16 17:49:38.296314 0 Ashburn Virginia United States US \N +291 209.50.173.207 3129 http \N \N t t 2025-11-16 17:49:44.666485 success 357 2025-11-16 17:11:05.106833 2025-11-16 17:49:44.666485 0 Ashburn Virginia United States US \N +296 45.3.38.143 3129 http \N \N t t 2025-11-16 17:49:47.122556 success 394 2025-11-16 17:11:05.113159 2025-11-16 17:49:47.122556 0 Ashburn Virginia United States US \N +329 216.26.228.197 3129 http \N \N t t 2025-11-16 17:50:04.317994 success 537 2025-11-16 17:11:05.153843 2025-11-16 17:50:04.317994 0 Ashburn Virginia United States US \N +312 65.111.15.226 3129 http \N \N t t 2025-11-16 17:49:54.850685 success 340 2025-11-16 17:11:05.13297 2025-11-16 17:49:54.850685 0 Ashburn Virginia United States US \N +328 209.50.175.212 3129 http \N \N t t 2025-11-16 17:50:03.777607 success 1072 2025-11-16 17:11:05.152205 2025-11-16 17:50:03.777607 0 Ashburn Virginia United States US \N +338 104.207.47.122 3129 http \N \N t t 2025-11-16 17:50:09.757243 success 339 2025-11-16 17:11:05.166354 2025-11-16 17:50:09.757243 0 Ashburn Virginia United States US \N +309 104.207.32.109 3129 http \N \N t t 2025-11-16 17:49:53.799675 success 339 2025-11-16 17:11:05.129254 2025-11-16 17:49:53.799675 0 Ashburn Virginia United States US \N +311 65.111.2.43 3129 http \N \N t t 2025-11-16 17:49:54.507108 success 356 2025-11-16 17:11:05.131766 2025-11-16 17:49:54.507108 0 Ashburn Virginia United States US \N +284 45.3.38.202 3129 http \N \N t t 2025-11-16 17:49:41.526901 success 354 2025-11-16 17:11:05.09816 2025-11-16 17:49:41.526901 0 Ashburn Virginia United States US \N +277 65.111.10.74 3129 http \N \N t t 2025-11-16 17:49:39.056517 success 415 2025-11-16 17:11:05.089167 2025-11-16 17:49:39.056517 0 Ashburn Virginia United States US \N +292 104.207.46.46 3129 http \N \N t t 2025-11-16 17:49:45.38626 success 716 2025-11-16 17:11:05.108053 2025-11-16 17:49:45.38626 0 Ashburn Virginia United States US \N +315 209.50.168.176 3129 http \N \N t t 2025-11-16 17:49:55.963169 success 357 2025-11-16 17:11:05.136486 2025-11-16 17:49:55.963169 0 Ashburn Virginia United States US \N +295 65.111.2.152 3129 http \N \N t t 2025-11-16 17:49:46.724991 success 406 2025-11-16 17:11:05.111887 2025-11-16 17:49:46.724991 0 Ashburn Virginia United States US \N +349 216.26.237.60 3129 http \N \N t t 2025-11-16 17:50:14.457513 success 391 2025-11-16 17:11:05.181622 2025-11-16 17:50:14.457513 0 Ashburn Virginia United States US \N +288 216.26.226.89 3129 http \N \N t t 2025-11-16 17:49:43.553732 success 480 2025-11-16 17:11:05.103078 2025-11-16 17:49:43.553732 0 Ashburn Virginia United States US \N +16 209.50.169.164 3129 http \N \N t t 2025-11-16 17:47:26.431765 success 341 2025-11-16 17:11:04.731092 2025-11-16 17:47:26.431765 0 Ashburn Virginia United States US 2025-11-17 07:19:31.533939 +325 216.26.231.156 3129 http \N \N t t 2025-11-16 17:50:00.804743 success 372 2025-11-16 17:11:05.148668 2025-11-16 17:50:00.804743 0 Ashburn Virginia United States US \N +270 104.207.33.252 3129 http \N \N t t 2025-11-16 17:49:36.126898 success 395 2025-11-16 17:11:05.080156 2025-11-16 17:49:36.126898 0 Ashburn Virginia United States US \N +274 65.111.3.39 3129 http \N \N t t 2025-11-16 17:49:37.560351 success 345 2025-11-16 17:11:05.085053 2025-11-16 17:49:37.560351 0 Ashburn Virginia United States US \N +345 65.111.7.141 3129 http \N \N t t 2025-11-16 17:50:12.729616 success 346 2025-11-16 17:11:05.176428 2025-11-16 17:50:12.729616 0 Ashburn Virginia United States US \N +348 104.207.34.8 3129 http \N \N t t 2025-11-16 17:50:14.063684 success 343 2025-11-16 17:11:05.180256 2025-11-16 17:50:14.063684 0 Ashburn Virginia United States US \N +286 209.50.172.85 3129 http \N \N t t 2025-11-16 17:49:42.68995 success 348 2025-11-16 17:11:05.100562 2025-11-16 17:49:42.68995 0 Ashburn Virginia United States US \N +273 104.207.42.224 3129 http \N \N t t 2025-11-16 17:49:37.211855 success 346 2025-11-16 17:11:05.083758 2025-11-16 17:49:37.211855 0 Ashburn Virginia United States US \N +290 65.111.12.42 3129 http \N \N t t 2025-11-16 17:49:44.307127 success 397 2025-11-16 17:11:05.105522 2025-11-16 17:49:44.307127 0 Ashburn Virginia United States US \N +302 65.111.7.222 3129 http \N \N t t 2025-11-16 17:49:49.327739 success 359 2025-11-16 17:11:05.120639 2025-11-16 17:49:49.327739 0 Ashburn Virginia United States US \N +294 104.167.19.205 3129 http \N \N t t 2025-11-16 17:49:46.31475 success 360 2025-11-16 17:11:05.110693 2025-11-16 17:49:46.31475 0 Ashburn Virginia United States US \N +280 45.3.50.175 3129 http \N \N t t 2025-11-16 17:49:40.115903 success 340 2025-11-16 17:11:05.092793 2025-11-16 17:49:40.115903 0 Ashburn Virginia United States US \N +346 216.26.234.203 3129 http \N \N t t 2025-11-16 17:50:13.328521 success 594 2025-11-16 17:11:05.17758 2025-11-16 17:50:13.328521 0 Ashburn Virginia United States US \N +279 216.26.231.197 3129 http \N \N t t 2025-11-16 17:49:39.772413 success 356 2025-11-16 17:11:05.091579 2025-11-16 17:49:39.772413 0 Ashburn Virginia United States US \N +352 216.26.224.55 3129 http \N \N t t 2025-11-16 17:50:15.937027 success 355 2025-11-16 17:11:05.185581 2025-11-16 17:50:15.937027 0 Ashburn Virginia United States US \N +306 216.26.226.165 3129 http \N \N t t 2025-11-16 17:49:51.86831 success 1417 2025-11-16 17:11:05.125504 2025-11-16 17:49:51.86831 0 Ashburn Virginia United States US \N +342 216.26.236.103 3129 http \N \N t t 2025-11-16 17:50:11.628838 success 801 2025-11-16 17:11:05.172565 2025-11-16 17:50:11.628838 0 Ashburn Virginia United States US \N +324 45.3.62.247 3129 http \N \N t t 2025-11-16 17:50:00.430127 success 359 2025-11-16 17:11:05.147492 2025-11-16 17:50:00.430127 0 Ashburn Virginia United States US \N +333 104.207.39.243 3129 http \N \N t t 2025-11-16 17:50:07.928749 success 432 2025-11-16 17:11:05.159416 2025-11-16 17:50:07.928749 0 Ashburn Virginia United States US \N +282 104.207.40.172 3129 http \N \N t t 2025-11-16 17:49:40.822102 success 351 2025-11-16 17:11:05.095448 2025-11-16 17:49:40.822102 0 Ashburn Virginia United States US \N +278 209.50.162.33 3129 http \N \N t t 2025-11-16 17:49:39.4143 success 354 2025-11-16 17:11:05.090357 2025-11-16 17:49:39.4143 0 Ashburn Virginia United States US \N +304 216.26.236.200 3129 http \N \N t t 2025-11-16 17:49:50.066786 success 390 2025-11-16 17:11:05.123016 2025-11-16 17:49:50.066786 0 Ashburn Virginia United States US \N +320 104.167.19.1 3129 http \N \N t t 2025-11-16 17:49:57.750952 success 358 2025-11-16 17:11:05.142503 2025-11-16 17:49:57.750952 0 Ashburn Virginia United States US \N +360 209.50.167.168 3129 http \N \N t t 2025-11-16 17:50:20.188276 success 382 2025-11-16 17:11:05.195618 2025-11-30 02:05:42.253688 2 Ashburn Virginia United States US \N +394 104.207.36.60 3129 http \N \N t t 2025-11-16 17:50:35.301522 success 1246 2025-11-16 17:11:05.238998 2025-11-16 17:50:35.301522 0 Ashburn Virginia United States US \N +372 45.3.48.129 3129 http \N \N t t 2025-11-16 17:50:24.879483 success 459 2025-11-16 17:11:05.211139 2025-11-16 17:50:24.879483 0 Ashburn Virginia United States US \N +419 45.3.32.143 3129 http \N \N t t 2025-11-16 17:50:46.505701 success 353 2025-11-16 17:11:05.269921 2025-11-16 17:50:46.505701 0 Ashburn Virginia United States US \N +355 216.26.234.215 3129 http \N \N t t 2025-11-16 17:50:17.595518 success 394 2025-11-16 17:11:05.18936 2025-11-16 17:50:17.595518 0 Ashburn Virginia United States US \N +357 193.56.28.227 3129 http \N \N t t 2025-11-16 17:50:18.348657 success 404 2025-11-16 17:11:05.191808 2025-11-16 17:50:18.348657 0 Ashburn Virginia United States US \N +393 104.207.33.215 3129 http \N \N t t 2025-11-16 17:50:34.052166 success 337 2025-11-16 17:11:05.237888 2025-11-16 17:50:34.052166 0 Ashburn Virginia United States US \N +438 104.207.36.235 3129 http \N \N t t 2025-11-16 17:50:53.807234 success 357 2025-11-16 17:11:05.293234 2025-11-16 17:50:53.807234 0 Ashburn Virginia United States US \N +382 216.26.227.229 3129 http \N \N t t 2025-11-16 17:50:28.549556 success 376 2025-11-16 17:11:05.22422 2025-11-16 17:50:28.549556 0 Ashburn Virginia United States US \N +422 45.3.49.251 3129 http \N \N t t 2025-11-16 17:50:47.757842 success 532 2025-11-16 17:11:05.273823 2025-11-16 17:50:47.757842 0 Ashburn Virginia United States US \N +434 216.26.227.73 3129 http \N \N t t 2025-11-16 17:50:52.329747 success 373 2025-11-16 17:11:05.288117 2025-11-16 17:50:52.329747 0 Ashburn Virginia United States US \N +410 65.111.7.83 3129 http \N \N t t 2025-11-16 17:50:42.475218 success 356 2025-11-16 17:11:05.258864 2025-11-16 17:50:42.475218 0 Ashburn Virginia United States US \N +374 209.50.174.26 3129 http \N \N t t 2025-11-16 17:50:25.597086 success 345 2025-11-16 17:11:05.213809 2025-11-16 17:50:25.597086 0 Ashburn Virginia United States US \N +366 45.3.50.191 3129 http \N \N t t 2025-11-16 17:50:22.473737 success 377 2025-11-16 17:11:05.203229 2025-11-16 17:50:22.473737 0 Ashburn Virginia United States US \N +411 45.3.34.43 3129 http \N \N t t 2025-11-16 17:50:42.821852 success 342 2025-11-16 17:11:05.259997 2025-11-16 17:50:42.821852 0 Ashburn Virginia United States US \N +420 45.3.34.159 3129 http \N \N t t 2025-11-16 17:50:46.861767 success 353 2025-11-16 17:11:05.271386 2025-11-16 17:50:46.861767 0 Ashburn Virginia United States US \N +405 45.3.62.46 3129 http \N \N t t 2025-11-16 17:50:40.718668 success 339 2025-11-16 17:11:05.252529 2025-11-16 17:50:40.718668 0 Ashburn Virginia United States US \N +368 104.207.45.147 3129 http \N \N t t 2025-11-16 17:50:23.202394 success 360 2025-11-16 17:11:05.205719 2025-11-16 17:50:23.202394 0 Ashburn Virginia United States US \N +436 45.3.39.59 3129 http \N \N t t 2025-11-16 17:50:53.103132 success 350 2025-11-16 17:11:05.290791 2025-11-16 17:50:53.103132 0 Ashburn Virginia United States US \N +398 65.111.8.195 3129 http \N \N t t 2025-11-16 17:50:37.431604 success 350 2025-11-16 17:11:05.243732 2025-11-16 17:50:37.431604 0 Ashburn Virginia United States US \N +423 209.50.170.84 3129 http \N \N t t 2025-11-16 17:50:48.263958 success 503 2025-11-16 17:11:05.275091 2025-11-16 17:50:48.263958 0 Ashburn Virginia United States US \N +369 45.3.50.67 3129 http \N \N t t 2025-11-16 17:50:23.695749 success 490 2025-11-16 17:11:05.206883 2025-11-16 17:50:23.695749 0 Ashburn Virginia United States US \N +391 65.111.9.151 3129 http \N \N t t 2025-11-16 17:50:33.207281 success 346 2025-11-16 17:11:05.235513 2025-11-16 17:50:33.207281 0 Ashburn Virginia United States US \N +409 104.167.19.108 3129 http \N \N t t 2025-11-16 17:50:42.115615 success 343 2025-11-16 17:11:05.257662 2025-11-16 17:50:42.115615 0 Ashburn Virginia United States US \N +401 104.167.25.53 3129 http \N \N t t 2025-11-16 17:50:39.155733 success 354 2025-11-16 17:11:05.247555 2025-11-16 17:50:39.155733 0 Ashburn Virginia United States US \N +364 209.50.168.43 3129 http \N \N t t 2025-11-16 17:50:21.684257 success 350 2025-11-16 17:11:05.200588 2025-11-16 17:50:21.684257 0 Ashburn Virginia United States US \N +361 65.111.0.16 3129 http \N \N t t 2025-11-16 17:50:20.619099 success 427 2025-11-16 17:11:05.196769 2025-11-16 17:50:20.619099 0 Ashburn Virginia United States US \N +353 209.50.165.248 3129 http \N \N t t 2025-11-16 17:50:16.29281 success 353 2025-11-16 17:11:05.186895 2025-11-16 17:50:16.29281 0 Ashburn Virginia United States US \N +435 216.26.237.249 3129 http \N \N t t 2025-11-16 17:50:52.748779 success 416 2025-11-16 17:11:05.289215 2025-11-16 17:50:52.748779 0 Ashburn Virginia United States US \N +370 104.207.39.187 3129 http \N \N t t 2025-11-16 17:50:24.053456 success 355 2025-11-16 17:11:05.20825 2025-11-16 17:50:24.053456 0 Ashburn Virginia United States US \N +373 209.50.161.166 3129 http \N \N t t 2025-11-16 17:50:25.248206 success 364 2025-11-16 17:11:05.212458 2025-11-16 17:50:25.248206 0 Ashburn Virginia United States US \N +403 209.50.165.221 3129 http \N \N t t 2025-11-16 17:50:40.016775 success 514 2025-11-16 17:11:05.249962 2025-11-16 17:50:40.016775 0 Ashburn Virginia United States US \N +413 209.50.169.244 3129 http \N \N t t 2025-11-16 17:50:43.520617 success 354 2025-11-16 17:11:05.262447 2025-11-16 17:50:43.520617 0 Ashburn Virginia United States US \N +378 65.111.10.196 3129 http \N \N t t 2025-11-16 17:50:27.107556 success 355 2025-11-16 17:11:05.219422 2025-11-16 17:50:27.107556 0 Ashburn Virginia United States US \N +428 216.26.225.181 3129 http \N \N t t 2025-11-16 17:50:50.055793 success 359 2025-11-16 17:11:05.280801 2025-11-16 17:50:50.055793 0 Ashburn Virginia United States US \N +381 209.50.175.221 3129 http \N \N t t 2025-11-16 17:50:28.171001 success 339 2025-11-16 17:11:05.223019 2025-11-16 17:50:28.171001 0 Ashburn Virginia United States US \N +356 104.207.39.136 3129 http \N \N t t 2025-11-16 17:50:17.941438 success 343 2025-11-16 17:11:05.190616 2025-11-16 17:50:17.941438 0 Ashburn Virginia United States US \N +389 65.111.1.21 3129 http \N \N t t 2025-11-16 17:50:32.499625 success 1621 2025-11-16 17:11:05.233169 2025-11-16 17:50:32.499625 0 Ashburn Virginia United States US \N +385 104.207.45.253 3129 http \N \N t t 2025-11-16 17:50:29.7909 success 412 2025-11-16 17:11:05.227967 2025-11-16 17:50:29.7909 0 Ashburn Virginia United States US \N +376 65.111.14.46 3129 http \N \N t t 2025-11-16 17:50:26.381616 success 342 2025-11-16 17:11:05.216932 2025-11-16 17:50:26.381616 0 Ashburn Virginia United States US \N +390 209.50.172.40 3129 http \N \N t t 2025-11-16 17:50:32.856801 success 354 2025-11-16 17:11:05.234351 2025-11-16 17:50:32.856801 0 Ashburn Virginia United States US \N +399 104.207.39.58 3129 http \N \N t t 2025-11-16 17:50:37.776553 success 341 2025-11-16 17:11:05.245028 2025-11-16 17:50:37.776553 0 Ashburn Virginia United States US \N +402 65.111.4.87 3129 http \N \N t t 2025-11-16 17:50:39.499347 success 341 2025-11-16 17:11:05.248828 2025-11-16 17:50:39.499347 0 Ashburn Virginia United States US \N +380 209.50.161.213 3129 http \N \N t t 2025-11-16 17:50:27.828072 success 357 2025-11-16 17:11:05.221896 2025-11-16 17:50:27.828072 0 Ashburn Virginia United States US \N +426 216.26.227.239 3129 http \N \N t t 2025-11-16 17:50:49.336305 success 351 2025-11-16 17:11:05.278427 2025-11-16 17:50:49.336305 0 Ashburn Virginia United States US \N +414 65.111.5.224 3129 http \N \N t t 2025-11-16 17:50:43.882763 success 359 2025-11-16 17:11:05.263818 2025-11-16 17:50:43.882763 0 Ashburn Virginia United States US \N +358 209.50.165.106 3129 http \N \N t t 2025-11-16 17:50:19.458085 success 1105 2025-11-16 17:11:05.192963 2025-11-16 17:50:19.458085 0 Ashburn Virginia United States US \N +387 104.207.46.194 3129 http \N \N t t 2025-11-16 17:50:30.494475 success 343 2025-11-16 17:11:05.23073 2025-11-16 17:50:30.494475 0 Ashburn Virginia United States US \N +430 104.207.35.112 3129 http \N \N t t 2025-11-16 17:50:50.78782 success 359 2025-11-16 17:11:05.283164 2025-11-16 17:50:50.78782 0 Ashburn Virginia United States US \N +407 209.50.165.58 3129 http \N \N t t 2025-11-16 17:50:41.420771 success 350 2025-11-16 17:11:05.255081 2025-11-16 17:50:41.420771 0 Ashburn Virginia United States US \N +416 104.207.45.118 3129 http \N \N t t 2025-11-16 17:50:45.030229 success 408 2025-11-16 17:11:05.266401 2025-11-16 17:50:45.030229 0 Ashburn Virginia United States US \N +386 104.207.32.143 3129 http \N \N t t 2025-11-16 17:50:30.148097 success 354 2025-11-16 17:11:05.229381 2025-11-16 17:50:30.148097 0 Ashburn Virginia United States US \N +447 45.3.37.109 3129 http \N \N t t 2025-11-16 17:51:00.436683 success 926 2025-11-16 17:11:05.304711 2025-11-16 17:51:00.436683 0 Ashburn Virginia United States US \N +452 209.50.174.176 3129 http \N \N t t 2025-11-16 17:51:04.2316 success 358 2025-11-16 17:11:05.311785 2025-11-16 17:51:04.2316 0 Ashburn Virginia United States US \N +508 65.111.10.162 3129 http \N \N t t 2025-11-16 17:28:08.553442 success 1388 2025-11-16 17:11:05.380227 2025-11-16 17:27:07.211181 0 Ashburn Virginia United States US \N +444 104.207.33.223 3129 http \N \N t t 2025-11-16 17:50:57.843436 success 1668 2025-11-16 17:11:05.301108 2025-11-16 17:50:57.843436 0 Ashburn Virginia United States US \N +498 216.26.238.203 3129 http \N \N t t 2025-11-16 17:51:25.125663 success 392 2025-11-16 17:11:05.368315 2025-11-16 17:51:25.125663 0 Ashburn Virginia United States US \N +464 209.50.166.102 3129 http \N \N t t 2025-11-16 17:51:09.154852 success 349 2025-11-16 17:11:05.32731 2025-11-16 17:51:09.154852 0 Ashburn Virginia United States US \N +485 65.111.7.25 3129 http \N \N t t 2025-11-16 17:51:19.27845 success 340 2025-11-16 17:11:05.352973 2025-11-16 17:51:19.27845 0 Ashburn Virginia United States US \N +513 104.207.42.77 3129 http \N \N t t 2025-11-16 17:28:04.598881 success 889 2025-11-16 17:11:05.385786 2025-11-16 17:27:12.128575 0 Ashburn Virginia United States US \N +475 209.50.168.124 3129 http \N \N t t 2025-11-16 17:51:14.918948 success 360 2025-11-16 17:11:05.34125 2025-11-16 17:51:14.918948 0 Ashburn Virginia United States US \N +478 65.111.12.216 3129 http \N \N t t 2025-11-16 17:51:15.97925 success 347 2025-11-16 17:11:05.344934 2025-11-16 17:51:15.97925 0 Ashburn Virginia United States US \N +515 104.207.41.108 3129 http \N \N t t 2025-11-16 17:28:03.332953 success 1095 2025-11-16 17:11:05.388189 2025-11-16 17:27:14.615229 0 Ashburn Virginia United States US \N +490 209.50.164.178 3129 http \N \N t t 2025-11-16 17:51:21.62663 success 349 2025-11-16 17:11:05.358634 2025-11-16 17:51:21.62663 0 Ashburn Virginia United States US \N +470 104.207.43.59 3129 http \N \N t t 2025-11-16 17:51:11.563121 success 344 2025-11-16 17:11:05.335177 2025-11-16 17:51:11.563121 0 Ashburn Virginia United States US \N +476 216.26.231.171 3129 http \N \N t t 2025-11-16 17:51:15.284147 success 362 2025-11-16 17:11:05.342469 2025-11-16 17:51:15.284147 0 Ashburn Virginia United States US \N +506 104.207.41.38 3129 http \N \N t t 2025-11-16 17:28:09.267121 success 360 2025-11-16 17:11:05.378011 2025-11-16 17:27:03.817649 0 Ashburn Virginia United States US \N +494 45.3.34.255 3129 http \N \N t t 2025-11-16 17:51:23.111826 success 349 2025-11-16 17:11:05.363254 2025-11-16 17:51:23.111826 0 Ashburn Virginia United States US \N +460 104.207.38.250 3129 http \N \N t t 2025-11-16 17:51:07.168826 success 353 2025-11-16 17:11:05.322387 2025-11-16 17:51:07.168826 0 Ashburn Virginia United States US \N +505 65.111.7.230 3129 http \N \N t t 2025-11-16 17:28:09.627813 success 353 2025-11-16 17:11:05.376883 2025-11-16 17:27:02.969058 0 Ashburn Virginia United States US \N +454 104.207.34.95 3129 http \N \N t t 2025-11-16 17:51:04.933164 success 351 2025-11-16 17:11:05.314159 2025-11-16 17:51:04.933164 0 Ashburn Virginia United States US \N +451 104.167.25.34 3129 http \N \N t t 2025-11-16 17:51:03.870101 success 341 2025-11-16 17:11:05.310134 2025-11-16 17:51:03.870101 0 Ashburn Virginia United States US \N +491 216.26.231.236 3129 http \N \N t t 2025-11-16 17:51:22.028442 success 399 2025-11-16 17:11:05.3599 2025-11-16 17:51:22.028442 0 Ashburn Virginia United States US \N +511 104.207.47.218 3129 http \N \N t t 2025-11-16 17:28:05.992519 success 1022 2025-11-16 17:11:05.383493 2025-11-16 17:27:10.432503 0 Ashburn Virginia United States US \N +500 209.50.172.41 3129 http \N \N t t 2025-11-16 17:51:25.956085 success 441 2025-11-16 17:11:05.371014 2025-11-16 17:51:25.956085 0 Ashburn Virginia United States US \N +497 45.3.36.85 3129 http \N \N t t 2025-11-16 17:51:24.729862 success 357 2025-11-16 17:11:05.367155 2025-11-16 17:51:24.729862 0 Ashburn Virginia United States US \N +463 216.26.233.2 3129 http \N \N t t 2025-11-16 17:51:08.8022 success 739 2025-11-16 17:11:05.326164 2025-11-16 17:51:08.8022 0 Ashburn Virginia United States US \N +493 216.26.231.248 3129 http \N \N t t 2025-11-16 17:51:22.75976 success 339 2025-11-16 17:11:05.362151 2025-11-16 17:51:22.75976 0 Ashburn Virginia United States US \N +449 209.50.164.228 3129 http \N \N t t 2025-11-16 17:51:03.182118 success 986 2025-11-16 17:11:05.307495 2025-11-16 17:51:03.182118 0 Ashburn Virginia United States US \N +516 216.26.236.8 3129 http \N \N t t 2025-11-16 17:28:02.228419 success 419 2025-11-16 17:11:05.389289 2025-11-16 17:27:15.498718 0 Ashburn Virginia United States US \N +495 65.111.3.179 3129 http \N \N t t 2025-11-16 17:51:24.013685 success 899 2025-11-16 17:11:05.36441 2025-11-16 17:51:24.013685 0 Ashburn Virginia United States US \N +524 104.207.34.11 3129 http \N \N t t 2025-11-16 17:27:59.011557 success 338 2025-11-16 17:11:05.398524 2025-11-16 17:27:23.508277 0 Ashburn Virginia United States US \N +453 45.3.39.61 3129 http \N \N t t 2025-11-16 17:51:04.576519 success 342 2025-11-16 17:11:05.313017 2025-11-16 17:51:04.576519 0 Ashburn Virginia United States US \N +480 216.26.226.167 3129 http \N \N t t 2025-11-16 17:51:16.669913 success 342 2025-11-16 17:11:05.347204 2025-11-16 17:51:16.669913 0 Ashburn Virginia United States US \N +472 216.26.233.43 3129 http \N \N t t 2025-11-16 17:51:13.840087 success 1910 2025-11-16 17:11:05.337434 2025-11-16 17:51:13.840087 0 Ashburn Virginia United States US \N +450 65.111.15.138 3129 http \N \N t t 2025-11-16 17:51:03.525467 success 340 2025-11-16 17:11:05.308868 2025-11-16 17:51:03.525467 0 Ashburn Virginia United States US \N +481 216.26.224.17 3129 http \N \N t t 2025-11-16 17:51:17.015488 success 343 2025-11-16 17:11:05.348415 2025-11-16 17:51:17.015488 0 Ashburn Virginia United States US \N +448 216.26.226.1 3129 http \N \N t t 2025-11-16 17:51:02.192386 success 1748 2025-11-16 17:11:05.30601 2025-11-16 17:51:02.192386 0 Ashburn Virginia United States US \N +487 216.26.226.121 3129 http \N \N t t 2025-11-16 17:51:20.425226 success 571 2025-11-16 17:11:05.355209 2025-11-16 17:51:20.425226 0 Ashburn Virginia United States US \N +488 216.26.235.230 3129 http \N \N t t 2025-11-16 17:51:20.918665 success 490 2025-11-16 17:11:05.356331 2025-11-16 17:51:20.918665 0 Ashburn Virginia United States US \N +465 216.26.227.211 3129 http \N \N t t 2025-11-16 17:51:09.512746 success 355 2025-11-16 17:11:05.32846 2025-11-16 17:51:09.512746 0 Ashburn Virginia United States US \N +518 104.207.38.165 3129 http \N \N t t 2025-11-16 17:28:01.234796 success 362 2025-11-16 17:11:05.39152 2025-11-16 17:27:17.326848 0 Ashburn Virginia United States US \N +443 45.3.37.116 3129 http \N \N t t 2025-11-16 17:50:56.172705 success 868 2025-11-16 17:11:05.299842 2025-11-16 17:50:56.172705 0 Ashburn Virginia United States US \N +457 209.50.168.51 3129 http \N \N t t 2025-11-16 17:51:05.968524 success 341 2025-11-16 17:11:05.318558 2025-11-16 17:51:05.968524 0 Ashburn Virginia United States US \N +522 65.111.6.96 3129 http \N \N t t 2025-11-16 17:27:59.784248 success 358 2025-11-16 17:11:05.396039 2025-11-16 17:27:21.141944 0 Ashburn Virginia United States US \N +462 216.26.238.38 3129 http \N \N t f 2025-11-16 17:51:08.059406 failed \N 2025-11-16 17:11:05.324954 2025-11-16 17:51:08.059406 0 Ashburn Virginia United States US \N +507 209.50.167.72 3129 http \N \N t t 2025-11-16 17:28:08.898268 success 337 2025-11-16 17:11:05.379092 2025-11-16 17:27:04.671783 0 Ashburn Virginia United States US \N +442 45.3.51.243 3129 http \N \N t t 2025-11-16 17:50:55.3006 success 359 2025-11-16 17:11:05.298421 2025-11-16 17:50:55.3006 0 Ashburn Virginia United States US \N +90 45.3.62.104 3129 http \N \N t t 2025-11-16 17:48:05.880685 success 343 2025-11-16 17:11:04.833281 2025-11-16 17:48:05.880685 0 Ashburn Virginia United States US 2025-11-17 07:20:47.39809 +482 45.3.62.188 3129 http \N \N t t 2025-11-16 17:51:17.564718 success 546 2025-11-16 17:11:05.349521 2025-11-16 17:51:17.564718 0 Ashburn Virginia United States US \N +502 104.207.32.193 3129 http \N \N t t 2025-11-16 17:51:26.661588 success 353 2025-11-16 17:11:05.37348 2025-11-16 17:51:26.661588 0 Ashburn Virginia United States US \N +445 104.207.38.6 3129 http \N \N t t 2025-11-16 17:50:58.200802 success 354 2025-11-16 17:11:05.302309 2025-11-16 17:50:58.200802 0 Ashburn Virginia United States US \N +527 104.167.25.251 3129 http \N \N t t 2025-11-16 17:27:57.279522 success 522 2025-11-16 17:11:05.402287 2025-11-16 17:27:26.359628 0 Ashburn Virginia United States US \N +559 216.26.225.196 3129 http \N \N t t 2025-11-16 17:28:00.329909 success 346 2025-11-16 17:11:05.441272 2025-11-16 17:28:00.329909 0 Ashburn Virginia United States US \N +566 104.207.36.73 3129 http \N \N t t 2025-11-16 17:28:07.872732 success 360 2025-11-16 17:11:05.449457 2025-11-16 17:28:07.872732 0 Ashburn Virginia United States US \N +568 216.26.232.108 3129 http \N \N t t 2025-11-16 17:28:09.610214 success 373 2025-11-16 17:11:05.451911 2025-11-16 17:28:09.610214 0 Ashburn Virginia United States US \N +601 65.111.8.199 3129 http \N \N t t 2025-11-16 17:27:14.386157 success 341 2025-11-16 17:11:05.491708 2025-11-16 17:11:05.491708 0 Ashburn Virginia United States US \N +582 216.26.235.114 3129 http \N \N t t 2025-11-16 17:28:24.204164 success 584 2025-11-16 17:11:05.469441 2025-11-16 17:28:24.204164 0 Ashburn Virginia United States US \N +544 45.3.50.34 3129 http \N \N t t 2025-11-16 17:27:45.832582 success 845 2025-11-16 17:11:05.42351 2025-11-16 17:27:43.803206 0 Ashburn Virginia United States US \N +542 45.3.34.56 3129 http \N \N t t 2025-11-16 17:27:47.456719 success 1083 2025-11-16 17:11:05.420978 2025-11-16 17:27:41.492803 0 Ashburn Virginia United States US \N +561 209.50.166.167 3129 http \N \N t t 2025-11-16 17:28:02.796527 success 420 2025-11-16 17:11:05.443476 2025-11-16 17:28:02.796527 0 Ashburn Virginia United States US \N +571 45.3.33.106 3129 http \N \N t t 2025-11-16 17:28:12.991138 success 763 2025-11-16 17:11:05.455699 2025-11-16 17:28:12.991138 0 Ashburn Virginia United States US \N +529 209.50.173.162 3129 http \N \N t t 2025-11-16 17:27:56.045553 success 614 2025-11-16 17:11:05.404799 2025-11-16 17:27:28.0639 0 Ashburn Virginia United States US \N +576 104.207.38.210 3129 http \N \N t t 2025-11-16 17:28:17.732662 success 754 2025-11-16 17:11:05.46159 2025-11-16 17:28:17.732662 0 Ashburn Virginia United States US \N +539 65.111.14.152 3129 http \N \N t t 2025-11-16 17:27:49.311783 success 689 2025-11-16 17:11:05.417485 2025-11-16 17:27:37.232655 0 Ashburn Virginia United States US \N +593 104.207.40.75 3129 http \N \N t t 2025-11-16 17:28:34.419141 success 351 2025-11-16 17:11:05.482325 2025-11-16 17:28:34.419141 0 Ashburn Virginia United States US \N +531 216.26.224.157 3129 http \N \N t t 2025-11-16 17:27:54.885036 success 920 2025-11-16 17:11:05.407309 2025-11-16 17:27:29.760725 0 Ashburn Virginia United States US \N +584 104.207.42.189 3129 http \N \N t t 2025-11-16 17:28:26.326131 success 746 2025-11-16 17:11:05.471819 2025-11-16 17:28:26.326131 0 Ashburn Virginia United States US \N +574 104.207.45.71 3129 http \N \N t t 2025-11-16 17:28:15.553298 success 353 2025-11-16 17:11:05.459148 2025-11-16 17:28:15.553298 0 Ashburn Virginia United States US \N +586 209.50.162.248 3129 http \N \N t t 2025-11-16 17:28:28.086646 success 400 2025-11-16 17:11:05.474163 2025-11-16 17:28:28.086646 0 Ashburn Virginia United States US \N +603 209.50.165.183 3129 http \N \N t t 2025-11-16 17:27:13.682477 success 339 2025-11-16 17:11:05.49424 2025-11-16 17:11:05.49424 0 Ashburn Virginia United States US \N +600 216.26.237.228 3129 http \N \N t t 2025-11-16 17:27:14.82809 success 433 2025-11-16 17:11:05.49006 2025-11-16 17:11:05.49006 0 Ashburn Virginia United States US \N +594 209.50.167.233 3129 http \N \N t t 2025-11-16 17:27:18.423468 success 401 2025-11-16 17:11:05.48343 2025-11-16 17:11:05.48343 0 Ashburn Virginia United States US \N +608 65.111.7.143 3129 http \N \N t t 2025-11-16 17:27:11.668712 success 1423 2025-11-16 17:11:05.500331 2025-11-16 17:11:05.500331 0 Ashburn Virginia United States US \N +532 65.111.14.175 3129 http \N \N t t 2025-11-16 17:27:53.95631 success 624 2025-11-16 17:11:05.408963 2025-11-16 17:27:30.604392 0 Ashburn Virginia United States US \N +581 104.207.44.55 3129 http \N \N t t 2025-11-16 17:28:23.115909 success 352 2025-11-16 17:11:05.468257 2025-11-16 17:28:23.115909 0 Ashburn Virginia United States US \N +533 104.167.25.133 3129 http \N \N t t 2025-11-16 17:27:53.323586 success 520 2025-11-16 17:11:05.410359 2025-11-16 17:27:31.458178 0 Ashburn Virginia United States US \N +607 216.26.233.80 3129 http \N \N t t 2025-11-16 17:27:12.115889 success 438 2025-11-16 17:11:05.499119 2025-11-16 17:11:05.499119 0 Ashburn Virginia United States US \N +587 216.26.235.172 3129 http \N \N t t 2025-11-16 17:28:28.981111 success 390 2025-11-16 17:11:05.475391 2025-11-16 17:28:28.981111 0 Ashburn Virginia United States US \N +535 104.207.44.86 3129 http \N \N t t 2025-11-16 17:27:52.183917 success 1168 2025-11-16 17:11:05.412731 2025-11-16 17:27:33.817352 0 Ashburn Virginia United States US \N +565 104.207.47.107 3129 http \N \N t t 2025-11-16 17:28:07.008501 success 1029 2025-11-16 17:11:05.448358 2025-11-16 17:28:07.008501 0 Ashburn Virginia United States US \N +537 45.3.48.217 3129 http \N \N t t 2025-11-16 17:27:50.471434 success 619 2025-11-16 17:11:05.415188 2025-11-16 17:27:35.526861 0 Ashburn Virginia United States US \N +604 45.3.33.65 3129 http \N \N t t 2025-11-16 17:27:13.33486 success 491 2025-11-16 17:11:05.495605 2025-11-16 17:11:05.495605 0 Ashburn Virginia United States US \N +611 104.207.35.151 3129 http \N \N t t 2025-11-16 17:27:09.502921 success 355 2025-11-16 17:11:05.504062 2025-11-16 17:11:05.504062 0 Ashburn Virginia United States US \N +579 45.3.39.98 3129 http \N \N t t 2025-11-16 17:28:20.95562 success 341 2025-11-16 17:11:05.465768 2025-11-16 17:28:20.95562 0 Ashburn Virginia United States US \N +558 104.207.39.68 3129 http \N \N t t 2025-11-16 17:27:59.480285 success 341 2025-11-16 17:11:05.440063 2025-11-16 17:27:59.480285 0 Ashburn Virginia United States US \N +585 65.111.4.146 3129 http \N \N t t 2025-11-16 17:28:27.183979 success 354 2025-11-16 17:11:05.472973 2025-11-16 17:28:27.183979 0 Ashburn Virginia United States US \N +557 216.26.227.49 3129 http \N \N t t 2025-11-16 17:27:58.635692 success 462 2025-11-16 17:11:05.438637 2025-11-16 17:27:58.635692 0 Ashburn Virginia United States US \N +553 45.3.37.254 3129 http \N \N t t 2025-11-16 17:27:54.189107 success 663 2025-11-16 17:11:05.43412 2025-11-16 17:27:54.189107 0 Ashburn Virginia United States US \N +548 45.3.39.158 3129 http \N \N t t 2025-11-16 17:27:48.381524 success 654 2025-11-16 17:11:05.428245 2025-11-16 17:27:48.381524 0 Ashburn Virginia United States US \N +550 45.3.39.2 3129 http \N \N t t 2025-11-16 17:27:50.702699 success 657 2025-11-16 17:11:05.430603 2025-11-16 17:27:50.702699 0 Ashburn Virginia United States US \N +563 65.111.14.232 3129 http \N \N t t 2025-11-16 17:28:04.627102 success 339 2025-11-16 17:11:05.445674 2025-11-16 17:28:04.627102 0 Ashburn Virginia United States US \N +597 104.207.38.111 3129 http \N \N t t 2025-11-16 17:27:16.810064 success 1217 2025-11-16 17:11:05.486778 2025-11-16 17:11:05.486778 0 Ashburn Virginia United States US \N +590 104.207.47.223 3129 http \N \N t t 2025-11-16 17:28:31.860417 success 344 2025-11-16 17:11:05.478642 2025-11-16 17:28:31.860417 0 Ashburn Virginia United States US \N +605 45.3.50.151 3129 http \N \N t t 2025-11-16 17:27:12.835133 success 343 2025-11-16 17:11:05.496735 2025-11-16 17:11:05.496735 0 Ashburn Virginia United States US \N +591 104.207.45.160 3129 http \N \N t t 2025-11-16 17:28:32.705178 success 340 2025-11-16 17:11:05.479798 2025-11-16 17:28:32.705178 0 Ashburn Virginia United States US \N +596 65.111.1.236 3129 http \N \N t t 2025-11-16 17:27:17.528992 success 710 2025-11-16 17:11:05.485649 2025-11-16 17:11:05.485649 0 Ashburn Virginia United States US \N +536 65.111.14.66 3129 http \N \N t t 2025-11-16 17:27:51.007636 success 527 2025-11-16 17:11:05.413975 2025-11-16 17:27:34.664326 0 Ashburn Virginia United States US \N +614 216.26.224.220 3129 http \N \N t t 2025-11-16 17:27:08.236155 success 356 2025-11-16 17:11:05.507533 2025-11-16 17:11:05.507533 0 Ashburn Virginia United States US \N +583 104.167.19.25 3129 http \N \N t t 2025-11-16 17:28:25.075593 success 368 2025-11-16 17:11:05.470605 2025-11-16 17:28:25.075593 0 Ashburn Virginia United States US \N +610 216.26.229.214 3129 http \N \N t t 2025-11-16 17:27:09.87325 success 357 2025-11-16 17:11:05.502882 2025-11-16 17:11:05.502882 0 Ashburn Virginia United States US \N +540 65.111.11.189 3129 http \N \N t t 2025-11-16 17:27:48.614807 success 608 2025-11-16 17:11:05.418608 2025-11-16 17:27:39.066671 0 Ashburn Virginia United States US \N +552 65.111.1.18 3129 http \N \N t t 2025-11-16 17:27:53.023214 success 660 2025-11-16 17:11:05.433033 2025-11-16 17:27:53.023214 0 Ashburn Virginia United States US \N +595 104.207.40.22 3129 http \N \N t t 2025-11-16 17:27:18.014435 success 476 2025-11-16 17:11:05.484529 2025-11-16 17:11:05.484529 0 Ashburn Virginia United States US \N +551 104.167.19.180 3129 http \N \N t t 2025-11-16 17:27:51.860385 success 655 2025-11-16 17:11:05.43183 2025-11-16 17:27:51.860385 0 Ashburn Virginia United States US \N +469 45.3.51.121 3129 http \N \N t t 2025-11-16 17:51:11.215559 success 352 2025-11-16 17:11:05.333661 2025-11-30 02:05:43.090696 2 Ashburn Virginia United States US \N +622 104.167.25.217 3129 http \N \N t t 2025-11-16 17:27:04.987929 success 430 2025-11-16 17:11:05.516735 2025-11-30 02:05:40.521448 2 Ashburn Virginia United States US \N +266 45.3.49.141 3129 http \N \N t t 2025-11-16 17:49:33.533617 success 373 2025-11-16 17:11:05.07529 2025-11-16 17:49:33.533617 0 Ashburn Virginia United States US \N +620 104.207.47.156 3129 http \N \N t t 2025-11-16 17:27:05.682475 success 338 2025-11-16 17:11:05.51424 2025-11-16 17:11:05.51424 0 Ashburn Virginia United States US \N +623 216.26.230.239 3129 http \N \N t t 2025-11-16 17:27:04.548499 success 341 2025-11-16 17:11:05.518234 2025-11-16 17:11:05.518234 0 Ashburn Virginia United States US \N +693 45.3.35.71 3129 http \N \N t t 2025-11-16 17:26:28.840983 success 387 2025-11-16 17:11:05.602585 2025-11-16 17:11:05.602585 0 Ashburn Virginia United States US \N +695 216.26.238.46 3129 http \N \N t t 2025-11-16 17:26:28.092636 success 529 2025-11-16 17:11:05.605216 2025-11-16 17:11:05.605216 0 Ashburn Virginia United States US \N +636 45.3.51.153 3129 http \N \N t t 2025-11-16 17:26:59.576208 success 340 2025-11-16 17:11:05.53442 2025-11-16 17:11:05.53442 0 Ashburn Virginia United States US \N +696 65.111.2.216 3129 http \N \N t t 2025-11-16 17:26:27.553388 success 357 2025-11-16 17:11:05.606611 2025-11-16 17:11:05.606611 0 Ashburn Virginia United States US \N +681 216.26.233.77 3129 http \N \N t t 2025-11-16 17:26:34.839113 success 387 2025-11-16 17:11:05.587696 2025-11-16 17:11:05.587696 0 Ashburn Virginia United States US \N +684 45.3.62.2 3129 http \N \N t t 2025-11-16 17:26:33.740329 success 540 2025-11-16 17:11:05.591253 2025-11-16 17:11:05.591253 0 Ashburn Virginia United States US \N +691 209.50.175.74 3129 http \N \N t t 2025-11-16 17:26:30.514251 success 899 2025-11-16 17:11:05.59915 2025-11-16 17:11:05.59915 0 Ashburn Virginia United States US \N +683 65.111.0.236 3129 http \N \N t t 2025-11-16 17:26:34.091556 success 342 2025-11-16 17:11:05.590041 2025-11-16 17:11:05.590041 0 Ashburn Virginia United States US \N +650 104.167.25.54 3129 http \N \N t t 2025-11-16 17:26:50.99568 success 1584 2025-11-16 17:11:05.550229 2025-11-16 17:11:05.550229 0 Ashburn Virginia United States US \N +666 104.207.32.155 3129 http \N \N t t 2025-11-16 17:26:42.925261 success 340 2025-11-16 17:11:05.569566 2025-11-16 17:11:05.569566 0 Ashburn Virginia United States US \N +642 216.26.237.242 3129 http \N \N t t 2025-11-16 17:26:55.677126 success 385 2025-11-16 17:11:05.541203 2025-11-16 17:11:05.541203 0 Ashburn Virginia United States US \N +638 65.111.2.150 3129 http \N \N t t 2025-11-16 17:26:58.389161 success 337 2025-11-16 17:11:05.536689 2025-11-16 17:11:05.536689 0 Ashburn Virginia United States US \N +652 104.207.45.106 3129 http \N \N t t 2025-11-16 17:26:49.043142 success 377 2025-11-16 17:11:05.552469 2025-11-16 17:11:05.552469 0 Ashburn Virginia United States US \N +625 104.207.40.124 3129 http \N \N t t 2025-11-16 17:27:03.638106 success 357 2025-11-16 17:11:05.520626 2025-11-16 17:11:05.520626 0 Ashburn Virginia United States US \N +699 216.26.235.35 3129 http \N \N t t 2025-11-16 17:26:26.409951 success 377 2025-11-16 17:11:05.610847 2025-11-16 17:11:05.610847 0 Ashburn Virginia United States US \N +630 104.167.25.226 3129 http \N \N t t 2025-11-16 17:27:01.764079 success 346 2025-11-16 17:11:05.527205 2025-11-16 17:11:05.527205 0 Ashburn Virginia United States US \N +654 209.50.169.249 3129 http \N \N t t 2025-11-16 17:26:48.294837 success 342 2025-11-16 17:11:05.554579 2025-11-16 17:11:05.554579 0 Ashburn Virginia United States US \N +648 104.207.47.17 3129 http \N \N t t 2025-11-16 17:26:52.497968 success 783 2025-11-16 17:11:05.547914 2025-11-16 17:11:05.547914 0 Ashburn Virginia United States US \N +662 65.111.9.167 3129 http \N \N t t 2025-11-16 17:26:44.368015 success 365 2025-11-16 17:11:05.56377 2025-11-16 17:11:05.56377 0 Ashburn Virginia United States US \N +631 45.3.62.26 3129 http \N \N t t 2025-11-16 17:27:01.406156 success 399 2025-11-16 17:11:05.528501 2025-11-16 17:11:05.528501 0 Ashburn Virginia United States US \N +692 216.26.224.161 3129 http \N \N t t 2025-11-16 17:26:29.605032 success 755 2025-11-16 17:11:05.600887 2025-11-16 17:11:05.600887 0 Ashburn Virginia United States US \N +670 104.207.38.78 3129 http \N \N t t 2025-11-16 17:26:41.408901 success 420 2025-11-16 17:11:05.574397 2025-11-16 17:11:05.574397 0 Ashburn Virginia United States US \N +660 65.111.3.93 3129 http \N \N t t 2025-11-16 17:26:45.660263 success 356 2025-11-16 17:11:05.561459 2025-11-16 17:11:05.561459 0 Ashburn Virginia United States US \N +669 216.26.236.10 3129 http \N \N t t 2025-11-16 17:26:41.846652 success 429 2025-11-16 17:11:05.573338 2025-11-16 17:11:05.573338 0 Ashburn Virginia United States US \N +697 45.3.49.181 3129 http \N \N t \N 2025-11-16 17:26:27.184767 failed \N 2025-11-16 17:11:05.608106 2025-11-16 17:11:05.608106 0 Ashburn Virginia United States US \N +677 216.26.229.242 3129 http \N \N t t 2025-11-16 17:26:38.782477 success 2056 2025-11-16 17:11:05.582959 2025-11-16 17:11:05.582959 0 Ashburn Virginia United States US \N +649 65.111.12.139 3129 http \N \N t t 2025-11-16 17:26:51.706139 success 701 2025-11-16 17:11:05.549083 2025-11-16 17:11:05.549083 0 Ashburn Virginia United States US \N +644 216.26.230.131 3129 http \N \N t t 2025-11-16 17:26:54.92007 success 951 2025-11-16 17:11:05.543484 2025-11-16 17:11:05.543484 0 Ashburn Virginia United States US \N +618 104.207.37.53 3129 http \N \N t t 2025-11-16 17:27:06.410735 success 356 2025-11-16 17:11:05.512152 2025-11-16 17:11:05.512152 0 Ashburn Virginia United States US \N +685 45.3.49.217 3129 http \N \N t t 2025-11-16 17:26:33.191852 success 343 2025-11-16 17:11:05.592439 2025-11-16 17:11:05.592439 0 Ashburn Virginia United States US \N +643 45.3.35.108 3129 http \N \N t t 2025-11-16 17:26:55.282908 success 353 2025-11-16 17:11:05.542251 2025-11-16 17:11:05.542251 0 Ashburn Virginia United States US \N +651 65.111.5.113 3129 http \N \N t t 2025-11-16 17:26:49.401114 success 348 2025-11-16 17:11:05.551368 2025-11-16 17:11:05.551368 0 Ashburn Virginia United States US \N +655 209.50.164.253 3129 http \N \N t t 2025-11-16 17:26:47.943504 success 698 2025-11-16 17:11:05.555811 2025-11-16 17:11:05.555811 0 Ashburn Virginia United States US \N +688 209.50.160.17 3129 http \N \N t t 2025-11-16 17:26:31.603118 success 359 2025-11-16 17:11:05.595783 2025-11-16 17:11:05.595783 0 Ashburn Virginia United States US \N +633 209.50.174.118 3129 http \N \N t t 2025-11-16 17:27:00.631032 success 346 2025-11-16 17:11:05.53091 2025-11-16 17:11:05.53091 0 Ashburn Virginia United States US \N +626 65.111.14.98 3129 http \N \N t t 2025-11-16 17:27:03.271927 success 352 2025-11-16 17:11:05.522257 2025-11-16 17:11:05.522257 0 Ashburn Virginia United States US \N +694 65.111.4.51 3129 http \N \N t t 2025-11-16 17:26:28.44447 success 342 2025-11-16 17:11:05.603969 2025-11-16 17:11:05.603969 0 Ashburn Virginia United States US \N +702 65.111.7.77 3129 http \N \N t t 2025-11-16 17:26:25.26215 success 342 2025-11-16 17:11:05.614957 2025-11-16 17:11:05.614957 0 Ashburn Virginia United States US \N +679 209.50.167.22 3129 http \N \N t t 2025-11-16 17:26:36.352157 success 440 2025-11-16 17:11:05.585468 2025-11-16 17:11:05.585468 0 Ashburn Virginia United States US \N +621 209.50.169.240 3129 http \N \N t t 2025-11-16 17:27:05.336082 success 339 2025-11-16 17:11:05.51527 2025-11-16 17:11:05.51527 0 Ashburn Virginia United States US \N +619 65.111.9.122 3129 http \N \N t t 2025-11-16 17:27:06.044893 success 354 2025-11-16 17:11:05.513241 2025-11-16 17:11:05.513241 0 Ashburn Virginia United States US \N +686 209.50.173.29 3129 http \N \N t t 2025-11-16 17:26:32.840952 success 868 2025-11-16 17:11:05.593542 2025-11-16 17:11:05.593542 0 Ashburn Virginia United States US \N +658 104.207.45.225 3129 http \N \N t t 2025-11-16 17:26:46.3812 success 364 2025-11-16 17:11:05.559021 2025-11-16 17:11:05.559021 0 Ashburn Virginia United States US \N +665 104.207.32.254 3129 http \N \N t t 2025-11-16 17:26:43.291552 success 356 2025-11-16 17:11:05.568213 2025-11-16 17:11:05.568213 0 Ashburn Virginia United States US \N +675 104.207.45.221 3129 http \N \N t t 2025-11-16 17:26:39.541254 success 360 2025-11-16 17:11:05.580099 2025-11-16 17:11:05.580099 0 Ashburn Virginia United States US \N +624 104.207.39.150 3129 http \N \N t t 2025-11-16 17:27:04.199252 success 552 2025-11-16 17:11:05.519504 2025-11-16 17:11:05.519504 0 Ashburn Virginia United States US \N +674 104.207.32.225 3129 http \N \N t t 2025-11-16 17:26:39.916275 success 366 2025-11-16 17:11:05.578998 2025-11-16 17:11:05.578998 0 Ashburn Virginia United States US \N +698 104.207.34.210 3129 http \N \N t t 2025-11-16 17:26:26.77103 success 350 2025-11-16 17:11:05.60947 2025-11-16 17:11:05.60947 0 Ashburn Virginia United States US \N +749 104.167.19.126 3129 http \N \N t t 2025-11-16 17:26:01.325175 success 520 2025-11-16 17:11:05.67378 2025-11-16 17:11:05.67378 0 Ashburn Virginia United States US \N +762 209.50.162.173 3129 http \N \N t t 2025-11-16 17:25:53.563323 success 1058 2025-11-16 17:11:05.68911 2025-11-30 02:05:40.079185 2 Ashburn Virginia United States US \N +710 104.207.45.179 3129 http \N \N t t 2025-11-16 17:26:22.238213 success 337 2025-11-16 17:11:05.624515 2025-11-30 02:05:42.656947 2 Ashburn Virginia United States US \N +733 104.167.19.182 3129 http \N \N t t 2025-11-16 17:26:09.870126 success 356 2025-11-16 17:11:05.654769 2025-11-16 17:11:05.654769 0 Ashburn Virginia United States US \N +787 104.167.25.171 3129 http \N \N t t 2025-11-16 17:25:41.989386 success 346 2025-11-16 17:11:05.719429 2025-11-16 17:11:05.719429 0 Ashburn Virginia United States US \N +757 104.207.36.107 3129 http \N \N t t 2025-11-16 17:25:55.483629 success 356 2025-11-16 17:11:05.683077 2025-11-16 17:11:05.683077 0 Ashburn Virginia United States US \N +771 104.207.38.213 3129 http \N \N t t 2025-11-16 17:25:49.010159 success 341 2025-11-16 17:11:05.699764 2025-11-16 17:11:05.699764 0 Ashburn Virginia United States US \N +754 104.207.41.42 3129 http \N \N t t 2025-11-16 17:25:56.607318 success 345 2025-11-16 17:11:05.679144 2025-11-16 17:11:05.679144 0 Ashburn Virginia United States US \N +738 104.207.42.141 3129 http \N \N t t 2025-11-16 17:26:07.157194 success 810 2025-11-16 17:11:05.661115 2025-11-16 17:11:05.661115 0 Ashburn Virginia United States US \N +746 104.207.43.150 3129 http \N \N t t 2025-11-16 17:26:02.448194 success 358 2025-11-16 17:11:05.67048 2025-11-16 17:11:05.67048 0 Ashburn Virginia United States US \N +745 104.207.44.220 3129 http \N \N t t 2025-11-16 17:26:02.797858 success 341 2025-11-16 17:11:05.669292 2025-11-16 17:11:05.669292 0 Ashburn Virginia United States US \N +773 104.207.45.59 3129 http \N \N t t 2025-11-16 17:25:48.245563 success 829 2025-11-16 17:11:05.703054 2025-11-16 17:11:05.703054 0 Ashburn Virginia United States US \N +720 193.56.28.92 3129 http \N \N t t 2025-11-16 17:26:17.093325 success 1089 2025-11-16 17:11:05.637916 2025-11-16 17:11:05.637916 0 Ashburn Virginia United States US \N +721 209.50.160.127 3129 http \N \N t t 2025-11-16 17:26:15.992982 success 564 2025-11-16 17:11:05.639169 2025-11-16 17:11:05.639169 0 Ashburn Virginia United States US \N +724 209.50.160.250 3129 http \N \N t t 2025-11-16 17:26:14.373682 success 391 2025-11-16 17:11:05.643109 2025-11-16 17:11:05.643109 0 Ashburn Virginia United States US \N +715 209.50.160.77 3129 http \N \N t t 2025-11-16 17:26:19.650872 success 377 2025-11-16 17:11:05.630491 2025-11-16 17:11:05.630491 0 Ashburn Virginia United States US \N +714 209.50.163.228 3129 http \N \N t t 2025-11-16 17:26:20.210869 success 551 2025-11-16 17:11:05.629175 2025-11-16 17:11:05.629175 0 Ashburn Virginia United States US \N +791 209.50.166.25 3129 http \N \N t t 2025-11-16 17:25:40.576921 success 341 2025-11-16 17:11:05.724912 2025-11-16 17:11:05.724912 0 Ashburn Virginia United States US \N +767 209.50.167.36 3129 http \N \N t t 2025-11-16 17:25:51.016905 success 374 2025-11-16 17:11:05.694679 2025-11-16 17:11:05.694679 0 Ashburn Virginia United States US \N +761 209.50.171.114 3129 http \N \N t t 2025-11-16 17:25:53.920737 success 347 2025-11-16 17:11:05.687998 2025-11-16 17:11:05.687998 0 Ashburn Virginia United States US \N +740 209.50.172.109 3129 http \N \N t t 2025-11-16 17:26:05.234993 success 353 2025-11-16 17:11:05.663335 2025-11-16 17:11:05.663335 0 Ashburn Virginia United States US \N +705 209.50.174.218 3129 http \N \N t t 2025-11-16 17:26:24.17241 success 360 2025-11-16 17:11:05.618533 2025-11-16 17:11:05.618533 0 Ashburn Virginia United States US \N +775 209.50.174.42 3129 http \N \N t t 2025-11-16 17:25:47.024196 success 341 2025-11-16 17:11:05.705698 2025-11-16 17:11:05.705698 0 Ashburn Virginia United States US \N +778 216.26.224.148 3129 http \N \N t t 2025-11-16 17:25:45.938356 success 339 2025-11-16 17:11:05.709343 2025-11-16 17:11:05.709343 0 Ashburn Virginia United States US \N +742 65.111.9.166 3129 http \N \N t t 2025-11-16 17:26:04.499457 success 346 2025-11-16 17:11:05.665591 2025-11-16 17:11:05.665591 0 Ashburn Virginia United States US \N +736 45.3.62.169 3129 http \N \N t t 2025-11-16 17:26:07.92753 success 346 2025-11-16 17:11:05.658478 2025-11-16 17:11:05.658478 0 Ashburn Virginia United States US \N +713 216.26.224.167 3129 http \N \N t t 2025-11-16 17:26:20.570066 success 350 2025-11-16 17:11:05.627945 2025-11-16 17:11:05.627945 0 Ashburn Virginia United States US \N +750 65.111.3.141 3129 http \N \N t t 2025-11-16 17:26:00.795102 success 1677 2025-11-16 17:11:05.674855 2025-11-16 17:11:05.674855 0 Ashburn Virginia United States US \N +711 216.26.225.139 3129 http \N \N t t 2025-11-16 17:26:21.891146 success 952 2025-11-16 17:11:05.625666 2025-11-16 17:11:05.625666 0 Ashburn Virginia United States US \N +712 216.26.227.181 3129 http \N \N t t 2025-11-16 17:26:20.929438 success 349 2025-11-16 17:11:05.62679 2025-11-16 17:11:05.62679 0 Ashburn Virginia United States US \N +744 216.26.235.162 3129 http \N \N t t 2025-11-16 17:26:03.199431 success 393 2025-11-16 17:11:05.667921 2025-11-16 17:11:05.667921 0 Ashburn Virginia United States US \N +728 45.3.50.133 3129 http \N \N t t 2025-11-16 17:26:12.707449 success 353 2025-11-16 17:11:05.648273 2025-11-16 17:11:05.648273 0 Ashburn Virginia United States US \N +781 216.26.232.110 3129 http \N \N t t 2025-11-16 17:25:44.659016 success 390 2025-11-16 17:11:05.712623 2025-11-16 17:11:05.712623 0 Ashburn Virginia United States US \N +755 216.26.235.170 3129 http \N \N t t 2025-11-16 17:25:56.249857 success 398 2025-11-16 17:11:05.680423 2025-11-16 17:11:05.680423 0 Ashburn Virginia United States US \N +739 65.111.5.184 3129 http \N \N t t 2025-11-16 17:26:06.338037 success 1090 2025-11-16 17:11:05.662236 2025-11-16 17:11:05.662236 0 Ashburn Virginia United States US \N +758 216.26.236.151 3129 http \N \N t t 2025-11-16 17:25:55.115992 success 464 2025-11-16 17:11:05.684288 2025-11-16 17:11:05.684288 0 Ashburn Virginia United States US \N +769 216.26.238.15 3129 http \N \N t t 2025-11-16 17:25:50.15072 success 407 2025-11-16 17:11:05.697199 2025-11-16 17:11:05.697199 0 Ashburn Virginia United States US \N +790 65.111.2.168 3129 http \N \N t t 2025-11-16 17:25:40.929701 success 343 2025-11-16 17:11:05.723807 2025-11-16 17:11:05.723807 0 Ashburn Virginia United States US \N +786 65.111.9.14 3129 http \N \N t t 2025-11-16 17:25:42.341058 success 343 2025-11-16 17:11:05.71802 2025-11-16 17:11:05.71802 0 Ashburn Virginia United States US \N +737 216.26.238.22 3129 http \N \N t t 2025-11-16 17:26:07.56831 success 400 2025-11-16 17:11:05.659907 2025-11-16 17:11:05.659907 0 Ashburn Virginia United States US \N +765 65.111.11.182 3129 http \N \N t t 2025-11-16 17:25:51.736005 success 344 2025-11-16 17:11:05.692385 2025-11-16 17:11:05.692385 0 Ashburn Virginia United States US \N +753 45.3.33.132 3129 http \N \N t t 2025-11-16 17:25:56.963095 success 344 2025-11-16 17:11:05.678049 2025-11-16 17:11:05.678049 0 Ashburn Virginia United States US \N +763 216.26.238.224 3129 http \N \N t t 2025-11-16 17:25:52.494895 success 380 2025-11-16 17:11:05.690206 2025-11-16 17:11:05.690206 0 Ashburn Virginia United States US \N +768 45.3.34.75 3129 http \N \N t t 2025-11-16 17:25:50.635362 success 472 2025-11-16 17:11:05.696178 2025-11-16 17:11:05.696178 0 Ashburn Virginia United States US \N +764 45.3.48.142 3129 http \N \N t t 2025-11-16 17:25:52.103725 success 356 2025-11-16 17:11:05.6913 2025-11-16 17:11:05.6913 0 Ashburn Virginia United States US \N +730 65.111.12.103 3129 http \N \N t t 2025-11-16 17:26:11.992637 success 369 2025-11-16 17:11:05.650866 2025-11-16 17:11:05.650866 0 Ashburn Virginia United States US \N +726 45.3.35.104 3129 http \N \N t t 2025-11-16 17:26:13.604497 success 411 2025-11-16 17:11:05.645687 2025-11-16 17:11:05.645687 0 Ashburn Virginia United States US \N +716 45.3.35.179 3129 http \N \N t t 2025-11-16 17:26:19.264009 success 336 2025-11-16 17:11:05.631775 2025-11-16 17:11:05.631775 0 Ashburn Virginia United States US \N +780 45.3.48.46 3129 http \N \N t t 2025-11-16 17:25:45.228625 success 561 2025-11-16 17:11:05.711511 2025-11-16 17:11:05.711511 0 Ashburn Virginia United States US \N +72 104.207.34.222 3129 http \N \N t t 2025-11-16 17:47:55.310889 success 527 2025-11-16 17:11:04.810005 2025-11-16 17:47:55.310889 0 Ashburn Virginia United States US 2025-11-17 07:20:43.951746 +708 65.111.1.187 3129 http \N \N t t 2025-11-16 17:26:23.073766 success 354 2025-11-16 17:11:05.622181 2025-11-16 17:11:05.622181 0 Ashburn Virginia United States US \N +784 45.3.62.148 3129 http \N \N t t 2025-11-16 17:25:43.398672 success 681 2025-11-16 17:11:05.715869 2025-11-16 17:11:05.715869 0 Ashburn Virginia United States US \N +729 65.111.10.117 3129 http \N \N t t 2025-11-16 17:26:12.343925 success 343 2025-11-16 17:11:05.649501 2025-11-16 17:11:05.649501 0 Ashburn Virginia United States US \N +799 104.167.19.121 3129 http \N \N t t 2025-11-16 17:25:37.552566 success 353 2025-11-16 17:11:05.73454 2025-11-16 17:11:05.73454 0 Ashburn Virginia United States US \N +829 104.207.34.10 3129 http \N \N t t 2025-11-16 17:25:22.674772 success 353 2025-11-16 17:11:05.769192 2025-11-16 17:11:05.769192 0 Ashburn Virginia United States US \N +845 104.207.37.239 3129 http \N \N t t 2025-11-16 17:25:13.404562 success 359 2025-11-16 17:11:05.786635 2025-11-16 17:11:05.786635 0 Ashburn Virginia United States US \N +797 104.207.38.107 3129 http \N \N t t 2025-11-16 17:25:38.268408 success 342 2025-11-16 17:11:05.732463 2025-11-16 17:11:05.732463 0 Ashburn Virginia United States US \N +794 104.207.39.92 3129 http \N \N t t 2025-11-16 17:25:39.510835 success 353 2025-11-16 17:11:05.72893 2025-11-16 17:11:05.72893 0 Ashburn Virginia United States US \N +818 104.207.40.142 3129 http \N \N t t 2025-11-16 17:25:28.849441 success 363 2025-11-16 17:11:05.755526 2025-11-16 17:11:05.755526 0 Ashburn Virginia United States US \N +798 104.207.40.210 3129 http \N \N t t 2025-11-16 17:25:37.917894 success 357 2025-11-16 17:11:05.733487 2025-11-16 17:11:05.733487 0 Ashburn Virginia United States US \N +807 104.207.43.116 3129 http \N \N t t 2025-11-16 17:25:34.464128 success 340 2025-11-16 17:11:05.743173 2025-11-16 17:11:05.743173 0 Ashburn Virginia United States US \N +809 104.207.43.43 3129 http \N \N t t 2025-11-16 17:25:33.760958 success 441 2025-11-16 17:11:05.745375 2025-11-16 17:11:05.745375 0 Ashburn Virginia United States US \N +838 104.207.45.38 3129 http \N \N t t 2025-11-16 17:25:17.373763 success 342 2025-11-16 17:11:05.778932 2025-11-16 17:11:05.778932 0 Ashburn Virginia United States US \N +837 193.56.28.105 3129 http \N \N t t 2025-11-16 17:25:17.722934 success 340 2025-11-16 17:11:05.777919 2025-11-16 17:11:05.777919 0 Ashburn Virginia United States US \N +825 209.50.160.152 3129 http \N \N t t 2025-11-16 17:25:24.87605 success 355 2025-11-16 17:11:05.763835 2025-11-16 17:11:05.763835 0 Ashburn Virginia United States US \N +850 209.50.162.208 3129 http \N \N t t 2025-11-16 17:25:11.027795 success 348 2025-11-16 17:11:05.792173 2025-11-16 17:11:05.792173 0 Ashburn Virginia United States US \N +853 209.50.163.149 3129 http \N \N t t 2025-11-16 17:25:09.956012 success 608 2025-11-16 17:11:05.795877 2025-11-16 17:11:05.795877 0 Ashburn Virginia United States US \N +805 209.50.163.77 3129 http \N \N t t 2025-11-16 17:25:35.387667 success 536 2025-11-16 17:11:05.741168 2025-11-16 17:11:05.741168 0 Ashburn Virginia United States US \N +872 209.50.164.90 3129 http \N \N t t 2025-11-16 17:24:58.475878 success 341 2025-11-16 17:11:05.817782 2025-11-16 17:11:05.817782 0 Ashburn Virginia United States US \N +822 209.50.165.22 3129 http \N \N t t 2025-11-16 17:25:26.783375 success 366 2025-11-16 17:11:05.760367 2025-11-16 17:11:05.760367 0 Ashburn Virginia United States US \N +846 209.50.166.176 3129 http \N \N t t 2025-11-16 17:25:13.037301 success 355 2025-11-16 17:11:05.787817 2025-11-16 17:11:05.787817 0 Ashburn Virginia United States US \N +864 209.50.167.39 3129 http \N \N t t 2025-11-16 17:25:01.834186 success 792 2025-11-16 17:11:05.808534 2025-11-16 17:11:05.808534 0 Ashburn Virginia United States US \N +876 209.50.168.213 3129 http \N \N t t 2025-11-16 17:24:56.759795 success 1495 2025-11-16 17:11:05.822855 2025-11-16 17:11:05.822855 0 Ashburn Virginia United States US \N +875 209.50.169.206 3129 http \N \N t t 2025-11-16 17:24:57.166014 success 397 2025-11-16 17:11:05.821673 2025-11-16 17:11:05.821673 0 Ashburn Virginia United States US \N +879 209.50.170.63 3129 http \N \N t t 2025-11-16 17:24:54.52588 success 640 2025-11-16 17:11:05.826469 2025-11-16 17:11:05.826469 0 Ashburn Virginia United States US \N +811 216.26.226.250 3129 http \N \N t t 2025-11-16 17:25:32.956378 success 357 2025-11-16 17:11:05.747566 2025-11-16 17:11:05.747566 0 Ashburn Virginia United States US \N +833 65.111.6.100 3129 http \N \N t t 2025-11-16 17:25:20.43396 success 764 2025-11-16 17:11:05.773413 2025-11-16 17:11:05.773413 0 Ashburn Virginia United States US \N +855 65.111.13.88 3129 http \N \N t t 2025-11-16 17:25:08.98214 success 842 2025-11-16 17:11:05.798048 2025-11-16 17:11:05.798048 0 Ashburn Virginia United States US \N +816 45.3.48.51 3129 http \N \N t t 2025-11-16 17:25:29.867007 success 359 2025-11-16 17:11:05.753266 2025-11-16 17:11:05.753266 0 Ashburn Virginia United States US \N +843 209.50.173.240 3129 http \N \N t t 2025-11-16 17:25:14.159584 success 385 2025-11-16 17:11:05.784414 2025-11-16 17:11:05.784414 0 Ashburn Virginia United States US \N +800 216.26.229.183 3129 http \N \N t t 2025-11-16 17:25:37.191197 success 352 2025-11-16 17:11:05.735683 2025-11-16 17:11:05.735683 0 Ashburn Virginia United States US \N +848 45.3.35.195 3129 http \N \N t t 2025-11-16 17:25:12.32467 success 341 2025-11-16 17:11:05.789925 2025-11-16 17:11:05.789925 0 Ashburn Virginia United States US \N +854 209.50.174.136 3129 http \N \N t t 2025-11-16 17:25:09.339124 success 347 2025-11-16 17:11:05.796968 2025-11-16 17:11:05.796968 0 Ashburn Virginia United States US \N +839 216.26.231.118 3129 http \N \N t t 2025-11-16 17:25:17.022255 success 358 2025-11-16 17:11:05.780011 2025-11-16 17:11:05.780011 0 Ashburn Virginia United States US \N +814 209.50.174.213 3129 http \N \N t t 2025-11-16 17:25:31.860356 success 851 2025-11-16 17:11:05.750968 2025-11-16 17:11:05.750968 0 Ashburn Virginia United States US \N +795 65.111.2.97 3129 http \N \N t t 2025-11-16 17:25:39.147994 success 512 2025-11-16 17:11:05.730128 2025-11-16 17:11:05.730128 0 Ashburn Virginia United States US \N +793 216.26.225.130 3129 http \N \N t t 2025-11-16 17:25:39.871349 success 351 2025-11-16 17:11:05.727531 2025-11-16 17:11:05.727531 0 Ashburn Virginia United States US \N +866 216.26.226.112 3129 http \N \N t t 2025-11-16 17:25:00.669387 success 363 2025-11-16 17:11:05.810807 2025-11-16 17:11:05.810807 0 Ashburn Virginia United States US \N +802 65.111.0.74 3129 http \N \N t t 2025-11-16 17:25:36.467585 success 343 2025-11-16 17:11:05.737972 2025-11-16 17:11:05.737972 0 Ashburn Virginia United States US \N +842 65.111.11.181 3129 http \N \N t t 2025-11-16 17:25:15.289825 success 1121 2025-11-16 17:11:05.783335 2025-11-16 17:11:05.783335 0 Ashburn Virginia United States US \N +851 216.26.227.66 3129 http \N \N t t 2025-11-16 17:25:10.670977 success 345 2025-11-16 17:11:05.793262 2025-11-16 17:11:05.793262 0 Ashburn Virginia United States US \N +859 45.3.38.220 3129 http \N \N t t 2025-11-16 17:25:07.036906 success 351 2025-11-16 17:11:05.80274 2025-11-16 17:11:05.80274 0 Ashburn Virginia United States US \N +820 216.26.231.229 3129 http \N \N t t 2025-11-16 17:25:27.511618 success 369 2025-11-16 17:11:05.758028 2025-11-16 17:11:05.758028 0 Ashburn Virginia United States US \N +863 65.111.14.19 3129 http \N \N t t 2025-11-16 17:25:02.37314 success 529 2025-11-16 17:11:05.807496 2025-11-16 17:11:05.807496 0 Ashburn Virginia United States US \N +835 45.3.39.4 3129 http \N \N t t 2025-11-16 17:25:19.296926 success 1203 2025-11-16 17:11:05.775686 2025-11-16 17:11:05.775686 0 Ashburn Virginia United States US \N +862 45.3.48.187 3129 http \N \N t t 2025-11-16 17:25:05.2373 success 2855 2025-11-16 17:11:05.80646 2025-11-16 17:11:05.80646 0 Ashburn Virginia United States US \N +841 65.111.5.16 3129 http \N \N t t 2025-11-16 17:25:15.659101 success 360 2025-11-16 17:11:05.782227 2025-11-16 17:11:05.782227 0 Ashburn Virginia United States US \N +858 65.111.3.143 3129 http \N \N t t 2025-11-16 17:25:07.406264 success 360 2025-11-16 17:11:05.801611 2025-11-16 17:11:05.801611 0 Ashburn Virginia United States US \N +824 65.111.6.194 3129 http \N \N t t 2025-11-16 17:25:25.846635 success 961 2025-11-16 17:11:05.762628 2025-11-16 17:11:05.762628 0 Ashburn Virginia United States US \N +844 45.3.49.90 3129 http \N \N t t 2025-11-16 17:25:13.765432 success 352 2025-11-16 17:11:05.785555 2025-11-16 17:11:05.785555 0 Ashburn Virginia United States US \N +832 216.26.238.160 3129 http \N \N t t 2025-11-16 17:25:20.823312 success 381 2025-11-16 17:11:05.772333 2025-11-16 17:11:05.772333 0 Ashburn Virginia United States US \N +823 45.3.49.100 3129 http \N \N t t 2025-11-16 17:25:26.408934 success 554 2025-11-16 17:11:05.761436 2025-11-16 17:11:05.761436 0 Ashburn Virginia United States US \N +861 65.111.13.177 3129 http \N \N t t 2025-11-16 17:25:06.266636 success 1021 2025-11-16 17:11:05.805089 2025-11-16 17:11:05.805089 0 Ashburn Virginia United States US \N +12 104.207.39.113 3129 http \N \N t t 2025-11-16 17:47:24.678634 success 1719 2025-11-16 17:11:04.724885 2025-11-16 17:47:24.678634 0 Ashburn Virginia United States US 2025-11-17 07:19:30.666547 +834 65.111.7.110 3129 http \N \N t t 2025-11-16 17:25:19.66062 success 353 2025-11-16 17:11:05.774521 2025-11-16 17:11:05.774521 0 Ashburn Virginia United States US \N +847 65.111.8.37 3129 http \N \N t t 2025-11-16 17:25:12.674064 success 341 2025-11-16 17:11:05.788827 2025-11-16 17:11:05.788827 0 Ashburn Virginia United States US \N +915 104.207.32.163 3129 http \N \N t t 2025-11-16 17:24:36.061625 success 408 2025-11-16 17:11:05.869163 2025-11-30 02:05:40.98516 2 Ashburn Virginia United States US \N +946 104.167.19.231 3129 http \N \N t t 2025-11-16 17:24:18.943428 success 369 2025-11-16 17:11:05.906187 2025-11-16 17:11:05.906187 0 Ashburn Virginia United States US \N +911 104.207.32.78 3129 http \N \N t t 2025-11-16 17:24:37.559699 success 340 2025-11-16 17:11:05.864898 2025-11-16 17:11:05.864898 0 Ashburn Virginia United States US \N +882 104.207.33.117 3129 http \N \N t t 2025-11-16 17:24:53.160731 success 917 2025-11-16 17:11:05.829732 2025-11-16 17:11:05.829732 0 Ashburn Virginia United States US \N +893 104.207.35.54 3129 http \N \N t t 2025-11-16 17:24:46.773069 success 940 2025-11-16 17:11:05.843193 2025-11-16 17:11:05.843193 0 Ashburn Virginia United States US \N +922 104.207.35.78 3129 http \N \N t t 2025-11-16 17:24:33.32477 success 358 2025-11-16 17:11:05.878029 2025-11-16 17:11:05.878029 0 Ashburn Virginia United States US \N +914 104.207.39.51 3129 http \N \N t t 2025-11-16 17:24:36.478327 success 359 2025-11-16 17:11:05.868104 2025-11-16 17:11:05.868104 0 Ashburn Virginia United States US \N +954 104.207.40.91 3129 http \N \N t t 2025-11-16 17:24:14.627783 success 645 2025-11-16 17:11:05.915092 2025-11-16 17:11:05.915092 0 Ashburn Virginia United States US \N +912 104.207.41.74 3129 http \N \N t t 2025-11-16 17:24:37.210711 success 356 2025-11-16 17:11:05.865943 2025-11-16 17:11:05.865943 0 Ashburn Virginia United States US \N +896 104.207.43.213 3129 http \N \N t t 2025-11-16 17:24:44.58271 success 568 2025-11-16 17:11:05.846688 2025-11-16 17:11:05.846688 0 Ashburn Virginia United States US \N +902 104.207.43.76 3129 http \N \N t t 2025-11-16 17:24:42.178459 success 347 2025-11-16 17:11:05.853618 2025-11-16 17:11:05.853618 0 Ashburn Virginia United States US \N +960 104.207.44.15 3129 http \N \N t t 2025-11-16 17:24:10.456397 success 339 2025-11-16 17:11:05.921936 2025-11-16 17:11:05.921936 0 Ashburn Virginia United States US \N +945 104.207.45.192 3129 http \N \N t t 2025-11-16 17:24:19.29444 success 343 2025-11-16 17:11:05.905154 2025-11-16 17:11:05.905154 0 Ashburn Virginia United States US \N +887 104.207.46.222 3129 http \N \N t t 2025-11-16 17:24:48.993007 success 356 2025-11-16 17:11:05.836323 2025-11-16 17:11:05.836323 0 Ashburn Virginia United States US \N +881 193.56.28.34 3129 http \N \N t t 2025-11-16 17:24:53.511906 success 341 2025-11-16 17:11:05.828625 2025-11-16 17:11:05.828625 0 Ashburn Virginia United States US \N +928 209.50.161.209 3129 http \N \N t t 2025-11-16 17:24:30.215045 success 337 2025-11-16 17:11:05.88568 2025-11-16 17:11:05.88568 0 Ashburn Virginia United States US \N +938 209.50.161.37 3129 http \N \N t t 2025-11-16 17:24:23.46619 success 369 2025-11-16 17:11:05.896996 2025-11-16 17:11:05.896996 0 Ashburn Virginia United States US \N +904 209.50.164.28 3129 http \N \N t t 2025-11-16 17:24:41.410356 success 1558 2025-11-16 17:11:05.856442 2025-11-16 17:11:05.856442 0 Ashburn Virginia United States US \N +908 209.50.166.204 3129 http \N \N t t 2025-11-16 17:24:38.762228 success 366 2025-11-16 17:11:05.860732 2025-11-16 17:11:05.860732 0 Ashburn Virginia United States US \N +883 209.50.168.137 3129 http \N \N t t 2025-11-16 17:24:52.235155 success 2044 2025-11-16 17:11:05.830994 2025-11-16 17:11:05.830994 0 Ashburn Virginia United States US \N +910 209.50.170.184 3129 http \N \N t t 2025-11-16 17:24:38.027304 success 458 2025-11-16 17:11:05.863532 2025-11-16 17:11:05.863532 0 Ashburn Virginia United States US \N +929 209.50.171.64 3129 http \N \N t t 2025-11-16 17:24:29.820253 success 540 2025-11-16 17:11:05.887077 2025-11-16 17:11:05.887077 0 Ashburn Virginia United States US \N +950 216.26.225.203 3129 http \N \N t t 2025-11-16 17:24:17.419068 success 957 2025-11-16 17:11:05.910659 2025-11-16 17:11:05.910659 0 Ashburn Virginia United States US \N +966 45.3.37.191 3129 http \N \N t t 2025-11-16 17:24:08.022796 success 520 2025-11-16 17:11:05.928732 2025-11-16 17:11:05.928732 0 Ashburn Virginia United States US \N +931 65.111.7.176 3129 http \N \N t t 2025-11-16 17:24:28.219288 success 434 2025-11-16 17:11:05.889129 2025-11-16 17:11:05.889129 0 Ashburn Virginia United States US \N +927 216.26.226.117 3129 http \N \N t t 2025-11-16 17:24:30.586137 success 362 2025-11-16 17:11:05.88421 2025-11-16 17:11:05.88421 0 Ashburn Virginia United States US \N +953 216.26.236.29 3129 http \N \N t t 2025-11-16 17:24:15.021415 success 385 2025-11-16 17:11:05.913933 2025-11-16 17:11:05.913933 0 Ashburn Virginia United States US \N +885 45.3.48.229 3129 http \N \N t t 2025-11-16 17:24:49.773294 success 338 2025-11-16 17:11:05.833708 2025-11-16 17:11:05.833708 0 Ashburn Virginia United States US \N +919 216.26.229.178 3129 http \N \N t t 2025-11-16 17:24:34.387133 success 342 2025-11-16 17:11:05.873987 2025-11-16 17:11:05.873987 0 Ashburn Virginia United States US \N +930 45.3.37.129 3129 http \N \N t t 2025-11-16 17:24:29.269518 success 1037 2025-11-16 17:11:05.88814 2025-11-16 17:11:05.88814 0 Ashburn Virginia United States US \N +889 45.3.32.94 3129 http \N \N t t 2025-11-16 17:24:48.284448 success 406 2025-11-16 17:11:05.838456 2025-11-16 17:11:05.838456 0 Ashburn Virginia United States US \N +905 65.111.8.184 3129 http \N \N t t 2025-11-16 17:24:39.841553 success 360 2025-11-16 17:11:05.857474 2025-11-16 17:11:05.857474 0 Ashburn Virginia United States US \N +917 216.26.226.242 3129 http \N \N t t 2025-11-16 17:24:35.295266 success 338 2025-11-16 17:11:05.871614 2025-11-16 17:11:05.871614 0 Ashburn Virginia United States US \N +909 216.26.228.108 3129 http \N \N t t 2025-11-16 17:24:38.379866 success 344 2025-11-16 17:11:05.86182 2025-11-16 17:11:05.86182 0 Ashburn Virginia United States US \N +949 45.3.34.153 3129 http \N \N t t 2025-11-16 17:24:17.855382 success 428 2025-11-16 17:11:05.909341 2025-11-16 17:11:05.909341 0 Ashburn Virginia United States US \N +942 45.3.50.100 3129 http \N \N t t 2025-11-16 17:24:20.965062 success 916 2025-11-16 17:11:05.901607 2025-11-16 17:11:05.901607 0 Ashburn Virginia United States US \N +932 65.111.9.235 3129 http \N \N t t 2025-11-16 17:24:27.776356 success 347 2025-11-16 17:11:05.890191 2025-11-16 17:11:05.890191 0 Ashburn Virginia United States US \N +963 216.26.228.220 3129 http \N \N t t 2025-11-16 17:24:09.396884 success 354 2025-11-16 17:11:05.925463 2025-11-16 17:11:05.925463 0 Ashburn Virginia United States US \N +962 45.3.32.93 3129 http \N \N t t 2025-11-16 17:24:09.752963 success 347 2025-11-16 17:11:05.924379 2025-11-16 17:11:05.924379 0 Ashburn Virginia United States US \N +884 216.26.232.117 3129 http \N \N t t 2025-11-16 17:24:50.184143 success 401 2025-11-16 17:11:05.832412 2025-11-16 17:11:05.832412 0 Ashburn Virginia United States US \N +943 216.26.235.9 3129 http \N \N t t 2025-11-16 17:24:20.039225 success 371 2025-11-16 17:11:05.902637 2025-11-16 17:11:05.902637 0 Ashburn Virginia United States US \N +956 45.3.36.167 3129 http \N \N t t 2025-11-16 17:24:13.559415 success 339 2025-11-16 17:11:05.917523 2025-11-16 17:11:05.917523 0 Ashburn Virginia United States US \N +925 216.26.236.206 3129 http \N \N t t 2025-11-16 17:24:31.335861 success 380 2025-11-16 17:11:05.881878 2025-11-16 17:11:05.881878 0 Ashburn Virginia United States US \N +926 45.3.33.152 3129 http \N \N t t 2025-11-16 17:24:30.945898 success 350 2025-11-16 17:11:05.883071 2025-11-16 17:11:05.883071 0 Ashburn Virginia United States US \N +935 45.3.48.45 3129 http \N \N t t 2025-11-16 17:24:26.538599 success 523 2025-11-16 17:11:05.89356 2025-11-16 17:11:05.89356 0 Ashburn Virginia United States US \N +955 45.3.49.35 3129 http \N \N t t 2025-11-16 17:24:13.973312 success 405 2025-11-16 17:11:05.916246 2025-11-16 17:11:05.916246 0 Ashburn Virginia United States US \N +892 65.111.11.54 3129 http \N \N t t 2025-11-16 17:24:47.120098 success 337 2025-11-16 17:11:05.842052 2025-11-16 17:11:05.842052 0 Ashburn Virginia United States US \N +933 65.111.1.161 3129 http \N \N t t 2025-11-16 17:24:27.420548 success 513 2025-11-16 17:11:05.891383 2025-11-16 17:11:05.891383 0 Ashburn Virginia United States US \N +967 65.111.12.207 3129 http \N \N t t 2025-11-16 17:24:07.492449 success 337 2025-11-16 17:11:05.929885 2025-11-16 17:11:05.929885 0 Ashburn Virginia United States US \N +897 65.111.14.173 3129 http \N \N t t 2025-11-16 17:24:44.005899 success 350 2025-11-16 17:11:05.847835 2025-11-16 17:11:05.847835 0 Ashburn Virginia United States US \N +901 65.111.3.15 3129 http \N \N t t 2025-11-16 17:24:42.527392 success 339 2025-11-16 17:11:05.852479 2025-11-16 17:11:05.852479 0 Ashburn Virginia United States US \N +36 104.207.41.175 3129 http \N \N t t 2025-11-16 17:47:36.884303 success 676 2025-11-16 17:11:04.762193 2025-11-16 17:47:36.884303 0 Ashburn Virginia United States US 2025-11-17 07:19:36.115147 +959 65.111.4.217 3129 http \N \N t t 2025-11-16 17:24:11.564565 success 1100 2025-11-16 17:11:05.920829 2025-11-16 17:11:05.920829 0 Ashburn Virginia United States US \N +903 104.207.40.34 3129 http \N \N t t 2025-11-16 17:24:41.820748 success 339 2025-11-16 17:11:05.855151 2025-11-16 17:11:05.855151 0 Ashburn Virginia United States US \N +974 104.207.47.21 3129 http \N \N t t 2025-11-16 17:24:04.109876 success 342 2025-11-16 17:11:05.937564 2025-11-16 17:11:05.937564 0 Ashburn Virginia United States US \N +84 209.50.163.205 3129 http \N \N t t 2025-11-16 17:48:02.612962 success 373 2025-11-16 17:11:04.825326 2025-11-16 17:48:02.612962 0 Ashburn Virginia United States US 2025-11-17 07:20:46.039873 +973 45.3.33.137 3129 http \N \N t t 2025-11-16 17:24:04.468249 success 349 2025-11-16 17:11:05.936522 2025-11-16 17:11:05.936522 0 Ashburn Virginia United States US \N +948 45.3.49.170 3129 http \N \N t t 2025-11-16 17:24:18.204715 success 340 2025-11-16 17:11:05.908269 2025-11-16 17:11:05.908269 0 Ashburn Virginia United States US \N +989 65.111.9.137 3129 http \N \N t t 2025-11-16 17:23:57.353122 success 341 2025-11-16 17:11:05.954355 2025-11-16 17:11:05.954355 0 Ashburn Virginia United States US \N +969 45.3.38.43 3129 http \N \N t t 2025-11-16 17:24:06.2551 success 643 2025-11-16 17:11:05.93215 2025-11-16 17:11:05.93215 0 Ashburn Virginia United States US \N +991 104.207.39.231 3129 http \N \N t t 2025-11-16 17:23:56.651469 success 380 2025-11-16 17:11:05.956646 2025-11-16 17:11:05.956646 0 Ashburn Virginia United States US \N +964 209.50.175.50 3129 http \N \N t t 2025-11-16 17:24:09.034705 success 652 2025-11-16 17:11:05.926486 2025-11-16 17:11:05.926486 0 Ashburn Virginia United States US \N +21 104.207.47.91 3129 http \N \N t t 2025-11-16 17:47:29.060571 success 358 2025-11-16 17:11:04.73864 2025-11-16 17:47:29.060571 0 Ashburn Virginia United States US 2025-11-17 07:19:32.678461 +996 104.207.38.49 3129 http \N \N t t 2025-11-16 17:23:54.396136 success 352 2025-11-16 17:11:05.962091 2025-11-16 17:11:05.962091 0 Ashburn Virginia United States US \N +913 45.3.36.251 3129 http \N \N t t 2025-11-16 17:24:36.845863 success 359 2025-11-16 17:11:05.867042 2025-11-16 17:11:05.867042 0 Ashburn Virginia United States US \N +916 209.50.165.23 3129 http \N \N t t 2025-11-16 17:24:35.643819 success 340 2025-11-16 17:11:05.870432 2025-11-16 17:11:05.870432 0 Ashburn Virginia United States US \N +998 65.111.6.35 3129 http \N \N t t 2025-11-16 17:23:53.670587 success 349 2025-11-16 17:11:05.96428 2025-11-16 17:11:05.96428 0 Ashburn Virginia United States US \N +71 104.207.42.132 3129 http \N \N t t 2025-11-16 17:47:54.778921 success 350 2025-11-16 17:11:04.808715 2025-11-16 17:47:54.778921 0 Ashburn Virginia United States US 2025-11-17 07:20:43.830281 +999 45.3.39.34 3129 http \N \N t t 2025-11-16 17:23:53.312417 success 657 2025-11-16 17:11:05.965479 2025-11-16 17:11:05.965479 0 Ashburn Virginia United States US \N +67 216.26.228.192 3129 http \N \N t t 2025-11-16 17:47:53.09796 success 343 2025-11-16 17:11:04.80309 2025-11-16 17:47:53.09796 0 Ashburn Virginia United States US 2025-11-17 07:20:43.213522 +971 65.111.4.163 3129 http \N \N t t 2025-11-16 17:24:05.234227 success 402 2025-11-16 17:11:05.934335 2025-11-16 17:11:05.934335 0 Ashburn Virginia United States US \N +907 104.207.32.175 3129 http \N \N t t 2025-11-16 17:24:39.110625 success 339 2025-11-16 17:11:05.859641 2025-11-16 17:11:05.859641 0 Ashburn Virginia United States US \N +984 216.26.235.178 3129 http \N \N t t 2025-11-16 17:23:59.574515 success 385 2025-11-16 17:11:05.948703 2025-11-16 17:11:05.948703 0 Ashburn Virginia United States US \N +978 193.56.28.114 3129 http \N \N t t 2025-11-16 17:24:01.749865 success 350 2025-11-16 17:11:05.941905 2025-11-16 17:11:05.941905 0 Ashburn Virginia United States US \N +11 216.26.225.117 3129 http \N \N t t 2025-11-16 17:47:22.95465 success 763 2025-11-16 17:11:04.723344 2025-11-16 17:47:22.95465 0 Ashburn Virginia United States US 2025-11-17 07:19:30.535259 +976 45.3.32.25 3129 http \N \N t t 2025-11-16 17:24:03.099395 success 372 2025-11-16 17:11:05.93976 2025-11-16 17:11:05.93976 0 Ashburn Virginia United States US \N +972 209.50.163.219 3129 http \N \N t t 2025-11-16 17:24:04.821677 success 341 2025-11-16 17:11:05.935413 2025-11-16 17:11:05.935413 0 Ashburn Virginia United States US \N +977 65.111.14.157 3129 http \N \N t t 2025-11-16 17:24:02.718521 success 959 2025-11-16 17:11:05.940804 2025-11-16 17:11:05.940804 0 Ashburn Virginia United States US \N +37 65.111.12.222 3129 http \N \N t t 2025-11-16 17:47:37.267517 success 379 2025-11-16 17:11:04.763615 2025-11-16 17:47:37.267517 0 Ashburn Virginia United States US 2025-11-17 07:19:36.342736 +993 65.111.3.194 3129 http \N \N t t 2025-11-16 17:23:55.897493 success 357 2025-11-16 17:11:05.958879 2025-11-16 17:11:05.958879 0 Ashburn Virginia United States US \N +982 45.3.62.38 3129 http \N \N t t 2025-11-16 17:24:00.305572 success 357 2025-11-16 17:11:05.946361 2025-11-16 17:11:05.946361 0 Ashburn Virginia United States US \N +975 65.111.1.232 3129 http \N \N t t 2025-11-16 17:24:03.757374 success 650 2025-11-16 17:11:05.938603 2025-11-16 17:11:05.938603 0 Ashburn Virginia United States US \N +952 45.3.49.179 3129 http \N \N t t 2025-11-16 17:24:15.379642 success 350 2025-11-16 17:11:05.912905 2025-11-16 17:11:05.912905 0 Ashburn Virginia United States US \N +988 209.50.161.104 3129 http \N \N t t 2025-11-16 17:23:58.057131 success 694 2025-11-16 17:11:05.953218 2025-11-16 17:11:05.953218 0 Ashburn Virginia United States US \N +987 45.3.32.59 3129 http \N \N t t 2025-11-16 17:23:58.451071 success 385 2025-11-16 17:11:05.952094 2025-11-16 17:11:05.952094 0 Ashburn Virginia United States US \N +940 65.111.1.238 3129 http \N \N t t 2025-11-16 17:24:22.73094 success 1351 2025-11-16 17:11:05.899324 2025-11-16 17:11:05.899324 0 Ashburn Virginia United States US \N +924 104.207.35.38 3129 http \N \N t t 2025-11-16 17:24:32.407512 success 1062 2025-11-16 17:11:05.88064 2025-11-16 17:11:05.88064 0 Ashburn Virginia United States US \N +32 209.50.168.129 3129 http \N \N t t 2025-11-16 17:47:34.512238 success 346 2025-11-16 17:11:04.756175 2025-11-16 17:47:34.512238 0 Ashburn Virginia United States US 2025-11-17 07:19:35.197124 +63 216.26.226.234 3129 http \N \N t t 2025-11-16 17:47:50.35571 success 359 2025-11-16 17:11:04.797674 2025-11-16 17:47:50.35571 0 Ashburn Virginia United States US 2025-11-17 07:20:42.293103 +997 45.3.38.32 3129 http \N \N t t 2025-11-16 17:23:54.034795 success 354 2025-11-16 17:11:05.96319 2025-11-16 17:11:05.96319 0 Ashburn Virginia United States US \N +980 45.3.51.82 3129 http \N \N t t 2025-11-16 17:24:01.025167 success 351 2025-11-16 17:11:05.944068 2025-11-16 17:11:05.944068 0 Ashburn Virginia United States US \N +979 65.111.1.230 3129 http \N \N t t 2025-11-16 17:24:01.390341 success 357 2025-11-16 17:11:05.942981 2025-11-16 17:11:05.942981 0 Ashburn Virginia United States US \N +50 65.111.12.90 3129 http \N \N t t 2025-11-16 17:47:43.791625 success 342 2025-11-16 17:11:04.780746 2025-11-16 17:47:43.791625 0 Ashburn Virginia United States US 2025-11-17 07:20:39.304633 +961 104.207.34.74 3129 http \N \N t t 2025-11-16 17:24:10.108649 success 348 2025-11-16 17:11:05.923301 2025-11-16 17:11:05.923301 0 Ashburn Virginia United States US \N +970 104.207.38.179 3129 http \N \N t t 2025-11-16 17:24:05.601616 success 358 2025-11-16 17:11:05.933214 2025-11-16 17:11:05.933214 0 Ashburn Virginia United States US \N +944 209.50.166.109 3129 http \N \N t t 2025-11-16 17:24:19.659499 success 356 2025-11-16 17:11:05.904022 2025-11-16 17:11:05.904022 0 Ashburn Virginia United States US \N +983 65.111.14.116 3129 http \N \N t t 2025-11-16 17:23:59.939648 success 357 2025-11-16 17:11:05.947403 2025-11-16 17:11:05.947403 0 Ashburn Virginia United States US \N +936 104.207.32.192 3129 http \N \N t t 2025-11-16 17:24:26.006339 success 1737 2025-11-16 17:11:05.894851 2025-11-16 17:11:05.894851 0 Ashburn Virginia United States US \N +992 104.207.45.140 3129 http \N \N t t 2025-11-16 17:23:56.263019 success 356 2025-11-16 17:11:05.957715 2025-11-16 17:11:05.957715 0 Ashburn Virginia United States US \N +968 104.167.25.234 3129 http \N \N t t 2025-11-16 17:24:07.146403 success 882 2025-11-16 17:11:05.930986 2025-11-16 17:11:05.930986 0 Ashburn Virginia United States US \N +906 209.50.174.36 3129 http \N \N t t 2025-11-16 17:24:39.473312 success 350 2025-11-16 17:11:05.858488 2025-11-16 17:11:05.858488 0 Ashburn Virginia United States US \N +994 216.26.227.245 3129 http \N \N t t 2025-11-16 17:23:55.531859 success 359 2025-11-16 17:11:05.959982 2025-11-16 17:11:05.959982 0 Ashburn Virginia United States US \N +46 216.26.230.236 3129 http \N \N t t 2025-11-16 17:47:41.38774 success 368 2025-11-16 17:11:04.775501 2025-11-16 17:47:41.38774 0 Ashburn Virginia United States US 2025-11-17 07:20:38.457662 +965 216.26.231.16 3129 http \N \N t t 2025-11-16 17:24:08.373743 success 342 2025-11-16 17:11:05.927575 2025-11-16 17:11:05.927575 0 Ashburn Virginia United States US \N +2 104.207.42.145 3129 http \N \N t t 2025-11-16 17:47:18.078257 success 353 2025-11-16 17:11:04.708969 2025-11-16 17:47:18.078257 0 Ashburn Virginia United States US 2025-11-17 07:19:28.289799 +827 104.207.39.120 3129 http \N \N t t 2025-11-16 17:25:23.396299 success 343 2025-11-16 17:11:05.766687 2025-11-16 17:11:05.766687 0 Ashburn Virginia United States US \N +89 216.26.224.78 3129 http \N \N t t 2025-11-16 17:48:05.534776 success 356 2025-11-16 17:11:04.83192 2025-11-16 17:48:05.534776 0 Ashburn Virginia United States US 2025-11-17 07:20:47.128396 +869 209.50.172.73 3129 http \N \N t t 2025-11-16 17:24:59.552922 success 343 2025-11-16 17:11:05.814083 2025-11-16 17:11:05.814083 0 Ashburn Virginia United States US \N +836 65.111.9.99 3129 http \N \N t t 2025-11-16 17:25:18.084674 success 354 2025-11-16 17:11:05.776811 2025-11-16 17:11:05.776811 0 Ashburn Virginia United States US \N +735 45.3.33.103 3129 http \N \N t t 2025-11-16 17:26:09.140915 success 1202 2025-11-16 17:11:05.657393 2025-11-16 17:11:05.657393 0 Ashburn Virginia United States US \N +891 104.207.34.250 3129 http \N \N t t 2025-11-16 17:24:47.476229 success 348 2025-11-16 17:11:05.840859 2025-11-16 17:11:05.840859 0 Ashburn Virginia United States US \N +150 45.3.37.48 3129 http \N \N t t 2025-11-16 17:48:33.248377 success 342 2025-11-16 17:11:04.917037 2025-11-16 17:48:33.248377 0 Ashburn Virginia United States US 2025-11-17 07:23:01.306943 +743 216.26.232.73 3129 http \N \N t t 2025-11-16 17:26:04.143134 success 931 2025-11-16 17:11:05.666836 2025-11-16 17:11:05.666836 0 Ashburn Virginia United States US \N +121 104.207.47.182 3129 http \N \N t t 2025-11-16 17:48:19.9108 success 339 2025-11-16 17:11:04.876095 2025-11-16 17:48:19.9108 0 Ashburn Virginia United States US 2025-11-17 07:21:54.46687 +747 216.26.235.129 3129 http \N \N t t 2025-11-16 17:26:02.080136 success 391 2025-11-16 17:11:05.671583 2025-11-16 17:11:05.671583 0 Ashburn Virginia United States US \N +772 65.111.2.180 3129 http \N \N t t 2025-11-16 17:25:48.661771 success 403 2025-11-16 17:11:05.701566 2025-11-16 17:11:05.701566 0 Ashburn Virginia United States US \N +766 104.207.45.121 3129 http \N \N t t 2025-11-16 17:25:51.381614 success 356 2025-11-16 17:11:05.693477 2025-11-16 17:11:05.693477 0 Ashburn Virginia United States US \N +840 209.50.170.85 3129 http \N \N t t 2025-11-16 17:25:16.655454 success 988 2025-11-16 17:11:05.781213 2025-11-16 17:11:05.781213 0 Ashburn Virginia United States US \N +138 209.50.165.233 3129 http \N \N t t 2025-11-16 17:48:28.246997 success 385 2025-11-16 17:11:04.899065 2025-11-16 17:48:28.246997 0 Ashburn Virginia United States US 2025-11-17 07:22:58.395614 +871 104.207.34.136 3129 http \N \N t t 2025-11-16 17:24:58.840827 success 355 2025-11-16 17:11:05.816649 2025-11-16 17:11:05.816649 0 Ashburn Virginia United States US \N +154 65.111.11.220 3129 http \N \N t t 2025-11-16 17:48:35.297409 success 344 2025-11-16 17:11:04.922659 2025-11-16 17:48:35.297409 0 Ashburn Virginia United States US 2025-11-17 07:23:02.229986 +830 65.111.14.131 3129 http \N \N t t 2025-11-16 17:25:22.314232 success 343 2025-11-16 17:11:05.770212 2025-11-16 17:11:05.770212 0 Ashburn Virginia United States US \N +857 45.3.35.243 3129 http \N \N t t 2025-11-16 17:25:07.772395 success 357 2025-11-16 17:11:05.800474 2025-11-16 17:11:05.800474 0 Ashburn Virginia United States US \N +167 209.50.175.25 3129 http \N \N t t 2025-11-16 17:48:40.527262 success 455 2025-11-16 17:11:04.940537 2025-11-16 17:48:40.527262 0 Ashburn Virginia United States US 2025-11-17 07:23:05.208218 +175 216.26.236.84 3129 http \N \N t t 2025-11-16 17:48:45.657094 success 768 2025-11-16 17:11:04.951169 2025-11-16 17:48:45.657094 0 Ashburn Virginia United States US 2025-11-17 07:23:07.039931 +134 209.50.173.129 3129 http \N \N t t 2025-11-16 17:48:25.672566 success 344 2025-11-16 17:11:04.8938 2025-11-16 17:48:25.672566 0 Ashburn Virginia United States US 2025-11-17 07:21:57.475185 +865 45.3.36.159 3129 http \N \N t t 2025-11-16 17:25:01.030291 success 351 2025-11-16 17:11:05.809579 2025-11-16 17:11:05.809579 0 Ashburn Virginia United States US \N +783 104.207.32.87 3129 http \N \N t t 2025-11-16 17:25:43.745993 success 339 2025-11-16 17:11:05.714759 2025-11-16 17:11:05.714759 0 Ashburn Virginia United States US \N +779 209.50.174.23 3129 http \N \N t t 2025-11-16 17:25:45.5905 success 354 2025-11-16 17:11:05.710425 2025-11-16 17:11:05.710425 0 Ashburn Virginia United States US \N +821 104.207.41.134 3129 http \N \N t t 2025-11-16 17:25:27.13359 success 342 2025-11-16 17:11:05.759254 2025-11-16 17:11:05.759254 0 Ashburn Virginia United States US \N +826 104.207.41.93 3129 http \N \N t t 2025-11-16 17:25:24.503739 success 1098 2025-11-16 17:11:05.76527 2025-11-16 17:11:05.76527 0 Ashburn Virginia United States US \N +831 216.26.233.120 3129 http \N \N t t 2025-11-16 17:25:21.962339 success 1131 2025-11-16 17:11:05.771256 2025-11-16 17:11:05.771256 0 Ashburn Virginia United States US \N +804 45.3.39.0 3129 http \N \N t t 2025-11-16 17:25:35.751449 success 355 2025-11-16 17:11:05.740122 2025-11-16 17:11:05.740122 0 Ashburn Virginia United States US \N +812 216.26.231.39 3129 http \N \N t t 2025-11-16 17:25:32.5902 success 357 2025-11-16 17:11:05.748686 2025-11-16 17:11:05.748686 0 Ashburn Virginia United States US \N +777 45.3.39.112 3129 http \N \N t t 2025-11-16 17:25:46.303179 success 356 2025-11-16 17:11:05.708182 2025-11-16 17:11:05.708182 0 Ashburn Virginia United States US \N +759 65.111.14.124 3129 http \N \N t t 2025-11-16 17:25:54.640784 success 356 2025-11-16 17:11:05.685495 2025-11-16 17:11:05.685495 0 Ashburn Virginia United States US \N +877 65.111.7.233 3129 http \N \N t t 2025-11-16 17:24:55.256103 success 351 2025-11-16 17:11:05.824379 2025-11-16 17:11:05.824379 0 Ashburn Virginia United States US \N +109 209.50.174.89 3129 http \N \N t t 2025-11-16 17:48:13.447053 success 437 2025-11-16 17:11:04.859399 2025-11-16 17:48:13.447053 0 Ashburn Virginia United States US 2025-11-17 07:21:51.717369 +760 45.3.48.99 3129 http \N \N t t 2025-11-16 17:25:54.274403 success 342 2025-11-16 17:11:05.686649 2025-11-16 17:11:05.686649 0 Ashburn Virginia United States US \N +158 193.56.28.148 3129 http \N \N t t 2025-11-16 17:48:36.80569 success 343 2025-11-16 17:11:04.928829 2025-11-16 17:48:36.80569 0 Ashburn Virginia United States US 2025-11-17 07:23:03.14027 +92 65.111.4.108 3129 http \N \N t t 2025-11-16 17:48:06.614659 success 367 2025-11-16 17:11:04.836049 2025-11-16 17:48:06.614659 0 Ashburn Virginia United States US 2025-11-17 07:21:47.840357 +852 104.207.46.23 3129 http \N \N t t 2025-11-16 17:25:10.316801 success 352 2025-11-16 17:11:05.794543 2025-11-16 17:11:05.794543 0 Ashburn Virginia United States US \N +105 65.111.11.61 3129 http \N \N t t 2025-11-16 17:48:11.965657 success 342 2025-11-16 17:11:04.854176 2025-11-16 17:48:11.965657 0 Ashburn Virginia United States US 2025-11-17 07:21:50.809594 +117 216.26.229.33 3129 http \N \N t t 2025-11-16 17:48:18.454257 success 350 2025-11-16 17:11:04.870514 2025-11-16 17:48:18.454257 0 Ashburn Virginia United States US 2025-11-17 07:21:53.550685 +873 216.26.238.135 3129 http \N \N t t 2025-11-16 17:24:58.124019 success 391 2025-11-16 17:11:05.819414 2025-11-16 17:11:05.819414 0 Ashburn Virginia United States US \N +801 65.111.12.132 3129 http \N \N t t 2025-11-16 17:25:36.827106 success 350 2025-11-16 17:11:05.736848 2025-11-16 17:11:05.736848 0 Ashburn Virginia United States US \N +886 216.26.224.243 3129 http \N \N t t 2025-11-16 17:24:49.427251 success 425 2025-11-16 17:11:05.835145 2025-11-16 17:11:05.835145 0 Ashburn Virginia United States US \N +856 104.207.32.239 3129 http \N \N t t 2025-11-16 17:25:08.130689 success 349 2025-11-16 17:11:05.799266 2025-11-16 17:11:05.799266 0 Ashburn Virginia United States US \N +774 104.207.47.80 3129 http \N \N t t 2025-11-16 17:25:47.394754 success 362 2025-11-16 17:11:05.70441 2025-11-16 17:11:05.70441 0 Ashburn Virginia United States US \N +785 209.50.161.113 3129 http \N \N t t 2025-11-16 17:25:42.709272 success 358 2025-11-16 17:11:05.716985 2025-11-16 17:11:05.716985 0 Ashburn Virginia United States US \N +171 209.50.165.48 3129 http \N \N t t 2025-11-16 17:48:43.44103 success 1475 2025-11-16 17:11:04.946036 2025-11-16 17:48:43.44103 0 Ashburn Virginia United States US 2025-11-17 07:23:06.12407 +789 209.50.168.76 3129 http \N \N t t 2025-11-16 17:25:41.287034 success 348 2025-11-16 17:11:05.7226 2025-11-16 17:11:05.7226 0 Ashburn Virginia United States US \N +894 209.50.175.165 3129 http \N \N t t 2025-11-16 17:24:45.820729 success 401 2025-11-16 17:11:05.844325 2025-11-16 17:11:05.844325 0 Ashburn Virginia United States US \N +751 216.26.228.149 3129 http \N \N t t 2025-11-16 17:25:59.10966 success 928 2025-11-16 17:11:05.67595 2025-11-16 17:11:05.67595 0 Ashburn Virginia United States US \N +808 216.26.231.201 3129 http \N \N t t 2025-11-16 17:25:34.114618 success 343 2025-11-16 17:11:05.744257 2025-11-16 17:11:05.744257 0 Ashburn Virginia United States US \N +792 65.111.4.46 3129 http \N \N t t 2025-11-16 17:25:40.226093 success 346 2025-11-16 17:11:05.726117 2025-11-16 17:11:05.726117 0 Ashburn Virginia United States US \N +598 65.111.2.79 3129 http \N \N t t 2025-11-16 17:27:15.584133 success 375 2025-11-16 17:11:05.487839 2025-11-16 17:11:05.487839 0 Ashburn Virginia United States US \N +47 104.207.44.126 3129 http \N \N t t 2025-11-16 17:47:42.429548 success 1038 2025-11-16 17:11:04.776762 2025-11-16 17:47:42.429548 0 Ashburn Virginia United States US 2025-11-17 07:20:38.63479 +719 45.3.39.155 3129 http \N \N t t 2025-11-16 17:26:18.018412 success 913 2025-11-16 17:11:05.636165 2025-11-16 17:11:05.636165 0 Ashburn Virginia United States US \N +704 45.3.37.59 3129 http \N \N t t 2025-11-16 17:26:24.537006 success 354 2025-11-16 17:11:05.617313 2025-11-16 17:11:05.617313 0 Ashburn Virginia United States US \N +570 216.26.229.160 3129 http \N \N t t 2025-11-16 17:28:11.723457 success 763 2025-11-16 17:11:05.454516 2025-11-16 17:28:11.723457 0 Ashburn Virginia United States US \N +653 104.207.43.196 3129 http \N \N t t 2025-11-16 17:26:48.656185 success 351 2025-11-16 17:11:05.55354 2025-11-16 17:11:05.55354 0 Ashburn Virginia United States US \N +538 216.26.230.104 3129 http \N \N t t 2025-11-16 17:27:49.843492 success 523 2025-11-16 17:11:05.41635 2025-11-16 17:27:36.388817 0 Ashburn Virginia United States US \N +646 45.3.49.155 3129 http \N \N t t 2025-11-16 17:26:53.213452 success 340 2025-11-16 17:11:05.545708 2025-11-16 17:11:05.545708 0 Ashburn Virginia United States US \N +226 104.207.36.214 3129 http \N \N t t 2025-11-16 17:49:14.671377 success 354 2025-11-16 17:11:05.023369 2025-11-16 17:49:14.671377 0 Ashburn Virginia United States US \N +667 216.26.229.68 3129 http \N \N t t 2025-11-16 17:26:42.576105 success 353 2025-11-16 17:11:05.571015 2025-11-16 17:11:05.571015 0 Ashburn Virginia United States US \N +661 104.207.46.12 3129 http \N \N t t 2025-11-16 17:26:45.294345 success 917 2025-11-16 17:11:05.562607 2025-11-16 17:11:05.562607 0 Ashburn Virginia United States US \N +640 209.50.165.5 3129 http \N \N t t 2025-11-16 17:26:57.688728 success 1583 2025-11-16 17:11:05.539042 2025-11-16 17:11:05.539042 0 Ashburn Virginia United States US \N +632 209.50.175.148 3129 http \N \N t t 2025-11-16 17:27:00.996514 success 355 2025-11-16 17:11:05.529673 2025-11-16 17:11:05.529673 0 Ashburn Virginia United States US \N +188 216.26.231.100 3129 http \N \N t t 2025-11-16 17:48:51.511145 success 360 2025-11-16 17:11:04.971529 2025-11-16 17:48:51.511145 0 Ashburn Virginia United States US 2025-11-17 07:24:09.981555 +701 216.26.235.157 3129 http \N \N t t 2025-11-16 17:26:25.650823 success 377 2025-11-16 17:11:05.613711 2025-11-16 17:11:05.613711 0 Ashburn Virginia United States US \N +634 45.3.39.170 3129 http \N \N t t 2025-11-16 17:27:00.276498 success 345 2025-11-16 17:11:05.532138 2025-11-16 17:11:05.532138 0 Ashburn Virginia United States US \N +656 104.207.47.188 3129 http \N \N t t 2025-11-16 17:26:47.236591 success 498 2025-11-16 17:11:05.55683 2025-11-16 17:11:05.55683 0 Ashburn Virginia United States US \N +703 45.3.37.179 3129 http \N \N t t 2025-11-16 17:26:24.910817 success 359 2025-11-16 17:11:05.616111 2025-11-16 17:11:05.616111 0 Ashburn Virginia United States US \N +217 45.3.50.252 3129 http \N \N t t 2025-11-16 17:49:09.999933 success 618 2025-11-16 17:11:05.010616 2025-11-16 17:49:09.999933 0 Ashburn Virginia United States US 2025-11-17 07:24:16.771404 +629 216.26.227.56 3129 http \N \N t t 2025-11-16 17:27:02.152991 success 380 2025-11-16 17:11:05.525967 2025-11-16 17:11:05.525967 0 Ashburn Virginia United States US \N +205 45.3.32.253 3129 http \N \N t t 2025-11-16 17:49:02.809214 success 698 2025-11-16 17:11:04.99457 2025-11-16 17:49:02.809214 0 Ashburn Virginia United States US 2025-11-17 07:24:13.639376 +706 216.26.224.186 3129 http \N \N t t 2025-11-16 17:26:23.804212 success 358 2025-11-16 17:11:05.619815 2025-11-16 17:11:05.619815 0 Ashburn Virginia United States US \N +580 209.50.161.240 3129 http \N \N t t 2025-11-16 17:28:22.259314 success 803 2025-11-16 17:11:05.467089 2025-11-16 17:28:22.259314 0 Ashburn Virginia United States US \N +602 104.207.42.86 3129 http \N \N t t 2025-11-16 17:27:14.0363 success 344 2025-11-16 17:11:05.493066 2025-11-16 17:11:05.493066 0 Ashburn Virginia United States US \N +578 209.50.170.141 3129 http \N \N t t 2025-11-16 17:28:20.110332 success 359 2025-11-16 17:11:05.464158 2025-11-16 17:28:20.110332 0 Ashburn Virginia United States US \N +242 104.207.34.69 3129 http \N \N t t 2025-11-16 17:49:22.971918 success 592 2025-11-16 17:11:05.043872 2025-11-16 17:49:22.971918 0 Ashburn Virginia United States US \N +700 216.26.229.152 3129 http \N \N t t 2025-11-16 17:26:26.019736 success 357 2025-11-16 17:11:05.612236 2025-11-16 17:11:05.612236 0 Ashburn Virginia United States US \N +222 216.26.229.124 3129 http \N \N t t 2025-11-16 17:49:13.094556 success 526 2025-11-16 17:11:05.0181 2025-11-16 17:49:13.094556 0 Ashburn Virginia United States US 2025-11-17 07:24:17.747138 +689 193.56.28.73 3129 http \N \N t t 2025-11-16 17:26:31.233985 success 354 2025-11-16 17:11:05.596852 2025-11-16 17:11:05.596852 0 Ashburn Virginia United States US \N +609 209.50.166.33 3129 http \N \N t t 2025-11-16 17:27:10.236858 success 355 2025-11-16 17:11:05.501755 2025-11-16 17:11:05.501755 0 Ashburn Virginia United States US \N +246 104.207.44.12 3129 http \N \N t t 2025-11-16 17:49:24.710537 success 666 2025-11-16 17:11:05.049299 2025-11-16 17:49:24.710537 0 Ashburn Virginia United States US \N +201 65.111.0.134 3129 http \N \N t t 2025-11-16 17:48:59.533978 success 611 2025-11-16 17:11:04.98954 2025-11-16 17:48:59.533978 0 Ashburn Virginia United States US 2025-11-17 07:24:12.694215 +718 65.111.12.181 3129 http \N \N t t 2025-11-16 17:26:18.560978 success 531 2025-11-16 17:11:05.634623 2025-11-16 17:11:05.634623 0 Ashburn Virginia United States US \N +680 209.50.169.140 3129 http \N \N t t 2025-11-16 17:26:35.90256 success 1055 2025-11-16 17:11:05.586563 2025-11-16 17:11:05.586563 0 Ashburn Virginia United States US \N +690 209.50.175.91 3129 http \N \N t t 2025-11-16 17:26:30.871115 success 347 2025-11-16 17:11:05.598029 2025-11-16 17:11:05.598029 0 Ashburn Virginia United States US \N +664 216.26.228.162 3129 http \N \N t t 2025-11-16 17:26:43.639288 success 339 2025-11-16 17:11:05.566693 2025-11-16 17:11:05.566693 0 Ashburn Virginia United States US \N +184 104.207.37.147 3129 http \N \N t t 2025-11-16 17:48:49.800046 success 383 2025-11-16 17:11:04.966245 2025-11-16 17:48:49.800046 0 Ashburn Virginia United States US 2025-11-17 07:24:09.058972 +672 104.207.34.160 3129 http \N \N t t 2025-11-16 17:26:40.624271 success 352 2025-11-16 17:11:05.576782 2025-11-16 17:11:05.576782 0 Ashburn Virginia United States US \N +668 216.26.229.146 3129 http \N \N t t 2025-11-16 17:26:42.212901 success 356 2025-11-16 17:11:05.572252 2025-11-16 17:11:05.572252 0 Ashburn Virginia United States US \N +687 104.207.43.139 3129 http \N \N t t 2025-11-16 17:26:31.963782 success 352 2025-11-16 17:11:05.594625 2025-11-16 17:11:05.594625 0 Ashburn Virginia United States US \N +238 65.111.3.215 3129 http \N \N t t 2025-11-16 17:49:21.024446 success 2251 2025-11-16 17:11:05.038645 2025-11-16 17:49:21.024446 0 Ashburn Virginia United States US \N +230 45.3.36.172 3129 http \N \N t t 2025-11-16 17:49:16.144593 success 370 2025-11-16 17:11:05.028218 2025-11-16 17:49:16.144593 0 Ashburn Virginia United States US \N +567 104.207.39.252 3129 http \N \N t t 2025-11-16 17:28:08.734289 success 358 2025-11-16 17:11:05.450539 2025-11-16 17:28:08.734289 0 Ashburn Virginia United States US \N +259 104.207.40.148 3129 http \N \N t t 2025-11-16 17:49:31.017536 success 911 2025-11-16 17:11:05.066625 2025-11-16 17:49:31.017536 0 Ashburn Virginia United States US \N +234 104.207.42.12 3129 http \N \N t t 2025-11-16 17:49:17.69976 success 345 2025-11-16 17:11:05.033337 2025-11-16 17:49:17.69976 0 Ashburn Virginia United States US \N +613 104.207.32.183 3129 http \N \N t t 2025-11-16 17:27:08.777407 success 533 2025-11-16 17:11:05.506463 2025-11-16 17:11:05.506463 0 Ashburn Virginia United States US \N +615 104.207.36.168 3129 http \N \N t t 2025-11-16 17:27:07.870677 success 355 2025-11-16 17:11:05.508664 2025-11-16 17:11:05.508664 0 Ashburn Virginia United States US \N +573 104.207.38.231 3129 http \N \N t t 2025-11-16 17:28:14.696176 success 360 2025-11-16 17:11:05.457952 2025-11-16 17:28:14.696176 0 Ashburn Virginia United States US \N +682 104.207.42.5 3129 http \N \N t t 2025-11-16 17:26:34.443883 success 344 2025-11-16 17:11:05.588849 2025-11-16 17:11:05.588849 0 Ashburn Virginia United States US \N +723 45.3.36.3 3129 http \N \N t t 2025-11-16 17:26:14.728165 success 342 2025-11-16 17:11:05.641391 2025-11-16 17:11:05.641391 0 Ashburn Virginia United States US \N +592 104.207.45.199 3129 http \N \N t t 2025-11-16 17:28:33.563713 success 353 2025-11-16 17:11:05.481262 2025-11-16 17:28:33.563713 0 Ashburn Virginia United States US \N +263 65.111.5.136 3129 http \N \N t t 2025-11-16 17:49:32.445071 success 360 2025-11-16 17:11:05.071403 2025-11-16 17:49:32.445071 0 Ashburn Virginia United States US \N +612 65.111.9.238 3129 http \N \N t t 2025-11-16 17:27:09.139177 success 353 2025-11-16 17:11:05.505398 2025-11-16 17:11:05.505398 0 Ashburn Virginia United States US \N +520 209.50.172.70 3129 http \N \N t t 2025-11-16 17:28:00.516865 success 343 2025-11-16 17:11:05.393755 2025-11-16 17:27:19.433625 0 Ashburn Virginia United States US \N +523 216.26.234.165 3129 http \N \N t t 2025-11-16 17:27:59.418017 success 395 2025-11-16 17:11:05.397151 2025-11-16 17:27:22.660692 0 Ashburn Virginia United States US \N +367 104.207.43.244 3129 http \N \N t t 2025-11-16 17:50:22.83948 success 363 2025-11-16 17:11:05.204484 2025-11-16 17:50:22.83948 0 Ashburn Virginia United States US \N +276 209.50.174.98 3129 http \N \N t t 2025-11-16 17:49:38.639025 success 339 2025-11-16 17:11:05.0878 2025-11-16 17:49:38.639025 0 Ashburn Virginia United States US \N +512 104.207.32.205 3129 http \N \N t t 2025-11-16 17:28:04.962518 success 355 2025-11-16 17:11:05.384552 2025-11-16 17:27:11.284214 0 Ashburn Virginia United States US \N +388 104.207.41.239 3129 http \N \N t t 2025-11-16 17:50:30.875739 success 378 2025-11-16 17:11:05.231935 2025-11-16 17:50:30.875739 0 Ashburn Virginia United States US \N +363 104.207.35.130 3129 http \N \N t t 2025-11-16 17:50:21.332024 success 343 2025-11-16 17:11:05.199276 2025-11-16 17:50:21.332024 0 Ashburn Virginia United States US \N +339 65.111.5.32 3129 http \N \N t t 2025-11-16 17:50:10.099991 success 339 2025-11-16 17:11:05.167976 2025-11-16 17:50:10.099991 0 Ashburn Virginia United States US \N +20 209.50.164.64 3129 http \N \N t t 2025-11-16 17:47:28.699387 success 860 2025-11-16 17:11:04.737054 2025-11-16 17:47:28.699387 0 Ashburn Virginia United States US 2025-11-17 07:19:32.455466 +379 45.3.49.149 3129 http \N \N t t 2025-11-16 17:50:27.467164 success 356 2025-11-16 17:11:05.220622 2025-11-16 17:50:27.467164 0 Ashburn Virginia United States US \N +528 209.50.175.240 3129 http \N \N t t 2025-11-16 17:27:56.747704 success 692 2025-11-16 17:11:05.403304 2025-11-16 17:27:27.209562 0 Ashburn Virginia United States US \N +281 65.111.1.95 3129 http \N \N t t 2025-11-16 17:49:40.467407 success 349 2025-11-16 17:11:05.094038 2025-11-16 17:49:40.467407 0 Ashburn Virginia United States US \N +318 104.207.45.24 3129 http \N \N t t 2025-11-16 17:49:57.032327 success 351 2025-11-16 17:11:05.140195 2025-11-16 17:49:57.032327 0 Ashburn Virginia United States US \N +995 209.50.164.136 3129 http \N \N t t 2025-11-16 17:23:55.16348 success 760 2025-11-16 17:11:05.961084 2025-11-16 17:11:05.961084 0 Ashburn Virginia United States US \N +467 104.207.45.122 3129 http \N \N t t 2025-11-16 17:51:10.504132 success 381 2025-11-16 17:11:05.33088 2025-11-16 17:51:10.504132 0 Ashburn Virginia United States US \N +455 209.50.160.74 3129 http \N \N t t 2025-11-16 17:51:05.279106 success 342 2025-11-16 17:11:05.315637 2025-11-16 17:51:05.279106 0 Ashburn Virginia United States US \N +15 209.50.165.180 3129 http \N \N t t 2025-11-16 17:47:26.087203 success 344 2025-11-16 17:11:04.729458 2025-11-16 17:47:26.087203 0 Ashburn Virginia United States US 2025-11-17 07:19:31.304064 +474 65.111.8.180 3129 http \N \N t t 2025-11-16 17:51:14.555375 success 369 2025-11-16 17:11:05.339897 2025-11-16 17:51:14.555375 0 Ashburn Virginia United States US \N +486 45.3.38.148 3129 http \N \N t t 2025-11-16 17:51:19.850787 success 569 2025-11-16 17:11:05.354021 2025-11-16 17:51:19.850787 0 Ashburn Virginia United States US \N +268 45.3.62.103 3129 http \N \N t t 2025-11-16 17:49:35.372281 success 449 2025-11-16 17:11:05.077652 2025-11-16 17:49:35.372281 0 Ashburn Virginia United States US \N +314 65.111.7.12 3129 http \N \N t t 2025-11-16 17:49:55.603282 success 357 2025-11-16 17:11:05.135319 2025-11-16 17:49:55.603282 0 Ashburn Virginia United States US \N +371 104.207.40.226 3129 http \N \N t t 2025-11-16 17:50:24.417785 success 361 2025-11-16 17:11:05.2095 2025-11-16 17:50:24.417785 0 Ashburn Virginia United States US \N +25 104.207.32.42 3129 http \N \N t t 2025-11-16 17:47:30.895928 success 486 2025-11-16 17:11:04.744842 2025-11-16 17:47:30.895928 0 Ashburn Virginia United States US 2025-11-17 07:19:33.592632 +343 216.26.234.30 3129 http \N \N t t 2025-11-16 17:50:12.030546 success 399 2025-11-16 17:11:05.173837 2025-11-16 17:50:12.030546 0 Ashburn Virginia United States US \N +305 209.50.169.72 3129 http \N \N t t 2025-11-16 17:49:50.447159 success 377 2025-11-16 17:11:05.124243 2025-11-16 17:49:50.447159 0 Ashburn Virginia United States US \N +272 216.26.226.228 3129 http \N \N t t 2025-11-16 17:49:36.861716 success 355 2025-11-16 17:11:05.082533 2025-11-16 17:49:36.861716 0 Ashburn Virginia United States US \N +359 104.207.42.85 3129 http \N \N t t 2025-11-16 17:50:19.803666 success 343 2025-11-16 17:11:05.194157 2025-11-16 17:50:19.803666 0 Ashburn Virginia United States US \N +446 65.111.13.94 3129 http \N \N t t 2025-11-16 17:50:59.507769 success 1304 2025-11-16 17:11:05.303466 2025-11-16 17:50:59.507769 0 Ashburn Virginia United States US \N +489 104.207.40.93 3129 http \N \N t t 2025-11-16 17:51:21.275177 success 353 2025-11-16 17:11:05.35748 2025-11-16 17:51:21.275177 0 Ashburn Virginia United States US \N +499 65.111.5.117 3129 http \N \N t t 2025-11-16 17:51:25.511489 success 383 2025-11-16 17:11:05.369439 2025-11-16 17:51:25.511489 0 Ashburn Virginia United States US \N +6 65.111.1.200 3129 http \N \N t t 2025-11-16 17:47:19.621544 success 361 2025-11-16 17:11:04.715712 2025-11-16 17:47:19.621544 0 Ashburn Virginia United States US 2025-11-17 07:19:29.289192 +322 104.207.43.205 3129 http \N \N t t 2025-11-16 17:49:59.600541 success 1499 2025-11-16 17:11:05.144964 2025-11-16 17:49:59.600541 0 Ashburn Virginia United States US \N +392 65.111.13.46 3129 http \N \N t t 2025-11-16 17:50:33.711404 success 501 2025-11-16 17:11:05.23666 2025-11-16 17:50:33.711404 0 Ashburn Virginia United States US \N +354 216.26.227.127 3129 http \N \N t t 2025-11-16 17:50:17.198117 success 901 2025-11-16 17:11:05.188075 2025-11-16 17:50:17.198117 0 Ashburn Virginia United States US \N +525 216.26.229.100 3129 http \N \N t t 2025-11-16 17:27:58.662879 success 849 2025-11-16 17:11:05.39998 2025-11-16 17:27:24.662148 0 Ashburn Virginia United States US \N +484 65.111.8.163 3129 http \N \N t t 2025-11-16 17:51:18.935984 success 907 2025-11-16 17:11:05.351803 2025-11-16 17:51:18.935984 0 Ashburn Virginia United States US \N +492 104.207.33.197 3129 http \N \N t t 2025-11-16 17:51:22.417675 success 386 2025-11-16 17:11:05.361031 2025-11-16 17:51:22.417675 0 Ashburn Virginia United States US \N +517 104.207.37.159 3129 http \N \N t t 2025-11-16 17:28:01.802009 success 558 2025-11-16 17:11:05.390388 2025-11-16 17:27:16.357852 0 Ashburn Virginia United States US \N +526 209.50.171.237 3129 http \N \N t t 2025-11-16 17:27:57.804914 success 517 2025-11-16 17:11:05.401142 2025-11-16 17:27:25.510166 0 Ashburn Virginia United States US \N +534 209.50.166.73 3129 http \N \N t t 2025-11-16 17:27:52.793855 success 600 2025-11-16 17:11:05.411462 2025-11-16 17:27:32.962889 0 Ashburn Virginia United States US \N +347 216.26.235.199 3129 http \N \N t t 2025-11-16 17:50:13.716772 success 384 2025-11-16 17:11:05.178976 2025-11-16 17:50:13.716772 0 Ashburn Virginia United States US \N +301 216.26.235.136 3129 http \N \N t t 2025-11-16 17:49:48.965545 success 399 2025-11-16 17:11:05.119425 2025-11-16 17:49:48.965545 0 Ashburn Virginia United States US \N +471 45.3.38.115 3129 http \N \N t t 2025-11-16 17:51:11.925913 success 360 2025-11-16 17:11:05.33638 2025-11-16 17:51:11.925913 0 Ashburn Virginia United States US \N +330 104.207.47.165 3129 http \N \N t t 2025-11-16 17:50:06.009246 success 1689 2025-11-16 17:11:05.155584 2025-11-16 17:50:06.009246 0 Ashburn Virginia United States US \N +530 104.207.43.206 3129 http \N \N t t 2025-11-16 17:27:55.423451 success 530 2025-11-16 17:11:05.406211 2025-11-16 17:27:28.914083 0 Ashburn Virginia United States US \N +310 45.3.32.80 3129 http \N \N t t 2025-11-16 17:49:54.147608 success 345 2025-11-16 17:11:05.130424 2025-11-16 17:49:54.147608 0 Ashburn Virginia United States US \N +351 193.56.28.51 3129 http \N \N t t 2025-11-16 17:50:15.57865 success 342 2025-11-16 17:11:05.184025 2025-11-16 17:50:15.57865 0 Ashburn Virginia United States US \N +437 209.50.160.30 3129 http \N \N t t 2025-11-16 17:50:53.44612 success 339 2025-11-16 17:11:05.292009 2025-11-16 17:50:53.44612 0 Ashburn Virginia United States US \N +440 209.50.165.177 3129 http \N \N t t 2025-11-16 17:50:54.502561 success 341 2025-11-16 17:11:05.295841 2025-11-16 17:50:54.502561 0 Ashburn Virginia United States US \N +297 209.50.165.93 3129 http \N \N t t 2025-11-16 17:49:47.487897 success 362 2025-11-16 17:11:05.114386 2025-11-16 17:49:47.487897 0 Ashburn Virginia United States US \N +110 104.207.43.11 3129 http \N \N t t 2025-11-16 17:48:13.80066 success 351 2025-11-16 17:11:04.860892 2025-11-30 02:05:41.404647 2 Ashburn Virginia United States US 2025-11-17 07:21:51.949892 +182 216.26.231.162 3129 http \N \N t t 2025-11-16 17:48:48.963446 success 340 2025-11-16 17:11:04.963046 2025-11-16 17:48:48.963446 0 Ashburn Virginia United States US 2025-11-17 07:24:08.598098 +130 104.207.36.167 3129 http \N \N t t 2025-11-16 17:48:24.24368 success 349 2025-11-16 17:11:04.888646 2025-11-16 17:48:24.24368 0 Ashburn Virginia United States US 2025-11-17 07:21:56.548176 +421 45.3.38.48 3129 http \N \N t t 2025-11-16 17:50:47.22253 success 358 2025-11-16 17:11:05.272639 2025-11-16 17:50:47.22253 0 Ashburn Virginia United States US \N +741 209.50.171.168 3129 http \N \N t t 2025-11-16 17:26:04.869876 success 358 2025-11-16 17:11:05.664445 2025-11-16 17:11:05.664445 0 Ashburn Virginia United States US \N +43 104.207.44.83 3129 http \N \N t f 2025-11-16 17:47:40.141481 failed \N 2025-11-16 17:11:04.771538 2025-11-16 17:47:40.141481 0 Ashburn Virginia United States US 2025-11-17 07:19:37.715913 +78 104.207.45.29 3129 http \N \N t t 2025-11-16 17:47:58.224071 success 340 2025-11-16 17:11:04.817714 2025-11-16 17:47:58.224071 0 Ashburn Virginia United States US 2025-11-17 07:20:44.705752 +396 216.26.228.226 3129 http \N \N t t 2025-11-16 17:50:36.732401 success 343 2025-11-16 17:11:05.241284 2025-11-16 17:50:36.732401 0 Ashburn Virginia United States US \N +155 45.3.48.154 3129 http \N \N t t 2025-11-16 17:48:35.745674 success 444 2025-11-16 17:11:04.924143 2025-11-16 17:48:35.745674 0 Ashburn Virginia United States US 2025-11-17 07:23:02.455775 +145 65.111.3.136 3129 http \N \N t t 2025-11-16 17:48:31.328101 success 540 2025-11-16 17:11:04.909714 2025-11-16 17:48:31.328101 0 Ashburn Virginia United States US 2025-11-17 07:23:00.20087 +404 45.3.37.210 3129 http \N \N t t 2025-11-16 17:50:40.376868 success 356 2025-11-16 17:11:05.251275 2025-11-16 17:50:40.376868 0 Ashburn Virginia United States US \N +140 104.167.25.240 3129 http \N \N t t 2025-11-16 17:48:28.944507 success 341 2025-11-16 17:11:04.901704 2025-11-16 17:48:28.944507 0 Ashburn Virginia United States US 2025-11-17 07:22:58.867061 +85 104.207.33.156 3129 http \N \N t t 2025-11-16 17:48:02.983739 success 367 2025-11-16 17:11:04.826573 2025-11-16 17:48:02.983739 0 Ashburn Virginia United States US 2025-11-17 07:20:46.30278 +42 104.207.35.115 3129 http \N \N t t 2025-11-16 17:47:39.23646 success 352 2025-11-16 17:11:04.770204 2025-11-16 17:47:39.23646 0 Ashburn Virginia United States US 2025-11-17 07:19:37.488199 +400 104.207.36.144 3129 http \N \N t t 2025-11-16 17:50:38.798927 success 1019 2025-11-16 17:11:05.246228 2025-11-16 17:50:38.798927 0 Ashburn Virginia United States US \N +60 104.207.36.3 3129 http \N \N t t 2025-11-16 17:47:49.081497 success 352 2025-11-16 17:11:04.793621 2025-11-16 17:47:49.081497 0 Ashburn Virginia United States US 2025-11-17 07:20:41.602866 +177 104.207.38.150 3129 http \N \N t t 2025-11-16 17:48:47.225171 success 617 2025-11-16 17:11:04.954058 2025-11-16 17:48:47.225171 0 Ashburn Virginia United States US 2025-11-17 07:23:07.500634 +186 104.207.38.181 3129 http \N \N t t 2025-11-16 17:48:50.78261 success 358 2025-11-16 17:11:04.968923 2025-11-16 17:48:50.78261 0 Ashburn Virginia United States US 2025-11-17 07:24:09.5166 +65 104.207.39.9 3129 http \N \N t t 2025-11-16 17:47:51.139886 success 365 2025-11-16 17:11:04.800517 2025-11-16 17:47:51.139886 0 Ashburn Virginia United States US 2025-11-17 07:20:42.754605 +31 104.207.39.98 3129 http \N \N t t 2025-11-16 17:47:34.160566 success 1295 2025-11-16 17:11:04.754575 2025-11-16 17:47:34.160566 0 Ashburn Virginia United States US 2025-11-17 07:19:34.966182 +180 104.207.43.164 3129 http \N \N t t 2025-11-16 17:48:48.273743 success 338 2025-11-16 17:11:04.959054 2025-11-16 17:48:48.273743 0 Ashburn Virginia United States US 2025-11-17 07:23:08.188974 +163 104.207.46.221 3129 http \N \N t t 2025-11-16 17:48:38.570562 success 359 2025-11-16 17:11:04.935375 2025-11-16 17:48:38.570562 0 Ashburn Virginia United States US 2025-11-17 07:23:04.284824 +51 104.207.46.224 3129 http \N \N t t 2025-11-16 17:47:44.474204 success 678 2025-11-16 17:11:04.781999 2025-11-16 17:47:44.474204 0 Ashburn Virginia United States US 2025-11-17 07:20:39.543352 +88 193.56.28.252 3129 http \N \N t t 2025-11-16 17:48:05.174805 success 1471 2025-11-16 17:11:04.830613 2025-11-16 17:48:05.174805 0 Ashburn Virginia United States US 2025-11-17 07:20:46.924349 +135 193.56.28.59 3129 http \N \N t t 2025-11-16 17:48:26.374089 success 691 2025-11-16 17:11:04.89522 2025-11-16 17:48:26.374089 0 Ashburn Virginia United States US 2025-11-17 07:21:57.697873 +40 209.50.161.186 3129 http \N \N t t 2025-11-16 17:47:38.327926 success 353 2025-11-16 17:11:04.767589 2025-11-16 17:47:38.327926 0 Ashburn Virginia United States US 2025-11-17 07:19:37.029772 +30 209.50.167.251 3129 http \N \N t t 2025-11-16 17:47:32.860522 success 359 2025-11-16 17:11:04.753004 2025-11-16 17:47:32.860522 0 Ashburn Virginia United States US 2025-11-17 07:19:34.73852 +104 209.50.168.50 3129 http \N \N t t 2025-11-16 17:48:11.620733 success 362 2025-11-16 17:11:04.8528 2025-11-16 17:48:11.620733 0 Ashburn Virginia United States US 2025-11-17 07:21:50.57841 +120 209.50.169.38 3129 http \N \N t t 2025-11-16 17:48:19.563579 success 342 2025-11-16 17:11:04.874607 2025-11-16 17:48:19.563579 0 Ashburn Virginia United States US 2025-11-17 07:21:54.239281 +408 209.50.170.50 3129 http \N \N t t 2025-11-16 17:50:41.769406 success 346 2025-11-16 17:11:05.256342 2025-11-16 17:50:41.769406 0 Ashburn Virginia United States US \N +165 209.50.171.207 3129 http \N \N t t 2025-11-16 17:48:39.706974 success 789 2025-11-16 17:11:04.937899 2025-11-16 17:48:39.706974 0 Ashburn Virginia United States US 2025-11-17 07:23:04.748958 +81 209.50.172.5 3129 http \N \N t t 2025-11-16 17:48:00.053432 success 521 2025-11-16 17:11:04.821693 2025-11-16 17:48:00.053432 0 Ashburn Virginia United States US 2025-11-17 07:20:45.245747 +56 209.50.174.211 3129 http \N \N t t 2025-11-16 17:47:47.663378 success 343 2025-11-16 17:11:04.788506 2025-11-16 17:47:47.663378 0 Ashburn Virginia United States US 2025-11-17 07:20:40.68322 +101 209.50.174.61 3129 http \N \N t t 2025-11-16 17:48:10.334234 success 358 2025-11-16 17:11:04.848967 2025-11-16 17:48:10.334234 0 Ashburn Virginia United States US 2025-11-17 07:21:49.885879 +59 209.50.175.216 3129 http \N \N t t 2025-11-16 17:47:48.726512 success 357 2025-11-16 17:11:04.792267 2025-11-16 17:47:48.726512 0 Ashburn Virginia United States US 2025-11-17 07:20:41.372175 +76 209.50.175.93 3129 http \N \N t t 2025-11-16 17:47:56.988728 success 357 2025-11-16 17:11:04.815249 2025-11-16 17:47:56.988728 0 Ashburn Virginia United States US 2025-11-17 07:20:44.4614 +417 216.26.227.159 3129 http \N \N t t 2025-11-16 17:50:45.769087 success 735 2025-11-16 17:11:05.267631 2025-11-16 17:50:45.769087 0 Ashburn Virginia United States US \N +151 216.26.233.160 3129 http \N \N t t 2025-11-16 17:48:33.64207 success 390 2025-11-16 17:11:04.918244 2025-11-16 17:48:33.64207 0 Ashburn Virginia United States US 2025-11-17 07:23:01.53477 +176 216.26.234.1 3129 http \N \N t t 2025-11-16 17:48:46.604367 success 944 2025-11-16 17:11:04.952639 2025-11-16 17:48:46.604367 0 Ashburn Virginia United States US 2025-11-17 07:23:07.267095 +45 216.26.238.175 3129 http \N \N t t 2025-11-16 17:47:41.016157 success 377 2025-11-16 17:11:04.774145 2025-11-16 17:47:41.016157 0 Ashburn Virginia United States US 2025-11-17 07:19:38.177612 +126 45.3.35.204 3129 http \N \N t t 2025-11-16 17:48:21.934109 success 357 2025-11-16 17:11:04.883237 2025-11-16 17:48:21.934109 0 Ashburn Virginia United States US 2025-11-17 07:21:55.617517 +429 45.3.36.158 3129 http \N \N t t 2025-11-16 17:50:50.425488 success 367 2025-11-16 17:11:05.281938 2025-11-16 17:50:50.425488 0 Ashburn Virginia United States US \N +113 45.3.51.191 3129 http \N \N t t 2025-11-16 17:48:16.949309 success 1994 2025-11-16 17:11:04.865003 2025-11-16 17:48:16.949309 0 Ashburn Virginia United States US 2025-11-17 07:21:52.634293 +178 45.3.51.230 3129 http \N \N t t 2025-11-16 17:48:47.58713 success 359 2025-11-16 17:11:04.95563 2025-11-16 17:48:47.58713 0 Ashburn Virginia United States US 2025-11-17 07:23:07.726893 +160 45.3.51.39 3129 http \N \N t t 2025-11-16 17:48:37.497796 success 338 2025-11-16 17:11:04.931469 2025-11-16 17:48:37.497796 0 Ashburn Virginia United States US 2025-11-17 07:23:03.610473 +170 45.3.62.31 3129 http \N \N t t 2025-11-16 17:48:41.96177 success 352 2025-11-16 17:11:04.944782 2025-11-16 17:48:41.96177 0 Ashburn Virginia United States US 2025-11-17 07:23:05.895158 +75 65.111.0.108 3129 http \N \N t t 2025-11-16 17:47:56.627121 success 351 2025-11-16 17:11:04.813843 2025-11-16 17:47:56.627121 0 Ashburn Virginia United States US 2025-11-17 07:20:44.336421 +129 65.111.11.217 3129 http \N \N t t 2025-11-16 17:48:23.88251 success 356 2025-11-16 17:11:04.887091 2025-11-16 17:48:23.88251 0 Ashburn Virginia United States US 2025-11-17 07:21:56.309187 +70 65.111.12.108 3129 http \N \N t t 2025-11-16 17:47:54.424025 success 618 2025-11-16 17:11:04.807421 2025-11-16 17:47:54.424025 0 Ashburn Virginia United States US 2025-11-17 07:20:43.710102 +425 65.111.13.103 3129 http \N \N t t 2025-11-16 17:50:48.98329 success 359 2025-11-16 17:11:05.277358 2025-11-16 17:50:48.98329 0 Ashburn Virginia United States US \N +95 65.111.4.218 3129 http \N \N t t 2025-11-16 17:48:08.022046 success 337 2025-11-16 17:11:04.840495 2025-11-16 17:48:08.022046 0 Ashburn Virginia United States US 2025-11-17 07:21:48.509787 +412 65.111.8.67 3129 http \N \N t t 2025-11-16 17:50:43.163331 success 338 2025-11-16 17:11:05.26123 2025-11-16 17:50:43.163331 0 Ashburn Virginia United States US \N +221 104.167.19.105 3129 http \N \N t t 2025-11-16 17:49:12.565417 success 870 2025-11-16 17:11:05.016685 2025-11-16 17:49:12.565417 0 Ashburn Virginia United States US 2025-11-17 07:24:17.517963 +255 104.167.19.113 3129 http \N \N t t 2025-11-16 17:49:29.024371 success 967 2025-11-16 17:11:05.061479 2025-11-16 17:49:29.024371 0 Ashburn Virginia United States US \N +748 104.167.19.173 3129 http \N \N t t 2025-11-16 17:26:01.678314 success 341 2025-11-16 17:11:05.672678 2025-11-16 17:11:05.672678 0 Ashburn Virginia United States US \N +249 104.167.19.225 3129 http \N \N t t 2025-11-16 17:49:26.20254 success 415 2025-11-16 17:11:05.053305 2025-11-16 17:49:26.20254 0 Ashburn Virginia United States US \N +69 193.56.28.165 3129 http \N \N t t 2025-11-16 17:47:53.801539 success 351 2025-11-16 17:11:04.806092 2025-11-16 17:47:53.801539 0 Ashburn Virginia United States US 2025-11-17 07:20:43.591451 +663 104.167.19.67 3129 http \N \N t t 2025-11-16 17:26:43.993333 success 346 2025-11-16 17:11:05.56503 2025-11-16 17:11:05.56503 0 Ashburn Virginia United States US \N +635 104.167.25.120 3129 http \N \N t t 2025-11-16 17:26:59.92303 success 338 2025-11-16 17:11:05.533282 2025-11-16 17:11:05.533282 0 Ashburn Virginia United States US \N +100 104.167.25.155 3129 http \N \N t t 2025-11-16 17:48:09.972745 success 343 2025-11-16 17:11:04.847668 2025-11-16 17:48:09.972745 0 Ashburn Virginia United States US 2025-11-17 07:21:49.655665 +285 104.167.25.200 3129 http \N \N t t 2025-11-16 17:49:42.337751 success 808 2025-11-16 17:11:05.099381 2025-11-16 17:49:42.337751 0 Ashburn Virginia United States US \N +951 104.167.25.22 3129 http \N \N t t 2025-11-16 17:24:16.453493 success 1065 2025-11-16 17:11:05.911836 2025-11-16 17:11:05.911836 0 Ashburn Virginia United States US \N +5 104.167.25.31 3129 http \N \N t t 2025-11-16 17:47:19.257025 success 470 2025-11-16 17:11:04.714162 2025-11-16 17:47:19.257025 0 Ashburn Virginia United States US 2025-11-17 07:19:29.023544 +466 104.167.25.35 3129 http \N \N t t 2025-11-16 17:51:10.118706 success 603 2025-11-16 17:11:05.329606 2025-11-16 17:51:10.118706 0 Ashburn Virginia United States US \N +510 104.167.25.75 3129 http \N \N t t 2025-11-16 17:28:06.804373 success 803 2025-11-16 17:11:05.382382 2025-11-16 17:27:08.931554 0 Ashburn Virginia United States US \N +628 104.167.25.97 3129 http \N \N t t 2025-11-16 17:27:02.524408 success 363 2025-11-16 17:11:05.524677 2025-11-16 17:11:05.524677 0 Ashburn Virginia United States US \N +332 104.207.32.107 3129 http \N \N t t 2025-11-16 17:50:07.49304 success 1077 2025-11-16 17:11:05.158189 2025-11-16 17:50:07.49304 0 Ashburn Virginia United States US \N +431 104.207.32.173 3129 http \N \N t t 2025-11-16 17:50:51.231534 success 441 2025-11-16 17:11:05.284429 2025-11-16 17:50:51.231534 0 Ashburn Virginia United States US \N +545 104.207.32.207 3129 http \N \N t t 2025-11-16 17:27:44.978636 success 586 2025-11-16 17:11:05.424707 2025-11-16 17:27:44.911028 0 Ashburn Virginia United States US \N +828 104.207.32.5 3129 http \N \N t t 2025-11-16 17:25:23.044901 success 361 2025-11-16 17:11:05.768119 2025-11-16 17:11:05.768119 0 Ashburn Virginia United States US \N +888 104.207.34.147 3129 http \N \N t t 2025-11-16 17:24:48.628265 success 335 2025-11-16 17:11:05.837403 2025-11-16 17:11:05.837403 0 Ashburn Virginia United States US \N +657 104.207.34.58 3129 http \N \N t t 2025-11-16 17:26:46.729407 success 341 2025-11-16 17:11:05.557937 2025-11-16 17:11:05.557937 0 Ashburn Virginia United States US \N +96 104.207.35.84 3129 http \N \N t t 2025-11-16 17:48:08.522481 success 497 2025-11-16 17:11:04.841829 2025-11-16 17:48:08.522481 0 Ashburn Virginia United States US 2025-11-17 07:21:48.739199 +616 104.207.36.20 3129 http \N \N t t 2025-11-16 17:27:07.507378 success 362 2025-11-16 17:11:05.509712 2025-11-16 17:11:05.509712 0 Ashburn Virginia United States US \N +289 104.207.36.213 3129 http \N \N t t 2025-11-16 17:49:43.906903 success 350 2025-11-16 17:11:05.104305 2025-11-16 17:49:43.906903 0 Ashburn Virginia United States US \N +335 104.207.36.252 3129 http \N \N t t 2025-11-16 17:50:08.668185 success 367 2025-11-16 17:11:05.162211 2025-11-16 17:50:08.668185 0 Ashburn Virginia United States US \N +477 104.207.37.26 3129 http \N \N t t 2025-11-16 17:51:15.629502 success 342 2025-11-16 17:11:05.343652 2025-11-16 17:51:15.629502 0 Ashburn Virginia United States US \N +365 104.207.38.1 3129 http \N \N t t 2025-11-16 17:50:22.091313 success 404 2025-11-16 17:11:05.201963 2025-11-16 17:50:22.091313 0 Ashburn Virginia United States US \N +782 104.207.38.208 3129 http \N \N t t 2025-11-16 17:25:44.259717 success 505 2025-11-16 17:11:05.713674 2025-11-16 17:11:05.713674 0 Ashburn Virginia United States US \N +867 104.207.38.209 3129 http \N \N t t 2025-11-16 17:25:00.296117 success 363 2025-11-16 17:11:05.811916 2025-11-16 17:11:05.811916 0 Ashburn Virginia United States US \N +985 104.207.38.28 3129 http \N \N t t 2025-11-16 17:23:59.181376 success 352 2025-11-16 17:11:05.949817 2025-11-16 17:11:05.949817 0 Ashburn Virginia United States US \N +549 104.207.38.61 3129 http \N \N t t 2025-11-16 17:27:49.543016 success 658 2025-11-16 17:11:05.429491 2025-11-16 17:27:49.543016 0 Ashburn Virginia United States US \N +509 104.207.39.181 3129 http \N \N t t 2025-11-16 17:28:07.156518 success 343 2025-11-16 17:11:05.381316 2025-11-16 17:27:08.05926 0 Ashburn Virginia United States US \N +673 104.207.39.211 3129 http \N \N t t 2025-11-16 17:26:40.264132 success 339 2025-11-16 17:11:05.577865 2025-11-16 17:11:05.577865 0 Ashburn Virginia United States US \N +555 104.207.40.119 3129 http \N \N t t 2025-11-16 17:27:56.510647 success 659 2025-11-16 17:11:05.436357 2025-11-16 17:27:56.510647 0 Ashburn Virginia United States US \N +564 104.207.41.10 3129 http \N \N t t 2025-11-16 17:28:05.475046 success 344 2025-11-16 17:11:05.447188 2025-11-16 17:28:05.475046 0 Ashburn Virginia United States US \N +898 104.207.41.147 3129 http \N \N t t 2025-11-16 17:24:43.646438 success 346 2025-11-16 17:11:05.848944 2025-11-16 17:11:05.848944 0 Ashburn Virginia United States US \N +27 104.207.42.130 3129 http \N \N t t 2025-11-16 17:47:31.783378 success 363 2025-11-16 17:11:04.747944 2025-11-16 17:47:31.783378 0 Ashburn Virginia United States US 2025-11-17 07:19:34.050408 +316 104.207.42.192 3129 http \N \N t t 2025-11-16 17:49:56.304672 success 339 2025-11-16 17:11:05.137763 2025-11-16 17:49:56.304672 0 Ashburn Virginia United States US \N +213 104.207.42.87 3129 http \N \N t t 2025-11-16 17:49:07.695362 success 873 2025-11-16 17:11:05.005327 2025-11-16 17:49:07.695362 0 Ashburn Virginia United States US 2025-11-17 07:24:15.650854 +819 104.207.43.163 3129 http \N \N t t 2025-11-16 17:25:28.477112 success 957 2025-11-16 17:11:05.756664 2025-11-16 17:11:05.756664 0 Ashburn Virginia United States US \N +395 104.207.43.250 3129 http \N \N t t 2025-11-16 17:50:36.386347 success 1082 2025-11-16 17:11:05.240164 2025-11-16 17:50:36.386347 0 Ashburn Virginia United States US \N +937 104.207.44.100 3129 http \N \N t t 2025-11-16 17:24:24.259934 success 785 2025-11-16 17:11:05.896002 2025-11-16 17:11:05.896002 0 Ashburn Virginia United States US \N +459 104.207.44.194 3129 http \N \N t t 2025-11-16 17:51:06.812538 success 354 2025-11-16 17:11:05.321107 2025-11-16 17:51:06.812538 0 Ashburn Virginia United States US \N +717 104.207.44.218 3129 http \N \N t t 2025-11-16 17:26:18.919004 success 349 2025-11-16 17:11:05.633411 2025-11-16 17:11:05.633411 0 Ashburn Virginia United States US \N +258 104.207.44.99 3129 http \N \N t t 2025-11-16 17:49:30.103503 success 344 2025-11-16 17:11:05.065418 2025-11-16 17:49:30.103503 0 Ashburn Virginia United States US \N +142 104.207.45.188 3129 http \N \N t t 2025-11-16 17:48:29.914305 success 618 2025-11-16 17:11:04.904322 2025-11-16 17:48:29.914305 0 Ashburn Virginia United States US 2025-11-17 07:22:59.401796 +560 104.207.45.190 3129 http \N \N t t 2025-11-16 17:28:01.873802 success 1040 2025-11-16 17:11:05.442352 2025-11-16 17:28:01.873802 0 Ashburn Virginia United States US \N +211 193.56.28.43 3129 http \N \N t t 2025-11-16 17:49:06.28531 success 618 2025-11-16 17:11:05.002259 2025-11-16 17:49:06.28531 0 Ashburn Virginia United States US 2025-11-17 07:24:15.118657 +207 209.50.160.23 3129 http \N \N t t 2025-11-16 17:49:03.966541 success 621 2025-11-16 17:11:04.997152 2025-11-16 17:49:03.966541 0 Ashburn Virginia United States US 2025-11-17 07:24:14.114498 +191 209.50.163.60 3129 http \N \N t t 2025-11-16 17:48:52.640155 success 341 2025-11-16 17:11:04.976282 2025-11-16 17:48:52.640155 0 Ashburn Virginia United States US 2025-11-17 07:24:10.672609 +216 209.50.172.9 3129 http \N \N t t 2025-11-16 17:49:09.37884 success 537 2025-11-16 17:11:05.009286 2025-11-16 17:49:09.37884 0 Ashburn Virginia United States US 2025-11-17 07:24:16.45353 +209 209.50.173.247 3129 http \N \N t t 2025-11-16 17:49:05.126406 success 613 2025-11-16 17:11:04.999511 2025-11-16 17:49:05.126406 0 Ashburn Virginia United States US 2025-11-17 07:24:14.645883 +196 216.26.224.153 3129 http \N \N t t 2025-11-16 17:48:55.631865 success 1541 2025-11-16 17:11:04.982965 2025-11-16 17:48:55.631865 0 Ashburn Virginia United States US 2025-11-17 07:24:11.568141 +202 216.26.231.124 3129 http \N \N t t 2025-11-16 17:49:00.947078 success 1410 2025-11-16 17:11:04.990856 2025-11-16 17:49:00.947078 0 Ashburn Virginia United States US 2025-11-17 07:24:12.966221 +197 216.26.231.210 3129 http \N \N t t 2025-11-16 17:48:56.882325 success 1247 2025-11-16 17:11:04.984315 2025-11-16 17:48:56.882325 0 Ashburn Virginia United States US 2025-11-17 07:24:11.690893 +645 104.207.46.40 3129 http \N \N t t 2025-11-16 17:26:53.959416 success 736 2025-11-16 17:11:05.544578 2025-11-16 17:11:05.544578 0 Ashburn Virginia United States US \N +432 104.207.46.76 3129 http \N \N t t 2025-11-16 17:50:51.590946 success 356 2025-11-16 17:11:05.285636 2025-11-16 17:50:51.590946 0 Ashburn Virginia United States US \N +458 104.207.47.133 3129 http \N \N t t 2025-11-16 17:51:06.454781 success 483 2025-11-16 17:11:05.319857 2025-11-16 17:51:06.454781 0 Ashburn Virginia United States US \N +39 193.56.28.173 3129 http \N \N t t 2025-11-16 17:47:37.971575 success 356 2025-11-16 17:11:04.766288 2025-11-16 17:47:37.971575 0 Ashburn Virginia United States US 2025-11-17 07:19:36.809085 +283 104.207.47.204 3129 http \N \N t t 2025-11-16 17:49:41.170056 success 344 2025-11-16 17:11:05.096857 2025-11-16 17:49:41.170056 0 Ashburn Virginia United States US \N +588 104.207.47.37 3129 http \N \N t t 2025-11-16 17:28:29.830965 success 346 2025-11-16 17:11:05.476432 2025-11-16 17:28:29.830965 0 Ashburn Virginia United States US \N +709 104.207.47.79 3129 http \N \N t t 2025-11-16 17:26:22.711185 success 464 2025-11-16 17:11:05.62339 2025-11-16 17:11:05.62339 0 Ashburn Virginia United States US \N +334 209.50.160.122 3129 http \N \N t t 2025-11-16 17:50:08.28986 success 358 2025-11-16 17:11:05.161024 2025-11-16 17:50:08.28986 0 Ashburn Virginia United States US \N +849 209.50.160.214 3129 http \N \N t t 2025-11-16 17:25:11.975346 success 938 2025-11-16 17:11:05.791008 2025-11-16 17:11:05.791008 0 Ashburn Virginia United States US \N +957 209.50.160.54 3129 http \N \N t t 2025-11-16 17:24:13.211954 success 356 2025-11-16 17:11:05.918574 2025-11-16 17:11:05.918574 0 Ashburn Virginia United States US \N +671 209.50.161.183 3129 http \N \N t t 2025-11-16 17:26:40.980643 success 348 2025-11-16 17:11:05.575644 2025-11-16 17:11:05.575644 0 Ashburn Virginia United States US \N +344 209.50.161.28 3129 http \N \N t t 2025-11-16 17:50:12.380454 success 347 2025-11-16 17:11:05.175147 2025-11-16 17:50:12.380454 0 Ashburn Virginia United States US \N +788 209.50.162.135 3129 http \N \N t t 2025-11-16 17:25:41.634735 success 338 2025-11-16 17:11:05.721306 2025-11-16 17:11:05.721306 0 Ashburn Virginia United States US \N +397 209.50.162.53 3129 http \N \N t t 2025-11-16 17:50:37.078154 success 342 2025-11-16 17:11:05.242509 2025-11-16 17:50:37.078154 0 Ashburn Virginia United States US \N +958 209.50.163.166 3129 http \N \N t t 2025-11-16 17:24:12.846778 success 1273 2025-11-16 17:11:05.919706 2025-11-16 17:11:05.919706 0 Ashburn Virginia United States US \N +265 209.50.163.9 3129 http \N \N t t 2025-11-16 17:49:33.15712 success 359 2025-11-16 17:11:05.073827 2025-11-16 17:49:33.15712 0 Ashburn Virginia United States US \N +817 209.50.163.98 3129 http \N \N t t 2025-11-16 17:25:29.498676 success 640 2025-11-16 17:11:05.754366 2025-11-16 17:11:05.754366 0 Ashburn Virginia United States US \N +479 209.50.164.196 3129 http \N \N t t 2025-11-16 17:51:16.324273 success 342 2025-11-16 17:11:05.346002 2025-11-16 17:51:16.324273 0 Ashburn Virginia United States US \N +504 209.50.165.116 3129 http \N \N t t 2025-11-16 17:28:09.990027 success 353 2025-11-16 17:11:05.375767 2025-11-16 17:27:02.106927 0 Ashburn Virginia United States US \N +947 209.50.166.187 3129 http \N \N t t 2025-11-16 17:24:18.565856 success 351 2025-11-16 17:11:05.907245 2025-11-16 17:11:05.907245 0 Ashburn Virginia United States US \N +406 209.50.166.63 3129 http \N \N t t 2025-11-16 17:50:41.067184 success 345 2025-11-16 17:11:05.253687 2025-11-16 17:50:41.067184 0 Ashburn Virginia United States US \N +181 209.50.167.195 3129 http \N \N t t 2025-11-16 17:48:48.620261 success 343 2025-11-16 17:11:04.961271 2025-11-16 17:48:48.620261 0 Ashburn Virginia United States US 2025-11-17 07:24:08.427657 +806 209.50.167.86 3129 http \N \N t t 2025-11-16 17:25:34.841862 success 369 2025-11-16 17:11:05.742161 2025-11-16 17:11:05.742161 0 Ashburn Virginia United States US \N +307 209.50.168.8 3129 http \N \N t t 2025-11-16 17:49:53.098204 success 1226 2025-11-16 17:11:05.126637 2025-11-16 17:49:53.098204 0 Ashburn Virginia United States US \N +599 209.50.169.101 3129 http \N \N t t 2025-11-16 17:27:15.199689 success 363 2025-11-16 17:11:05.48894 2025-11-16 17:11:05.48894 0 Ashburn Virginia United States US \N +776 209.50.169.163 3129 http \N \N t t 2025-11-16 17:25:46.673736 success 358 2025-11-16 17:11:05.707011 2025-11-16 17:11:05.707011 0 Ashburn Virginia United States US \N +293 209.50.169.53 3129 http \N \N t t 2025-11-16 17:49:45.951134 success 560 2025-11-16 17:11:05.109556 2025-11-16 17:49:45.951134 0 Ashburn Virginia United States US \N +115 209.50.170.138 3129 http \N \N t t 2025-11-16 17:48:17.670414 success 358 2025-11-16 17:11:04.867574 2025-11-16 17:48:17.670414 0 Ashburn Virginia United States US 2025-11-17 07:21:53.091364 +880 209.50.171.22 3129 http \N \N t t 2025-11-16 17:24:53.877234 success 357 2025-11-16 17:11:05.827513 2025-11-16 17:11:05.827513 0 Ashburn Virginia United States US \N +796 209.50.172.1 3129 http \N \N t t 2025-11-16 17:25:38.625451 success 348 2025-11-16 17:11:05.731439 2025-11-16 17:11:05.731439 0 Ashburn Virginia United States US \N +456 209.50.172.100 3129 http \N \N t t 2025-11-16 17:51:05.624482 success 342 2025-11-16 17:11:05.31718 2025-11-16 17:51:05.624482 0 Ashburn Virginia United States US \N +439 209.50.172.114 3129 http \N \N t t 2025-11-16 17:50:54.157573 success 347 2025-11-16 17:11:05.294446 2025-11-16 17:50:54.157573 0 Ashburn Virginia United States US \N +572 209.50.172.16 3129 http \N \N t t 2025-11-16 17:28:13.832309 success 339 2025-11-16 17:11:05.456865 2025-11-16 17:28:13.832309 0 Ashburn Virginia United States US \N +383 209.50.172.89 3129 http \N \N t t 2025-11-16 17:50:29.031631 success 479 2025-11-16 17:11:05.225441 2025-11-16 17:50:29.031631 0 Ashburn Virginia United States US \N +676 209.50.173.183 3129 http \N \N t t 2025-11-16 17:26:39.171767 success 366 2025-11-16 17:11:05.58154 2025-11-16 17:11:05.58154 0 Ashburn Virginia United States US \N +725 209.50.174.235 3129 http \N \N t t 2025-11-16 17:26:13.972646 success 357 2025-11-16 17:11:05.644261 2025-11-16 17:11:05.644261 0 Ashburn Virginia United States US \N +986 209.50.175.139 3129 http \N \N t t 2025-11-16 17:23:58.819233 success 359 2025-11-16 17:11:05.950909 2025-11-16 17:11:05.950909 0 Ashburn Virginia United States US \N +732 209.50.175.202 3129 http \N \N t t 2025-11-16 17:26:11.263713 success 1385 2025-11-16 17:11:05.653463 2025-11-16 17:11:05.653463 0 Ashburn Virginia United States US \N +895 209.50.175.214 3129 http \N \N t t 2025-11-16 17:24:45.411411 success 783 2025-11-16 17:11:05.845555 2025-11-16 17:11:05.845555 0 Ashburn Virginia United States US \N +659 209.50.175.95 3129 http \N \N t t 2025-11-16 17:26:46.008998 success 340 2025-11-16 17:11:05.560103 2025-11-16 17:11:05.560103 0 Ashburn Virginia United States US \N +362 209.50.175.97 3129 http \N \N t t 2025-11-16 17:50:20.984981 success 362 2025-11-16 17:11:05.198081 2025-11-16 17:50:20.984981 0 Ashburn Virginia United States US \N +206 216.26.224.184 3129 http \N \N t t 2025-11-16 17:49:03.34139 success 530 2025-11-16 17:11:04.995916 2025-11-16 17:49:03.34139 0 Ashburn Virginia United States US 2025-11-17 07:24:13.845502 +327 216.26.224.24 3129 http \N \N t t 2025-11-16 17:50:02.7008 success 664 2025-11-16 17:11:05.151056 2025-11-16 17:50:02.7008 0 Ashburn Virginia United States US \N +569 216.26.224.64 3129 http \N \N t t 2025-11-16 17:28:10.458037 success 345 2025-11-16 17:11:05.453265 2025-11-16 17:28:10.458037 0 Ashburn Virginia United States US \N +813 216.26.225.169 3129 http \N \N t t 2025-11-16 17:25:32.223945 success 353 2025-11-16 17:11:05.749774 2025-11-16 17:11:05.749774 0 Ashburn Virginia United States US \N +461 216.26.225.182 3129 http \N \N t t 2025-11-16 17:51:07.515075 success 343 2025-11-16 17:11:05.323581 2025-11-16 17:51:07.515075 0 Ashburn Virginia United States US \N +752 216.26.226.10 3129 http \N \N t t 2025-11-16 17:25:58.160514 success 1185 2025-11-16 17:11:05.677004 2025-11-16 17:11:05.677004 0 Ashburn Virginia United States US \N +193 216.26.226.91 3129 http \N \N t t 2025-11-16 17:48:53.364326 success 358 2025-11-16 17:11:04.978926 2025-11-16 17:48:53.364326 0 Ashburn Virginia United States US 2025-11-17 07:24:11.19349 +923 216.26.227.147 3129 http \N \N t t 2025-11-16 17:24:32.956919 success 540 2025-11-16 17:11:05.879407 2025-11-16 17:11:05.879407 0 Ashburn Virginia United States US \N +9 216.26.227.241 3129 http \N \N t t 2025-11-16 17:47:21.007394 success 345 2025-11-16 17:11:04.720798 2025-11-16 17:47:21.007394 0 Ashburn Virginia United States US 2025-11-17 07:19:30.08279 +878 216.26.228.204 3129 http \N \N t t 2025-11-16 17:24:54.894818 success 359 2025-11-16 17:11:05.82544 2025-11-16 17:11:05.82544 0 Ashburn Virginia United States US \N +514 216.26.229.132 3129 http \N \N t t 2025-11-16 17:28:03.7007 success 359 2025-11-16 17:11:05.386947 2025-11-16 17:27:12.991112 0 Ashburn Virginia United States US \N +606 216.26.229.204 3129 http \N \N t t 2025-11-16 17:27:12.481741 success 357 2025-11-16 17:11:05.497946 2025-11-16 17:11:05.497946 0 Ashburn Virginia United States US \N +35 216.26.229.29 3129 http \N \N t t 2025-11-16 17:47:36.203625 success 363 2025-11-16 17:11:04.76072 2025-11-16 17:47:36.203625 0 Ashburn Virginia United States US 2025-11-17 07:19:35.884551 +377 216.26.230.162 3129 http \N \N t t 2025-11-16 17:50:26.749692 success 365 2025-11-16 17:11:05.218087 2025-11-16 17:50:26.749692 0 Ashburn Virginia United States US \N +308 216.26.231.145 3129 http \N \N t t 2025-11-16 17:49:53.456804 success 353 2025-11-16 17:11:05.127998 2025-11-16 17:49:53.456804 0 Ashburn Virginia United States US \N +562 104.207.46.252 3129 http \N \N t t 2025-11-16 17:28:03.785004 success 485 2025-11-16 17:11:05.444555 2025-11-16 17:28:03.785004 0 Ashburn Virginia United States US \N +637 216.26.231.44 3129 http \N \N t t 2025-11-16 17:26:59.228318 success 830 2025-11-16 17:11:05.535541 2025-11-16 17:11:05.535541 0 Ashburn Virginia United States US \N +860 216.26.232.44 3129 http \N \N t t 2025-11-16 17:25:06.676155 success 400 2025-11-16 17:11:05.80391 2025-11-16 17:11:05.80391 0 Ashburn Virginia United States US \N +64 216.26.232.72 3129 http \N \N t t 2025-11-16 17:47:50.770553 success 412 2025-11-16 17:11:04.799015 2025-11-16 17:47:50.770553 0 Ashburn Virginia United States US 2025-11-17 07:20:42.524392 +554 216.26.232.85 3129 http \N \N t t 2025-11-16 17:27:55.347925 success 654 2025-11-16 17:11:05.435193 2025-11-16 17:27:55.347925 0 Ashburn Virginia United States US \N +483 216.26.233.130 3129 http \N \N t t 2025-11-16 17:51:18.025192 success 457 2025-11-16 17:11:05.350622 2025-11-16 17:51:18.025192 0 Ashburn Virginia United States US \N +54 216.26.233.178 3129 http \N \N t t 2025-11-16 17:47:46.933321 success 427 2025-11-16 17:11:04.785958 2025-11-16 17:47:46.933321 0 Ashburn Virginia United States US 2025-11-17 07:20:40.224888 +900 216.26.233.231 3129 http \N \N t t 2025-11-16 17:24:42.927147 success 391 2025-11-16 17:11:05.851178 2025-11-16 17:11:05.851178 0 Ashburn Virginia United States US \N +336 216.26.234.175 3129 http \N \N t t 2025-11-16 17:50:09.059734 success 388 2025-11-16 17:11:05.163373 2025-11-16 17:50:09.059734 0 Ashburn Virginia United States US \N +722 216.26.235.181 3129 http \N \N t t 2025-11-16 17:26:15.417969 success 679 2025-11-16 17:11:05.640244 2025-11-16 17:11:05.640244 0 Ashburn Virginia United States US \N +93 216.26.235.190 3129 http \N \N t t 2025-11-16 17:48:07.272592 success 654 2025-11-16 17:11:04.837577 2025-11-16 17:48:07.272592 0 Ashburn Virginia United States US 2025-11-17 07:21:48.063095 +890 216.26.235.62 3129 http \N \N t t 2025-11-16 17:24:47.86792 success 382 2025-11-16 17:11:05.839563 2025-11-16 17:11:05.839563 0 Ashburn Virginia United States US \N +627 216.26.236.100 3129 http \N \N t t 2025-11-16 17:27:02.911705 success 379 2025-11-16 17:11:05.523357 2025-11-16 17:11:05.523357 0 Ashburn Virginia United States US \N +251 216.26.236.205 3129 http \N \N t t 2025-11-16 17:49:26.956356 success 386 2025-11-16 17:11:05.056311 2025-11-16 17:49:26.956356 0 Ashburn Virginia United States US \N +575 216.26.237.152 3129 http \N \N t t 2025-11-16 17:28:16.474991 success 419 2025-11-16 17:11:05.460375 2025-11-16 17:28:16.474991 0 Ashburn Virginia United States US \N +503 216.26.237.64 3129 http \N \N t t 2025-11-16 17:28:10.373934 success 374 2025-11-16 17:11:05.374636 2025-11-16 17:27:01.249454 0 Ashburn Virginia United States US \N +287 216.26.238.119 3129 http \N \N t t 2025-11-16 17:49:43.070104 success 377 2025-11-16 17:11:05.101822 2025-11-16 17:49:43.070104 0 Ashburn Virginia United States US \N +326 216.26.238.41 3129 http \N \N t t 2025-11-16 17:50:02.032954 success 1225 2025-11-16 17:11:05.14985 2025-11-16 17:50:02.032954 0 Ashburn Virginia United States US \N +727 216.26.238.54 3129 http \N \N t t 2025-11-16 17:26:13.183577 success 465 2025-11-16 17:11:05.646971 2025-11-16 17:11:05.646971 0 Ashburn Virginia United States US \N +918 45.3.32.159 3129 http \N \N t t 2025-11-16 17:24:34.948589 success 552 2025-11-16 17:11:05.872696 2025-11-16 17:11:05.872696 0 Ashburn Virginia United States US \N +415 45.3.32.242 3129 http \N \N t t 2025-11-16 17:50:44.61798 success 732 2025-11-16 17:11:05.265155 2025-11-16 17:50:44.61798 0 Ashburn Virginia United States US \N +868 45.3.33.98 3129 http \N \N t t 2025-11-16 17:24:59.923126 success 359 2025-11-16 17:11:05.81298 2025-11-16 17:11:05.81298 0 Ashburn Virginia United States US \N +187 45.3.34.24 3129 http \N \N t t 2025-11-16 17:48:51.147832 success 362 2025-11-16 17:11:04.970196 2025-11-16 17:48:51.147832 0 Ashburn Virginia United States US 2025-11-17 07:24:09.751054 +920 45.3.35.137 3129 http \N \N t t 2025-11-16 17:24:34.036785 success 340 2025-11-16 17:11:05.875563 2025-11-16 17:11:05.875563 0 Ashburn Virginia United States US \N +473 45.3.35.139 3129 http \N \N t t 2025-11-16 17:51:14.183951 success 341 2025-11-16 17:11:05.338661 2025-11-16 17:51:14.183951 0 Ashburn Virginia United States US \N +810 45.3.36.54 3129 http \N \N t t 2025-11-16 17:25:33.311956 success 347 2025-11-16 17:11:05.746392 2025-11-16 17:11:05.746392 0 Ashburn Virginia United States US \N +1000 45.3.37.135 3129 http \N \N t t 2025-11-16 17:23:52.645682 success 838 2025-11-16 17:11:05.966548 2025-11-16 17:11:05.966548 0 Ashburn Virginia United States US \N +547 45.3.37.247 3129 http \N \N t t 2025-11-16 17:27:47.224335 success 657 2025-11-16 17:11:05.427068 2025-11-16 17:27:47.224335 0 Ashburn Virginia United States US \N +543 45.3.37.97 3129 http \N \N t t 2025-11-16 17:27:46.365242 success 524 2025-11-16 17:11:05.422146 2025-11-16 17:27:42.646842 0 Ashburn Virginia United States US \N +731 45.3.38.6 3129 http \N \N t t 2025-11-16 17:26:11.614985 success 341 2025-11-16 17:11:05.652043 2025-11-16 17:11:05.652043 0 Ashburn Virginia United States US \N +433 45.3.39.228 3129 http \N \N t t 2025-11-16 17:50:51.953083 success 358 2025-11-16 17:11:05.286957 2025-11-16 17:50:51.953083 0 Ashburn Virginia United States US \N +803 45.3.39.45 3129 http \N \N t t 2025-11-16 17:25:36.116353 success 357 2025-11-16 17:11:05.739053 2025-11-16 17:11:05.739053 0 Ashburn Virginia United States US \N +921 45.3.39.46 3129 http \N \N t t 2025-11-16 17:24:33.687548 success 353 2025-11-16 17:11:05.876848 2025-11-16 17:11:05.876848 0 Ashburn Virginia United States US \N +577 45.3.39.5 3129 http \N \N t t 2025-11-16 17:28:19.247279 success 1012 2025-11-16 17:11:05.462749 2025-11-16 17:28:19.247279 0 Ashburn Virginia United States US \N +941 45.3.48.161 3129 http \N \N t t 2025-11-16 17:24:21.371329 success 396 2025-11-16 17:11:05.900483 2025-11-16 17:11:05.900483 0 Ashburn Virginia United States US \N +647 45.3.48.36 3129 http \N \N t t 2025-11-16 17:26:52.863629 success 357 2025-11-16 17:11:05.546855 2025-11-16 17:11:05.546855 0 Ashburn Virginia United States US \N +427 45.3.49.10 3129 http \N \N t t 2025-11-16 17:50:49.693304 success 354 2025-11-16 17:11:05.279578 2025-11-16 17:50:49.693304 0 Ashburn Virginia United States US \N +337 45.3.49.118 3129 http \N \N t t 2025-11-16 17:50:09.415017 success 352 2025-11-16 17:11:05.164937 2025-11-16 17:50:09.415017 0 Ashburn Virginia United States US \N +468 45.3.49.70 3129 http \N \N t t 2025-11-16 17:51:10.859768 success 352 2025-11-16 17:11:05.33245 2025-11-16 17:51:10.859768 0 Ashburn Virginia United States US \N +641 45.3.50.21 3129 http \N \N t t 2025-11-16 17:26:56.096386 success 411 2025-11-16 17:11:05.540141 2025-11-16 17:11:05.540141 0 Ashburn Virginia United States US \N +639 45.3.50.213 3129 http \N \N t t 2025-11-16 17:26:58.042017 success 344 2025-11-16 17:11:05.53798 2025-11-16 17:11:05.53798 0 Ashburn Virginia United States US \N +441 65.111.2.57 3129 http \N \N t t 2025-11-16 17:50:54.937669 success 432 2025-11-16 17:11:05.29708 2025-11-16 17:50:54.937669 0 Ashburn Virginia United States US \N +240 65.111.3.30 3129 http \N \N t t 2025-11-16 17:49:22.023768 success 342 2025-11-16 17:11:05.041162 2025-11-16 17:49:22.023768 0 Ashburn Virginia United States US \N +496 65.111.3.88 3129 http \N \N t t 2025-11-16 17:51:24.369147 success 352 2025-11-16 17:11:05.365606 2025-11-16 17:51:24.369147 0 Ashburn Virginia United States US \N +340 65.111.4.138 3129 http \N \N t t 2025-11-16 17:50:10.441873 success 339 2025-11-16 17:11:05.169309 2025-11-16 17:50:10.441873 0 Ashburn Virginia United States US \N +519 65.111.5.48 3129 http \N \N t t 2025-11-16 17:28:00.863679 success 339 2025-11-16 17:11:05.39262 2025-11-16 17:27:18.589936 0 Ashburn Virginia United States US \N +815 65.111.6.157 3129 http \N \N t t 2025-11-16 17:25:31.001469 success 1125 2025-11-16 17:11:05.752201 2025-11-16 17:11:05.752201 0 Ashburn Virginia United States US \N +756 65.111.6.220 3129 http \N \N t t 2025-11-16 17:25:55.83921 success 345 2025-11-16 17:11:05.681872 2025-11-16 17:11:05.681872 0 Ashburn Virginia United States US \N +934 65.111.6.63 3129 http \N \N t t 2025-11-16 17:24:26.898426 success 351 2025-11-16 17:11:05.892453 2025-11-16 17:11:05.892453 0 Ashburn Virginia United States US \N +678 65.111.7.167 3129 http \N \N t t 2025-11-16 17:26:36.712227 success 351 2025-11-16 17:11:05.58441 2025-11-16 17:11:05.58441 0 Ashburn Virginia United States US \N +424 65.111.7.228 3129 http \N \N t t 2025-11-16 17:50:48.621721 success 355 2025-11-16 17:11:05.276238 2025-11-16 17:50:48.621721 0 Ashburn Virginia United States US \N +939 65.111.8.231 3129 http \N \N t t 2025-11-16 17:24:23.088023 success 347 2025-11-16 17:11:05.898014 2025-11-16 17:11:05.898014 0 Ashburn Virginia United States US \N +1 65.111.9.140 3129 http \N \N t t 2025-11-16 17:47:17.72046 success 415 2025-11-16 17:11:04.704379 2025-11-16 17:47:17.72046 0 Ashburn Virginia United States US 2025-11-17 07:19:28.027279 +19 209.50.164.218 3129 http \N \N t t 2025-11-16 17:47:27.836393 success 345 2025-11-16 17:11:04.735564 2025-11-16 17:47:27.836393 0 Ashburn Virginia United States US 2025-11-17 07:19:32.219959 +29 209.50.164.26 3129 http \N \N t t 2025-11-16 17:47:32.497489 success 354 2025-11-16 17:11:04.751577 2025-11-16 17:47:32.497489 0 Ashburn Virginia United States US 2025-11-17 07:19:34.509213 +3 209.50.168.115 3129 http \N \N t t 2025-11-16 17:47:18.425073 success 343 2025-11-16 17:11:04.710706 2025-11-16 17:47:18.425073 0 Ashburn Virginia United States US 2025-11-17 07:19:28.491858 +61 209.50.169.121 3129 http \N \N t t 2025-11-16 17:47:49.635842 success 551 2025-11-16 17:11:04.795162 2025-11-16 17:47:49.635842 0 Ashburn Virginia United States US 2025-11-17 07:20:41.833589 +26 209.50.169.14 3129 http \N \N t t 2025-11-16 17:47:31.41715 success 517 2025-11-16 17:11:04.746392 2025-11-16 17:47:31.41715 0 Ashburn Virginia United States US 2025-11-17 07:19:33.824601 +33 209.50.171.214 3129 http \N \N t t 2025-11-16 17:47:35.472872 success 956 2025-11-16 17:11:04.757844 2025-11-16 17:47:35.472872 0 Ashburn Virginia United States US 2025-11-17 07:19:35.423623 +58 209.50.171.29 3129 http \N \N t t 2025-11-16 17:47:48.365535 success 347 2025-11-16 17:11:04.791032 2025-11-16 17:47:48.365535 0 Ashburn Virginia United States US 2025-11-17 07:20:41.14195 +68 209.50.171.63 3129 http \N \N t t 2025-11-16 17:47:53.44647 success 345 2025-11-16 17:11:04.804375 2025-11-16 17:47:53.44647 0 Ashburn Virginia United States US 2025-11-17 07:20:43.468975 +28 209.50.172.191 3129 http \N \N t t 2025-11-16 17:47:32.140294 success 353 2025-11-16 17:11:04.749495 2025-11-16 17:47:32.140294 0 Ashburn Virginia United States US 2025-11-17 07:19:34.281518 +73 209.50.172.220 3129 http \N \N t t 2025-11-16 17:47:55.923451 success 609 2025-11-16 17:11:04.81127 2025-11-16 17:47:55.923451 0 Ashburn Virginia United States US 2025-11-17 07:20:44.074073 +34 216.26.224.193 3129 http \N \N t t 2025-11-16 17:47:35.8365 success 360 2025-11-16 17:11:04.759192 2025-11-16 17:47:35.8365 0 Ashburn Virginia United States US 2025-11-17 07:19:35.65484 +74 216.26.227.111 3129 http \N \N t t 2025-11-16 17:47:56.271892 success 345 2025-11-16 17:11:04.812568 2025-11-16 17:47:56.271892 0 Ashburn Virginia United States US 2025-11-17 07:20:44.195507 +22 216.26.230.146 3129 http \N \N t t 2025-11-16 17:47:29.666084 success 602 2025-11-16 17:11:04.740183 2025-11-16 17:47:29.666084 0 Ashburn Virginia United States US 2025-11-17 07:19:32.906096 +82 216.26.231.51 3129 http \N \N t t 2025-11-16 17:48:00.769595 success 713 2025-11-16 17:11:04.822886 2025-11-16 17:48:00.769595 0 Ashburn Virginia United States US 2025-11-17 07:20:45.515343 +55 216.26.232.17 3129 http \N \N t t 2025-11-16 17:47:47.317115 success 379 2025-11-16 17:11:04.787266 2025-11-16 17:47:47.317115 0 Ashburn Virginia United States US 2025-11-17 07:20:40.451848 +49 216.26.232.26 3129 http \N \N t t 2025-11-16 17:47:43.443296 success 383 2025-11-16 17:11:04.779469 2025-11-16 17:47:43.443296 0 Ashburn Virginia United States US 2025-11-17 07:20:39.074474 +7 216.26.234.2 3129 http \N \N t t 2025-11-16 17:47:20.309951 success 685 2025-11-16 17:11:04.717181 2025-11-16 17:47:20.309951 0 Ashburn Virginia United States US 2025-11-17 07:19:29.561154 +18 216.26.238.0 3129 http \N \N t t 2025-11-16 17:47:27.485865 success 697 2025-11-16 17:11:04.734042 2025-11-16 17:47:27.485865 0 Ashburn Virginia United States US 2025-11-17 07:19:31.993263 +83 45.3.32.116 3129 http \N \N t t 2025-11-16 17:48:02.236981 success 1463 2025-11-16 17:11:04.824081 2025-11-16 17:48:02.236981 0 Ashburn Virginia United States US 2025-11-17 07:20:45.772099 +23 45.3.34.185 3129 http \N \N t t 2025-11-16 17:47:30.012201 success 342 2025-11-16 17:11:04.741681 2025-11-16 17:47:30.012201 0 Ashburn Virginia United States US 2025-11-17 07:19:33.135375 +4 45.3.34.225 3129 http \N \N t t 2025-11-16 17:47:18.783579 success 352 2025-11-16 17:11:04.712782 2025-11-16 17:47:18.783579 0 Ashburn Virginia United States US 2025-11-17 07:19:28.755984 +521 209.50.167.170 3129 http \N \N t \N 2025-11-16 17:28:00.164847 failed \N 2025-11-16 17:11:05.394903 2025-11-16 17:27:20.297314 0 Ashburn Virginia United States US \N +707 45.3.50.44 3129 http \N \N t t 2025-11-16 17:26:23.435461 success 353 2025-11-16 17:11:05.621039 2025-11-16 17:11:05.621039 0 Ashburn Virginia United States US \N +870 45.3.51.176 3129 http \N \N t t 2025-11-16 17:24:59.200832 success 350 2025-11-16 17:11:05.815296 2025-11-16 17:11:05.815296 0 Ashburn Virginia United States US \N +418 45.3.51.6 3129 http \N \N t t 2025-11-16 17:50:46.148599 success 377 2025-11-16 17:11:05.268813 2025-11-16 17:50:46.148599 0 Ashburn Virginia United States US \N +899 45.3.51.72 3129 http \N \N t t 2025-11-16 17:24:43.291439 success 355 2025-11-16 17:11:05.850056 2025-11-16 17:11:05.850056 0 Ashburn Virginia United States US \N +770 45.3.62.180 3129 http \N \N t t 2025-11-16 17:25:49.734455 success 715 2025-11-16 17:11:05.698275 2025-11-16 17:11:05.698275 0 Ashburn Virginia United States US \N +298 65.111.0.150 3129 http \N \N t t 2025-11-16 17:49:47.832348 success 342 2025-11-16 17:11:05.115714 2025-11-16 17:49:47.832348 0 Ashburn Virginia United States US \N +80 65.111.0.163 3129 http \N \N t t 2025-11-16 17:47:59.528615 success 513 2025-11-16 17:11:04.82038 2025-11-16 17:47:59.528615 0 Ashburn Virginia United States US 2025-11-17 07:20:45.000989 +501 65.111.0.182 3129 http \N \N t t 2025-11-16 17:51:26.304815 success 345 2025-11-16 17:11:05.372333 2025-11-16 17:51:26.304815 0 Ashburn Virginia United States US \N +546 65.111.0.251 3129 http \N \N t t 2025-11-16 17:27:46.063299 success 649 2025-11-16 17:11:05.425943 2025-11-16 17:27:46.063299 0 Ashburn Virginia United States US \N +203 65.111.0.31 3129 http \N \N t t 2025-11-16 17:49:01.484704 success 534 2025-11-16 17:11:04.992082 2025-11-16 17:49:01.484704 0 Ashburn Virginia United States US 2025-11-17 07:24:13.170927 +617 65.111.11.16 3129 http \N \N t t 2025-11-16 17:27:07.136414 success 717 2025-11-16 17:11:05.510967 2025-11-16 17:11:05.510967 0 Ashburn Virginia United States US \N +384 65.111.13.209 3129 http \N \N t t 2025-11-16 17:50:29.375347 success 341 2025-11-16 17:11:05.226679 2025-11-16 17:50:29.375347 0 Ashburn Virginia United States US \N +874 65.111.13.8 3129 http \N \N t t 2025-11-16 17:24:57.723615 success 549 2025-11-16 17:11:05.820575 2025-11-16 17:11:05.820575 0 Ashburn Virginia United States US \N +734 65.111.13.80 3129 http \N \N t t 2025-11-16 17:26:09.505141 success 354 2025-11-16 17:11:05.656144 2025-11-16 17:11:05.656144 0 Ashburn Virginia United States US \N +375 65.111.13.96 3129 http \N \N t t 2025-11-16 17:50:26.036323 success 436 2025-11-16 17:11:05.215546 2025-11-16 17:50:26.036323 0 Ashburn Virginia United States US \N +541 65.111.14.108 3129 http \N \N t t 2025-11-16 17:27:47.999083 success 533 2025-11-16 17:11:05.419863 2025-11-16 17:27:40.426054 0 Ashburn Virginia United States US \N +146 65.111.14.132 3129 http \N \N t t 2025-11-16 17:48:31.680862 success 349 2025-11-16 17:11:04.911 2025-11-16 17:48:31.680862 0 Ashburn Virginia United States US 2025-11-17 07:23:00.470735 +981 65.111.14.140 3129 http \N \N t t 2025-11-16 17:24:00.665161 success 350 2025-11-16 17:11:05.945173 2025-11-16 17:11:05.945173 0 Ashburn Virginia United States US \N +303 65.111.15.198 3129 http \N \N t t 2025-11-16 17:49:49.673864 success 343 2025-11-16 17:11:05.121829 2025-11-16 17:49:49.673864 0 Ashburn Virginia United States US \N +125 65.111.15.33 3129 http \N \N t t 2025-11-16 17:48:21.564528 success 364 2025-11-16 17:11:04.881674 2025-11-16 17:48:21.564528 0 Ashburn Virginia United States US 2025-11-17 07:21:55.386469 +556 65.111.2.18 3129 http \N \N t t 2025-11-16 17:27:57.671174 success 656 2025-11-16 17:11:05.437547 2025-11-16 17:27:57.671174 0 Ashburn Virginia United States US \N +\. + + +-- +-- Data for Name: proxy_test_jobs; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.proxy_test_jobs (id, status, total_proxies, tested_proxies, passed_proxies, failed_proxies, started_at, completed_at, created_at, updated_at) FROM stdin; +1 cancelled 1000 502 500 2 2025-11-16 17:47:17.299316 2025-11-16 18:10:34.814097 2025-11-16 17:47:17.291838 2025-11-16 18:10:34.814097 +2 cancelled 1000 0 0 0 \N 2025-11-16 18:55:59.832704 2025-11-16 18:54:43.84894 2025-11-16 18:55:59.832704 +\. + + +-- +-- Data for Name: sandbox_crawl_jobs; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.sandbox_crawl_jobs (id, dispensary_id, sandbox_id, job_type, status, priority, scheduled_at, started_at, completed_at, worker_id, result_summary, error_message, created_at, updated_at, category, template_name) FROM stdin; +1 72 \N detection pending 10 2025-12-01 15:16:43.844071+00 \N \N \N {} \N 2025-12-01 15:16:43.844071+00 2025-12-01 15:16:43.844071+00 product \N +2 73 \N detection pending 10 2025-12-01 15:16:43.848509+00 \N \N \N {} \N 2025-12-01 15:16:43.848509+00 2025-12-01 15:16:43.848509+00 product \N +3 74 \N detection pending 10 2025-12-01 15:16:43.852408+00 \N \N \N {} \N 2025-12-01 15:16:43.852408+00 2025-12-01 15:16:43.852408+00 product \N +4 75 \N detection pending 10 2025-12-01 15:16:43.855839+00 \N \N \N {} \N 2025-12-01 15:16:43.855839+00 2025-12-01 15:16:43.855839+00 product \N +5 76 \N detection pending 10 2025-12-01 15:16:43.858921+00 \N \N \N {} \N 2025-12-01 15:16:43.858921+00 2025-12-01 15:16:43.858921+00 product \N +6 78 \N detection pending 10 2025-12-01 15:16:43.862309+00 \N \N \N {} \N 2025-12-01 15:16:43.862309+00 2025-12-01 15:16:43.862309+00 product \N +7 82 \N detection pending 10 2025-12-01 15:16:43.865393+00 \N \N \N {} \N 2025-12-01 15:16:43.865393+00 2025-12-01 15:16:43.865393+00 product \N +8 83 \N detection pending 10 2025-12-01 15:16:43.867741+00 \N \N \N {} \N 2025-12-01 15:16:43.867741+00 2025-12-01 15:16:43.867741+00 product \N +9 85 \N detection pending 10 2025-12-01 15:16:43.871099+00 \N \N \N {} \N 2025-12-01 15:16:43.871099+00 2025-12-01 15:16:43.871099+00 product \N +10 71 \N detection pending 10 2025-12-01 15:16:43.874041+00 \N \N \N {} \N 2025-12-01 15:16:43.874041+00 2025-12-01 15:16:43.874041+00 product \N +\. + + +-- +-- Data for Name: settings; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.settings (key, value, description, updated_at) FROM stdin; +scrape_interval_hours 4 How often to scrape stores (in hours) 2025-11-14 19:28:15.540317 +scrape_specials_time 00:01 Time to scrape specials daily (HH:MM in 24h format) 2025-11-14 19:28:15.540317 +analytics_retention_days 365 How many days to keep analytics data 2025-11-14 19:28:15.540317 +proxy_timeout_ms 3000 Proxy timeout in milliseconds 2025-11-14 19:28:15.540317 +proxy_test_url https://httpbin.org/ip URL to test proxies against 2025-11-14 19:28:15.540317 +\. + + +-- +-- Data for Name: specials; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.specials (id, store_id, product_id, name, description, discount_amount, discount_percentage, special_price, original_price, valid_date, created_at, updated_at) FROM stdin; +\. + + +-- +-- Data for Name: store_crawl_schedule; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.store_crawl_schedule (id, store_id, enabled, interval_hours, daily_special_enabled, daily_special_time, priority, created_at, updated_at, last_status, last_summary, last_run_at, last_error) FROM stdin; +1 19 t \N t \N 0 2025-12-01 08:12:11.872576+00 2025-12-01 08:12:11.890294+00 error No dispensary linked - cannot determine provider 2025-12-01 08:12:11.890294+00 Store is not linked to a dispensary. Link it in the Dispensaries page. +5 18 t \N t \N 0 2025-12-01 08:12:11.874054+00 2025-12-01 08:12:11.889707+00 error No dispensary linked - cannot determine provider 2025-12-01 08:12:11.889707+00 Store is not linked to a dispensary. Link it in the Dispensaries page. +2 1 t \N t \N 0 2025-12-01 08:12:11.873329+00 2025-12-01 08:12:11.891784+00 running Running Dutchie production crawl... 2025-12-01 08:12:11.891784+00 \N +4 20 t \N t \N 0 2025-12-01 08:12:11.873755+00 2025-12-01 08:12:11.8906+00 error No dispensary linked - cannot determine provider 2025-12-01 08:12:11.8906+00 Store is not linked to a dispensary. Link it in the Dispensaries page. +3 21 t \N t \N 0 2025-12-01 08:12:11.873317+00 2025-12-01 08:12:11.890975+00 error No dispensary linked - cannot determine provider 2025-12-01 08:12:11.890975+00 Store is not linked to a dispensary. Link it in the Dispensaries page. +\. + + +-- +-- Data for Name: stores; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.stores (id, name, slug, dutchie_url, active, scrape_enabled, last_scraped_at, created_at, updated_at, logo_url, timezone, dispensary_id) FROM stdin; +19 Curaleaf - 83rd Ave curaleaf-az-83rd-dispensary https://dutchie.com/dispensary/curaleaf-83rd-ave t t \N 2025-11-16 20:51:41.889389 2025-11-16 20:51:41.889389 https://curaleaf.com/favicon.ico America/Phoenix 256 +23 Curaleaf - Gilbert curaleaf-az-gilbert https://dutchie.com/dispensary/curaleaf-gilbert t t \N 2025-11-16 20:51:41.89543 2025-11-16 20:51:41.89543 https://curaleaf.com/favicon.ico America/Phoenix 257 +24 Curaleaf - Glendale East curaleaf-az-glendale-east https://dutchie.com/dispensary/curaleaf-glendale-east t t \N 2025-11-16 20:51:41.896867 2025-11-16 20:51:41.896867 https://curaleaf.com/favicon.ico America/Phoenix 258 +25 Curaleaf - Glendale East Kind Relief curaleaf-az-glendale-east-the-kind-relief https://dutchie.com/dispensary/curaleaf-glendale-east-kind-relief t t \N 2025-11-16 20:51:41.898424 2025-11-16 20:51:41.898424 https://curaleaf.com/favicon.ico America/Phoenix 259 +20 Curaleaf - Bell curaleaf-az-bell https://dutchie.com/dispensary/curaleaf-bell-road t t 2025-12-01 07:42:04.601117 2025-11-16 20:51:41.891612 2025-11-16 20:51:41.891612 https://curaleaf.com/favicon.ico America/Phoenix 260 +21 Curaleaf - Camelback curaleaf-az-camelback https://dutchie.com/dispensary/curaleaf-camelback t t 2025-12-01 07:42:09.686907 2025-11-16 20:51:41.893039 2025-11-16 20:51:41.893039 https://curaleaf.com/favicon.ico America/Phoenix 261 +22 Curaleaf - Central curaleaf-az-central https://dutchie.com/dispensary/curaleaf-central t t 2025-12-01 07:42:14.799273 2025-11-16 20:51:41.894209 2025-11-16 20:51:41.894209 https://curaleaf.com/favicon.ico America/Phoenix 262 +1 Deeply Rooted deeply-rooted-az https://dutchie.com/embedded-menu/AZ-Deeply-Rooted t t 2025-12-01 07:41:48.775433 2025-11-14 19:28:15.682156 2025-11-16 19:51:44.511981 https://azdeeplyrooted.com/favicon.ico America/Phoenix 112 +18 Curaleaf - 48th Street curaleaf-az-48th-street https://dutchie.com/dispensary/curaleaf-dispensary-48th-street t t 2025-11-17 01:30:27.396589 2025-11-16 20:51:41.883135 2025-11-16 20:51:41.883135 https://curaleaf.com/favicon.ico America/Phoenix 255 +26 Curaleaf - Glendale curaleaf-az-glendale https://dutchie.com/dispensary/curaleaf-glendale t t \N 2025-11-16 20:51:41.899904 2025-11-16 20:51:41.899904 https://curaleaf.com/favicon.ico America/Phoenix 263 +27 Curaleaf - Midtown curaleaf-az-midtown https://dutchie.com/dispensary/curaleaf-dispensary-midtown t t \N 2025-11-16 20:51:41.901596 2025-11-16 20:51:41.901596 https://curaleaf.com/favicon.ico America/Phoenix 264 +28 Curaleaf - Peoria curaleaf-dispensary-peoria https://dutchie.com/dispensary/curaleaf-dispensary-peoria t t \N 2025-11-16 20:51:41.902956 2025-11-16 20:51:41.902956 https://curaleaf.com/favicon.ico America/Phoenix 265 +29 Curaleaf - Phoenix Airport curaleaf-az-phoenix https://dutchie.com/dispensary/curaleaf-phoenix t t \N 2025-11-16 20:51:41.904447 2025-11-16 20:51:41.904447 https://curaleaf.com/favicon.ico America/Phoenix 266 +30 Curaleaf - Queen Creek curaleaf-az-queen-creek https://dutchie.com/dispensary/curaleaf-queen-creek t t \N 2025-11-16 20:51:41.905947 2025-11-16 20:51:41.905947 https://curaleaf.com/favicon.ico America/Phoenix 267 +31 Curaleaf - Queen Creek WHOA curaleaf-az-queen-creek-whoa-qc-inc https://dutchie.com/dispensary/curaleaf-queen-creek-whoa t t \N 2025-11-16 20:51:41.907497 2025-11-16 20:51:41.907497 https://curaleaf.com/favicon.ico America/Phoenix 268 +32 Curaleaf - Scottsdale Natural Remedy curaleaf-az-scottsdale-natural-remedy-patient-center https://dutchie.com/dispensary/curaleaf-dispensary-scottsdale t t \N 2025-11-16 20:51:41.908695 2025-11-16 20:51:41.908695 https://curaleaf.com/favicon.ico America/Phoenix 269 +33 Curaleaf - Sedona curaleaf-az-sedona https://dutchie.com/dispensary/curaleaf-dispensary-sedona t t \N 2025-11-16 20:51:41.909892 2025-11-16 20:51:41.909892 https://curaleaf.com/favicon.ico America/Phoenix 270 +34 Curaleaf - Tucson curaleaf-az-tucson https://dutchie.com/dispensary/curaleaf-tucson t t \N 2025-11-16 20:51:41.910996 2025-11-16 20:51:41.910996 https://curaleaf.com/favicon.ico America/Phoenix 271 +35 Curaleaf - Youngtown curaleaf-az-youngtown https://dutchie.com/dispensary/curaleaf-youngtown t t \N 2025-11-16 20:51:41.912097 2025-11-16 20:51:41.912097 https://curaleaf.com/favicon.ico America/Phoenix 272 +36 Sol Flower - Sun City sol-flower-az-sun-city https://dutchie.com/dispensary/sol-flower-dispensary t t \N 2025-11-17 04:05:24.48593 2025-11-17 04:05:24.48593 https://dutchie.com/favicon.ico America/Phoenix 273 +37 Sol Flower - South Tucson sol-flower-az-south-tucson https://dutchie.com/dispensary/sol-flower-dispensary-south-tucson t t \N 2025-11-17 04:05:24.489171 2025-11-17 04:05:24.489171 https://dutchie.com/favicon.ico America/Phoenix 274 +38 Sol Flower - North Tucson sol-flower-az-north-tucson https://dutchie.com/dispensary/sol-flower-dispensary-north-tucson t t \N 2025-11-17 04:05:24.4905 2025-11-17 04:05:24.4905 https://dutchie.com/favicon.ico America/Phoenix 275 +39 Sol Flower - McClintock (Tempe) sol-flower-az-mcclintock https://dutchie.com/dispensary/sol-flower-dispensary-mcclintock t t \N 2025-11-17 04:05:24.491656 2025-11-17 04:05:24.491656 https://dutchie.com/favicon.ico America/Phoenix 276 +40 Sol Flower - Deer Valley (Phoenix) sol-flower-az-deer-valley https://dutchie.com/dispensary/sol-flower-dispensary-deer-valley t t \N 2025-11-17 04:05:24.492654 2025-11-17 04:05:24.492654 https://dutchie.com/favicon.ico America/Phoenix 277 +\. + + +-- +-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.users (id, email, password_hash, role, created_at, updated_at) FROM stdin; +1 admin@example.com $2b$10$cs2Ne0tQWXR9B2JxyD4h5e/aju026Gav6debmEyyGT0zurrNcN.8C superadmin 2025-11-14 19:28:15.679084 2025-11-14 19:28:15.679084 +\. + + +-- +-- Data for Name: wp_dutchie_api_permissions; Type: TABLE DATA; Schema: public; Owner: dutchie +-- + +COPY public.wp_dutchie_api_permissions (id, user_name, api_key, allowed_ips, allowed_domains, is_active, created_at, last_used_at) FROM stdin; +1 Deeply Rooted 5baf422e8741bac0022ee5d7146f34f5575a68ab06f563ca6814ee74f24b2298 \N *.deeplyrooted.com 1 2025-11-18 21:58:16.369978 \N +\. + + +-- +-- Name: api_token_usage_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.api_token_usage_id_seq', 1, false); + + +-- +-- Name: api_tokens_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.api_tokens_id_seq', 1, false); + + +-- +-- Name: azdhs_list_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.azdhs_list_id_seq', 182, true); + + +-- +-- Name: batch_history_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.batch_history_id_seq', 150, true); + + +-- +-- Name: brand_history_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.brand_history_id_seq', 1, false); + + +-- +-- Name: brand_scrape_jobs_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.brand_scrape_jobs_id_seq', 90, true); + + +-- +-- Name: brands_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.brands_id_seq', 1, false); + + +-- +-- Name: campaign_products_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.campaign_products_id_seq', 1, false); + + +-- +-- Name: campaigns_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.campaigns_id_seq', 17, true); + + +-- +-- Name: categories_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.categories_id_seq', 103, true); + + +-- +-- Name: clicks_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.clicks_id_seq', 1, false); + + +-- +-- Name: crawl_jobs_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.crawl_jobs_id_seq', 1, false); + + +-- +-- Name: crawler_sandboxes_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.crawler_sandboxes_id_seq', 1, false); + + +-- +-- Name: crawler_schedule_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.crawler_schedule_id_seq', 2, true); + + +-- +-- Name: crawler_templates_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.crawler_templates_id_seq', 2, true); + + +-- +-- Name: dispensaries_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.dispensaries_id_seq', 277, true); + + +-- +-- Name: dispensary_changes_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.dispensary_changes_id_seq', 1, true); + + +-- +-- Name: dispensary_crawl_jobs_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.dispensary_crawl_jobs_id_seq', 10, true); + + +-- +-- Name: dispensary_crawl_schedule_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.dispensary_crawl_schedule_id_seq', 220, true); + + +-- +-- Name: failed_proxies_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.failed_proxies_id_seq', 1, false); + + +-- +-- Name: jobs_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.jobs_id_seq', 1, false); + + +-- +-- Name: price_history_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.price_history_id_seq', 1, false); + + +-- +-- Name: product_categories_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.product_categories_id_seq', 831, true); + + +-- +-- Name: products_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.products_id_seq', 3796, true); + + +-- +-- Name: proxies_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.proxies_id_seq', 1000, true); + + +-- +-- Name: proxy_test_jobs_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.proxy_test_jobs_id_seq', 2, true); + + +-- +-- Name: sandbox_crawl_jobs_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.sandbox_crawl_jobs_id_seq', 10, true); + + +-- +-- Name: specials_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.specials_id_seq', 1, false); + + +-- +-- Name: store_crawl_schedule_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.store_crawl_schedule_id_seq', 10, true); + + +-- +-- Name: stores_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.stores_id_seq', 40, true); + + +-- +-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.users_id_seq', 17, true); + + +-- +-- Name: wp_dutchie_api_permissions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dutchie +-- + +SELECT pg_catalog.setval('public.wp_dutchie_api_permissions_id_seq', 1, true); + + +-- +-- Name: api_token_usage api_token_usage_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_token_usage + ADD CONSTRAINT api_token_usage_pkey PRIMARY KEY (id); + + +-- +-- Name: api_tokens api_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: api_tokens api_tokens_token_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_token_key UNIQUE (token); + + +-- +-- Name: azdhs_list azdhs_list_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.azdhs_list + ADD CONSTRAINT azdhs_list_pkey PRIMARY KEY (id); + + +-- +-- Name: batch_history batch_history_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.batch_history + ADD CONSTRAINT batch_history_pkey PRIMARY KEY (id); + + +-- +-- Name: brand_history brand_history_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_history + ADD CONSTRAINT brand_history_pkey PRIMARY KEY (id); + + +-- +-- Name: brand_scrape_jobs brand_scrape_jobs_dispensary_id_brand_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs + ADD CONSTRAINT brand_scrape_jobs_dispensary_id_brand_slug_key UNIQUE (dispensary_id, brand_slug); + + +-- +-- Name: brand_scrape_jobs brand_scrape_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs + ADD CONSTRAINT brand_scrape_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: brands brands_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands + ADD CONSTRAINT brands_pkey PRIMARY KEY (id); + + +-- +-- Name: campaign_products campaign_products_campaign_id_product_id_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_campaign_id_product_id_key UNIQUE (campaign_id, product_id); + + +-- +-- Name: campaign_products campaign_products_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_pkey PRIMARY KEY (id); + + +-- +-- Name: campaigns campaigns_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaigns + ADD CONSTRAINT campaigns_pkey PRIMARY KEY (id); + + +-- +-- Name: campaigns campaigns_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaigns + ADD CONSTRAINT campaigns_slug_key UNIQUE (slug); + + +-- +-- Name: categories categories_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_pkey PRIMARY KEY (id); + + +-- +-- Name: categories categories_store_id_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_store_id_slug_key UNIQUE (store_id, slug); + + +-- +-- Name: clicks clicks_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks + ADD CONSTRAINT clicks_pkey PRIMARY KEY (id); + + +-- +-- Name: crawl_jobs crawl_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawl_jobs + ADD CONSTRAINT crawl_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: crawler_sandboxes crawler_sandboxes_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_sandboxes + ADD CONSTRAINT crawler_sandboxes_pkey PRIMARY KEY (id); + + +-- +-- Name: crawler_schedule crawler_schedule_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_schedule + ADD CONSTRAINT crawler_schedule_pkey PRIMARY KEY (id); + + +-- +-- Name: crawler_templates crawler_templates_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_templates + ADD CONSTRAINT crawler_templates_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensaries dispensaries_azdhs_id_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_azdhs_id_key UNIQUE (azdhs_id); + + +-- +-- Name: dispensaries dispensaries_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensaries dispensaries_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_slug_key UNIQUE (slug); + + +-- +-- Name: dispensary_changes dispensary_changes_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes + ADD CONSTRAINT dispensary_changes_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensary_crawl_jobs dispensary_crawl_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs + ADD CONSTRAINT dispensary_crawl_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: dispensary_crawl_schedule dispensary_crawl_schedule_dispensary_id_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule + ADD CONSTRAINT dispensary_crawl_schedule_dispensary_id_key UNIQUE (dispensary_id); + + +-- +-- Name: dispensary_crawl_schedule dispensary_crawl_schedule_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule + ADD CONSTRAINT dispensary_crawl_schedule_pkey PRIMARY KEY (id); + + +-- +-- Name: failed_proxies failed_proxies_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.failed_proxies + ADD CONSTRAINT failed_proxies_pkey PRIMARY KEY (id); + + +-- +-- Name: jobs jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.jobs + ADD CONSTRAINT jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: price_history price_history_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.price_history + ADD CONSTRAINT price_history_pkey PRIMARY KEY (id); + + +-- +-- Name: product_categories product_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories + ADD CONSTRAINT product_categories_pkey PRIMARY KEY (id); + + +-- +-- Name: product_categories product_categories_product_id_category_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories + ADD CONSTRAINT product_categories_product_id_category_slug_key UNIQUE (product_id, category_slug); + + +-- +-- Name: products products_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_pkey PRIMARY KEY (id); + + +-- +-- Name: products products_store_id_slug_unique; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_store_id_slug_unique UNIQUE (store_id, slug); + + +-- +-- Name: proxies proxies_host_port_protocol_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxies + ADD CONSTRAINT proxies_host_port_protocol_key UNIQUE (host, port, protocol); + + +-- +-- Name: proxies proxies_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxies + ADD CONSTRAINT proxies_pkey PRIMARY KEY (id); + + +-- +-- Name: proxy_test_jobs proxy_test_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.proxy_test_jobs + ADD CONSTRAINT proxy_test_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: sandbox_crawl_jobs sandbox_crawl_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs + ADD CONSTRAINT sandbox_crawl_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.settings + ADD CONSTRAINT settings_pkey PRIMARY KEY (key); + + +-- +-- Name: specials specials_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials + ADD CONSTRAINT specials_pkey PRIMARY KEY (id); + + +-- +-- Name: store_crawl_schedule store_crawl_schedule_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule + ADD CONSTRAINT store_crawl_schedule_pkey PRIMARY KEY (id); + + +-- +-- Name: stores stores_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores + ADD CONSTRAINT stores_pkey PRIMARY KEY (id); + + +-- +-- Name: stores stores_slug_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores + ADD CONSTRAINT stores_slug_key UNIQUE (slug); + + +-- +-- Name: crawler_schedule uq_crawler_schedule_type; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_schedule + ADD CONSTRAINT uq_crawler_schedule_type UNIQUE (schedule_type); + + +-- +-- Name: store_crawl_schedule uq_store_crawl_schedule_store; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule + ADD CONSTRAINT uq_store_crawl_schedule_store UNIQUE (store_id); + + +-- +-- Name: crawler_templates uq_template_name; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_templates + ADD CONSTRAINT uq_template_name UNIQUE (provider, name, version); + + +-- +-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_email_key UNIQUE (email); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: wp_dutchie_api_permissions wp_dutchie_api_permissions_api_key_key; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.wp_dutchie_api_permissions + ADD CONSTRAINT wp_dutchie_api_permissions_api_key_key UNIQUE (api_key); + + +-- +-- Name: wp_dutchie_api_permissions wp_dutchie_api_permissions_pkey; Type: CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.wp_dutchie_api_permissions + ADD CONSTRAINT wp_dutchie_api_permissions_pkey PRIMARY KEY (id); + + +-- +-- Name: failed_proxies_host_port_protocol_idx; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE UNIQUE INDEX failed_proxies_host_port_protocol_idx ON public.failed_proxies USING btree (host, port, protocol); + + +-- +-- Name: idx_api_token_usage_created_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_token_usage_created_at ON public.api_token_usage USING btree (created_at); + + +-- +-- Name: idx_api_token_usage_endpoint; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_token_usage_endpoint ON public.api_token_usage USING btree (endpoint); + + +-- +-- Name: idx_api_token_usage_token_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_token_usage_token_id ON public.api_token_usage USING btree (token_id); + + +-- +-- Name: idx_api_tokens_active; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_tokens_active ON public.api_tokens USING btree (active); + + +-- +-- Name: idx_api_tokens_token; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_api_tokens_token ON public.api_tokens USING btree (token); + + +-- +-- Name: idx_batch_history_product; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_batch_history_product ON public.batch_history USING btree (product_id, recorded_at DESC); + + +-- +-- Name: idx_batch_history_recorded; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_batch_history_recorded ON public.batch_history USING btree (recorded_at DESC); + + +-- +-- Name: idx_brand_history_brand; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_history_brand ON public.brand_history USING btree (brand_name, event_at DESC); + + +-- +-- Name: idx_brand_history_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_history_dispensary ON public.brand_history USING btree (dispensary_id, event_at DESC); + + +-- +-- Name: idx_brand_history_event; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_history_event ON public.brand_history USING btree (event_type, event_at DESC); + + +-- +-- Name: idx_brand_jobs_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_jobs_dispensary ON public.brand_scrape_jobs USING btree (dispensary_id); + + +-- +-- Name: idx_brand_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_jobs_status ON public.brand_scrape_jobs USING btree (status); + + +-- +-- Name: idx_brand_jobs_worker; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brand_jobs_worker ON public.brand_scrape_jobs USING btree (worker_id) WHERE (worker_id IS NOT NULL); + + +-- +-- Name: idx_brands_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brands_dispensary ON public.brands USING btree (dispensary_id); + + +-- +-- Name: idx_brands_last_seen; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brands_last_seen ON public.brands USING btree (last_seen_at DESC); + + +-- +-- Name: idx_brands_store_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_brands_store_id ON public.brands USING btree (store_id); + + +-- +-- Name: idx_brands_store_name; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE UNIQUE INDEX idx_brands_store_name ON public.brands USING btree (store_id, name); + + +-- +-- Name: idx_categories_dispensary_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_categories_dispensary_id ON public.categories USING btree (dispensary_id); + + +-- +-- Name: idx_categories_parent_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_categories_parent_id ON public.categories USING btree (parent_id); + + +-- +-- Name: idx_categories_path; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_categories_path ON public.categories USING btree (path); + + +-- +-- Name: idx_clicks_campaign_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_clicks_campaign_id ON public.clicks USING btree (campaign_id); + + +-- +-- Name: idx_clicks_clicked_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_clicks_clicked_at ON public.clicks USING btree (clicked_at); + + +-- +-- Name: idx_clicks_product_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_clicks_product_id ON public.clicks USING btree (product_id); + + +-- +-- Name: idx_crawl_jobs_pending; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_pending ON public.crawl_jobs USING btree (scheduled_at) WHERE ((status)::text = 'pending'::text); + + +-- +-- Name: idx_crawl_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_status ON public.crawl_jobs USING btree (status); + + +-- +-- Name: idx_crawl_jobs_store_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_store_status ON public.crawl_jobs USING btree (store_id, status); + + +-- +-- Name: idx_crawl_jobs_store_time; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_crawl_jobs_store_time ON public.crawl_jobs USING btree (store_id, created_at DESC); + + +-- +-- Name: idx_disp_brand_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_brand_mode ON public.dispensaries USING btree (brand_crawler_mode); + + +-- +-- Name: idx_disp_brand_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_brand_provider ON public.dispensaries USING btree (brand_provider); + + +-- +-- Name: idx_disp_metadata_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_metadata_mode ON public.dispensaries USING btree (metadata_crawler_mode); + + +-- +-- Name: idx_disp_metadata_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_metadata_provider ON public.dispensaries USING btree (metadata_provider); + + +-- +-- Name: idx_disp_product_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_product_mode ON public.dispensaries USING btree (product_crawler_mode); + + +-- +-- Name: idx_disp_product_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_product_provider ON public.dispensaries USING btree (product_provider); + + +-- +-- Name: idx_disp_specials_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_specials_mode ON public.dispensaries USING btree (specials_crawler_mode); + + +-- +-- Name: idx_disp_specials_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_disp_specials_provider ON public.dispensaries USING btree (specials_provider); + + +-- +-- Name: idx_dispensaries_azdhs_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_azdhs_id ON public.dispensaries USING btree (azdhs_id); + + +-- +-- Name: idx_dispensaries_city; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_city ON public.dispensaries USING btree (city); + + +-- +-- Name: idx_dispensaries_crawl_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_crawl_status ON public.dispensaries USING btree (crawl_status); + + +-- +-- Name: idx_dispensaries_crawler_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_crawler_mode ON public.dispensaries USING btree (crawler_mode); + + +-- +-- Name: idx_dispensaries_crawler_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_crawler_status ON public.dispensaries USING btree (crawler_status); + + +-- +-- Name: idx_dispensaries_dutchie_production; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_dutchie_production ON public.dispensaries USING btree (id) WHERE (((menu_provider)::text = 'dutchie'::text) AND ((crawler_mode)::text = 'production'::text)); + + +-- +-- Name: idx_dispensaries_location; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_location ON public.dispensaries USING btree (latitude, longitude) WHERE ((latitude IS NOT NULL) AND (longitude IS NOT NULL)); + + +-- +-- Name: idx_dispensaries_menu_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_menu_status ON public.dispensaries USING btree (menu_scrape_status); + + +-- +-- Name: idx_dispensaries_needs_detection; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_needs_detection ON public.dispensaries USING btree (id) WHERE ((menu_provider IS NULL) OR (menu_provider_confidence < 70)); + + +-- +-- Name: idx_dispensaries_next_crawl; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_next_crawl ON public.dispensaries USING btree (next_crawl_at) WHERE (scrape_enabled = true); + + +-- +-- Name: idx_dispensaries_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_provider ON public.dispensaries USING btree (menu_provider); + + +-- +-- Name: idx_dispensaries_provider_confidence; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_provider_confidence ON public.dispensaries USING btree (menu_provider_confidence); + + +-- +-- Name: idx_dispensaries_sandbox; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_sandbox ON public.dispensaries USING btree (id) WHERE ((crawler_mode)::text = 'sandbox'::text); + + +-- +-- Name: idx_dispensaries_slug; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_slug ON public.dispensaries USING btree (slug); + + +-- +-- Name: idx_dispensaries_state; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensaries_state ON public.dispensaries USING btree (state); + + +-- +-- Name: idx_dispensary_changes_created_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_created_at ON public.dispensary_changes USING btree (created_at DESC); + + +-- +-- Name: idx_dispensary_changes_dispensary_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_dispensary_status ON public.dispensary_changes USING btree (dispensary_id, status); + + +-- +-- Name: idx_dispensary_changes_requires_recrawl; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_requires_recrawl ON public.dispensary_changes USING btree (requires_recrawl) WHERE (requires_recrawl = true); + + +-- +-- Name: idx_dispensary_changes_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_changes_status ON public.dispensary_changes USING btree (status); + + +-- +-- Name: idx_dispensary_crawl_jobs_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_dispensary ON public.dispensary_crawl_jobs USING btree (dispensary_id, created_at DESC); + + +-- +-- Name: idx_dispensary_crawl_jobs_pending; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_pending ON public.dispensary_crawl_jobs USING btree (priority DESC, scheduled_at) WHERE ((status)::text = 'pending'::text); + + +-- +-- Name: idx_dispensary_crawl_jobs_recent; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_recent ON public.dispensary_crawl_jobs USING btree (created_at DESC); + + +-- +-- Name: idx_dispensary_crawl_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_jobs_status ON public.dispensary_crawl_jobs USING btree (status); + + +-- +-- Name: idx_dispensary_crawl_schedule_active; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_active ON public.dispensary_crawl_schedule USING btree (is_active); + + +-- +-- Name: idx_dispensary_crawl_schedule_next_run; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_next_run ON public.dispensary_crawl_schedule USING btree (next_run_at) WHERE (is_active = true); + + +-- +-- Name: idx_dispensary_crawl_schedule_priority; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_priority ON public.dispensary_crawl_schedule USING btree (priority DESC, next_run_at); + + +-- +-- Name: idx_dispensary_crawl_schedule_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_dispensary_crawl_schedule_status ON public.dispensary_crawl_schedule USING btree (last_status); + + +-- +-- Name: idx_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_jobs_status ON public.jobs USING btree (status); + + +-- +-- Name: idx_jobs_store_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_jobs_store_id ON public.jobs USING btree (store_id); + + +-- +-- Name: idx_jobs_type; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_jobs_type ON public.jobs USING btree (type); + + +-- +-- Name: idx_price_history_product; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_price_history_product ON public.price_history USING btree (product_id, recorded_at DESC); + + +-- +-- Name: idx_price_history_recorded; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_price_history_recorded ON public.price_history USING btree (recorded_at DESC); + + +-- +-- Name: idx_product_categories_product; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_product_categories_product ON public.product_categories USING btree (product_id); + + +-- +-- Name: idx_product_categories_slug; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_product_categories_slug ON public.product_categories USING btree (category_slug, last_seen_at DESC); + + +-- +-- Name: idx_products_availability_by_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_availability_by_dispensary ON public.products USING btree (dispensary_id, availability_status) WHERE (dispensary_id IS NOT NULL); + + +-- +-- Name: idx_products_availability_by_store; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_availability_by_store ON public.products USING btree (store_id, availability_status) WHERE (store_id IS NOT NULL); + + +-- +-- Name: idx_products_availability_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_availability_status ON public.products USING btree (availability_status); + + +-- +-- Name: idx_products_dispensary_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_dispensary_id ON public.products USING btree (dispensary_id); + + +-- +-- Name: idx_products_stock_quantity; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_stock_quantity ON public.products USING btree (stock_quantity) WHERE (stock_quantity IS NOT NULL); + + +-- +-- Name: idx_products_stock_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_products_stock_status ON public.products USING btree (stock_status); + + +-- +-- Name: idx_proxies_location; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_proxies_location ON public.proxies USING btree (country_code, state, city); + + +-- +-- Name: idx_proxy_test_jobs_created_at; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_proxy_test_jobs_created_at ON public.proxy_test_jobs USING btree (created_at DESC); + + +-- +-- Name: idx_proxy_test_jobs_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_proxy_test_jobs_status ON public.proxy_test_jobs USING btree (status); + + +-- +-- Name: idx_sandbox_active_per_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE UNIQUE INDEX idx_sandbox_active_per_dispensary ON public.crawler_sandboxes USING btree (dispensary_id) WHERE ((status)::text <> ALL ((ARRAY['moved_to_production'::character varying, 'failed'::character varying])::text[])); + + +-- +-- Name: idx_sandbox_category; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_category ON public.crawler_sandboxes USING btree (category); + + +-- +-- Name: idx_sandbox_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_dispensary ON public.crawler_sandboxes USING btree (dispensary_id); + + +-- +-- Name: idx_sandbox_job_category; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_category ON public.sandbox_crawl_jobs USING btree (category); + + +-- +-- Name: idx_sandbox_job_dispensary; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_dispensary ON public.sandbox_crawl_jobs USING btree (dispensary_id); + + +-- +-- Name: idx_sandbox_job_pending; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_pending ON public.sandbox_crawl_jobs USING btree (scheduled_at) WHERE ((status)::text = 'pending'::text); + + +-- +-- Name: idx_sandbox_job_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_job_status ON public.sandbox_crawl_jobs USING btree (status); + + +-- +-- Name: idx_sandbox_mode; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_mode ON public.crawler_sandboxes USING btree (mode); + + +-- +-- Name: idx_sandbox_status; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_status ON public.crawler_sandboxes USING btree (status); + + +-- +-- Name: idx_sandbox_suspected_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_suspected_provider ON public.crawler_sandboxes USING btree (suspected_menu_provider); + + +-- +-- Name: idx_sandbox_template; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_sandbox_template ON public.crawler_sandboxes USING btree (template_name); + + +-- +-- Name: idx_specials_product_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_specials_product_id ON public.specials USING btree (product_id); + + +-- +-- Name: idx_specials_store_date; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_specials_store_date ON public.specials USING btree (store_id, valid_date DESC); + + +-- +-- Name: idx_stores_dispensary_id; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_stores_dispensary_id ON public.stores USING btree (dispensary_id); + + +-- +-- Name: idx_template_active; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_template_active ON public.crawler_templates USING btree (is_active); + + +-- +-- Name: idx_template_default; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_template_default ON public.crawler_templates USING btree (provider, is_default_for_provider) WHERE (is_default_for_provider = true); + + +-- +-- Name: idx_template_provider; Type: INDEX; Schema: public; Owner: dutchie +-- + +CREATE INDEX idx_template_provider ON public.crawler_templates USING btree (provider); + + +-- +-- Name: api_tokens api_tokens_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER api_tokens_updated_at BEFORE UPDATE ON public.api_tokens FOR EACH ROW EXECUTE FUNCTION public.update_api_token_updated_at(); + + +-- +-- Name: crawl_jobs trigger_crawl_jobs_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_crawl_jobs_updated_at BEFORE UPDATE ON public.crawl_jobs FOR EACH ROW EXECUTE FUNCTION public.update_schedule_updated_at(); + + +-- +-- Name: crawler_schedule trigger_crawler_schedule_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_crawler_schedule_updated_at BEFORE UPDATE ON public.crawler_schedule FOR EACH ROW EXECUTE FUNCTION public.update_schedule_updated_at(); + + +-- +-- Name: sandbox_crawl_jobs trigger_sandbox_job_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_sandbox_job_updated_at BEFORE UPDATE ON public.sandbox_crawl_jobs FOR EACH ROW EXECUTE FUNCTION public.update_sandbox_timestamp(); + + +-- +-- Name: crawler_sandboxes trigger_sandbox_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_sandbox_updated_at BEFORE UPDATE ON public.crawler_sandboxes FOR EACH ROW EXECUTE FUNCTION public.update_sandbox_timestamp(); + + +-- +-- Name: dispensary_changes trigger_set_requires_recrawl; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_set_requires_recrawl BEFORE INSERT ON public.dispensary_changes FOR EACH ROW EXECUTE FUNCTION public.set_requires_recrawl(); + + +-- +-- Name: store_crawl_schedule trigger_store_crawl_schedule_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_store_crawl_schedule_updated_at BEFORE UPDATE ON public.store_crawl_schedule FOR EACH ROW EXECUTE FUNCTION public.update_schedule_updated_at(); + + +-- +-- Name: crawler_templates trigger_template_updated_at; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_template_updated_at BEFORE UPDATE ON public.crawler_templates FOR EACH ROW EXECUTE FUNCTION public.update_sandbox_timestamp(); + + +-- +-- Name: brand_scrape_jobs trigger_update_brand_scrape_jobs_timestamp; Type: TRIGGER; Schema: public; Owner: dutchie +-- + +CREATE TRIGGER trigger_update_brand_scrape_jobs_timestamp BEFORE UPDATE ON public.brand_scrape_jobs FOR EACH ROW EXECUTE FUNCTION public.update_brand_scrape_jobs_updated_at(); + + +-- +-- Name: api_token_usage api_token_usage_token_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_token_usage + ADD CONSTRAINT api_token_usage_token_id_fkey FOREIGN KEY (token_id) REFERENCES public.api_tokens(id) ON DELETE CASCADE; + + +-- +-- Name: api_tokens api_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: batch_history batch_history_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.batch_history + ADD CONSTRAINT batch_history_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: brand_history brand_history_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_history + ADD CONSTRAINT brand_history_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: brand_scrape_jobs brand_scrape_jobs_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brand_scrape_jobs + ADD CONSTRAINT brand_scrape_jobs_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id); + + +-- +-- Name: brands brands_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands + ADD CONSTRAINT brands_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id); + + +-- +-- Name: brands brands_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.brands + ADD CONSTRAINT brands_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: campaign_products campaign_products_campaign_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) ON DELETE CASCADE; + + +-- +-- Name: campaign_products campaign_products_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.campaign_products + ADD CONSTRAINT campaign_products_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: categories categories_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: categories categories_parent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.categories(id) ON DELETE CASCADE; + + +-- +-- Name: categories categories_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.categories + ADD CONSTRAINT categories_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: clicks clicks_campaign_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks + ADD CONSTRAINT clicks_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) ON DELETE SET NULL; + + +-- +-- Name: clicks clicks_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.clicks + ADD CONSTRAINT clicks_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: crawl_jobs crawl_jobs_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawl_jobs + ADD CONSTRAINT crawl_jobs_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: crawler_sandboxes crawler_sandboxes_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.crawler_sandboxes + ADD CONSTRAINT crawler_sandboxes_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dispensaries dispensaries_azdhs_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensaries + ADD CONSTRAINT dispensaries_azdhs_id_fkey FOREIGN KEY (azdhs_id) REFERENCES public.azdhs_list(id); + + +-- +-- Name: dispensary_changes dispensary_changes_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes + ADD CONSTRAINT dispensary_changes_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dispensary_changes dispensary_changes_reviewed_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_changes + ADD CONSTRAINT dispensary_changes_reviewed_by_fkey FOREIGN KEY (reviewed_by) REFERENCES public.users(id); + + +-- +-- Name: dispensary_crawl_jobs dispensary_crawl_jobs_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs + ADD CONSTRAINT dispensary_crawl_jobs_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: dispensary_crawl_jobs dispensary_crawl_jobs_schedule_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_jobs + ADD CONSTRAINT dispensary_crawl_jobs_schedule_id_fkey FOREIGN KEY (schedule_id) REFERENCES public.dispensary_crawl_schedule(id) ON DELETE SET NULL; + + +-- +-- Name: dispensary_crawl_schedule dispensary_crawl_schedule_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.dispensary_crawl_schedule + ADD CONSTRAINT dispensary_crawl_schedule_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: jobs jobs_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.jobs + ADD CONSTRAINT jobs_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: price_history price_history_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.price_history + ADD CONSTRAINT price_history_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_categories product_categories_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.product_categories + ADD CONSTRAINT product_categories_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: products products_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.categories(id) ON DELETE SET NULL; + + +-- +-- Name: products products_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: products products_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: sandbox_crawl_jobs sandbox_crawl_jobs_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs + ADD CONSTRAINT sandbox_crawl_jobs_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE CASCADE; + + +-- +-- Name: sandbox_crawl_jobs sandbox_crawl_jobs_sandbox_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.sandbox_crawl_jobs + ADD CONSTRAINT sandbox_crawl_jobs_sandbox_id_fkey FOREIGN KEY (sandbox_id) REFERENCES public.crawler_sandboxes(id) ON DELETE SET NULL; + + +-- +-- Name: specials specials_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials + ADD CONSTRAINT specials_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: specials specials_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.specials + ADD CONSTRAINT specials_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: store_crawl_schedule store_crawl_schedule_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.store_crawl_schedule + ADD CONSTRAINT store_crawl_schedule_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id) ON DELETE CASCADE; + + +-- +-- Name: stores stores_dispensary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: dutchie +-- + +ALTER TABLE ONLY public.stores + ADD CONSTRAINT stores_dispensary_id_fkey FOREIGN KEY (dispensary_id) REFERENCES public.dispensaries(id) ON DELETE SET NULL; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict F5QdqipUuR85d4CLaQukm3wJGewHHNKubAwSHT2cYXx4to2xUDcizlCKa2H5aOs + diff --git a/backups/dispensaries-export-20251202.json b/backups/dispensaries-export-20251202.json new file mode 100644 index 00000000..23313766 --- /dev/null +++ b/backups/dispensaries-export-20251202.json @@ -0,0 +1,11985 @@ +[ + { + "id": 71, + "azdhs_id": 1, + "name": "SWC Prescott", + "company_name": "203 Organix, Llc", + "address": "123 E Merritt St", + "city": "Prescott", + "state": "AZ", + "zip": "86301", + "status_line": "Operating · Marijuana Facility · (312) 819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=SWC%20Prescott", + "latitude": null, + "longitude": null, + "dba_name": "SWC Prescott by Zen Leaf", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/prescott/?utm_source=google&utm_medium=gbp-order&utm_campaign=az-prescott", + "google_rating": "4.7", + "google_review_count": 2312, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "swc-prescott", + "created_at": "2025-11-17T14:29:34.188Z", + "updated_at": "2025-12-01T22:16:43.872Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 72, + "azdhs_id": 2, + "name": "The Mint Dispensary", + "company_name": "4245 Investments Llc", + "address": "330 E Southern Ave #37", + "city": "Mesa", + "state": "AZ", + "zip": "85210", + "status_line": "Operating · Marijuana Facility · (480) 664-1470", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Mint%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis - Mesa", + "phone": "4806641470", + "email": null, + "website": "https://mintdeals.com/mesa-az/", + "google_rating": "4.7", + "google_review_count": 5993, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-mint-dispensary", + "created_at": "2025-11-17T14:29:34.191Z", + "updated_at": "2025-12-01T22:16:43.841Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 73, + "azdhs_id": 3, + "name": "D2 Dispensary", + "company_name": "46 Wellness Llc", + "address": "7139 E 22nd St", + "city": "Tucson", + "state": "AZ", + "zip": "85710", + "status_line": "Operating · Marijuana Facility · (520) 214-3232", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=D2%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "D2 Dispensary - Cannabis Destination + Drive Thru", + "phone": "5202143232", + "email": null, + "website": "http://d2dispensary.com/", + "google_rating": "4.8", + "google_review_count": 5706, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "d2-dispensary", + "created_at": "2025-11-17T14:29:34.194Z", + "updated_at": "2025-12-01T22:16:43.847Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 74, + "azdhs_id": 4, + "name": "Ponderosa Dispensary", + "company_name": "480 License Holdings, LLC", + "address": "25 East Blacklidge Drive", + "city": "Tucson", + "state": "AZ", + "zip": "85705", + "status_line": "Operating · Marijuana Establishment · 480-201-0000", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Ponderosa Dispensary Tucson", + "phone": "4802010000", + "email": null, + "website": "https://www.pondyaz.com/locations", + "google_rating": "4.8", + "google_review_count": 520, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "ponderosa-dispensary", + "created_at": "2025-11-17T14:29:34.195Z", + "updated_at": "2025-12-01T22:16:43.850Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 75, + "azdhs_id": 5, + "name": "Ponderosa Dispensary", + "company_name": "ABACA Ponderosa, LLC", + "address": "21035 N Cave Creek Rd Ste 3 & 4", + "city": "Phoenix", + "state": "AZ", + "zip": "85024", + "status_line": "Operating · Marijuana Facility · (480) 213-1402", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Ponderosa Dispensary Phoenix", + "phone": "4802131402", + "email": null, + "website": "https://www.pondyaz.com/locations", + "google_rating": "4.7", + "google_review_count": 2561, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "ponderosa-dispensary-phoenix", + "created_at": "2025-11-17T14:29:34.197Z", + "updated_at": "2025-12-01T22:16:43.854Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 76, + "azdhs_id": 6, + "name": "Trulieve of Tatum", + "company_name": "Abedon Saiz Llc", + "address": "16635 N tatum blvd, 110", + "city": "Phoenix", + "state": "AZ", + "zip": "85032", + "status_line": "Operating · Marijuana Facility · 770-330-0831", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tatum", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Phoenix Dispensary Tatum", + "phone": "7703300831", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/phoenix-tatum?utm_source=gmb&utm_medium=organic&utm_campaign=phoenix-tatum", + "google_rating": "4.4", + "google_review_count": 194, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-tatum", + "created_at": "2025-11-17T14:29:34.199Z", + "updated_at": "2025-12-01T22:16:43.857Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 77, + "azdhs_id": 7, + "name": "Absolute Health Care Inc", + "company_name": "Absolute Health Care Inc", + "address": "175 S Hamilton Pl Bldg 4 Ste 110", + "city": "Gilbert", + "state": "AZ", + "zip": "85233", + "status_line": "Operating · Marijuana Facility · 480-361-0078", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Absolute%20Health%20Care%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Gilbert", + "phone": "4803610078", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-gilbert?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.6", + "google_review_count": 3423, + "menu_url": "https://dutchie.com/dispensary/curaleaf-gilbert", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "absolute-health-care-inc", + "created_at": "2025-11-17T14:29:34.200Z", + "updated_at": "2025-11-17T21:46:36.087Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 78, + "azdhs_id": 8, + "name": "Trulieve of Phoenix Alhambra", + "company_name": "Ad, Llc", + "address": "2630 W Indian School Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85017", + "status_line": "Operating · Marijuana Facility · 770-330-0831", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Phoenix%20Alhambra", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Phoenix Dispensary Alhambra", + "phone": "7703300831", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/phoenix-alhambra?utm_source=gmb&utm_medium=organic&utm_campaign=alhambra", + "google_rating": "4.5", + "google_review_count": 2917, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-phoenix-alhambra", + "created_at": "2025-11-17T14:29:34.203Z", + "updated_at": "2025-12-01T22:16:43.860Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 79, + "azdhs_id": 9, + "name": "AGI Management LLC", + "company_name": "AGI Management LLC", + "address": "1035 W Main St", + "city": "Quartzsite", + "state": "AZ", + "zip": "85346", + "status_line": "Operating · Marijuana Establishment · 480-234-2343", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=AGI%20Management%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "4802342343", + "email": null, + "website": "https://example-dispensary.com", + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "agi-management-llc", + "created_at": "2025-11-17T14:29:34.204Z", + "updated_at": "2025-11-18T00:32:14.006Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 80, + "azdhs_id": 10, + "name": "All Greens Inc", + "company_name": "All Greens Inc", + "address": "10032 W Bell Rd Ste 100", + "city": "Sun City", + "state": "AZ", + "zip": "85351", + "status_line": "Operating · Marijuana Facility · (623) 214-0801", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=All%20Greens%20Inc", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6232140801", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "all-greens-inc", + "created_at": "2025-11-17T14:29:34.206Z", + "updated_at": "2025-11-17T14:29:34.206Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 81, + "azdhs_id": 11, + "name": "All Rebel Rockers Inc", + "company_name": "All Rebel Rockers Inc", + "address": "4730 S 48th St", + "city": "Phoenix", + "state": "AZ", + "zip": "85040", + "status_line": "Operating · Marijuana Facility · 602-807-5005", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=All%20Rebel%20Rockers%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary 48th Street", + "phone": "6028075005", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-48th-street?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.6", + "google_review_count": 1381, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "all-rebel-rockers-inc", + "created_at": "2025-11-17T14:29:34.208Z", + "updated_at": "2025-12-01T14:41:59.449Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "unknown", + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": "unknown", + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "specials_html_pattern": true + }, + "brand_provider": "unknown", + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": "unknown", + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 82, + "azdhs_id": 12, + "name": "Apache County Dispensary LLC", + "company_name": "Apache County Dispensary LLC", + "address": "900 East Main Street", + "city": "Springerville", + "state": "AZ", + "zip": "85938", + "status_line": "Operating · Marijuana Establishment · 620-921-5967", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Apache%20County%20Dispensary%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Key Cannabis Dispensary Springerville", + "phone": "6209215967", + "email": null, + "website": "https://keycannabis.com/shop/springerville-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=springerville", + "google_rating": "4.7", + "google_review_count": 126, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "apache-county-dispensary-llc", + "created_at": "2025-11-17T14:29:34.209Z", + "updated_at": "2025-12-01T22:16:43.863Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 83, + "azdhs_id": 13, + "name": "Apollo Labs", + "company_name": "Apollo Labs", + "address": "17301 North Perimeter Drive, suite 100", + "city": "Scottsdale", + "state": "AZ", + "zip": "85255", + "status_line": "Operating · Marijuana Laboratory · (917) 340-1566", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Apollo%20Labs", + "latitude": null, + "longitude": null, + "dba_name": "Apollo Labs", + "phone": "9173401566", + "email": null, + "website": "http://www.apollolabscorp.com/", + "google_rating": "5.0", + "google_review_count": 7, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "apollo-labs", + "created_at": "2025-11-17T14:29:34.211Z", + "updated_at": "2025-12-01T22:16:43.866Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 84, + "azdhs_id": 14, + "name": "Arizona Cannabis Society Inc", + "company_name": "Arizona Cannabis Society Inc", + "address": "8376 N El Mirage Rd Bldg 2 Ste 2", + "city": "El Mirage", + "state": "AZ", + "zip": "85335", + "status_line": "Operating · Marijuana Facility · (888) 249-2927", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Arizona%20Cannabis%20Society%20Inc", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "8882492927", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "arizona-cannabis-society-inc", + "created_at": "2025-11-17T14:29:34.212Z", + "updated_at": "2025-11-17T14:29:34.212Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 85, + "azdhs_id": 15, + "name": "Arizona Golden Leaf Wellness, Llc", + "company_name": "Arizona Golden Leaf Wellness, Llc", + "address": "5390 W Ina Rd", + "city": "Marana", + "state": "AZ", + "zip": "85743", + "status_line": "Operating · Marijuana Facility · (520) 620-9123", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Arizona%20Golden%20Leaf%20Wellness%2C%20Llc", + "latitude": null, + "longitude": null, + "dba_name": "NatureMed", + "phone": "5206209123", + "email": null, + "website": "https://naturemedaz.com/", + "google_rating": "4.8", + "google_review_count": 3791, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "arizona-golden-leaf-wellness-llc", + "created_at": "2025-11-17T14:29:34.213Z", + "updated_at": "2025-12-01T22:16:43.869Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "queued_detection", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 86, + "azdhs_id": 16, + "name": "Nature's Medicines", + "company_name": "Arizona Natural Pain Solutions Inc.", + "address": "701 East Dunlap Avenue, Suite 9", + "city": "Phoenix", + "state": "AZ", + "zip": "85020", + "status_line": "Operating · Marijuana Facility · (602) 903-3769", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6029033769", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nature-s-medicines", + "created_at": "2025-11-17T14:29:34.214Z", + "updated_at": "2025-11-17T14:29:34.214Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 87, + "azdhs_id": 17, + "name": "Arizona Natures Wellness", + "company_name": "Arizona Natures Wellness", + "address": "1610 West State Route 89a", + "city": "Sedona", + "state": "AZ", + "zip": "86336", + "status_line": "Operating · Marijuana Facility · 928-202-3512", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Arizona%20Natures%20Wellness", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "9282023512", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": "https://dutchie.com/dispensary/curaleaf-dispensary-sedona", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "arizona-natures-wellness", + "created_at": "2025-11-17T14:29:34.216Z", + "updated_at": "2025-11-17T14:29:34.216Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 88, + "azdhs_id": 18, + "name": "Arizona Organix", + "company_name": "Arizona Organix", + "address": "5303 W Glendale Ave", + "city": "Glendale", + "state": "AZ", + "zip": "85301", + "status_line": "Operating · Marijuana Facility · (623) 937-2752", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Arizona%20Organix", + "latitude": null, + "longitude": null, + "dba_name": "Arizona Organix Dispensary", + "phone": "6239372752", + "email": null, + "website": "https://www.arizonaorganix.org/", + "google_rating": "4.2", + "google_review_count": 2983, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "arizona-organix", + "created_at": "2025-11-17T14:29:34.217Z", + "updated_at": "2025-11-17T21:46:35.933Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 89, + "azdhs_id": 19, + "name": "Nirvana Center", + "company_name": "Arizona Tree Equity 2", + "address": "2209 South 6th Avenue", + "city": "Tucson", + "state": "AZ", + "zip": "85713", + "status_line": "Operating · Marijuana Establishment · (928) 642-2250", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center", + "latitude": null, + "longitude": null, + "dba_name": "Nirvana Cannabis - Tucson", + "phone": "9286422250", + "email": null, + "website": "https://nirvanacannabis.com/", + "google_rating": "4.7", + "google_review_count": 2156, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nirvana-center", + "created_at": "2025-11-17T14:29:34.218Z", + "updated_at": "2025-11-17T21:46:35.934Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 90, + "azdhs_id": 20, + "name": "Arizona Wellness Center Safford", + "company_name": "Arizona Wellness Center Safford LLC", + "address": "1362 W Thatcher Blvd", + "city": "Safford", + "state": "AZ", + "zip": "85546", + "status_line": "Operating · Marijuana Dispensary · 623-521-6899", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Arizona%20Wellness%20Center%20Safford", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6235216899", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "arizona-wellness-center-safford", + "created_at": "2025-11-17T14:29:34.220Z", + "updated_at": "2025-11-17T14:29:34.220Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 91, + "azdhs_id": 21, + "name": "Key Cannabis", + "company_name": "Arizona Wellness Collective 3, Inc", + "address": "1911 W Broadway Rd 23", + "city": "Mesa", + "state": "AZ", + "zip": "85202", + "status_line": "Operating · Marijuana Facility · (480) 912-4444", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Key%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "Key Cannabis Dispensary Mesa", + "phone": "4809124444", + "email": null, + "website": "https://keycannabis.com/shop/mesa-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=mesa", + "google_rating": "4.2", + "google_review_count": 680, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "key-cannabis", + "created_at": "2025-11-17T14:29:34.221Z", + "updated_at": "2025-11-17T21:46:36.090Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 92, + "azdhs_id": 22, + "name": "TruMed Dispensary", + "company_name": "Az Compassionate Care Inc", + "address": "1613 N 40th St", + "city": "Phoenix", + "state": "AZ", + "zip": "85008", + "status_line": "Operating · Marijuana Facility · (602) 275-1279", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=TruMed%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "TruMed Dispensary", + "phone": "6022751279", + "email": null, + "website": "https://trumedaz.com/", + "google_rating": "4.5", + "google_review_count": 1807, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trumed-dispensary", + "created_at": "2025-11-17T14:29:34.223Z", + "updated_at": "2025-11-17T21:46:35.935Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 93, + "azdhs_id": 23, + "name": "AZ Flower Power LLC", + "company_name": "AZ Flower Power LLC", + "address": "11343 East Apache Trail", + "city": "Apache Junction", + "state": "AZ", + "zip": "85120", + "status_line": "Operating · Marijuana Establishment · (917) 375-3900", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=AZ%20Flower%20Power%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "9173753900", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "az-flower-power-llc", + "created_at": "2025-11-17T14:29:34.224Z", + "updated_at": "2025-11-17T14:29:34.224Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 94, + "azdhs_id": 24, + "name": "AZC1", + "company_name": "AZCL1", + "address": "4695 N Oracle Rd Ste 117", + "city": "Tucson", + "state": "AZ", + "zip": "85705", + "status_line": "Operating · Marijuana Facility · 520-293-3315", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=AZC1", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "5202933315", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": "https://dutchie.com/dispensary/curaleaf-tucson", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "azc1", + "created_at": "2025-11-17T14:29:34.225Z", + "updated_at": "2025-11-17T14:29:34.225Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 95, + "azdhs_id": 25, + "name": "Zen Leaf Chandler", + "company_name": "AZGM 3, LLC", + "address": "7200 W Chandler Blvd Ste 7", + "city": "Chandler", + "state": "AZ", + "zip": "85226", + "status_line": "Operating · Marijuana Facility · 312-819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Chandler", + "latitude": null, + "longitude": null, + "dba_name": "Zen Leaf Dispensary Chandler", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/chandler/?utm_campaign=az-chandler&utm_medium=gbp&utm_source=google", + "google_rating": "4.8", + "google_review_count": 3044, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zen-leaf-chandler", + "created_at": "2025-11-17T14:29:34.226Z", + "updated_at": "2025-11-17T21:46:35.937Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 96, + "azdhs_id": 26, + "name": "Greenleef Medical", + "company_name": "Bailey Management LLC", + "address": "253 Chase Creek St", + "city": "Clifton", + "state": "AZ", + "zip": "85533", + "status_line": "Operating · Marijuana Facility · 480-652-3622", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Greenleef%20Medical", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "4806523622", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "greenleef-medical", + "created_at": "2025-11-17T14:29:34.228Z", + "updated_at": "2025-11-17T14:29:34.228Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 97, + "azdhs_id": 27, + "name": "Blue Palo Verde 1, LLC", + "company_name": "Blue Palo Verde 1, LLC", + "address": "7710 South Wilmot Road, Suite 100", + "city": "Tucson", + "state": "AZ", + "zip": "85756", + "status_line": "Operating · Marijuana Establishment · 586-855-6649", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Blue%20Palo%20Verde%201%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Halo Cannabis", + "phone": "5868556649", + "email": null, + "website": "https://thegreenhalo.com/", + "google_rating": "4.4", + "google_review_count": 1580, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "blue-palo-verde-1-llc", + "created_at": "2025-11-17T14:29:34.229Z", + "updated_at": "2025-11-17T21:46:36.091Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 98, + "azdhs_id": 28, + "name": "Sticky Saguaro", + "company_name": "Border Health, Inc", + "address": "12338 East Riggs Road", + "city": "Chandler", + "state": "AZ", + "zip": "85249", + "status_line": "Operating · Marijuana Facility · (602) 644-9188", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sticky%20Saguaro", + "latitude": null, + "longitude": null, + "dba_name": "Sticky Saguaro", + "phone": "6026449188", + "email": null, + "website": "https://stickysaguaro.com/", + "google_rating": "4.6", + "google_review_count": 1832, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sticky-saguaro", + "created_at": "2025-11-17T14:29:34.231Z", + "updated_at": "2025-11-17T21:46:35.939Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 99, + "azdhs_id": 29, + "name": "HANA MEDS", + "company_name": "Broken Arrow Herbal Center Inc", + "address": "1732 W Commerce Point Pl", + "city": "Sahuarita", + "state": "AZ", + "zip": "85614", + "status_line": "Operating · Marijuana Facility · (520) 289-8030", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=HANA%20MEDS", + "latitude": null, + "longitude": null, + "dba_name": "Hana Dispensary Green Valley", + "phone": "5202898030", + "email": null, + "website": "https://hanadispensaries.com/location/green-valley-az/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.6", + "google_review_count": 1087, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "hana-meds", + "created_at": "2025-11-17T14:29:34.232Z", + "updated_at": "2025-11-17T21:46:36.093Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 100, + "azdhs_id": 30, + "name": "The Flower Shop Az", + "company_name": "Buds & Roses, Inc", + "address": "5205 E University Dr", + "city": "Mesa", + "state": "AZ", + "zip": "85205", + "status_line": "Operating · Marijuana Facility · (480) 500-5054", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az", + "latitude": null, + "longitude": null, + "dba_name": "The Flower Shop - Mesa", + "phone": "4805005054", + "email": null, + "website": "https://theflowershopusa.com/mesa?utm_source=google-business&utm_medium=organic", + "google_rating": "4.4", + "google_review_count": 2604, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-flower-shop-az", + "created_at": "2025-11-17T14:29:34.233Z", + "updated_at": "2025-11-17T21:46:35.941Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 101, + "azdhs_id": 31, + "name": "Trulieve of Scottsdale Dispensary", + "company_name": "Byers Dispensary Inc", + "address": "15190 N Hayden Rd", + "city": "Scottsdale", + "state": "AZ", + "zip": "85260", + "status_line": "Operating · Marijuana Facility · 850-508-0261", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Scottsdale%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Scottsdale Dispensary", + "phone": "8505080261", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/scottsdale?utm_source=gmb&utm_medium=organic&utm_campaign=scottsdale", + "google_rating": "4.3", + "google_review_count": 819, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-scottsdale-dispensary", + "created_at": "2025-11-17T14:29:34.235Z", + "updated_at": "2025-11-17T21:46:36.094Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 102, + "azdhs_id": 32, + "name": "SC Labs", + "company_name": "C4 Laboratories", + "address": "7650 East Evans Rd Unit A, UNIT A", + "city": "Scottsdale", + "state": "AZ", + "zip": "85260", + "status_line": "Operating · Marijuana Laboratory · (480) 219-6460", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=SC%20Labs", + "latitude": null, + "longitude": null, + "dba_name": "SC Labs | Arizona (Formerly C4 Laboratories)", + "phone": "4802196460", + "email": null, + "website": "http://www.sclabs.com/", + "google_rating": "4.9", + "google_review_count": 10, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sc-labs", + "created_at": "2025-11-17T14:29:34.236Z", + "updated_at": "2025-11-17T21:46:35.943Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 103, + "azdhs_id": 33, + "name": "Releaf", + "company_name": "Cactus Bloom Facilities Management LLC", + "address": "436 Naugle Ave", + "city": "Patagonia", + "state": "AZ", + "zip": "85624", + "status_line": "Operating · Marijuana Establishment · 520-982-9212", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Releaf", + "latitude": null, + "longitude": null, + "dba_name": "Releaf 85624", + "phone": "5209829212", + "email": null, + "website": "https://dbloomtucson.com/releaf-85624/", + "google_rating": "4.8", + "google_review_count": 221, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "releaf", + "created_at": "2025-11-17T14:29:34.237Z", + "updated_at": "2025-11-17T21:46:35.945Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 104, + "azdhs_id": 34, + "name": "The Phoenix", + "company_name": "Cannabis Research Group Inc", + "address": "9897 W McDowell Rd #720", + "city": "Tolleson", + "state": "AZ", + "zip": "85353", + "status_line": "Operating · Marijuana Facility · (480) 420-0377", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Phoenix", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "4804200377", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-phoenix", + "created_at": "2025-11-17T14:29:34.239Z", + "updated_at": "2025-11-17T14:29:34.239Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 105, + "azdhs_id": 35, + "name": "Sunday Goods", + "company_name": "Cardinal Square, Inc", + "address": "13150 W Bell Rd", + "city": "Surprise", + "state": "AZ", + "zip": "85378", + "status_line": "Operating · Marijuana Facility · 520-808-3111", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods", + "latitude": null, + "longitude": null, + "dba_name": "Sunday Goods Surprise", + "phone": "5208083111", + "email": null, + "website": "https://sundaygoods.com/location/sunday-goods-surprise-az-cannabis-dispensary/?utm_source=google&utm_medium=gbp&utm_campaign=surprise_gbp", + "google_rating": "4.4", + "google_review_count": 13, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sunday-goods", + "created_at": "2025-11-17T14:29:34.240Z", + "updated_at": "2025-11-17T21:46:35.946Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 106, + "azdhs_id": 36, + "name": "Catalina Hills Botanical Care", + "company_name": "Catalina Hills Botanical Care Inc", + "address": "2918 N Central Ave", + "city": "Phoenix", + "state": "AZ", + "zip": "85012", + "status_line": "Operating · Marijuana Facility · 602-466-1087", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Catalina%20Hills%20Botanical%20Care", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6024661087", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": "https://dutchie.com/dispensary/curaleaf-dispensary-midtown", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "catalina-hills-botanical-care", + "created_at": "2025-11-17T14:29:34.242Z", + "updated_at": "2025-11-17T14:29:34.242Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 107, + "azdhs_id": 37, + "name": "HANA MEDS", + "company_name": "Cjk Inc", + "address": "3411 E Corona Ave, 100", + "city": "Phoenix", + "state": "AZ", + "zip": "85040", + "status_line": "Operating · Marijuana Facility · 602 491-0420", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=HANA%20MEDS", + "latitude": null, + "longitude": null, + "dba_name": "Hana Dispensary Phoenix", + "phone": "", + "email": null, + "website": "http://www.hanadispensaries.com/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.7", + "google_review_count": 1129, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "hana-meds-phoenix", + "created_at": "2025-11-17T14:29:34.243Z", + "updated_at": "2025-11-17T21:46:36.106Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 108, + "azdhs_id": 38, + "name": "Trulieve Of Sierra Vista", + "company_name": "Cochise County Wellness, LLC", + "address": "1633 S Highway 92, Ste 7", + "city": "Sierra Vista", + "state": "AZ", + "zip": "85635", + "status_line": "Operating · Marijuana Facility · 480-677-1755", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Of%20Sierra%20Vista", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Sierra Vista Dispensary", + "phone": "4806771755", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/sierra-vista?utm_source=gmb&utm_medium=organic&utm_campaign=sierra-vista", + "google_rating": "4.4", + "google_review_count": 488, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-sierra-vista", + "created_at": "2025-11-17T14:29:34.244Z", + "updated_at": "2025-11-17T21:46:35.948Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 109, + "azdhs_id": 39, + "name": "Botanica", + "company_name": "Copper State Herbal Center Inc", + "address": "6205 N Travel Center Dr", + "city": "Tucson", + "state": "AZ", + "zip": "85741", + "status_line": "Operating · Marijuana Facility · (520) 395-0230", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Botanica", + "latitude": null, + "longitude": null, + "dba_name": "Botanica", + "phone": "5203950230", + "email": null, + "website": "https://botanica.us/", + "google_rating": "4.6", + "google_review_count": 940, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "botanica", + "created_at": "2025-11-17T14:29:34.246Z", + "updated_at": "2025-11-17T21:46:35.949Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 110, + "azdhs_id": 40, + "name": "Sol Flower", + "company_name": "CSI Solutions, Inc.", + "address": "14980 N 78th Way", + "city": "Scottsdale", + "state": "AZ", + "zip": "85260", + "status_line": "Operating · Marijuana Facility · 480-420-3300", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary Scottsdale Airpark", + "phone": "4804203300", + "email": null, + "website": "https://www.livewithsol.com/scottsdale-airpark-menu-recreational?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing", + "google_rating": "4.6", + "google_review_count": 692, + "menu_url": "https://dutchie.com/dispensary/sol-flower-dispensary-south-tucson", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sol-flower", + "created_at": "2025-11-17T14:29:34.247Z", + "updated_at": "2025-12-02T12:41:12.858Z", + "menu_provider": "dutchie", + "menu_provider_confidence": 100, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": null, + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "dutchie", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 111, + "azdhs_id": 41, + "name": "Curious Cultivators I LLC", + "company_name": "Curious Cultivators I LLC", + "address": "200 London Bridge Road, 100", + "city": "Lake Havasu City", + "state": "AZ", + "zip": "86403", + "status_line": "Operating · Marijuana Establishment · (310) 694-4397", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Curious%20Cultivators%20I%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Story Cannabis Dispensary Lake Havasu", + "phone": "3106944397", + "email": null, + "website": "https://storycannabis.com/dispensary-locations/arizona/havasu-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=lake_havasu&utm_term=click&utm_content=website", + "google_rating": "4.7", + "google_review_count": 403, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "curious-cultivators-i-llc", + "created_at": "2025-11-17T14:29:34.248Z", + "updated_at": "2025-11-17T21:46:35.951Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 112, + "azdhs_id": 42, + "name": "Deeply Rooted Boutique Cannabis Company", + "company_name": "Desert Boyz", + "address": "11725 NW Grand Ave", + "city": "El Mirage", + "state": "AZ", + "zip": "85335", + "status_line": "Operating · Marijuana Establishment · (480) 708-0296", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Deeply%20Rooted%20Boutique%20Cannabis%20Company", + "latitude": null, + "longitude": null, + "dba_name": "Deeply Rooted Boutique Cannabis Company Dispensary", + "phone": "4807080296", + "email": null, + "website": "http://azdeeplyrooted.com/", + "google_rating": "4.8", + "google_review_count": 568, + "menu_url": "https://dutchie.com/embedded-menu/AZ-Deeply-Rooted", + "scraper_template": "dutchie", + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "deeply-rooted-boutique-cannabis-company", + "created_at": "2025-11-17T14:29:34.250Z", + "updated_at": "2025-12-02T00:56:51.156Z", + "menu_provider": "dutchie", + "menu_provider_confidence": 100, + "crawler_mode": "production", + "crawler_status": "running", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": "2025-12-01T07:41:48.779Z", + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/embedded-menu/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/embedded-menu/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/embedded-menu/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/embedded-menu/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "dutchie", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 113, + "azdhs_id": 43, + "name": "JARS Cannabis", + "company_name": "Desert Medical Campus Inc", + "address": "10040 N. Metro Parkway W", + "city": "Phoenix", + "state": "AZ", + "zip": "85051", + "status_line": "Operating · Marijuana Facility · 602-870-8700", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Phoenix Metrocenter", + "phone": "6028708700", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.8", + "google_review_count": 11971, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis", + "created_at": "2025-11-17T14:29:34.251Z", + "updated_at": "2025-11-17T21:46:35.952Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 114, + "azdhs_id": 44, + "name": "Green Pharms", + "company_name": "Desertview Wellness & Healing Solutions, LLC", + "address": "600 South 80th Avenue, 100", + "city": "Tolleson", + "state": "AZ", + "zip": "85353", + "status_line": "Operating · Marijuana Facility · (928) 522-6337", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Green%20Pharms", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "9285226337", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "green-pharms", + "created_at": "2025-11-17T14:29:34.253Z", + "updated_at": "2025-11-17T14:29:34.253Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 115, + "azdhs_id": 45, + "name": "Devine Desert Healing Inc", + "company_name": "Devine Desert Healing Inc", + "address": "17201 N 19th Ave", + "city": "Phoenix", + "state": "AZ", + "zip": "85023", + "status_line": "Operating · Marijuana Facility · 602-388-4400", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Devine%20Desert%20Healing%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Bell", + "phone": "6023884400", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-bell?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.5", + "google_review_count": 2873, + "menu_url": "https://dutchie.com/dispensary/curaleaf-bell-road", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "devine-desert-healing-inc", + "created_at": "2025-11-17T14:29:34.254Z", + "updated_at": "2025-12-01T14:42:04.602Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": "2025-12-01T07:42:04.602Z", + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 116, + "azdhs_id": 46, + "name": "JARS Cannabis", + "company_name": "Dreem Green Inc", + "address": "2412 East University Drive", + "city": "Phoenix", + "state": "AZ", + "zip": "85034", + "status_line": "Operating · Marijuana Facility · (602) 675-6999", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Phoenix Airport", + "phone": "6026756999", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 10901, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-phoenix-2", + "created_at": "2025-11-17T14:29:34.255Z", + "updated_at": "2025-11-17T21:46:35.954Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 117, + "azdhs_id": 47, + "name": "Nature's Wonder", + "company_name": "DYNAMIC TRIO HOLDINGS LLC", + "address": "6812 East Cave Creek Road, 2, 2A and 3", + "city": "Cave Creek", + "state": "AZ", + "zip": "85331", + "status_line": "Operating · Marijuana Establishment · 480-861-3649", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nature's%20Wonder", + "latitude": null, + "longitude": null, + "dba_name": "Nature's Wonder Cave Creek Dispensary", + "phone": "4808613649", + "email": null, + "website": "https://natureswonderaz.com/cave-creek-dispensary-menu-recreational", + "google_rating": "4.5", + "google_review_count": 453, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nature-s-wonder", + "created_at": "2025-11-17T14:29:34.257Z", + "updated_at": "2025-11-17T21:46:36.109Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 118, + "azdhs_id": 48, + "name": "Earth's Healing Inc", + "company_name": "Earth's Healing Inc", + "address": "2075 E Benson Hwy", + "city": "Tucson", + "state": "AZ", + "zip": "85714", + "status_line": "Operating · Marijuana Facility · (520) 373-5779", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Earth's%20Healing%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Earth's Healing South", + "phone": "5203735779", + "email": null, + "website": "https://earthshealing.org/", + "google_rating": "4.8", + "google_review_count": 7608, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "earth-s-healing-inc", + "created_at": "2025-11-17T14:29:34.258Z", + "updated_at": "2025-11-17T21:46:35.956Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 119, + "azdhs_id": 49, + "name": "East Valley Patient Wellness Group Inc", + "company_name": "East Valley Patient Wellness Group Inc", + "address": "13650 N 99th Ave", + "city": "Sun City", + "state": "AZ", + "zip": "85351", + "status_line": "Operating · Marijuana Facility · (623) 246-8080", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=East%20Valley%20Patient%20Wellness%20Group%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary Sun City", + "phone": "6232468080", + "email": null, + "website": "https://www.livewithsol.com/sun-city-dispensary?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing", + "google_rating": "4.6", + "google_review_count": 1115, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "east-valley-patient-wellness-group-inc", + "created_at": "2025-11-17T14:29:34.259Z", + "updated_at": "2025-11-17T21:46:35.957Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 120, + "azdhs_id": 50, + "name": "Mint Cannabis", + "company_name": "Eba Holdings Inc.", + "address": "8729 E Manzanita Dr", + "city": "Scottsdale", + "state": "AZ", + "zip": "85258", + "status_line": "Operating · Marijuana Facility · (480) 749-6468", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Mint%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis - Scottsdale", + "phone": "4807496468", + "email": null, + "website": "https://mintdeals.com/scottsdale-az/", + "google_rating": "4.4", + "google_review_count": 763, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mint-cannabis", + "created_at": "2025-11-17T14:29:34.260Z", + "updated_at": "2025-11-17T21:46:35.960Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 121, + "azdhs_id": 51, + "name": "Encore Labs Arizona", + "company_name": "Encore Labs Arizona", + "address": "16624 North 90th Street, #101", + "city": "Scottsdale", + "state": "AZ", + "zip": "85260", + "status_line": "Operating · Marijuana Laboratory · (626) 653-3414", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Encore%20Labs%20Arizona", + "latitude": null, + "longitude": null, + "dba_name": "Encore Labs AZ", + "phone": "6266533414", + "email": null, + "website": "https://www.encorelabs.com/", + "google_rating": "5.0", + "google_review_count": 2, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "encore-labs-arizona", + "created_at": "2025-11-17T14:29:34.262Z", + "updated_at": "2025-11-17T21:46:36.111Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 122, + "azdhs_id": 52, + "name": "Zanzibar", + "company_name": "FJM Group LLC", + "address": "60 W Main St", + "city": "Quartzsite", + "state": "AZ", + "zip": "85346", + "status_line": "Operating · Marijuana Establishment · (520) 907-2181", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zanzibar", + "latitude": null, + "longitude": null, + "dba_name": "Zanzibar dispensary", + "phone": "5209072181", + "email": null, + "website": "https://dutchie.com/dispensary/Zanzibar-Cannabis-Dispensary/products/flower", + "google_rating": "4.0", + "google_review_count": 71, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zanzibar", + "created_at": "2025-11-17T14:29:34.263Z", + "updated_at": "2025-11-17T21:46:35.962Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 123, + "azdhs_id": 53, + "name": "The Downtown Dispensary", + "company_name": "Forever 46 Llc", + "address": "221 E 6th St, Suite 105", + "city": "Tucson", + "state": "AZ", + "zip": "85705", + "status_line": "Operating · Marijuana Facility · (520) 838-0492", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Downtown%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "D2 Dispensary - Downtown Cannabis Gallery", + "phone": "5208380492", + "email": null, + "website": "http://d2dispensary.com/", + "google_rating": "4.8", + "google_review_count": 5290, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-downtown-dispensary", + "created_at": "2025-11-17T14:29:34.264Z", + "updated_at": "2025-11-17T21:46:35.964Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 124, + "azdhs_id": 54, + "name": "Zen Leaf Phoenix (Cave Creek Rd)", + "company_name": "Fort Consulting, Llc", + "address": "12401 N Cave Creek Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85022", + "status_line": "Operating · Marijuana Facility · 312-819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Phoenix%20(Cave%20Creek%20Rd)", + "latitude": null, + "longitude": null, + "dba_name": "Zen Leaf Dispensary Phoenix (Cave Creek Rd.)", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/phoenix-n-cave-creek/?utm_campaign=az-phoenix-cave-creek&utm_medium=gbp&utm_source=google", + "google_rating": "4.6", + "google_review_count": 2720, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zen-leaf-phoenix-cave-creek-rd-", + "created_at": "2025-11-17T14:29:34.265Z", + "updated_at": "2025-11-17T21:46:36.112Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 125, + "azdhs_id": 55, + "name": "Trulieve of Tucson", + "company_name": "Fort Mountain Consulting, Llc", + "address": "4659 E 22nd St", + "city": "Tucson", + "state": "AZ", + "zip": "85711", + "status_line": "Operating · Marijuana Facility · 770-330-0831", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Tucson Dispensary", + "phone": "7703300831", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/tucson-toumey-park?utm_source=gmb&utm_medium=organic&utm_campaign=tucson-swan", + "google_rating": "4.6", + "google_review_count": 1169, + "menu_url": "https://dutchie.com/dispensary/curaleaf-tucson", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-tucson", + "created_at": "2025-11-17T14:29:34.266Z", + "updated_at": "2025-12-02T12:45:10.113Z", + "menu_provider": "dutchie", + "menu_provider_confidence": 100, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": null, + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "dutchie", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 126, + "azdhs_id": 56, + "name": "Full Spectrum Lab, LLC", + "company_name": "Full Spectrum Lab, LLC", + "address": "3865 E 34th St, Ste 109", + "city": "Tucson", + "state": "AZ", + "zip": "85713", + "status_line": "Operating · Marijuana Laboratory · (520) 838-0695", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Full%20Spectrum%20Lab%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Full Spectrum Lab, LLC", + "phone": "5208380695", + "email": null, + "website": "https://fullspectrumlab.com/", + "google_rating": "4.5", + "google_review_count": 2, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "full-spectrum-lab-llc", + "created_at": "2025-11-17T14:29:34.267Z", + "updated_at": "2025-11-17T21:46:35.968Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 127, + "azdhs_id": 57, + "name": "Farm Fresh", + "company_name": "Fwa Inc", + "address": "790 N Lake Havasu Ave #4", + "city": "Lake Havasu City", + "state": "AZ", + "zip": "86404", + "status_line": "Operating · Marijuana Facility · (928) 733-6339", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Farm%20Fresh", + "latitude": null, + "longitude": null, + "dba_name": "Farm Fresh Medical/Recreational Marijuana Dispensary", + "phone": "9287336339", + "email": null, + "website": "http://farmfreshdispensary.com/", + "google_rating": "4.6", + "google_review_count": 642, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "farm-fresh", + "created_at": "2025-11-17T14:29:34.269Z", + "updated_at": "2025-11-17T21:46:36.114Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 128, + "azdhs_id": 58, + "name": "The Mint Dispensary", + "company_name": "G.T.L. Llc", + "address": "2444 W Northern Ave", + "city": "Phoenix", + "state": "AZ", + "zip": "85021", + "status_line": "Operating · Marijuana Facility · (480) 749-6468", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Mint%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis - Northern Ave", + "phone": "4807496468", + "email": null, + "website": "https://mintdeals.com/phoenix-az/", + "google_rating": "4.6", + "google_review_count": 1233, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-mint-dispensary-phoenix", + "created_at": "2025-11-17T14:29:34.270Z", + "updated_at": "2025-11-17T21:46:36.116Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 129, + "azdhs_id": 59, + "name": "JARS Cannabis", + "company_name": "Gila Dreams X, LLC", + "address": "100 East State Highway 260", + "city": "Payson", + "state": "AZ", + "zip": "85541", + "status_line": "Operating · Marijuana Establishment · 928-474-2420", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Payson", + "phone": "9284742420", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 3259, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-payson-1", + "created_at": "2025-11-17T14:29:34.271Z", + "updated_at": "2025-11-17T21:46:35.970Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 130, + "azdhs_id": 60, + "name": "Earth's Healing North", + "company_name": "Globe Farmacy Inc", + "address": "78 W River Rd", + "city": "Tucson", + "state": "AZ", + "zip": "85704", + "status_line": "Operating · Marijuana Facility · (520) 909-6612", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Earth's%20Healing%20North", + "latitude": null, + "longitude": null, + "dba_name": "Earth's Healing North", + "phone": "5209096612", + "email": null, + "website": "https://earthshealing.org/", + "google_rating": "4.8", + "google_review_count": 7149, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "earth-s-healing-north", + "created_at": "2025-11-17T14:29:34.272Z", + "updated_at": "2025-11-17T21:46:35.972Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 131, + "azdhs_id": 61, + "name": "Trulieve of Peoria Dispensary", + "company_name": "Green Desert Patient Center Of Peoria", + "address": "9275 W Peoria Ave, Ste 104", + "city": "Peoria", + "state": "AZ", + "zip": "85345", + "status_line": "Operating · Marijuana Facility · 850-559-7734", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Peoria%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Peoria Dispensary", + "phone": "8505597734", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/peoria?utm_source=gmb&utm_medium=organic&utm_campaign=peoria", + "google_rating": "4.7", + "google_review_count": 2931, + "menu_url": "https://dutchie.com/dispensary/curaleaf-dispensary-peoria", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-peoria-dispensary", + "created_at": "2025-11-17T14:29:34.274Z", + "updated_at": "2025-12-02T12:45:38.901Z", + "menu_provider": "dutchie", + "menu_provider_confidence": 100, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": null, + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "dutchie", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 132, + "azdhs_id": 62, + "name": "Nature's Medicines", + "company_name": "Green Hills Patient Center Inc", + "address": "16913 East Enterprise Drive, 201, 202, 203", + "city": "Fountain Hills", + "state": "AZ", + "zip": "85268", + "status_line": "Operating · Marijuana Facility · (928) 537-4888", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "9285374888", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nature-s-medicines-fountain-hills", + "created_at": "2025-11-17T14:29:34.275Z", + "updated_at": "2025-11-17T14:29:34.275Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 133, + "azdhs_id": 63, + "name": "Sunday Goods", + "company_name": "Green Lightning, LLC", + "address": "723 N Scottsdale Rd", + "city": "Tempe", + "state": "AZ", + "zip": "85281", + "status_line": "Operating · Marijuana Establishment · (480)-219-1300", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods", + "latitude": null, + "longitude": null, + "dba_name": "Sunday Goods Tempe", + "phone": "", + "email": null, + "website": "https://sundaygoods.com/location/dispensary-tempe-az/?utm_source=google&utm_medium=gbp&utm_campaign=tempe_gbp", + "google_rating": "4.1", + "google_review_count": 685, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sunday-goods-tempe", + "created_at": "2025-11-17T14:29:34.276Z", + "updated_at": "2025-11-17T21:46:35.974Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 134, + "azdhs_id": 64, + "name": "Southern Arizona Integrated Therapies (Tucson SAINTS)", + "company_name": "Green Medicine", + "address": "112 S Kolb Rd", + "city": "Tucson", + "state": "AZ", + "zip": "85710", + "status_line": "Operating · Marijuana Facility · (520) 886-1003", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Southern%20Arizona%20Integrated%20Therapies%20(Tucson%20SAINTS)", + "latitude": null, + "longitude": null, + "dba_name": "SAINTS Dispensary", + "phone": "5208861003", + "email": null, + "website": "https://www.tucsonsaints.com/", + "google_rating": "4.8", + "google_review_count": 1704, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "southern-arizona-integrated-therapies-tucson-saints-", + "created_at": "2025-11-17T14:29:34.277Z", + "updated_at": "2025-11-17T21:46:35.976Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 135, + "azdhs_id": 65, + "name": "Trulieve Bisbee Dispensary", + "company_name": "Green Sky Patient Center Of Scottsdale North Inc", + "address": "1191 S Naco Hwy", + "city": "Bisbee", + "state": "AZ", + "zip": "85603", + "status_line": "Operating · Marijuana Facility · 850-559-7734", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Bisbee%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Bisbee Dispensary", + "phone": "8505597734", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/?utm_source=gmb&utm_medium=organic&utm_campaign=bisbee", + "google_rating": "3.5", + "google_review_count": 13, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-bisbee-dispensary", + "created_at": "2025-11-17T14:29:34.279Z", + "updated_at": "2025-11-17T21:46:35.977Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 136, + "azdhs_id": 66, + "name": "Greenmed, Inc", + "company_name": "Greenmed, Inc", + "address": "6464 E Tanque Verde Rd", + "city": "Tucson", + "state": "AZ", + "zip": "85715", + "status_line": "Operating · Marijuana Facility · (520) 886-2484", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Greenmed%2C%20Inc", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "5208862484", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "greenmed-inc", + "created_at": "2025-11-17T14:29:34.280Z", + "updated_at": "2025-11-17T14:29:34.280Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 137, + "azdhs_id": 67, + "name": "Marigold Dispensary", + "company_name": "Greens Goddess Products, Inc", + "address": "2601 W. Dunlap Avenue, 21", + "city": "Phoenix", + "state": "AZ", + "zip": "85017", + "status_line": "Operating · Marijuana Facility · (602) 900-4557", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Marigold%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Key Cannabis Dispensary Phoenix", + "phone": "6029004557", + "email": null, + "website": "https://keycannabis.com/shop/phoenix-az/?utm_source=terrayn_gbp&utm_medium=organic&utm_campaign=phoenix", + "google_rating": "4.7", + "google_review_count": 2664, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "marigold-dispensary", + "created_at": "2025-11-17T14:29:34.281Z", + "updated_at": "2025-11-17T21:46:36.119Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 138, + "azdhs_id": 68, + "name": "Grunge Free LLC", + "company_name": "Grunge Free LLC", + "address": "700 North Pinal Parkway Avenue", + "city": "Florence", + "state": "AZ", + "zip": "85132", + "status_line": "Operating · Marijuana Establishment · (917)375-3900", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Grunge%20Free%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Nirvana Cannabis - Florence", + "phone": "9173753900", + "email": null, + "website": "https://nirvanacannabis.com/", + "google_rating": "4.8", + "google_review_count": 782, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "grunge-free-llc", + "created_at": "2025-11-17T14:29:34.282Z", + "updated_at": "2025-11-17T21:46:35.978Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 139, + "azdhs_id": 69, + "name": "Ponderosa Dispensary", + "company_name": "H4L Ponderosa, LLC", + "address": "7343 S 89th Pl", + "city": "Mesa", + "state": "AZ", + "zip": "85212", + "status_line": "Operating · Marijuana Facility · (480) 213-1402", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Ponderosa Dispensary Queen Creek", + "phone": "4802131402", + "email": null, + "website": "https://www.pondyaz.com/locations", + "google_rating": "4.6", + "google_review_count": 1709, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "ponderosa-dispensary-mesa-1", + "created_at": "2025-11-17T14:29:34.284Z", + "updated_at": "2025-11-17T21:46:35.980Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 140, + "azdhs_id": 70, + "name": "Healing Healthcare 3 Inc", + "company_name": "Healing Healthcare 3 Inc", + "address": "1040 E Camelback Rd, Ste A", + "city": "Phoenix", + "state": "AZ", + "zip": "85014", + "status_line": "Operating · Marijuana Facility · 602-354-3094", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Healing%20Healthcare%203%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Camelback", + "phone": "6023543094", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-camelback?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.5", + "google_review_count": 2853, + "menu_url": "https://dutchie.com/dispensary/curaleaf-camelback", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "healing-healthcare-3-inc", + "created_at": "2025-11-17T14:29:34.285Z", + "updated_at": "2025-12-01T14:42:09.688Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": "2025-12-01T07:42:09.688Z", + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 141, + "azdhs_id": 71, + "name": "Consume Cannabis", + "company_name": "Health Center Navajo, Inc", + "address": "1350 N Penrod Rd", + "city": "Show Low", + "state": "AZ", + "zip": "85901", + "status_line": "Operating · Marijuana Facility · (520)808-3111", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Consume%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "Consume Cannabis - Show Low", + "phone": "5208083111", + "email": null, + "website": "https://www.consumecannabis.com/dispensaries/show-low", + "google_rating": "4.4", + "google_review_count": 1111, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "consume-cannabis", + "created_at": "2025-11-17T14:29:34.286Z", + "updated_at": "2025-11-17T21:46:36.120Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 142, + "azdhs_id": 72, + "name": "Herbal Wellness Center Inc", + "company_name": "Herbal Wellness Center Inc", + "address": "4126 W Indian School Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85019", + "status_line": "Operating · Marijuana Facility · 602-910-4152", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Herbal%20Wellness%20Center%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Herbal Wellness Center West", + "phone": "6029104152", + "email": null, + "website": "https://herbalwellnesscenter.com/", + "google_rating": "4.3", + "google_review_count": 2364, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "herbal-wellness-center-inc", + "created_at": "2025-11-17T14:29:34.287Z", + "updated_at": "2025-11-17T21:46:35.983Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 143, + "azdhs_id": 73, + "name": "Trulieve of Chandler Dispensary", + "company_name": "High Desert Healing Llc", + "address": "13433 E. Chandler Blvd. Suite A", + "city": "Chandler", + "state": "AZ", + "zip": "85225", + "status_line": "Operating · Marijuana Facility · 954-817-2370", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Chandler%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Chandler Dispensary", + "phone": "9548172370", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/chandler?utm_source=gmb&utm_medium=organic&utm_campaign=chandler", + "google_rating": "4.0", + "google_review_count": 1134, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-chandler-dispensary", + "created_at": "2025-11-17T14:29:34.288Z", + "updated_at": "2025-11-17T21:46:35.984Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 144, + "azdhs_id": 74, + "name": "Trulieve of Avondale Dispensary", + "company_name": "High Desert Healing Llc", + "address": "3828 S Vermeersch Rd", + "city": "Avondale", + "state": "AZ", + "zip": "85323", + "status_line": "Operating · Marijuana Facility · 954-817-2370", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Avondale%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Avondale Dispensary", + "phone": "9548172370", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/avondale?utm_source=gmb&utm_medium=organic&utm_campaign=avondale", + "google_rating": "4.3", + "google_review_count": 1046, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-avondale-dispensary", + "created_at": "2025-11-17T14:29:34.290Z", + "updated_at": "2025-11-17T21:46:36.121Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 145, + "azdhs_id": 75, + "name": "Ponderosa Dispensary", + "company_name": "High Mountain Health, Llc", + "address": "1250 S Plaza Way Ste A", + "city": "Flagstaff", + "state": "AZ", + "zip": "86001", + "status_line": "Operating · Marijuana Facility · (928) 774-5467", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Ponderosa Dispensary Flagstaff", + "phone": "9287745467", + "email": null, + "website": "https://www.pondyaz.com/locations", + "google_rating": "4.5", + "google_review_count": 1340, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "ponderosa-dispensary-flagstaff", + "created_at": "2025-11-17T14:29:34.291Z", + "updated_at": "2025-11-17T21:46:35.986Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 146, + "azdhs_id": 76, + "name": "Higher than High I LLC", + "company_name": "Higher than High I LLC", + "address": "1302 West Industrial Drive", + "city": "Coolidge", + "state": "AZ", + "zip": "85128", + "status_line": "Operating · Marijuana Establishment · 480-861-3649", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Higher%20than%20High%20I%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "4808613649", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "higher-than-high-i-llc", + "created_at": "2025-11-17T14:29:34.293Z", + "updated_at": "2025-11-17T14:29:34.293Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 147, + "azdhs_id": 77, + "name": "Holistic Patient Wellness Group", + "company_name": "Holistic Patient Wellness Group", + "address": "1322 N Mcclintock Dr", + "city": "Tempe", + "state": "AZ", + "zip": "85281", + "status_line": "Operating · Marijuana Facility · (480) 795-6363", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Holistic%20Patient%20Wellness%20Group", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary McClintock", + "phone": "4807956363", + "email": null, + "website": "https://www.livewithsol.com/tempe-mcclintock-dispensary?utm_source=gmb&utm_medium=organic&utm_campaign=gmb-listing", + "google_rating": "4.7", + "google_review_count": 2789, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "holistic-patient-wellness-group", + "created_at": "2025-11-17T14:29:34.294Z", + "updated_at": "2025-11-17T21:46:35.988Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 148, + "azdhs_id": 78, + "name": "Jamestown Center", + "company_name": "Jamestown Center", + "address": "4104 E 32nd St", + "city": "Yuma", + "state": "AZ", + "zip": "85365", + "status_line": "Operating · Marijuana Facility · (928) 344-1735", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Jamestown%20Center", + "latitude": null, + "longitude": null, + "dba_name": "Yuma Dispensary", + "phone": "9283441735", + "email": null, + "website": "http://yumadispensary.com/", + "google_rating": "4.4", + "google_review_count": 1187, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jamestown-center", + "created_at": "2025-11-17T14:29:34.295Z", + "updated_at": "2025-11-17T21:46:35.990Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 149, + "azdhs_id": 79, + "name": "BEST Dispensary", + "company_name": "Jamestown Center", + "address": "1962 N. Higley Rd", + "city": "Mesa", + "state": "AZ", + "zip": "85205", + "status_line": "Operating · Marijuana Facility · 623-264-2378", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=BEST%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "BEST Dispensary", + "phone": "6232642378", + "email": null, + "website": "http://www.bestdispensary.com/", + "google_rating": "4.6", + "google_review_count": 482, + "menu_url": "https://best.treez.io/onlinemenu/?customerType=ADULT", + "scraper_template": "treez", + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "best-dispensary", + "created_at": "2025-11-17T14:29:34.297Z", + "updated_at": "2025-11-18T15:01:45.117Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 150, + "azdhs_id": 80, + "name": "Joint Junkies I LLC", + "company_name": "Joint Junkies I LLC", + "address": "26427 S Arizona Ave", + "city": "Chandler", + "state": "AZ", + "zip": "85248", + "status_line": "Operating · Marijuana Establishment · (928) 638-5831", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Joint%20Junkies%20I%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Story Cannabis Dispensary South Chandler", + "phone": "9286385831", + "email": null, + "website": "https://storycannabis.com/dispensary-locations/arizona/south-chandler-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=south_chandler&utm_term=click&utm_content=website", + "google_rating": "4.4", + "google_review_count": 1227, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "joint-junkies-i-llc", + "created_at": "2025-11-17T14:29:34.298Z", + "updated_at": "2025-11-17T21:46:36.122Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 151, + "azdhs_id": 81, + "name": "Juicy Joint I LLC", + "company_name": "Juicy Joint I LLC", + "address": "3550 North Lane, #110", + "city": "Bullhead City", + "state": "AZ", + "zip": "86442", + "status_line": "Operating · Marijuana Establishment · (928) 324-6062", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Juicy%20Joint%20I%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "9283246062", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "juicy-joint-i-llc", + "created_at": "2025-11-17T14:29:34.299Z", + "updated_at": "2025-11-17T14:29:34.299Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 152, + "azdhs_id": 82, + "name": "K Group Partners", + "company_name": "K Group Partners Llc", + "address": "11200 W Michigan Ave Ste 5", + "city": "Youngtown", + "state": "AZ", + "zip": "85363", + "status_line": "Operating · Marijuana Facility · 623-444-5977", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=K%20Group%20Partners", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Youngtown", + "phone": "6234445977", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-youngtown?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.7", + "google_review_count": 2421, + "menu_url": "https://dutchie.com/dispensary/curaleaf-youngtown", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "k-group-partners", + "created_at": "2025-11-17T14:29:34.300Z", + "updated_at": "2025-11-17T21:46:35.994Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 153, + "azdhs_id": 83, + "name": "Sol Flower", + "company_name": "Kannaboost Technology Inc", + "address": "2424 W University Dr, Ste. 101 & 119", + "city": "Tempe", + "state": "AZ", + "zip": "85281", + "status_line": "Operating · Marijuana Facility · 480-644-2071", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary University", + "phone": "4806442071", + "email": null, + "website": "https://www.livewithsol.com/locations/tempe-university/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.6", + "google_review_count": 1149, + "menu_url": "https://dutchie.com/dispensary/sol-flower-dispensary-mcclintock", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sol-flower-tempe", + "created_at": "2025-11-17T14:29:34.302Z", + "updated_at": "2025-11-17T21:46:35.995Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 154, + "azdhs_id": 84, + "name": "Kaycha AZ LLC", + "company_name": "Kaycha AZ LLC", + "address": "1231 W Warner Rd, Ste 105", + "city": "Tempe", + "state": "AZ", + "zip": "85284", + "status_line": "Operating · Marijuana Laboratory · (770) 365-7752", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Kaycha%20AZ%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Kaycha Labs - Arizona", + "phone": "7703657752", + "email": null, + "website": "https://www.kaychalabs.com/", + "google_rating": "5.0", + "google_review_count": 0, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "kaycha-az-llc", + "created_at": "2025-11-17T14:29:34.303Z", + "updated_at": "2025-11-17T21:46:36.123Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 155, + "azdhs_id": 85, + "name": "Kind Meds Inc", + "company_name": "Kind Meds Inc", + "address": "2152 S Vineyard St Ste 120", + "city": "Mesa", + "state": "AZ", + "zip": "85210", + "status_line": "Operating · Marijuana Facility · (480) 686-9302", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Kind%20Meds%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Kind Meds", + "phone": "4806869302", + "email": null, + "website": "http://kindmedsaz.com/", + "google_rating": "3.8", + "google_review_count": 260, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "kind-meds-inc", + "created_at": "2025-11-17T14:29:34.304Z", + "updated_at": "2025-11-17T21:46:36.125Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 156, + "azdhs_id": 86, + "name": "Trulieve of Phoenix Dispensary", + "company_name": "Kwerles Inc", + "address": "2017 W. Peoria Avenue, Suite A", + "city": "Phoenix", + "state": "AZ", + "zip": "85029", + "status_line": "Operating · Marijuana Facility · 850-508-0261", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Phoenix%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "8505080261", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-phoenix-dispensary", + "created_at": "2025-11-17T14:29:34.306Z", + "updated_at": "2025-11-17T14:29:34.306Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 157, + "azdhs_id": 87, + "name": "L1 Management, Llc", + "company_name": "L1 Management, Llc", + "address": "1525 N. Granite Reef Rd.", + "city": "Scottsdale", + "state": "AZ", + "zip": "85257", + "status_line": "Operating · Marijuana Laboratory · (602) 616-8167", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=L1%20Management%2C%20Llc", + "latitude": null, + "longitude": null, + "dba_name": "Level One Labs", + "phone": "6026168167", + "email": null, + "website": "https://levelonelabs.com/", + "google_rating": "5.0", + "google_review_count": 7, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "l1-management-llc", + "created_at": "2025-11-17T14:29:34.307Z", + "updated_at": "2025-11-17T21:46:35.997Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 158, + "azdhs_id": 88, + "name": "JARS Cannabis", + "company_name": "Lawrence Health Services LLC", + "address": "2250 Highway 60, Suite M", + "city": "Globe", + "state": "AZ", + "zip": "85501", + "status_line": "Operating · Marijuana Establishment · 928-793-2550", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Globe", + "phone": "9287932550", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.8", + "google_review_count": 959, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-globe", + "created_at": "2025-11-17T14:29:34.308Z", + "updated_at": "2025-11-17T21:46:35.998Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 159, + "azdhs_id": 89, + "name": "JARS Cannabis", + "company_name": "Legacy & Co., Inc.", + "address": "3001 North 24th Street, A", + "city": "Phoenix", + "state": "AZ", + "zip": "85016", + "status_line": "Operating · Marijuana Facility · (623) 936-9333", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6239369333", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-phoenix-3", + "created_at": "2025-11-17T14:29:34.309Z", + "updated_at": "2025-11-17T14:29:34.309Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 160, + "azdhs_id": 90, + "name": "Cookies", + "company_name": "Life Changers Investments LLC", + "address": "2715 South Hardy Drive", + "city": "Tempe", + "state": "AZ", + "zip": "85282", + "status_line": "Operating · Marijuana Establishment · (480) 452-7275", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Cookies", + "latitude": null, + "longitude": null, + "dba_name": "Cookies Cannabis Dispensary Tempe", + "phone": "4804527275", + "email": null, + "website": "https://tempe.cookies.co/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.7", + "google_review_count": 5626, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "cookies", + "created_at": "2025-11-17T14:29:34.310Z", + "updated_at": "2025-11-17T21:46:36.126Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 161, + "azdhs_id": 91, + "name": "Mint Cannabis", + "company_name": "M&T Retail Facility 1, LLC", + "address": "1211 North 75th Avenue", + "city": "Phoenix", + "state": "AZ", + "zip": "85043", + "status_line": "Operating · Marijuana Facility · 480-749-6468", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Mint%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis - 75th Ave", + "phone": "4807496468", + "email": null, + "website": "https://mintdeals.com/75-ave-phx/", + "google_rating": "4.7", + "google_review_count": 1463, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mint-cannabis-phoenix", + "created_at": "2025-11-17T14:29:34.312Z", + "updated_at": "2025-11-17T21:46:36.000Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 162, + "azdhs_id": 92, + "name": "MCCSE214, LLC", + "company_name": "MCCSE214, LLC", + "address": "1975 E Northern Ave", + "city": "Kingman", + "state": "AZ", + "zip": "86409", + "status_line": "Operating · Marijuana Establishment · 928-263-6348", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=MCCSE214%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "9282636348", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mccse214-llc", + "created_at": "2025-11-17T14:29:34.313Z", + "updated_at": "2025-11-17T14:29:34.313Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 163, + "azdhs_id": 93, + "name": "MCCSE240, LLC", + "company_name": "MCCSE240, LLC", + "address": "12555 NW Grand Ave, B", + "city": "El Mirage", + "state": "AZ", + "zip": "85335", + "status_line": "Operating · Marijuana Establishment · 602-351-5450", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=MCCSE240%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6023515450", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mccse240-llc", + "created_at": "2025-11-17T14:29:34.314Z", + "updated_at": "2025-11-17T14:29:34.314Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 164, + "azdhs_id": 94, + "name": "MCCSE29, LLC", + "company_name": "MCCSE29, LLC", + "address": "12323 W Camelback Rd", + "city": "Litchfield Park", + "state": "AZ", + "zip": "85340", + "status_line": "Operating · Marijuana Establishment · (602) 903-3665", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=MCCSE29%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6029033665", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mccse29-llc", + "created_at": "2025-11-17T14:29:34.316Z", + "updated_at": "2025-11-17T14:29:34.316Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 165, + "azdhs_id": 95, + "name": "MCCSE82, LLC", + "company_name": "MCCSE82, LLC", + "address": "46639 North Black Canyon Highway, 1-2", + "city": "New River", + "state": "AZ", + "zip": "85087", + "status_line": "Operating · Marijuana Establishment · 928-299-5145", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=MCCSE82%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis New River", + "phone": "9282995145", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 3180, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mccse82-llc", + "created_at": "2025-11-17T14:29:34.317Z", + "updated_at": "2025-11-17T21:46:36.001Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 166, + "azdhs_id": 96, + "name": "MCD-SE Venture 25, LLC", + "company_name": "MCD-SE Venture 25, LLC", + "address": "15235 North Dysart Road, 111 D", + "city": "El Mirage", + "state": "AZ", + "zip": "85335", + "status_line": "Operating · Marijuana Establishment · 602-931-3663", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=MCD-SE%20Venture%2025%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis Dispensary - EL MIRAGE", + "phone": "6029313663", + "email": null, + "website": "https://mintdeals.com/az-el-mirage/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps", + "google_rating": "4.7", + "google_review_count": 249, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mcd-se-venture-25-llc", + "created_at": "2025-11-17T14:29:34.318Z", + "updated_at": "2025-11-17T21:46:36.127Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 167, + "azdhs_id": 97, + "name": "MCD-SE Venture 26, LLC", + "company_name": "MCD-SE Venture 26, LLC", + "address": "15235 North Dysart Road, 11C", + "city": "El Mirage", + "state": "AZ", + "zip": "85335", + "status_line": "Operating · Marijuana Establishment · 602-931-3663", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=MCD-SE%20Venture%2026%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis Dispensary - EL MIRAGE", + "phone": "6029313663", + "email": null, + "website": "https://mintdeals.com/az-el-mirage/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps", + "google_rating": "4.7", + "google_review_count": 249, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mcd-se-venture-26-llc", + "created_at": "2025-11-17T14:29:34.320Z", + "updated_at": "2025-11-17T21:46:36.003Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 168, + "azdhs_id": 98, + "name": "Trulieve of Casa Grande", + "company_name": "Medical Pain Relief Inc", + "address": "1860 E Salk Dr Ste B-1", + "city": "Casa Grande", + "state": "AZ", + "zip": "85122", + "status_line": "Operating · Marijuana Facility · 850-508-0261", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Casa%20Grande", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Casa Grande Dispensary", + "phone": "8505080261", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/casa-grande?utm_source=gmb&utm_medium=organic&utm_campaign=casa-grande", + "google_rating": "4.1", + "google_review_count": 1817, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-casa-grande", + "created_at": "2025-11-17T14:29:34.321Z", + "updated_at": "2025-11-17T21:46:36.004Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 169, + "azdhs_id": 99, + "name": "Desert Bloom Releaf Center", + "company_name": "Medmar Tanque Verde Llc", + "address": "8060 E 22nd St Ste 108", + "city": "Tucson", + "state": "AZ", + "zip": "85710", + "status_line": "Operating · Marijuana Facility · 520-886-1760", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Desert%20Bloom%20Releaf%20Center", + "latitude": null, + "longitude": null, + "dba_name": "Desert Bloom Re-Leaf Center", + "phone": "5208861760", + "email": null, + "website": "http://www.dbloomtucson.com/", + "google_rating": "4.2", + "google_review_count": 1634, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "desert-bloom-releaf-center", + "created_at": "2025-11-17T14:29:34.322Z", + "updated_at": "2025-11-17T21:46:36.007Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 170, + "azdhs_id": 100, + "name": "MK Associates LLC", + "company_name": "MK Associates LLC", + "address": "3270 AZ-82", + "city": "Sonoita", + "state": "AZ", + "zip": "85637", + "status_line": "Operating · Marijuana Establishment · (703) 915-2159", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=MK%20Associates%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "7039152159", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "mk-associates-llc", + "created_at": "2025-11-17T14:29:34.323Z", + "updated_at": "2025-11-17T14:29:34.323Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 171, + "azdhs_id": 101, + "name": "Nirvana Center Phoenix", + "company_name": "Mmj Apothecary", + "address": "9240 West Northern Avenue, Ste. 103b", + "city": "Peoria", + "state": "AZ", + "zip": "85345", + "status_line": "Operating · Marijuana Facility · (928) 684-8880", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Phoenix", + "latitude": null, + "longitude": null, + "dba_name": "Ponderosa Dispensary Glendale", + "phone": "9286848880", + "email": null, + "website": "https://www.pondyaz.com/locations", + "google_rating": "4.5", + "google_review_count": 1057, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nirvana-center-phoenix", + "created_at": "2025-11-17T14:29:34.326Z", + "updated_at": "2025-11-17T21:46:36.128Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 172, + "azdhs_id": 102, + "name": "JARS Cannabis", + "company_name": "Mohave Cannabis Club 1, LLC", + "address": "4236 E Juanita Ave", + "city": "Mesa", + "state": "AZ", + "zip": "85206", + "status_line": "Operating · Marijuana Facility · 480-420-0064", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Mesa", + "phone": "4804200064", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 7637, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-mesa", + "created_at": "2025-11-17T14:29:34.327Z", + "updated_at": "2025-11-17T21:46:36.010Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 173, + "azdhs_id": 103, + "name": "JARS Cannabis", + "company_name": "Mohave Cannabis Club 2, LLC", + "address": "20340 N Lake Pleasant Rd. Ste 107", + "city": "Peoria", + "state": "AZ", + "zip": "85382", + "status_line": "Operating · Marijuana Facility · 623-246-1065", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Peoria", + "phone": "6232461065", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.8", + "google_review_count": 2462, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-peoria", + "created_at": "2025-11-17T14:29:34.328Z", + "updated_at": "2025-11-17T21:46:36.011Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 174, + "azdhs_id": 104, + "name": "JARS Cannabis", + "company_name": "Mohave Cannabis Club 3, LLC", + "address": "20224 N 27th Ave, Ste 103", + "city": "Phoenix", + "state": "AZ", + "zip": "85027", + "status_line": "Operating · Marijuana Facility · 623-233-5133", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis North Phoenix", + "phone": "6232335133", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.8", + "google_review_count": 4327, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-phoenix-4", + "created_at": "2025-11-17T14:29:34.330Z", + "updated_at": "2025-11-17T21:46:36.013Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 175, + "azdhs_id": 105, + "name": "JARS Cannabis", + "company_name": "Mohave Cannabis Club 4, LLC", + "address": "8028 E State Route 69", + "city": "Prescott Valley", + "state": "AZ", + "zip": "86314", + "status_line": "Operating · Marijuana Facility · 480-939-4002", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Prescott Valley", + "phone": "4809394002", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 1838, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-prescott-valley", + "created_at": "2025-11-17T14:29:34.331Z", + "updated_at": "2025-11-17T21:46:36.015Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 176, + "azdhs_id": 106, + "name": "Trulieve of Roosevelt Row", + "company_name": "Mohave Valley Consulting, Llc", + "address": "1007 N 7th St", + "city": "Phoenix", + "state": "AZ", + "zip": "85006", + "status_line": "Operating · Marijuana Facility · 770-330-0831", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Roosevelt%20Row", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Phoenix Dispensary Roosevelt", + "phone": "7703300831", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/phoenix-roosevelt?utm_source=gmb&utm_medium=organic&utm_campaign=phoenix-roosevelt", + "google_rating": "4.4", + "google_review_count": 670, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-roosevelt-row", + "created_at": "2025-11-17T14:29:34.333Z", + "updated_at": "2025-11-17T21:46:36.129Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 177, + "azdhs_id": 107, + "name": "Natural Herbal Remedies Inc", + "company_name": "Natural Herbal Remedies Inc", + "address": "3333 S Central Ave", + "city": "Phoenix", + "state": "AZ", + "zip": "85040", + "status_line": "Operating · Marijuana Facility · 480-739-0366", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Natural%20Herbal%20Remedies%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Central Phoenix", + "phone": "4807390366", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-central?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.6", + "google_review_count": 2577, + "menu_url": "https://dutchie.com/dispensary/curaleaf-central", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "natural-herbal-remedies-inc", + "created_at": "2025-11-17T14:29:34.334Z", + "updated_at": "2025-12-01T14:42:14.800Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": "2025-12-01T07:42:14.800Z", + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"zhX-_Y9x0IPLY21dJNESA\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 178, + "azdhs_id": 108, + "name": "Green Farmacy", + "company_name": "Natural Relief Clinic Inc", + "address": "4456 E Thomas Road", + "city": "Phoenix", + "state": "AZ", + "zip": "85018", + "status_line": "Operating · Marijuana Facility · (520) 686-8708", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Green%20Farmacy", + "latitude": null, + "longitude": null, + "dba_name": "YiLo Superstore (Arcadia) - SKY HARBOR", + "phone": "5206868708", + "email": null, + "website": "https://yilo.com/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.6", + "google_review_count": 586, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "green-farmacy", + "created_at": "2025-11-17T14:29:34.335Z", + "updated_at": "2025-11-17T21:46:36.018Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 179, + "azdhs_id": 109, + "name": "YiLo Superstore", + "company_name": "Natural Relief Clinic Inc", + "address": "2841 W Thunderbird Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85032", + "status_line": "Operating · Marijuana Facility · (602) 539-2828", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=YiLo%20Superstore", + "latitude": null, + "longitude": null, + "dba_name": "YiLo Superstore (Phoenix)", + "phone": "6025392828", + "email": null, + "website": "https://yilo.com/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.3", + "google_review_count": 880, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "yilo-superstore", + "created_at": "2025-11-17T14:29:34.337Z", + "updated_at": "2025-11-17T21:46:36.020Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 180, + "azdhs_id": 110, + "name": "Natural Remedy Patient Center", + "company_name": "Natural Remedy Patient Center", + "address": "16277 N Greenway Hayden Loop, 1st Floor", + "city": "Scottsdale", + "state": "AZ", + "zip": "85260", + "status_line": "Operating · Marijuana Facility · 602-842-0020", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Natural%20Remedy%20Patient%20Center", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Scottsdale", + "phone": "6028420020", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-scottsdale?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.6", + "google_review_count": 849, + "menu_url": "https://dutchie.com/dispensary/curaleaf-dispensary-scottsdale", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "natural-remedy-patient-center", + "created_at": "2025-11-17T14:29:34.338Z", + "updated_at": "2025-11-17T21:46:36.130Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 181, + "azdhs_id": 111, + "name": "Trulieve of Baseline Dispensary", + "company_name": "Nature Med Inc", + "address": "1821 W Baseline Rd", + "city": "Guadalupe", + "state": "AZ", + "zip": "85283", + "status_line": "Operating · Marijuana Facility · 850-508-0261", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Baseline%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Baseline Dispensary", + "phone": "8505080261", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/guadalupe?utm_source=gmb&utm_medium=organic&utm_campaign=baseline", + "google_rating": "4.4", + "google_review_count": 1297, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-baseline-dispensary", + "created_at": "2025-11-17T14:29:34.339Z", + "updated_at": "2025-11-17T21:46:36.022Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 182, + "azdhs_id": 112, + "name": "The Flower Shop Az", + "company_name": "Nature's Healing Center Inc", + "address": "10827 S 51st St, Ste 104", + "city": "Phoenix", + "state": "AZ", + "zip": "85044", + "status_line": "Operating · Marijuana Facility · (480) 500-5054", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az", + "latitude": null, + "longitude": null, + "dba_name": "The Flower Shop - Ahwatukee", + "phone": "4805005054", + "email": null, + "website": "https://theflowershopusa.com/ahwatukee?utm_source=google-business&utm_medium=organic", + "google_rating": "4.4", + "google_review_count": 1291, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-flower-shop-az-phoenix-1", + "created_at": "2025-11-17T14:29:34.341Z", + "updated_at": "2025-11-17T21:46:36.023Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 183, + "azdhs_id": 113, + "name": "The Flower Shop Az", + "company_name": "Nature's Healing Center Inc", + "address": "3155 E Mcdowell Rd Ste 2", + "city": "Phoenix", + "state": "AZ", + "zip": "85008", + "status_line": "Operating · Marijuana Facility · (480) 500-5054", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Flower%20Shop%20Az", + "latitude": null, + "longitude": null, + "dba_name": "The Flower Shop - Phoenix", + "phone": "4805005054", + "email": null, + "website": "https://theflowershopusa.com/phoenix?utm_source=google-business&utm_medium=organic", + "google_rating": "4.4", + "google_review_count": 1211, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-flower-shop-az-phoenix-2", + "created_at": "2025-11-17T14:29:34.342Z", + "updated_at": "2025-11-17T21:46:36.131Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 184, + "azdhs_id": 114, + "name": "Nature's Wonder Inc", + "company_name": "Nature's Wonder Inc", + "address": "260 W Apache Trail Dr", + "city": "Apache Junction", + "state": "AZ", + "zip": "85120", + "status_line": "Operating · Marijuana Facility · (480) 861-3649", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nature's%20Wonder%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Nature's Wonder Apache Junction Dispensary", + "phone": "4808613649", + "email": null, + "website": "https://natureswonderaz.com/apache-junction-dispensary-menu-recreational", + "google_rating": "4.7", + "google_review_count": 1891, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nature-s-wonder-inc", + "created_at": "2025-11-17T14:29:34.344Z", + "updated_at": "2025-11-17T21:46:36.025Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 185, + "azdhs_id": 115, + "name": "Nirvana Center Dispensaries", + "company_name": "Nirvana Enterprises AZ, LLC", + "address": "2 North 35th Avenue", + "city": "Phoenix", + "state": "AZ", + "zip": "85009", + "status_line": "Operating · Marijuana Facility · (480) 378-6917", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Dispensaries", + "latitude": null, + "longitude": null, + "dba_name": "Backpack Boyz - Phoenix", + "phone": "4803786917", + "email": null, + "website": "https://www.backpackboyz.com/content/arizona", + "google_rating": "4.8", + "google_review_count": 6915, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nirvana-center-dispensaries", + "created_at": "2025-11-17T14:29:34.345Z", + "updated_at": "2025-11-17T21:46:36.132Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 186, + "azdhs_id": 116, + "name": "Non Profit Patient Center Inc", + "company_name": "Non Profit Patient Center Inc", + "address": "2960 West Grand Avenue, Bldg. A and B", + "city": "Phoenix", + "state": "AZ", + "zip": "85017", + "status_line": "Operating · Marijuana Facility · 480-861-3649", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Non%20Profit%20Patient%20Center%20Inc", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "4808613649", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "non-profit-patient-center-inc", + "created_at": "2025-11-17T14:29:34.347Z", + "updated_at": "2025-11-17T14:29:34.347Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 187, + "azdhs_id": 117, + "name": "Ocotillo Vista, Inc.", + "company_name": "Ocotillo Vista, Inc.", + "address": "2330 North 75th Avenue", + "city": "Phoenix", + "state": "AZ", + "zip": "85035", + "status_line": "Operating · Marijuana Facility · 602-786-7988", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Ocotillo%20Vista%2C%20Inc.", + "latitude": null, + "longitude": null, + "dba_name": "Nirvana Cannabis - 75th Ave (West Phoenix)", + "phone": "6027867988", + "email": null, + "website": "https://nirvanacannabis.com/", + "google_rating": "4.7", + "google_review_count": 5296, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "ocotillo-vista-inc-", + "created_at": "2025-11-17T14:29:34.348Z", + "updated_at": "2025-11-17T21:46:36.026Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 188, + "azdhs_id": 118, + "name": "Organica Patient Group Inc", + "company_name": "Organica Patient Group Inc", + "address": "1720 E. Deer Valley Rd., Suite 101", + "city": "Phoenix", + "state": "AZ", + "zip": "85204", + "status_line": "Operating · Marijuana Facility · 602-910-4152", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Organica%20Patient%20Group%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Herbal Wellness Center North", + "phone": "6029104152", + "email": null, + "website": "https://herbalwellnesscenter.com/", + "google_rating": "4.6", + "google_review_count": 936, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "organica-patient-group-inc", + "created_at": "2025-11-17T14:29:34.349Z", + "updated_at": "2025-11-17T21:46:36.028Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 189, + "azdhs_id": 119, + "name": "Trulieve of Glendale", + "company_name": "Pahana, Inc.", + "address": "13631 N 59th Ave, Ste B110", + "city": "Glendale", + "state": "AZ", + "zip": "85304", + "status_line": "Operating · Marijuana Facility · 850-508-0261", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Glendale", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Glendale Dispensary", + "phone": "8505080261", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/glendale?utm_source=gmb&utm_medium=organic&utm_campaign=glendale", + "google_rating": "4.2", + "google_review_count": 599, + "menu_url": "https://dutchie.com/dispensary/curaleaf-glendale", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-glendale", + "created_at": "2025-11-17T14:29:34.351Z", + "updated_at": "2025-12-02T12:44:10.122Z", + "menu_provider": "dutchie", + "menu_provider_confidence": 100, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": null, + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "dutchie", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 190, + "azdhs_id": 120, + "name": "Zen Leaf Arcadia", + "company_name": "Patient Alternative Relief Center, LLC", + "address": "2710 E Indian School Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85016", + "status_line": "Operating · Marijuana Facility · 312-819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Arcadia", + "latitude": null, + "longitude": null, + "dba_name": "Zen Leaf Dispensary Arcadia", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/phoenix-arcadia/?utm_campaign=az-phoenix-arcadia&utm_medium=gbp&utm_source=google", + "google_rating": "4.7", + "google_review_count": 2099, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zen-leaf-arcadia", + "created_at": "2025-11-17T14:29:34.352Z", + "updated_at": "2025-11-17T21:46:36.133Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 191, + "azdhs_id": 121, + "name": "Trulieve of Tucson Grant Dispensary", + "company_name": "Patient Care Center 301, Inc.", + "address": "2734 E Grant Rd", + "city": "Tucson", + "state": "AZ", + "zip": "85716", + "status_line": "Operating · Marijuana Facility · 850-559-7734", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson%20Grant%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "8505597734", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-tucson-grant-dispensary", + "created_at": "2025-11-17T14:29:34.353Z", + "updated_at": "2025-11-17T14:29:34.353Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 192, + "azdhs_id": 122, + "name": "JARS Cannabis", + "company_name": "Payson Dreams LLC", + "address": "108 N Tonto St", + "city": "Payson", + "state": "AZ", + "zip": "85541", + "status_line": "Operating · Marijuana Dispensary · 248-755-7633", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Payson", + "phone": "2487557633", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 3259, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-payson-2", + "created_at": "2025-11-17T14:29:34.355Z", + "updated_at": "2025-11-17T21:46:36.031Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 193, + "azdhs_id": 123, + "name": "Zen Leaf Phoenix (Dunlap Ave.)", + "company_name": "Perpetual Healthcare, LLC", + "address": "4244 W Dunlap Rd Ste 1", + "city": "Phoenix", + "state": "AZ", + "zip": "85051", + "status_line": "Operating · Marijuana Facility · 312-819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Phoenix%20(Dunlap%20Ave.)", + "latitude": null, + "longitude": null, + "dba_name": "Zen Leaf Dispensary Phoenix (Dunlap Ave.)", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/phoenix-w-dunlap/?utm_source=google&utm_medium=gbp&utm_campaign=az-phoenix-dunlap", + "google_rating": "4.7", + "google_review_count": 3082, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zen-leaf-phoenix-dunlap-ave-", + "created_at": "2025-11-17T14:29:34.356Z", + "updated_at": "2025-11-17T21:46:36.032Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 194, + "azdhs_id": 124, + "name": "Phoenix Relief Center Inc", + "company_name": "Phoenix Relief Center Inc", + "address": "6330 S 35th Ave, Ste 104", + "city": "Phoenix", + "state": "AZ", + "zip": "85041", + "status_line": "Operating · Marijuana Facility · (602) 276-3401", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Phoenix%20Relief%20Center%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "PRC by Sunday Goods", + "phone": "6022763401", + "email": null, + "website": "https://sundaygoods.com/location/dispensary-laveen-phoenix-az/?utm_source=google&utm_medium=gbp&utm_campaign=prc_sundaygoods", + "google_rating": "4.6", + "google_review_count": 1691, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "phoenix-relief-center-inc", + "created_at": "2025-11-17T14:29:34.358Z", + "updated_at": "2025-11-17T21:46:36.135Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 195, + "azdhs_id": 125, + "name": "Phytotherapeutics Of Tucson", + "company_name": "Phytotherapeutics Of Tucson", + "address": "2175 N 83rd Ave", + "city": "Phoenix", + "state": "AZ", + "zip": "85035", + "status_line": "Operating · Marijuana Facility · 623-244-5349", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Phytotherapeutics%20Of%20Tucson", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Pavilions", + "phone": "6232445349", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-pavilions?utm_source=google&utm_medium=organic&utm_campaign=gmb-menu", + "google_rating": "4.5", + "google_review_count": 2239, + "menu_url": "https://dutchie.com/dispensary/curaleaf-83rd-ave", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "phytotherapeutics-of-tucson", + "created_at": "2025-11-17T14:29:34.359Z", + "updated_at": "2025-11-17T21:46:36.033Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 196, + "azdhs_id": 126, + "name": "Pinal County Wellness Center", + "company_name": "Pinal County Wellness Center", + "address": "8970 N 91st Ave", + "city": "Peoria", + "state": "AZ", + "zip": "85345", + "status_line": "Operating · Marijuana Facility · 623-233-1010", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Pinal%20County%20Wellness%20Center", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Peoria", + "phone": "6232331010", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-peoria?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.6", + "google_review_count": 1866, + "menu_url": "https://dutchie.com/dispensary/curaleaf-dispensary-peoria", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "pinal-county-wellness-center", + "created_at": "2025-11-17T14:29:34.361Z", + "updated_at": "2025-11-17T21:46:36.035Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 197, + "azdhs_id": 127, + "name": "JARS Cannabis", + "company_name": "Piper's Shop LLC", + "address": "1809 W Thatcher Blvd", + "city": "Safford", + "state": "AZ", + "zip": "85546", + "status_line": "Operating · Marijuana Establishment · 928-424-1313", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Safford", + "phone": "9284241313", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 725, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-safford", + "created_at": "2025-11-17T14:29:34.362Z", + "updated_at": "2025-11-17T21:46:36.037Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 198, + "azdhs_id": 128, + "name": "Pleasant Plants I LLC", + "company_name": "Pleasant Plants I LLC", + "address": "6676 West Bell Road", + "city": "Glendale", + "state": "AZ", + "zip": "85308", + "status_line": "Operating · Marijuana Establishment · (520) 727-7754", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Pleasant%20Plants%20I%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Story Cannabis Dispensary Bell Glendale", + "phone": "5207277754", + "email": null, + "website": "https://storycannabis.com/dispensary-locations/arizona/glendale-dispensary/?utm_source=google&utm_medium=Link&utm_campaign=bell_glandale&utm_id=googlelisting", + "google_rating": "4.4", + "google_review_count": 1137, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "pleasant-plants-i-llc", + "created_at": "2025-11-17T14:29:34.363Z", + "updated_at": "2025-11-17T21:46:36.136Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 199, + "azdhs_id": 129, + "name": "Ponderosa Dispensary", + "company_name": "Ponderosa Botanical Care Inc", + "address": "318 South Bracken Lane", + "city": "Chandler", + "state": "AZ", + "zip": "85224", + "status_line": "Operating · Marijuana Facility · (623) 877-3934", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Ponderosa Dispensary Chandler", + "phone": "6238773934", + "email": null, + "website": "https://www.pondyaz.com/locations", + "google_rating": "4.7", + "google_review_count": 839, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "ponderosa-dispensary-chandler", + "created_at": "2025-11-17T14:29:34.365Z", + "updated_at": "2025-11-17T21:46:36.039Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 200, + "azdhs_id": 130, + "name": "Pp Wellness Center", + "company_name": "Pp Wellness Center", + "address": "8160 W Union Hills Dr Ste A106", + "city": "Glendale", + "state": "AZ", + "zip": "85308", + "status_line": "Operating · Marijuana Facility · (623) 385-1310", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Pp%20Wellness%20Center", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Glendale", + "phone": "6233851310", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-glendale?utm_source=google&utm_medium=organic&utm_campaign=gmb-menu", + "google_rating": "4.6", + "google_review_count": 2166, + "menu_url": "https://dutchie.com/dispensary/curaleaf-glendale", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "pp-wellness-center", + "created_at": "2025-11-17T14:29:34.366Z", + "updated_at": "2025-11-17T21:46:36.040Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 201, + "azdhs_id": 131, + "name": "Trulieve of Tucson Menlo Park Dispensary", + "company_name": "Purplemed Inc", + "address": "1010 S Fwy Ste 130", + "city": "Tucson", + "state": "AZ", + "zip": "85745", + "status_line": "Operating · Marijuana Facility · 850-559-7734", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tucson%20Menlo%20Park%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Tucson Dispensary Menlo Park", + "phone": "8505597734", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/tucson-menlo-park?utm_source=gmb&utm_medium=organic&utm_campaign=tucson-menlo", + "google_rating": "4.2", + "google_review_count": 645, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-tucson-menlo-park-dispensary", + "created_at": "2025-11-17T14:29:34.367Z", + "updated_at": "2025-11-17T21:46:36.137Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 202, + "azdhs_id": 132, + "name": "The Prime Leaf", + "company_name": "Rainbow Collective Inc", + "address": "4220 E Speedway Blvd", + "city": "Tucson", + "state": "AZ", + "zip": "85712", + "status_line": "Operating · Marijuana Facility · (520) 207-2753", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Prime%20Leaf", + "latitude": null, + "longitude": null, + "dba_name": "The Prime Leaf", + "phone": "5202072753", + "email": null, + "website": "http://www.theprimeleaf.com/", + "google_rating": "4.5", + "google_review_count": 1305, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-prime-leaf", + "created_at": "2025-11-17T14:29:34.369Z", + "updated_at": "2025-11-17T21:46:36.042Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 203, + "azdhs_id": 133, + "name": "Noble Herb", + "company_name": "Rch Wellness Center", + "address": "522 E Route 66", + "city": "Flagstaff", + "state": "AZ", + "zip": "86001", + "status_line": "Operating · Marijuana Facility · (928) 351-7775", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Noble%20Herb", + "latitude": null, + "longitude": null, + "dba_name": "Noble Herb Flagstaff Dispensary", + "phone": "9283517775", + "email": null, + "website": "http://www.nobleherbaz.com/", + "google_rating": "4.4", + "google_review_count": 1774, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "noble-herb", + "created_at": "2025-11-17T14:29:34.370Z", + "updated_at": "2025-11-17T21:46:36.043Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 204, + "azdhs_id": 134, + "name": "Arizona Natural Concepts", + "company_name": "Rjk Ventures, Inc.", + "address": "1039 East Carefree Highway", + "city": "Phoenix", + "state": "AZ", + "zip": "85085", + "status_line": "Operating · Marijuana Facility · (602) 224-5999", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Arizona%20Natural%20Concepts", + "latitude": null, + "longitude": null, + "dba_name": "Arizona Natural Concepts Marijuana Dispensary", + "phone": "6022245999", + "email": null, + "website": "https://ancdispensary.com/", + "google_rating": "4.7", + "google_review_count": 717, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "arizona-natural-concepts", + "created_at": "2025-11-17T14:29:34.372Z", + "updated_at": "2025-11-17T21:46:36.045Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 205, + "azdhs_id": 135, + "name": "S Flower N Phoenix, Inc.", + "company_name": "S Flower N Phoenix, Inc.", + "address": "3217 E Shea Blvd, Suite 1 A", + "city": "Phoenix", + "state": "AZ", + "zip": "85028", + "status_line": "Operating · Marijuana Facility · (623) 582-0436", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20N%20Phoenix%2C%20Inc.", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary 32nd & Shea", + "phone": "6235820436", + "email": null, + "website": "https://www.livewithsol.com/deer-valley-dispensary/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.7", + "google_review_count": 570, + "menu_url": "https://dutchie.com/dispensary/sol-flower-dispensary-deer-valley", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "s-flower-n-phoenix-inc-", + "created_at": "2025-11-17T14:29:34.373Z", + "updated_at": "2025-11-17T21:46:36.139Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 206, + "azdhs_id": 136, + "name": "Sol Flower", + "company_name": "S Flower SE 1, Inc", + "address": "6026 North Oracle Road", + "city": "Tucson", + "state": "AZ", + "zip": "85704", + "status_line": "Operating · Marijuana Establishment · 602-828-7204", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6028287204", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": "https://dutchie.com/dispensary/sol-flower-dispensary-north-tucson", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sol-flower-tucson-1", + "created_at": "2025-11-17T14:29:34.375Z", + "updated_at": "2025-11-17T14:29:34.375Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 207, + "azdhs_id": 137, + "name": "Sol Flower", + "company_name": "S Flower SE 2, Inc.", + "address": "3000 West Valencia Road, Suite 210", + "city": "Tucson", + "state": "AZ", + "zip": "85746", + "status_line": "Operating · Marijuana Establishment · (602) 828-7204", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sol%20Flower", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary South Tucson", + "phone": "6028287204", + "email": null, + "website": "https://www.livewithsol.com/locations/south-tucson/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.7", + "google_review_count": 1914, + "menu_url": "https://dutchie.com/dispensary/sol-flower-dispensary-south-tucson", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sol-flower-tucson-2", + "created_at": "2025-11-17T14:29:34.376Z", + "updated_at": "2025-11-17T21:46:36.046Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 208, + "azdhs_id": 138, + "name": "S Flower SE 3 Inc.", + "company_name": "S Flower SE 3 Inc.", + "address": "4837 North 1st Avenue, Ste 102", + "city": "Tucson", + "state": "AZ", + "zip": "85718", + "status_line": "Operating · Marijuana Establishment · 602-828-7204", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20SE%203%20Inc.", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary North Tucson", + "phone": "6028287204", + "email": null, + "website": "https://www.livewithsol.com/locations/north-tucson/?utm_source=gmb&utm_medium=organic", + "google_rating": "4.9", + "google_review_count": 2080, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "s-flower-se-3-inc-", + "created_at": "2025-11-17T14:29:34.377Z", + "updated_at": "2025-11-17T21:46:36.048Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 209, + "azdhs_id": 139, + "name": "S Flower SE 4, Inc.", + "company_name": "S Flower SE 4, Inc.", + "address": "6437 North Oracle Road", + "city": "Tucson", + "state": "AZ", + "zip": "85704", + "status_line": "Operating · Marijuana Establishment · (480) 720-2943", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=S%20Flower%20SE%204%2C%20Inc.", + "latitude": null, + "longitude": null, + "dba_name": "Sol Flower Dispensary Casas Adobes", + "phone": "4807202943", + "email": null, + "website": "https://www.livewithsol.com/locations/casas-adobes/", + "google_rating": "4.8", + "google_review_count": 869, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "s-flower-se-4-inc-", + "created_at": "2025-11-17T14:29:34.379Z", + "updated_at": "2025-11-17T21:46:36.050Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 210, + "azdhs_id": 140, + "name": "Cannabist", + "company_name": "Salubrious Wellness Clinic Inc", + "address": "520 S Price Rd, Ste 1 & 2", + "city": "Tempe", + "state": "AZ", + "zip": "85281", + "status_line": "Operating · Marijuana Facility · (312) 819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Cannabist", + "latitude": null, + "longitude": null, + "dba_name": "Cannabist Tempe by Zen Leaf", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/tempe/", + "google_rating": "4.6", + "google_review_count": 1655, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "cannabist", + "created_at": "2025-11-17T14:29:34.380Z", + "updated_at": "2025-11-17T21:46:36.140Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 211, + "azdhs_id": 141, + "name": "Sea Of Green Llc", + "company_name": "Sea Of Green Llc", + "address": "6844 East Parkway Norte", + "city": "Mesa", + "state": "AZ", + "zip": "85212", + "status_line": "Operating · Marijuana Facility · (480) 325-5000", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sea%20Of%20Green%20Llc", + "latitude": null, + "longitude": null, + "dba_name": "truBLISS | Medical & Recreational Marijuana Dispensary", + "phone": "4803255000", + "email": null, + "website": "http://trubliss.com/", + "google_rating": "4.8", + "google_review_count": 4762, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sea-of-green-llc", + "created_at": "2025-11-17T14:29:34.382Z", + "updated_at": "2025-11-17T21:46:36.052Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 212, + "azdhs_id": 142, + "name": "Serenity Smoke, LLC", + "company_name": "Serenity Smoke, LLC", + "address": "19 West Main Street", + "city": "Springerville", + "state": "AZ", + "zip": "85938", + "status_line": "Not Operating · Marijuana Establishment", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Serenity%20Smoke%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "serenity-smoke-llc", + "created_at": "2025-11-17T14:29:34.383Z", + "updated_at": "2025-11-17T14:29:34.383Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 213, + "azdhs_id": 143, + "name": "Trulieve Maricopa Dispensary", + "company_name": "Sherri Dunn, Llc", + "address": "44405 W. Honeycutt Avenue", + "city": "maricopa", + "state": "AZ", + "zip": "85139", + "status_line": "Operating · Marijuana Facility · 954-817-2370", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20Maricopa%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Maricopa Dispensary", + "phone": "9548172370", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/maricopa?utm_source=gmb&utm_medium=organic&utm_campaign=maricopa", + "google_rating": "3.2", + "google_review_count": 203, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-maricopa-dispensary", + "created_at": "2025-11-17T14:29:34.384Z", + "updated_at": "2025-11-17T21:46:36.142Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 214, + "azdhs_id": 144, + "name": "Trulieve of Cottonwood Dispensary", + "company_name": "Sherri Dunn, Llc", + "address": "2400 E Sr 89a", + "city": "Cottonwood", + "state": "AZ", + "zip": "86326", + "status_line": "Operating · Marijuana Facility · 954-817-2370", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Cottonwood%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Cottonwood Dispensary", + "phone": "9548172370", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/cottonwood?utm_source=gmb&utm_medium=organic&utm_campaign=cottonwood", + "google_rating": "4.4", + "google_review_count": 1201, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-cottonwood-dispensary", + "created_at": "2025-11-17T14:29:34.386Z", + "updated_at": "2025-11-17T21:46:36.054Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 215, + "azdhs_id": 145, + "name": "Nature's Medicines", + "company_name": "Sixth Street Enterprises Inc", + "address": "6840 West Grand Ave", + "city": "Glendale", + "state": "AZ", + "zip": "85301", + "status_line": "Operating · Marijuana Facility · (623) 301-8478", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines", + "latitude": null, + "longitude": null, + "dba_name": "Story Cannabis Dispensary Grand Glendale", + "phone": "6233018478", + "email": null, + "website": "https://storycannabis.com/dispensary-locations/arizona/glendale-grand-ave-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=grand_glendale&utm_term=click&utm_content=website", + "google_rating": "4.5", + "google_review_count": 3046, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nature-s-medicines-glendale", + "created_at": "2025-11-17T14:29:34.387Z", + "updated_at": "2025-11-17T21:46:36.143Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 216, + "azdhs_id": 146, + "name": "Nature's Medicines", + "company_name": "Sixth Street Enterprises Inc", + "address": "2439 W Mcdowell Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85009", + "status_line": "Operating · Marijuana Facility · 480-420-3145", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nature's%20Medicines", + "latitude": null, + "longitude": null, + "dba_name": "Story Cannabis Dispensary McDowell", + "phone": "4804203145", + "email": null, + "website": "https://storycannabis.com/shop/arizona/phoenix-mcdowell-dispensary/rec-menu/?utm_source=google&utm_medium=listing&utm_campaign=mcdowell&utm_term=click&utm_content=website", + "google_rating": "4.6", + "google_review_count": 6641, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nature-s-medicines-phoenix-2", + "created_at": "2025-11-17T14:29:34.388Z", + "updated_at": "2025-11-17T21:46:36.056Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 217, + "azdhs_id": 147, + "name": "Sky Analytical Laboratories", + "company_name": "Sky Analytical Laboratories", + "address": "1122 East Washington Street", + "city": "Phoenix", + "state": "AZ", + "zip": "85034", + "status_line": "Operating · Marijuana Laboratory · (623) 262-4330", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sky%20Analytical%20Laboratories", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "6232624330", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sky-analytical-laboratories", + "created_at": "2025-11-17T14:29:34.390Z", + "updated_at": "2025-11-17T14:29:34.390Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 218, + "azdhs_id": 148, + "name": "Smithers CTS Arizona LLC", + "company_name": "Smithers CTS Arizona LLC", + "address": "734 W Highland Avenue, 2nd floor", + "city": "Phoenix", + "state": "AZ", + "zip": "85013", + "status_line": "Operating · Marijuana Laboratory · (954) 696-7791", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Smithers%20CTS%20Arizona%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Smithers Cannabis Testing Services Arizona", + "phone": "9546967791", + "email": null, + "website": "https://www.smithers.com/industries/cannabis-testing/contact-us/arizona-cannabis-testing-services", + "google_rating": "5.0", + "google_review_count": 3, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "smithers-cts-arizona-llc", + "created_at": "2025-11-17T14:29:34.391Z", + "updated_at": "2025-11-17T21:46:36.059Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 219, + "azdhs_id": 149, + "name": "The Clifton Bakery LLC", + "company_name": "Sonoran Flower LLC", + "address": "700 S. CORONADO BLVD", + "city": "CLIFTON", + "state": "AZ", + "zip": "85533", + "status_line": "Operating · Marijuana Establishment · 520-241-7777", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Clifton%20Bakery%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Clifton Bakery", + "phone": "5202417777", + "email": null, + "website": "http://thecliftonbakery.com/", + "google_rating": "4.8", + "google_review_count": 93, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-clifton-bakery-llc", + "created_at": "2025-11-17T14:29:34.393Z", + "updated_at": "2025-11-17T21:46:36.061Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 220, + "azdhs_id": 150, + "name": "Ponderosa Dispensary", + "company_name": "Soothing Ponderosa, LLC", + "address": "5550 E Mcdowell Rd, Ste 103", + "city": "Mesa", + "state": "AZ", + "zip": "85215", + "status_line": "Operating · Marijuana Facility · (480) 213-1402", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Ponderosa%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Ponderosa Dispensary Mesa", + "phone": "4802131402", + "email": null, + "website": "https://www.pondyaz.com/locations", + "google_rating": "4.7", + "google_review_count": 1940, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "ponderosa-dispensary-mesa-2", + "created_at": "2025-11-17T14:29:34.394Z", + "updated_at": "2025-11-17T21:46:36.145Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 221, + "azdhs_id": 151, + "name": "Nirvana Center Dispensaries", + "company_name": "SSW Ventures, LLC", + "address": "702 East Buckeye Road", + "city": "Phoenix", + "state": "AZ", + "zip": "85034", + "status_line": "Operating · Marijuana Facility · (602) 786-7988", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Nirvana%20Center%20Dispensaries", + "latitude": null, + "longitude": null, + "dba_name": "Nirvana Cannabis - 7th St (Downtown Phoenix)", + "phone": "6027867988", + "email": null, + "website": "https://nirvanacannabis.com/", + "google_rating": "4.9", + "google_review_count": 3212, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "nirvana-center-dispensaries-phoenix-2", + "created_at": "2025-11-17T14:29:34.395Z", + "updated_at": "2025-11-17T21:46:36.063Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 222, + "azdhs_id": 152, + "name": "Steep Hill Arizona Laboratory", + "company_name": "Steep Hill Arizona Laboratory", + "address": "14620 North Cave Creek Road, 3", + "city": "Phoenix", + "state": "AZ", + "zip": "85022", + "status_line": "Operating · Marijuana Laboratory · 602-920-7808", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Steep%20Hill%20Arizona%20Laboratory", + "latitude": null, + "longitude": null, + "dba_name": "Steep Hill Arizona Laboratory", + "phone": "6029207808", + "email": null, + "website": "http://www.steephillarizona.com/", + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "steep-hill-arizona-laboratory", + "created_at": "2025-11-17T14:29:34.397Z", + "updated_at": "2025-11-17T21:46:36.065Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 223, + "azdhs_id": 153, + "name": "Superior Organics", + "company_name": "Superior Organics", + "address": "211 S 57th Dr", + "city": "Phoenix", + "state": "AZ", + "zip": "85043", + "status_line": "Operating · Marijuana Facility · (602) 926-9100", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Superior%20Organics", + "latitude": null, + "longitude": null, + "dba_name": "The Superior Dispensary", + "phone": "6029269100", + "email": null, + "website": "https://thesuperiordispensary.com/", + "google_rating": "4.5", + "google_review_count": 959, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "superior-organics", + "created_at": "2025-11-17T14:29:34.398Z", + "updated_at": "2025-11-17T21:46:36.067Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 224, + "azdhs_id": 154, + "name": "Trulieve of Apache Junction Dispensary", + "company_name": "Svaccha, Llc", + "address": "1985 W Apache Trail Ste 4", + "city": "Apache Junction", + "state": "AZ", + "zip": "85120", + "status_line": "Operating · Marijuana Facility · 954-817-2370", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Apache%20Junction%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Apache Junction Dispensary", + "phone": "9548172370", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/apache-junction?utm_source=gmb&utm_medium=organic&utm_campaign=apache-junction", + "google_rating": "4.3", + "google_review_count": 408, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-apache-junction-dispensary", + "created_at": "2025-11-17T14:29:34.399Z", + "updated_at": "2025-11-17T21:46:36.147Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 225, + "azdhs_id": 155, + "name": "Trulieve of Tempe Dispensary", + "company_name": "Svaccha, Llc", + "address": "710 W Elliot Rd, Ste 102", + "city": "Tempe", + "state": "AZ", + "zip": "85284", + "status_line": "Operating · Marijuana Facility · 954-817-2370", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Tempe%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Tempe Dispensary", + "phone": "9548172370", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/tempe?utm_source=gmb&utm_medium=organic&utm_campaign=tempe", + "google_rating": "4.2", + "google_review_count": 790, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-tempe-dispensary", + "created_at": "2025-11-17T14:29:34.401Z", + "updated_at": "2025-11-17T21:46:36.069Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 226, + "azdhs_id": 156, + "name": "Swallowtail 3, LLC", + "company_name": "Swallowtail 3, LLC", + "address": "5210 South Priest Dr, A, Suite A", + "city": "Guadalupe", + "state": "AZ", + "zip": "85283", + "status_line": "Operating · Marijuana Establishment · 480-749-6468", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Swallowtail%203%2C%20LLC", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis - Tempe", + "phone": "4807496468", + "email": null, + "website": "https://mintdeals.com/tempe-az/?utm_source=google&utm_medium=gmb&utm_campaign=local_maps", + "google_rating": "4.7", + "google_review_count": 7231, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "swallowtail-3-llc", + "created_at": "2025-11-17T14:29:34.402Z", + "updated_at": "2025-11-17T21:46:36.072Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 227, + "azdhs_id": 157, + "name": "Trulieve of Mesa North Dispensary", + "company_name": "Sweet 5, Llc", + "address": "1150 W McLellan Rd", + "city": "Mesa", + "state": "AZ", + "zip": "85201", + "status_line": "Operating · Marijuana Facility · 770-330-0831", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Mesa%20North%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Mesa Dispensary North", + "phone": "7703300831", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/mesa-north?utm_source=gmb&utm_medium=organic&utm_campaign=north-mesa", + "google_rating": "4.3", + "google_review_count": 600, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-mesa-north-dispensary", + "created_at": "2025-11-17T14:29:34.404Z", + "updated_at": "2025-11-17T21:46:36.149Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 228, + "azdhs_id": 158, + "name": "Fire. Dispensary", + "company_name": "The Desert Valley Pharmacy Inc", + "address": "2825 W Thomas Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85017", + "status_line": "Operating · Marijuana Facility · 480-861-3649", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Fire.%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Nature's Wonder Phoenix Dispensary", + "phone": "4808613649", + "email": null, + "website": "https://natureswonderaz.com/phoenix-dispensary-menu-recreational", + "google_rating": "4.8", + "google_review_count": 2122, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "fire-dispensary", + "created_at": "2025-11-17T14:29:34.405Z", + "updated_at": "2025-11-17T21:46:36.074Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 229, + "azdhs_id": 159, + "name": "Trulieve of Mesa South Dispensary", + "company_name": "The Giving Tree Wellness Center Of Mesa Inc", + "address": "938 E Juanita Ave", + "city": "Mesa", + "state": "AZ", + "zip": "85204", + "status_line": "Operating · Marijuana Facility · 850-559-7734", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Trulieve%20of%20Mesa%20South%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Trulieve Phoenix Dispensary Alhambra", + "phone": "7703300831", + "email": null, + "website": "https://www.trulieve.com/dispensaries/arizona/phoenix-alhambra?utm_source=gmb&utm_medium=organic&utm_campaign=alhambra", + "google_rating": "4.5", + "google_review_count": 2917, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "trulieve-of-mesa-south-dispensary", + "created_at": "2025-11-17T14:29:34.406Z", + "updated_at": "2025-11-17T21:34:09.940Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 230, + "azdhs_id": 160, + "name": "Giving Tree Dispensary", + "company_name": "The Giving Tree Wellness Center Of North Phoenix Inc.", + "address": "701 West Union Hills Drive", + "city": "Phoenix", + "state": "AZ", + "zip": "85027", + "status_line": "Operating · Marijuana Facility · (623) 242-9080", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Giving%20Tree%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Giving Tree Dispensary", + "phone": "6232429080", + "email": null, + "website": "https://dutchie.com/stores/Nirvana-North-Phoenix", + "google_rating": "4.6", + "google_review_count": 1932, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "giving-tree-dispensary", + "created_at": "2025-11-17T14:29:34.408Z", + "updated_at": "2025-11-17T21:46:36.076Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 231, + "azdhs_id": 161, + "name": "The Green Halo Llc", + "company_name": "The Green Halo Llc", + "address": "3906 North Oracle Road", + "city": "Tucson", + "state": "AZ", + "zip": "85705", + "status_line": "Operating · Marijuana Facility · (520) 664-2251", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Green%20Halo%20Llc", + "latitude": null, + "longitude": null, + "dba_name": null, + "phone": "5206642251", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-green-halo-llc", + "created_at": "2025-11-17T14:29:34.409Z", + "updated_at": "2025-11-17T14:29:34.409Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 232, + "azdhs_id": 162, + "name": "Green Pharms", + "company_name": "The Healing Center Farmacy Llc", + "address": "7235 E Hampton Ave Unit 115", + "city": "Mesa", + "state": "AZ", + "zip": "85209", + "status_line": "Operating · Marijuana Facility · (480) 410-6704", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Green%20Pharms", + "latitude": null, + "longitude": null, + "dba_name": "GreenPharms Dispensary Mesa", + "phone": "4804106704", + "email": null, + "website": "http://greenpharms.com/", + "google_rating": "4.6", + "google_review_count": 3982, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "green-pharms-mesa", + "created_at": "2025-11-17T14:29:34.410Z", + "updated_at": "2025-11-17T21:46:36.078Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 233, + "azdhs_id": 163, + "name": "Health For Life Crismon", + "company_name": "The Healing Center Wellness Center Llc", + "address": "9949 E Apache Trail", + "city": "Mesa", + "state": "AZ", + "zip": "85207", + "status_line": "Operating · Marijuana Facility · (480) 400-1170", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Health%20For%20Life%20Crismon", + "latitude": null, + "longitude": null, + "dba_name": "Health for Life - Crismon - Medical and Recreational Cannabis Dispensary", + "phone": "4804001170", + "email": null, + "website": "https://healthforlifeaz.com/crismon/?utm_source=google&utm_medium=organic&utm_campaign=gbp-crimson", + "google_rating": "4.5", + "google_review_count": 1993, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "health-for-life-crismon", + "created_at": "2025-11-17T14:29:34.412Z", + "updated_at": "2025-11-17T21:46:36.150Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 234, + "azdhs_id": 164, + "name": "Sunday Goods", + "company_name": "The Health Center Of Cochise Inc", + "address": "1616 E. Glendale Ave.", + "city": "Phoenix", + "state": "AZ", + "zip": "85020", + "status_line": "Operating · Marijuana Facility · 520-808-3111", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Sunday%20Goods", + "latitude": null, + "longitude": null, + "dba_name": "Sunday Goods North Central Phoenix", + "phone": "5208083111", + "email": null, + "website": "https://sundaygoods.com/location/dispensary-phoenix-az/?utm_source=google&utm_medium=gbp&utm_campaign=phoenix_gbp", + "google_rating": "4.4", + "google_review_count": 652, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "sunday-goods-phoenix", + "created_at": "2025-11-17T14:29:34.413Z", + "updated_at": "2025-11-17T21:46:36.079Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 235, + "azdhs_id": 165, + "name": "The Kind Relief Inc", + "company_name": "The Kind Relief Inc", + "address": "18423 E San Tan Blvd Ste #1", + "city": "Queen Creek", + "state": "AZ", + "zip": "85142", + "status_line": "Operating · Marijuana Facility · 480-550-9121", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Kind%20Relief%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Queen Creek", + "phone": "4805509121", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-az-queen-creek?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.7", + "google_review_count": 4110, + "menu_url": "https://dutchie.com/dispensary/curaleaf-queen-creek", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-kind-relief-inc", + "created_at": "2025-11-17T14:29:34.415Z", + "updated_at": "2025-11-17T21:46:36.081Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 236, + "azdhs_id": 166, + "name": "Zen Leaf Mesa", + "company_name": "The Medicine Room Llc", + "address": "550 W Mckellips Rd Bldg 1", + "city": "Mesa", + "state": "AZ", + "zip": "85201", + "status_line": "Operating · Marijuana Facility · 312-819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Mesa", + "latitude": null, + "longitude": null, + "dba_name": "Zen Leaf Mesa", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/mesa/?utm_campaign=az-mesa&utm_medium=gbp&utm_source=google", + "google_rating": "4.8", + "google_review_count": 2602, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zen-leaf-mesa", + "created_at": "2025-11-17T14:29:34.416Z", + "updated_at": "2025-11-17T21:46:36.083Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 237, + "azdhs_id": 167, + "name": "Total Accountability Patient Care", + "company_name": "Total Accountability Patient Care", + "address": "1525 N. Park Ave", + "city": "Tucson", + "state": "AZ", + "zip": "85719", + "status_line": "Operating · Marijuana Facility · (520) 586-8710", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Total%20Accountability%20Patient%20Care", + "latitude": null, + "longitude": null, + "dba_name": "The Prime Leaf", + "phone": "5205868710", + "email": null, + "website": "https://theprimeleaf.com/", + "google_rating": "4.6", + "google_review_count": 669, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "total-accountability-patient-care", + "created_at": "2025-11-17T14:29:34.418Z", + "updated_at": "2025-11-17T21:46:36.151Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 238, + "azdhs_id": 168, + "name": "Total Accountability Systems I Inc", + "company_name": "Total Accountability Systems I Inc", + "address": "6287 E Copper Hill Dr. Ste A", + "city": "Prescott Valley", + "state": "AZ", + "zip": "86314", + "status_line": "Operating · Marijuana Facility · (928) 3505870", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Total%20Accountability%20Systems%20I%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Nirvana Cannabis - Prescott Valley", + "phone": "", + "email": null, + "website": "https://nirvanacannabis.com/", + "google_rating": "4.7", + "google_review_count": 4098, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "total-accountability-systems-i-inc", + "created_at": "2025-11-17T14:29:34.419Z", + "updated_at": "2025-11-17T21:46:36.096Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 239, + "azdhs_id": 169, + "name": "Oasis", + "company_name": "Total Health & Wellness Inc", + "address": "17006 S Weber Dr", + "city": "Chandler", + "state": "AZ", + "zip": "85226", + "status_line": "Operating · Marijuana Facility · (480) 626-7333", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Oasis", + "latitude": null, + "longitude": null, + "dba_name": "Story Cannabis Dispensary North Chandler", + "phone": "4806267333", + "email": null, + "website": "https://storycannabis.com/dispensary-locations/arizona/north-chandler-dispensary/?utm_source=google&utm_medium=listing&utm_campaign=north_chandler&utm_term=click&utm_content=website", + "google_rating": "4.3", + "google_review_count": 853, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "oasis", + "created_at": "2025-11-17T14:29:34.420Z", + "updated_at": "2025-11-17T21:46:36.152Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 240, + "azdhs_id": 170, + "name": "Total Health & Wellness Inc", + "company_name": "Total Health & Wellness Inc", + "address": "3830 North 7th Street", + "city": "Phoenix", + "state": "AZ", + "zip": "85014", + "status_line": "Operating · Marijuana Facility · (623) 295-1788", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Total%20Health%20%26%20Wellness%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Nirvana Cannabis - Tucson", + "phone": "9286422250", + "email": null, + "website": "https://nirvanacannabis.com/", + "google_rating": "4.7", + "google_review_count": 2156, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "total-health-wellness-inc", + "created_at": "2025-11-17T14:29:34.422Z", + "updated_at": "2025-11-17T21:34:09.946Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 241, + "azdhs_id": 171, + "name": "Uncle Harry Inc", + "company_name": "Uncle Harry Inc", + "address": "17036 North Cave Creek Rd", + "city": "Phoenix", + "state": "AZ", + "zip": "85032", + "status_line": "Operating · Marijuana Facility · (818) 822-9888", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Uncle%20Harry%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Mint Cannabis - Phoenix", + "phone": "8188229888", + "email": null, + "website": "https://mintdeals.com/phoenix-az/", + "google_rating": "4.7", + "google_review_count": 2658, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "uncle-harry-inc", + "created_at": "2025-11-17T14:29:34.423Z", + "updated_at": "2025-11-17T21:46:36.097Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 242, + "azdhs_id": 172, + "name": "The Good Dispensary", + "company_name": "Valley Healing Group Inc", + "address": "1842 W Broadway Rd", + "city": "Mesa", + "state": "AZ", + "zip": "85202", + "status_line": "Operating · Marijuana Facility · (480) 900-8042", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=The%20Good%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "The GOOD Dispensary", + "phone": "4809008042", + "email": null, + "website": "https://thegooddispensary.com/", + "google_rating": "4.8", + "google_review_count": 5884, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "the-good-dispensary", + "created_at": "2025-11-17T14:29:34.424Z", + "updated_at": "2025-11-17T21:46:36.098Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 243, + "azdhs_id": 173, + "name": "Valley Of The Sun Medical Dispensary, Inc.", + "company_name": "Valley Of The Sun Medical Dispensary, Inc.", + "address": "16200 W Eddie Albert Way", + "city": "Goodyear", + "state": "AZ", + "zip": "85338", + "status_line": "Operating · Marijuana Facility · (623) 932-3859", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Valley%20Of%20The%20Sun%20Medical%20Dispensary%2C%20Inc.", + "latitude": null, + "longitude": null, + "dba_name": "Valley of the Sun Dispensary", + "phone": "6239323859", + "email": null, + "website": "http://votsmd.com/?utm_source=local&utm_medium=organic&utm_campaign=gmb", + "google_rating": "4.0", + "google_review_count": 598, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "valley-of-the-sun-medical-dispensary-inc-", + "created_at": "2025-11-17T14:29:34.426Z", + "updated_at": "2025-11-17T21:46:36.154Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 244, + "azdhs_id": 174, + "name": "Zen Leaf Gilbert", + "company_name": "Vending Logistics Llc", + "address": "5409 S Power Rd", + "city": "Mesa", + "state": "AZ", + "zip": "85212", + "status_line": "Operating · Marijuana Facility · 312-819-5061", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zen%20Leaf%20Gilbert", + "latitude": null, + "longitude": null, + "dba_name": "Zen Leaf Dispensary Gilbert", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/gilbert/?utm_source=google&utm_medium=gbp&utm_campaign=az-gilbert", + "google_rating": "4.8", + "google_review_count": 2791, + "menu_url": "https://dutchie.com/dispensary/curaleaf-gilbert", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zen-leaf-gilbert", + "created_at": "2025-11-17T14:29:34.427Z", + "updated_at": "2025-12-02T12:40:11.108Z", + "menu_provider": "dutchie", + "menu_provider_confidence": 100, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": "dutchie", + "product_confidence": 100, + "product_crawler_mode": "production", + "last_product_scan_at": null, + "product_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "specials_provider": "dutchie", + "specials_confidence": 100, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "specials_html_pattern": true, + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "brand_provider": "dutchie", + "brand_confidence": 100, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "metadata_provider": "dutchie", + "metadata_confidence": 100, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": { + "dutchie_script_/menu": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/shop": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/deals": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/order": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/brands": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_mainPage": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/dispensary/[cName]\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/products": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}", + "dutchie_script_/specials": "inline:{\"props\":{\"pageProps\":{}},\"page\":\"/_error\",\"query\":{},\"buildId\":\"R2oGSL7bFrCaIR1aJCj85\",\"assetPrefix\":\"https://assets2.dutchie.com\",\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}" + }, + "provider_type": "dutchie", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 245, + "azdhs_id": 175, + "name": "Flor Verde Dispensary", + "company_name": "Verde Americano, LLC", + "address": "1115 Circulo Mercado", + "city": "Rio Rico", + "state": "AZ", + "zip": "85648", + "status_line": "Operating · Marijuana Dispensary · 602 689-3559", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Flor%20Verde%20Dispensary", + "latitude": null, + "longitude": null, + "dba_name": "Green Med Wellness", + "phone": "", + "email": null, + "website": null, + "google_rating": null, + "google_review_count": null, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "flor-verde-dispensary", + "created_at": "2025-11-17T14:29:34.428Z", + "updated_at": "2025-11-17T14:29:34.428Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 246, + "azdhs_id": 176, + "name": "Medusa Farms", + "company_name": "Verde Dispensary Inc", + "address": "3490 N Bank St", + "city": "Kingman", + "state": "AZ", + "zip": "86409", + "status_line": "Operating · Marijuana Facility · (928) 421-0020", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Medusa%20Farms", + "latitude": null, + "longitude": null, + "dba_name": "Zen Leaf Dispensary Chandler", + "phone": "3128195061", + "email": null, + "website": "https://zenleafdispensaries.com/locations/chandler/?utm_campaign=az-chandler&utm_medium=gbp&utm_source=google", + "google_rating": "4.8", + "google_review_count": 3044, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "medusa-farms", + "created_at": "2025-11-17T14:29:34.430Z", + "updated_at": "2025-11-17T21:34:09.948Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 247, + "azdhs_id": 177, + "name": "White Mountain Health Center Inc", + "company_name": "White Mountain Health Center Inc", + "address": "9420 W Bell Rd Ste 108", + "city": "Sun City", + "state": "AZ", + "zip": "85351", + "status_line": "Operating · Marijuana Facility · (623) 374-4141", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=White%20Mountain%20Health%20Center%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "White Mountain Health Center", + "phone": "6233744141", + "email": null, + "website": "https://whitemountainhealthcenter.com/", + "google_rating": "4.7", + "google_review_count": 1664, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "white-mountain-health-center-inc", + "created_at": "2025-11-17T14:29:34.431Z", + "updated_at": "2025-11-17T21:46:36.101Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 248, + "azdhs_id": 178, + "name": "Whoa Qc Inc", + "company_name": "Whoa Qc Inc", + "address": "5558 W. Bell Rd.", + "city": "Glendale", + "state": "AZ", + "zip": "85308", + "status_line": "Operating · Marijuana Facility · 602-535-0999", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Whoa%20Qc%20Inc", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Glendale East", + "phone": "6025350999", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-az-glendale-east?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.8", + "google_review_count": 3990, + "menu_url": "https://dutchie.com/dispensary/curaleaf-glendale-east", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "whoa-qc-inc", + "created_at": "2025-11-17T14:29:34.432Z", + "updated_at": "2025-11-17T21:46:36.102Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 249, + "azdhs_id": 179, + "name": "Wickenburg Alternative Medicine Llc", + "company_name": "Wickenburg Alternative Medicine Llc", + "address": "12620 N Cave Creek Road, Ste 1", + "city": "Phoenix", + "state": "AZ", + "zip": "85022", + "status_line": "Operating · Marijuana Facility · (623) 478-2233", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Wickenburg%20Alternative%20Medicine%20Llc", + "latitude": null, + "longitude": null, + "dba_name": "Sticky Saguaro", + "phone": "6026449188", + "email": null, + "website": "https://stickysaguaro.com/", + "google_rating": "4.6", + "google_review_count": 1832, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "wickenburg-alternative-medicine-llc", + "created_at": "2025-11-17T14:29:34.434Z", + "updated_at": "2025-11-17T21:34:09.949Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 250, + "azdhs_id": 180, + "name": "Woodstock 1", + "company_name": "Woodstock 1", + "address": "1629 N 195 Ave, 101", + "city": "Buckeye", + "state": "AZ", + "zip": "85396", + "status_line": "Operating · Marijuana Establishment · 602-980-1505", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Woodstock%201", + "latitude": null, + "longitude": null, + "dba_name": "Waddell's Longhorn", + "phone": "6029801505", + "email": null, + "website": "https://www.waddellslonghorn.com/", + "google_rating": "4.2", + "google_review_count": 1114, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "woodstock-1", + "created_at": "2025-11-17T14:29:34.435Z", + "updated_at": "2025-11-17T21:46:36.104Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 251, + "azdhs_id": 181, + "name": "JARS Cannabis", + "company_name": "Yuma County Dispensary LLC", + "address": "3345 E County 15th St", + "city": "Somerton", + "state": "AZ", + "zip": "85350", + "status_line": "Operating · Marijuana Establishment · 928-919-8667", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=JARS%20Cannabis", + "latitude": null, + "longitude": null, + "dba_name": "JARS Cannabis Yuma", + "phone": "9289198667", + "email": null, + "website": "https://jarscannabis.com/", + "google_rating": "4.9", + "google_review_count": 2154, + "menu_url": null, + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "jars-cannabis-somerton", + "created_at": "2025-11-17T14:29:34.436Z", + "updated_at": "2025-11-17T21:46:36.105Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + }, + { + "id": 252, + "azdhs_id": 182, + "name": "Zonacare", + "company_name": "Zonacare", + "address": "4415 East Monroe Street", + "city": "Phoenix", + "state": "AZ", + "zip": "85034", + "status_line": "Operating · Marijuana Facility · 602-396-5757", + "azdhs_url": "https://azcarecheck.azdhs.gov/s/?name=Zonacare", + "latitude": null, + "longitude": null, + "dba_name": "Curaleaf Dispensary Phoenix Airport", + "phone": "6023965757", + "email": null, + "website": "https://curaleaf.com/stores/curaleaf-dispensary-phoenix-airport?utm_source=google&utm_medium=gmb&utm_campaign=gmb-menu", + "google_rating": "4.4", + "google_review_count": 2705, + "menu_url": "https://dutchie.com/dispensary/curaleaf-phoenix", + "scraper_template": null, + "scraper_config": null, + "last_menu_scrape": null, + "menu_scrape_status": "pending", + "slug": "zonacare", + "created_at": "2025-11-17T14:29:34.438Z", + "updated_at": "2025-11-17T21:46:36.155Z", + "menu_provider": null, + "menu_provider_confidence": 0, + "crawler_mode": "production", + "crawler_status": "idle", + "last_menu_error_at": null, + "last_error_message": null, + "provider_detection_data": {}, + "product_provider": null, + "product_confidence": 0, + "product_crawler_mode": "sandbox", + "last_product_scan_at": null, + "product_detection_data": {}, + "specials_provider": null, + "specials_confidence": 0, + "specials_crawler_mode": "sandbox", + "last_specials_scan_at": null, + "specials_detection_data": {}, + "brand_provider": null, + "brand_confidence": 0, + "brand_crawler_mode": "sandbox", + "last_brand_scan_at": null, + "brand_detection_data": {}, + "metadata_provider": null, + "metadata_confidence": 0, + "metadata_crawler_mode": "sandbox", + "last_metadata_scan_at": null, + "metadata_detection_data": {}, + "provider_type": "unknown", + "scrape_enabled": false, + "last_crawl_at": null, + "next_crawl_at": null, + "crawl_status": "pending", + "crawl_error": null, + "consecutive_failures": 0, + "total_crawls": 0, + "successful_crawls": 0 + } +] \ No newline at end of file diff --git a/crawlsy-menus.zip b/crawlsy-menus.zip new file mode 100644 index 00000000..2f92c50d Binary files /dev/null and b/crawlsy-menus.zip differ diff --git a/frontend/.env.local b/frontend/.env.local new file mode 100644 index 00000000..9df65e28 --- /dev/null +++ b/frontend/.env.local @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:3010 diff --git a/frontend/dist/assets/index-DKdvsMWP.css b/frontend/dist/assets/index-DKdvsMWP.css new file mode 100644 index 00000000..191c0ce7 --- /dev/null +++ b/frontend/dist/assets/index-DKdvsMWP.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root,[data-theme]{background-color:var(--fallback-b1,oklch(var(--b1)/1));color:var(--fallback-bc,oklch(var(--bc)/1))}@supports not (color: oklch(0% 0 0)){:root{color-scheme:light;--fallback-p: #491eff;--fallback-pc: #d4dbff;--fallback-s: #ff41c7;--fallback-sc: #fff9fc;--fallback-a: #00cfbd;--fallback-ac: #00100d;--fallback-n: #2b3440;--fallback-nc: #d7dde4;--fallback-b1: #ffffff;--fallback-b2: #e5e6e6;--fallback-b3: #e5e6e6;--fallback-bc: #1f2937;--fallback-in: #00b3f0;--fallback-inc: #000000;--fallback-su: #00ca92;--fallback-suc: #000000;--fallback-wa: #ffc22d;--fallback-wac: #000000;--fallback-er: #ff6f70;--fallback-erc: #000000}@media (prefers-color-scheme: dark){:root{color-scheme:dark;--fallback-p: #7582ff;--fallback-pc: #050617;--fallback-s: #ff71cf;--fallback-sc: #190211;--fallback-a: #00c7b5;--fallback-ac: #000e0c;--fallback-n: #2a323c;--fallback-nc: #a6adbb;--fallback-b1: #1d232a;--fallback-b2: #191e24;--fallback-b3: #15191e;--fallback-bc: #a6adbb;--fallback-in: #00b3f0;--fallback-inc: #000000;--fallback-su: #00ca92;--fallback-suc: #000000;--fallback-wa: #ffc22d;--fallback-wac: #000000;--fallback-er: #ff6f70;--fallback-erc: #000000}}}html{-webkit-tap-highlight-color:transparent}*{scrollbar-color:color-mix(in oklch,currentColor 35%,transparent) transparent}*:hover{scrollbar-color:color-mix(in oklch,currentColor 60%,transparent) transparent}:root{color-scheme:light;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 89.824% .06192 275.75;--ac: 15.352% .0368 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 49.12% .3096 275.75;--s: 69.71% .329 342.55;--sc: 98.71% .0106 342.55;--a: 76.76% .184 183.61;--n: 32.1785% .02476 255.701624;--nc: 89.4994% .011585 252.096176;--b1: 100% 0 0;--b2: 96.1151% 0 0;--b3: 92.4169% .00108 197.137559;--bc: 27.8078% .029596 256.847952}@media (prefers-color-scheme: dark){:root{color-scheme:dark;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 13.138% .0392 275.75;--sc: 14.96% .052 342.55;--ac: 14.902% .0334 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 65.69% .196 275.75;--s: 74.8% .26 342.55;--a: 74.51% .167 183.61;--n: 31.3815% .021108 254.139175;--nc: 74.6477% .0216 264.435964;--b1: 25.3267% .015896 252.417568;--b2: 23.2607% .013807 253.100675;--b3: 21.1484% .01165 254.087939;--bc: 74.6477% .0216 264.435964}}[data-theme=light]{color-scheme:light;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 89.824% .06192 275.75;--ac: 15.352% .0368 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 49.12% .3096 275.75;--s: 69.71% .329 342.55;--sc: 98.71% .0106 342.55;--a: 76.76% .184 183.61;--n: 32.1785% .02476 255.701624;--nc: 89.4994% .011585 252.096176;--b1: 100% 0 0;--b2: 96.1151% 0 0;--b3: 92.4169% .00108 197.137559;--bc: 27.8078% .029596 256.847952}[data-theme=dark]{color-scheme:dark;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 13.138% .0392 275.75;--sc: 14.96% .052 342.55;--ac: 14.902% .0334 183.61;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-btn: .5rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--tab-border: 1px;--tab-radius: .5rem;--p: 65.69% .196 275.75;--s: 74.8% .26 342.55;--a: 74.51% .167 183.61;--n: 31.3815% .021108 254.139175;--nc: 74.6477% .0216 264.435964;--b1: 25.3267% .015896 252.417568;--b2: 23.2607% .013807 253.100675;--b3: 21.1484% .01165 254.087939;--bc: 74.6477% .0216 264.435964}[data-theme=cupcake]{color-scheme:light;--in: 72.06% .191 231.6;--su: 64.8% .15 160;--wa: 84.71% .199 83.87;--er: 71.76% .221 22.18;--pc: 15.2344% .017892 200.026556;--sc: 15.787% .020249 356.29965;--ac: 15.8762% .029206 78.618794;--nc: 84.7148% .013247 313.189598;--inc: 0% 0 0;--suc: 0% 0 0;--wac: 0% 0 0;--erc: 0% 0 0;--rounded-box: 1rem;--rounded-badge: 1.9rem;--animation-btn: .25s;--animation-input: .2s;--btn-focus-scale: .95;--border-btn: 1px;--p: 76.172% .089459 200.026556;--s: 78.9351% .101246 356.29965;--a: 79.3811% .146032 78.618794;--n: 23.5742% .066235 313.189598;--b1: 97.7882% .00418 56.375637;--b2: 93.9822% .007638 61.449292;--b3: 91.5861% .006811 53.440502;--bc: 23.5742% .066235 313.189598;--rounded-btn: 1.9rem;--tab-border: 2px;--tab-radius: .7rem}.alert{display:grid;width:100%;grid-auto-flow:row;align-content:flex-start;align-items:center;justify-items:center;gap:1rem;text-align:center;border-radius:var(--rounded-box, 1rem);border-width:1px;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));padding:1rem;--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--alert-bg: var(--fallback-b2,oklch(var(--b2)/1));--alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));background-color:var(--alert-bg)}@media (min-width: 640px){.alert{grid-auto-flow:column;grid-template-columns:auto minmax(auto,1fr);justify-items:start;text-align:start}}.avatar.placeholder>div{display:flex;align-items:center;justify-content:center}.badge{display:inline-flex;align-items:center;justify-content:center;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);transition-duration:.2s;height:1.25rem;font-size:.875rem;line-height:1.25rem;width:-moz-fit-content;width:fit-content;padding-left:.563rem;padding-right:.563rem;border-radius:var(--rounded-badge, 1.9rem);border-width:1px;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}@media (hover:hover){.label a:hover{--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.menu li>*:not(ul,.menu-title,details,.btn):active,.menu li>*:not(ul,.menu-title,details,.btn).active,.menu li>details>summary:active{--tw-bg-opacity: 1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.table tr.hover:hover,.table tr.hover:nth-child(2n):hover{--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.hover:hover,.table-zebra tr.hover:nth-child(2n):hover{--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}}.btn{display:inline-flex;height:3rem;min-height:3rem;flex-shrink:0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-wrap:wrap;align-items:center;justify-content:center;border-radius:var(--rounded-btn, .5rem);border-color:transparent;border-color:oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity));padding-left:1rem;padding-right:1rem;text-align:center;font-size:.875rem;line-height:1em;gap:.5rem;font-weight:600;text-decoration-line:none;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);border-width:var(--border-btn, 1px);transition-property:color,background-color,border-color,opacity,box-shadow,transform;--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);outline-color:var(--fallback-bc,oklch(var(--bc)/1));background-color:oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity));--tw-bg-opacity: 1;--tw-border-opacity: 1}.btn-disabled,.btn[disabled],.btn:disabled{pointer-events:none}:where(.btn:is(input[type=checkbox])),:where(.btn:is(input[type=radio])){width:auto;-webkit-appearance:none;-moz-appearance:none;appearance:none}.btn:is(input[type=checkbox]):after,.btn:is(input[type=radio]):after{--tw-content: attr(aria-label);content:var(--tw-content)}.card{position:relative;display:flex;flex-direction:column;border-radius:var(--rounded-box, 1rem)}.card:focus{outline:2px solid transparent;outline-offset:2px}.card-body{display:flex;flex:1 1 auto;flex-direction:column;padding:var(--padding-card, 2rem);gap:.5rem}.card-body :where(p){flex-grow:1}.card-actions{display:flex;flex-wrap:wrap;align-items:flex-start;gap:.5rem}.card figure{display:flex;align-items:center;justify-content:center}.card.image-full{display:grid}.card.image-full:before{position:relative;content:"";z-index:10;border-radius:var(--rounded-box, 1rem);--tw-bg-opacity: 1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));opacity:.75}.card.image-full:before,.card.image-full>*{grid-column-start:1;grid-row-start:1}.card.image-full>figure img{height:100%;-o-object-fit:cover;object-fit:cover}.card.image-full>.card-body{position:relative;z-index:20;--tw-text-opacity: 1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.carousel{display:inline-flex;overflow-x:scroll;scroll-snap-type:x mandatory;scroll-behavior:smooth;-ms-overflow-style:none;scrollbar-width:none}.checkbox{flex-shrink:0;--chkbg: var(--fallback-bc,oklch(var(--bc)/1));--chkfg: var(--fallback-b1,oklch(var(--b1)/1));height:1.5rem;width:1.5rem;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-border-opacity: .2}.collapse:not(td):not(tr):not(colgroup){visibility:visible}.collapse{position:relative;display:grid;overflow:hidden;grid-template-rows:max-content 0fr;transition:grid-template-rows .2s;width:100%;border-radius:var(--rounded-box, 1rem)}.collapse-title,.collapse>input[type=checkbox],.collapse>input[type=radio],.collapse-content{grid-column-start:1;grid-row-start:1}.collapse>input[type=checkbox],.collapse>input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;opacity:0}:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){height:100%;width:100%;z-index:1}.collapse[open],.collapse-open,.collapse:focus:not(.collapse-close){grid-template-rows:max-content 1fr}.collapse:not(.collapse-close):has(>input[type=checkbox]:checked),.collapse:not(.collapse-close):has(>input[type=radio]:checked){grid-template-rows:max-content 1fr}.collapse[open]>.collapse-content,.collapse-open>.collapse-content,.collapse:focus:not(.collapse-close)>.collapse-content,.collapse:not(.collapse-close)>input[type=checkbox]:checked~.collapse-content,.collapse:not(.collapse-close)>input[type=radio]:checked~.collapse-content{visibility:visible;min-height:-moz-fit-content;min-height:fit-content}.dropdown{position:relative;display:inline-block}.dropdown>*:not(summary):focus{outline:2px solid transparent;outline-offset:2px}.dropdown .dropdown-content{position:absolute}.dropdown:is(:not(details)) .dropdown-content{visibility:hidden;opacity:0;transform-origin:top;--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);transition-duration:.2s}.dropdown.dropdown-open .dropdown-content,.dropdown:not(.dropdown-hover):focus .dropdown-content,.dropdown:focus-within .dropdown-content{visibility:visible;opacity:1}@media (hover: hover){.dropdown.dropdown-hover:hover .dropdown-content{visibility:visible;opacity:1}.btm-nav>*.disabled:hover,.btm-nav>*[disabled]:hover{pointer-events:none;--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}.btn:hover{--tw-border-opacity: 1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn:hover{background-color:color-mix(in oklab,oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%,black);border-color:color-mix(in oklab,oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%,black)}}@supports not (color: oklch(0% 0 0)){.btn:hover{background-color:var(--btn-color, var(--fallback-b2));border-color:var(--btn-color, var(--fallback-b2))}}.btn.glass:hover{--glass-opacity: 25%;--glass-border-opacity: 15%}.btn-ghost:hover{border-color:transparent}@supports (color: oklch(0% 0 0)){.btn-ghost:hover{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}}.btn-outline:hover{--tw-border-opacity: 1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary:hover{--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-primary:hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black)}}.btn-outline.btn-secondary:hover{--tw-text-opacity: 1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-secondary:hover{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black)}}.btn-outline.btn-accent:hover{--tw-text-opacity: 1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-accent:hover{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black)}}.btn-outline.btn-success:hover{--tw-text-opacity: 1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-success:hover{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black)}}.btn-outline.btn-info:hover{--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-info:hover{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black)}}.btn-outline.btn-warning:hover{--tw-text-opacity: 1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-warning:hover{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black)}}.btn-outline.btn-error:hover{--tw-text-opacity: 1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-error:hover{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black)}}.btn-disabled:hover,.btn[disabled]:hover,.btn:disabled:hover{--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}@supports (color: color-mix(in oklab,black,black)){.btn:is(input[type=checkbox]:checked):hover,.btn:is(input[type=radio]:checked):hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black)}}.dropdown.dropdown-hover:hover .dropdown-content{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{cursor:pointer;outline:2px solid transparent;outline-offset:2px}@supports (color: oklch(0% 0 0)){:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{background-color:var(--fallback-bc,oklch(var(--bc)/.1))}}}.dropdown:is(details) summary::-webkit-details-marker{display:none}.label{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;align-items:center;justify-content:space-between;padding:.5rem .25rem}.input{flex-shrink:1;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:3rem;padding-left:1rem;padding-right:1rem;font-size:1rem;line-height:2;line-height:1.5rem;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.input[type=number]::-webkit-inner-spin-button,.input-md[type=number]::-webkit-inner-spin-button{margin-top:-1rem;margin-bottom:-1rem;margin-inline-end:-1rem}.input-sm[type=number]::-webkit-inner-spin-button{margin-top:0;margin-bottom:0;margin-inline-end:-0px}.join{display:inline-flex;align-items:stretch;border-radius:var(--rounded-btn, .5rem)}.join :where(.join-item){border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:0;border-start-start-radius:0}.join .join-item:not(:first-child):not(:last-child),.join *:not(:first-child):not(:last-child) .join-item{border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:0;border-start-start-radius:0}.join .join-item:first-child:not(:last-child),.join *:first-child:not(:last-child) .join-item{border-start-end-radius:0;border-end-end-radius:0}.join .dropdown .join-item:first-child:not(:last-child),.join *:first-child:not(:last-child) .dropdown .join-item{border-start-end-radius:inherit;border-end-end-radius:inherit}.join :where(.join-item:first-child:not(:last-child)),.join :where(*:first-child:not(:last-child) .join-item){border-end-start-radius:inherit;border-start-start-radius:inherit}.join .join-item:last-child:not(:first-child),.join *:last-child:not(:first-child) .join-item{border-end-start-radius:0;border-start-start-radius:0}.join :where(.join-item:last-child:not(:first-child)),.join :where(*:last-child:not(:first-child) .join-item){border-start-end-radius:inherit;border-end-end-radius:inherit}@supports not selector(:has(*)){:where(.join *){border-radius:inherit}}@supports selector(:has(*)){:where(.join *:has(.join-item)){border-radius:inherit}}.link{cursor:pointer;text-decoration-line:underline}.menu{display:flex;flex-direction:column;flex-wrap:wrap;font-size:.875rem;line-height:1.25rem;padding:.5rem}.menu :where(li ul){position:relative;white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem}.menu :where(li:not(.menu-title)>*:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){display:grid;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;grid-auto-columns:minmax(auto,max-content) auto max-content;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu li.disabled{cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;user-select:none;color:var(--fallback-bc,oklch(var(--bc)/.3))}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}:where(.menu li){position:relative;display:flex;flex-shrink:0;flex-direction:column;flex-wrap:wrap;align-items:stretch}:where(.menu li) .badge{justify-self:end}.progress{position:relative;width:100%;-webkit-appearance:none;-moz-appearance:none;appearance:none;overflow:hidden;height:.5rem;border-radius:var(--rounded-box, 1rem);background-color:var(--fallback-bc,oklch(var(--bc)/.2))}.range{height:1.5rem;width:100%;cursor:pointer;-moz-appearance:none;appearance:none;-webkit-appearance:none;--range-shdw: var(--fallback-bc,oklch(var(--bc)/1));overflow:hidden;border-radius:var(--rounded-box, 1rem);background-color:transparent}.range:focus{outline:none}.select{display:inline-flex;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:3rem;min-height:3rem;padding-inline-start:1rem;padding-inline-end:2.5rem;font-size:.875rem;line-height:1.25rem;line-height:2;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-size:4px 4px,4px 4px;background-repeat:no-repeat}.select[multiple]{height:auto}.stats{display:inline-grid;border-radius:var(--rounded-box, 1rem);--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}:where(.stats){grid-auto-flow:column;overflow-x:auto}.steps .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-columns:auto;grid-template-rows:repeat(2,minmax(0,1fr));grid-template-rows:40px 1fr;place-items:center;text-align:center;min-width:4rem}.table{position:relative;width:100%;border-radius:var(--rounded-box, 1rem);text-align:left;font-size:.875rem;line-height:1.25rem}.table :where(.table-pin-rows thead tr){position:sticky;top:0;z-index:1;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table :where(.table-pin-rows tfoot tr){position:sticky;bottom:0;z-index:1;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table :where(.table-pin-cols tr th){position:sticky;left:0;right:0;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table-zebra tbody tr:nth-child(2n) :where(.table-pin-cols tr th){--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.textarea{min-height:3rem;flex-shrink:1;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;line-height:2;border-radius:var(--rounded-btn, .5rem);border-width:1px;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.toggle{flex-shrink:0;--tglbg: var(--fallback-b1,oklch(var(--b1)/1));--handleoffset: 1.5rem;--handleoffsetcalculator: calc(var(--handleoffset) * -1);--togglehandleborder: 0 0;height:1.5rem;width:3rem;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--rounded-badge, 1.9rem);border-width:1px;border-color:currentColor;background-color:currentColor;color:var(--fallback-bc,oklch(var(--bc)/.5));transition:background,box-shadow var(--animation-input, .2s) ease-out;box-shadow:var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset,var(--togglehandleborder)}.alert-info{border-color:var(--fallback-in,oklch(var(--in)/.2));--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));--alert-bg: var(--fallback-in,oklch(var(--in)/1));--alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1))}.badge-info{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.badge-success{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.badge-warning{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.badge-error{border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.badge-ghost{--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.badge-outline.badge-info{--tw-text-opacity: 1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.badge-outline.badge-success{--tw-text-opacity: 1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.badge-outline.badge-warning{--tw-text-opacity: 1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.badge-outline.badge-error{--tw-text-opacity: 1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btm-nav>*:where(.active){border-top-width:2px;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.btm-nav>*.disabled,.btm-nav>*[disabled]{pointer-events:none;--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}.btm-nav>* .label{font-size:1rem;line-height:1.5rem}@media (prefers-reduced-motion: no-preference){.btn{animation:button-pop var(--animation-btn, .25s) ease-out}}.btn:active:hover,.btn:active:focus{animation:button-pop 0s ease-out;transform:scale(var(--btn-focus-scale, .97))}@supports not (color: oklch(0% 0 0)){.btn{background-color:var(--btn-color, var(--fallback-b2));border-color:var(--btn-color, var(--fallback-b2))}.btn-primary{--btn-color: var(--fallback-p)}.btn-secondary{--btn-color: var(--fallback-s)}}@supports (color: color-mix(in oklab,black,black)){.btn-outline.btn-primary.btn-active{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,black)}.btn-outline.btn-secondary.btn-active{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,black)}.btn-outline.btn-accent.btn-active{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,black)}.btn-outline.btn-success.btn-active{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,black)}.btn-outline.btn-info.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,black)}.btn-outline.btn-warning.btn-active{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,black)}.btn-outline.btn-error.btn-active{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,black)}}.btn:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px}.btn-primary{--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@supports (color: oklch(0% 0 0)){.btn-primary{--btn-color: var(--p)}.btn-secondary{--btn-color: var(--s)}}.btn-secondary{--tw-text-opacity: 1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));outline-color:var(--fallback-s,oklch(var(--s)/1))}.btn.glass{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn.glass.btn-active{--glass-opacity: 25%;--glass-border-opacity: 15%}.btn-ghost{border-width:1px;border-color:transparent;background-color:transparent;color:currentColor;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn-ghost.btn-active{border-color:transparent;background-color:var(--fallback-bc,oklch(var(--bc)/.2))}.btn-outline{border-color:currentColor;background-color:transparent;--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.btn-outline.btn-active{--tw-border-opacity: 1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary{--tw-text-opacity: 1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.btn-outline.btn-primary.btn-active{--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn-outline.btn-secondary{--tw-text-opacity: 1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.btn-outline.btn-secondary.btn-active{--tw-text-opacity: 1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.btn-outline.btn-accent{--tw-text-opacity: 1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.btn-outline.btn-accent.btn-active{--tw-text-opacity: 1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.btn-outline.btn-success{--tw-text-opacity: 1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.btn-outline.btn-success.btn-active{--tw-text-opacity: 1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.btn-outline.btn-info{--tw-text-opacity: 1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.btn-outline.btn-info.btn-active{--tw-text-opacity: 1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.btn-outline.btn-warning{--tw-text-opacity: 1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.btn-outline.btn-warning.btn-active{--tw-text-opacity: 1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.btn-outline.btn-error{--tw-text-opacity: 1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btn-outline.btn-error.btn-active{--tw-text-opacity: 1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.btn.btn-disabled,.btn[disabled],.btn:disabled{--tw-border-opacity: 0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity: .2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity: .2}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity: 1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked):focus-visible,.btn:is(input[type=radio]:checked):focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}@keyframes button-pop{0%{transform:scale(var(--btn-focus-scale, .98))}40%{transform:scale(1.02)}to{transform:scale(1)}}.card :where(figure:first-child){overflow:hidden;border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-start-radius:unset;border-end-end-radius:unset}.card :where(figure:last-child){overflow:hidden;border-start-start-radius:unset;border-start-end-radius:unset;border-end-start-radius:inherit;border-end-end-radius:inherit}.card:focus-visible{outline:2px solid currentColor;outline-offset:2px}.card.bordered{border-width:1px;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.card.compact .card-body{padding:1rem;font-size:.875rem;line-height:1.25rem}.card-title{display:flex;align-items:center;gap:.5rem;font-size:1.25rem;line-height:1.75rem;font-weight:600}.card.image-full :where(figure){overflow:hidden;border-radius:inherit}.carousel::-webkit-scrollbar{display:none}.checkbox:focus{box-shadow:none}.checkbox:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/1))}.checkbox:disabled{border-width:0px;cursor:not-allowed;border-color:transparent;--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.2}.checkbox:checked,.checkbox[aria-checked=true]{background-repeat:no-repeat;animation:checkmark var(--animation-input, .2s) ease-out;background-color:var(--chkbg);background-image:linear-gradient(-45deg,transparent 65%,var(--chkbg) 65.99%),linear-gradient(45deg,transparent 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,transparent 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,transparent 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%)}.checkbox:indeterminate{--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-repeat:no-repeat;animation:checkmark var(--animation-input, .2s) ease-out;background-image:linear-gradient(90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(-90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%)}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}details.collapse{width:100%}details.collapse summary{position:relative;display:block;outline:2px solid transparent;outline-offset:2px}details.collapse summary::-webkit-details-marker{display:none}.collapse:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/1))}.collapse:has(.collapse-title:focus-visible),.collapse:has(>input[type=checkbox]:focus-visible),.collapse:has(>input[type=radio]:focus-visible){outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/1))}.collapse:not(.collapse-open):not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-open):not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-open):not(.collapse-close)>.collapse-title{cursor:pointer}.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open])>.collapse-title{cursor:unset}.collapse-title,:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){padding:1rem;padding-inline-end:3rem;min-height:3.75rem;transition:background-color .2s ease-out}.collapse[open]>:where(.collapse-content),.collapse-open>:where(.collapse-content),.collapse:focus:not(.collapse-close)>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input[type=checkbox]:checked~.collapse-content),.collapse:not(.collapse-close)>:where(input[type=radio]:checked~.collapse-content){padding-bottom:1rem;transition:padding .2s ease-out,background-color .2s ease-out}.collapse[open].collapse-arrow>.collapse-title:after,.collapse-open.collapse-arrow>.collapse-title:after,.collapse-arrow:focus:not(.collapse-close)>.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after{--tw-translate-y: -50%;--tw-rotate: 225deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.collapse[open].collapse-plus>.collapse-title:after,.collapse-open.collapse-plus>.collapse-title:after,.collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after{content:"−"}.dropdown.dropdown-open .dropdown-content,.dropdown:focus .dropdown-content,.dropdown:focus-within .dropdown-content{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.input input{--tw-bg-opacity: 1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:transparent}.input input:focus{outline:2px solid transparent;outline-offset:2px}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{box-shadow:none;border-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:has(>input[disabled]),.input-disabled,.input:disabled,.input[disabled]{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.input:has(>input[disabled])::-moz-placeholder,.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.input:has(>input[disabled])::placeholder,.input-disabled::placeholder,.input:disabled::placeholder,.input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.join>:where(*:not(:first-child)){margin-top:0;margin-bottom:0;margin-inline-start:-1px}.join>:where(*:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn) * -1)}.link-primary{--tw-text-opacity: 1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){@media (hover:hover){.link-primary:hover{color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,black)}}}.link:focus{outline:2px solid transparent;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}.loading{pointer-events:none;display:inline-block;aspect-ratio:1 / 1;width:1.5rem;background-color:currentColor;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")}.loading-spinner{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")}.loading-lg{width:2.5rem}:where(.menu li:empty){--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.1;margin:.5rem 1rem;height:1px}.menu :where(li ul):before{position:absolute;bottom:.75rem;inset-inline-start:0px;top:.75rem;width:1px;--tw-bg-opacity: 1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.1;content:""}.menu :where(li:not(.menu-title)>*:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn, .5rem);padding:.5rem 1rem;text-align:start;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);transition-duration:.2s;text-wrap:balance}:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>*:not(ul,details,.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):is(summary):not(.active,.btn):focus-visible{cursor:pointer;background-color:var(--fallback-bc,oklch(var(--bc)/.1));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));outline:2px solid transparent;outline-offset:2px}.menu li>*:not(ul,.menu-title,details,.btn):active,.menu li>*:not(ul,.menu-title,details,.btn).active,.menu li>details>summary:active{--tw-bg-opacity: 1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.menu :where(li>details>summary)::-webkit-details-marker{display:none}.menu :where(li>details>summary):after,.menu :where(li>.menu-dropdown-toggle):after{justify-self:end;display:block;margin-top:-.5rem;height:.5rem;width:.5rem;transform:rotate(45deg);transition-property:transform,margin-top;transition-duration:.3s;transition-timing-function:cubic-bezier(.4,0,.2,1);content:"";transform-origin:75% 75%;box-shadow:2px 2px;pointer-events:none}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{transform:rotate(225deg);margin-top:0}.mockup-phone .display{overflow:hidden;border-radius:40px;margin-top:-25px}.mockup-browser .mockup-browser-toolbar .input{position:relative;margin-left:auto;margin-right:auto;display:block;height:1.75rem;width:24rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));padding-left:2rem;direction:ltr}.mockup-browser .mockup-browser-toolbar .input:before{content:"";position:absolute;left:.5rem;top:50%;aspect-ratio:1 / 1;height:.75rem;--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:9999px;border-width:2px;border-color:currentColor;opacity:.6}.mockup-browser .mockup-browser-toolbar .input:after{content:"";position:absolute;left:1.25rem;top:50%;height:.5rem;--tw-translate-y: 25%;--tw-rotate: -45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:9999px;border-width:1px;border-color:currentColor;opacity:.6}@keyframes modal-pop{0%{opacity:0}}.progress::-moz-progress-bar{border-radius:var(--rounded-box, 1rem);background-color:currentColor}.progress:indeterminate{--progress-color: var(--fallback-bc,oklch(var(--bc)/1));background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-size:200%;background-position-x:15%;animation:progress-loading 5s ease-in-out infinite}.progress::-webkit-progress-bar{border-radius:var(--rounded-box, 1rem);background-color:transparent}.progress::-webkit-progress-value{border-radius:var(--rounded-box, 1rem);background-color:currentColor}.progress:indeterminate::-moz-progress-bar{background-color:transparent;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-size:200%;background-position-x:15%;animation:progress-loading 5s ease-in-out infinite}@keyframes progress-loading{50%{background-position-x:-115%}}@keyframes radiomark{0%{box-shadow:0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset}50%{box-shadow:0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset}to{box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}}.range:focus-visible::-webkit-slider-thumb{--focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset}.range:focus-visible::-moz-range-thumb{--focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset}.range::-webkit-slider-runnable-track{height:.5rem;width:100%;border-radius:var(--rounded-box, 1rem);background-color:var(--fallback-bc,oklch(var(--bc)/.1))}.range::-moz-range-track{height:.5rem;width:100%;border-radius:var(--rounded-box, 1rem);background-color:var(--fallback-bc,oklch(var(--bc)/.1))}.range::-webkit-slider-thumb{position:relative;height:1.5rem;width:1.5rem;border-radius:var(--rounded-box, 1rem);border-style:none;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));-moz-appearance:none;appearance:none;-webkit-appearance:none;top:50%;color:var(--range-shdw);transform:translateY(-50%);--filler-size: 100rem;--filler-offset: .6rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow, 0 0),calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size)}.range::-moz-range-thumb{position:relative;height:1.5rem;width:1.5rem;border-radius:var(--rounded-box, 1rem);border-style:none;--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));top:50%;color:var(--range-shdw);--filler-size: 100rem;--filler-offset: .5rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow, 0 0),calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size)}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.select-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.select:focus{box-shadow:none;border-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.select-disabled,.select:disabled,.select[disabled]{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.select-disabled::-moz-placeholder,.select:disabled::-moz-placeholder,.select[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.select-disabled::placeholder,.select:disabled::placeholder,.select[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.select-multiple,.select[multiple],.select[size].select:not([size="1"]){background-image:none;padding-right:1rem}[dir=rtl] .select{background-position:calc(0% + 12px) calc(1px + 50%),calc(0% + 16px) calc(1px + 50%)}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}:where(.stats)>:not([hidden])~:not([hidden]){--tw-divide-x-reverse: 0;border-right-width:calc(1px * var(--tw-divide-x-reverse));border-left-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)));--tw-divide-y-reverse: 0;border-top-width:calc(0px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(0px * var(--tw-divide-y-reverse))}[dir=rtl] .stats>*:not([hidden])~*:not([hidden]){--tw-divide-x-reverse: 1}.steps .step:before{top:0;grid-column-start:1;grid-row-start:1;height:.5rem;width:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:"";margin-inline-start:-100%}.steps .step:after{content:counter(step);counter-increment:step;z-index:1;position:relative;grid-column-start:1;grid-row-start:1;display:grid;height:2rem;width:2rem;place-items:center;place-self:center;border-radius:9999px;--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.steps .step:first-child:before{content:none}.steps .step[data-content]:after{content:attr(data-content)}.table:where([dir=rtl],[dir=rtl] *){text-align:right}.table :where(th,td){padding:.75rem 1rem;vertical-align:middle}.table tr.active,.table tr.active:nth-child(2n),.table-zebra tbody tr:nth-child(2n){--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.active,.table-zebra tr.active:nth-child(2n),.table-zebra-zebra tbody tr:nth-child(2n){--tw-bg-opacity: 1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.table :where(thead tr,tbody tr:not(:last-child),tbody tr:first-child:last-child){border-bottom-width:1px;--tw-border-opacity: 1;border-bottom-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.table :where(thead,tfoot){white-space:nowrap;font-size:.75rem;line-height:1rem;font-weight:700;color:var(--fallback-bc,oklch(var(--bc)/.6))}.table :where(tfoot){border-top-width:1px;--tw-border-opacity: 1;border-top-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.textarea:focus{box-shadow:none;border-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.textarea-disabled,.textarea:disabled,.textarea[disabled]{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.textarea-disabled::-moz-placeholder,.textarea:disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}.textarea-disabled::placeholder,.textarea:disabled::placeholder,.textarea[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity: .2}@keyframes toast-pop{0%{transform:scale(.9);opacity:0}to{transform:scale(1);opacity:1}}[dir=rtl] .toggle{--handleoffsetcalculator: calc(var(--handleoffset) * 1)}.toggle:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))}.toggle:hover{background-color:currentColor}.toggle:checked,.toggle[aria-checked=true]{background-image:none;--handleoffsetcalculator: var(--handleoffset);--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:checked,[dir=rtl] .toggle[aria-checked=true]{--handleoffsetcalculator: calc(var(--handleoffset) * -1)}.toggle:indeterminate{--tw-text-opacity: 1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));box-shadow:calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset}[dir=rtl] .toggle:indeterminate{box-shadow:calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset}.toggle:disabled{cursor:not-allowed;--tw-border-opacity: 1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));background-color:transparent;opacity:.3;--togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset, var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset}.artboard.phone{width:320px}.badge-sm{height:1rem;font-size:.75rem;line-height:1rem;padding-left:.438rem;padding-right:.438rem}.badge-lg{height:1.5rem;font-size:1rem;line-height:1.5rem;padding-left:.688rem;padding-right:.688rem}.btm-nav-xs>*:where(.active){border-top-width:1px}.btm-nav-sm>*:where(.active){border-top-width:2px}.btm-nav-md>*:where(.active){border-top-width:2px}.btm-nav-lg>*:where(.active){border-top-width:4px}.btn-xs{height:1.5rem;min-height:1.5rem;padding-left:.5rem;padding-right:.5rem;font-size:.75rem}.btn-sm{height:2rem;min-height:2rem;padding-left:.75rem;padding-right:.75rem;font-size:.875rem}.btn-square:where(.btn-xs){height:1.5rem;width:1.5rem;padding:0}.btn-square:where(.btn-sm){height:2rem;width:2rem;padding:0}.btn-circle:where(.btn-xs){height:1.5rem;width:1.5rem;border-radius:9999px;padding:0}.btn-circle:where(.btn-sm){height:2rem;width:2rem;border-radius:9999px;padding:0}.input-sm{height:2rem;padding-left:.75rem;padding-right:.75rem;font-size:.875rem;line-height:2rem}.join.join-vertical{flex-direction:column}.join.join-vertical .join-item:first-child:not(:last-child),.join.join-vertical *:first-child:not(:last-child) .join-item{border-end-start-radius:0;border-end-end-radius:0;border-start-start-radius:inherit;border-start-end-radius:inherit}.join.join-vertical .join-item:last-child:not(:first-child),.join.join-vertical *:last-child:not(:first-child) .join-item{border-start-start-radius:0;border-start-end-radius:0;border-end-start-radius:inherit;border-end-end-radius:inherit}.join.join-horizontal{flex-direction:row}.join.join-horizontal .join-item:first-child:not(:last-child),.join.join-horizontal *:first-child:not(:last-child) .join-item{border-end-end-radius:0;border-start-end-radius:0;border-end-start-radius:inherit;border-start-start-radius:inherit}.join.join-horizontal .join-item:last-child:not(:first-child),.join.join-horizontal *:last-child:not(:first-child) .join-item{border-end-start-radius:0;border-start-start-radius:0;border-end-end-radius:inherit;border-start-end-radius:inherit}.select-sm{height:2rem;min-height:2rem;padding-left:.75rem;padding-right:2rem;font-size:.875rem;line-height:2rem}[dir=rtl] .select-sm{padding-left:2rem;padding-right:.75rem}.steps-horizontal .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-rows:repeat(2,minmax(0,1fr));place-items:center;text-align:center}.steps-vertical .step{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(1,minmax(0,1fr))}.card-compact .card-body{padding:1rem;font-size:.875rem;line-height:1.25rem}.card-compact .card-title{margin-bottom:.25rem}.card-normal .card-body{padding:var(--padding-card, 2rem);font-size:1rem;line-height:1.5rem}.card-normal .card-title{margin-bottom:.75rem}.join.join-vertical>:where(*:not(:first-child)){margin-left:0;margin-right:0;margin-top:-1px}.join.join-vertical>:where(*:not(:first-child)):is(.btn){margin-top:calc(var(--border-btn) * -1)}.join.join-horizontal>:where(*:not(:first-child)){margin-top:0;margin-bottom:0;margin-inline-start:-1px}.join.join-horizontal>:where(*:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn) * -1);margin-top:0}.steps-horizontal .step{grid-template-rows:40px 1fr;grid-template-columns:auto;min-width:4rem}.steps-horizontal .step:before{height:.5rem;width:100%;--tw-translate-x: 0px;--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));content:"";margin-inline-start:-100%}.steps-horizontal .step:where([dir=rtl],[dir=rtl] *):before{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.steps-vertical .step{gap:.5rem;grid-template-columns:40px 1fr;grid-template-rows:auto;min-height:4rem;justify-items:start}.steps-vertical .step:before{height:100%;width:.5rem;--tw-translate-x: -50%;--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));margin-inline-start:50%}.steps-vertical .step:where([dir=rtl],[dir=rtl] *):before{--tw-translate-x: 50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.table-xs :not(thead):not(tfoot) tr{font-size:.75rem;line-height:1rem}.table-xs :where(th,td){padding:.25rem .5rem}.table-sm :not(thead):not(tfoot) tr{font-size:.875rem;line-height:1.25rem}.table-sm :where(th,td){padding:.5rem .75rem}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.left-3{left:.75rem}.right-0{right:0}.right-2{right:.5rem}.top-1\/2{top:50%}.top-2{top:.5rem}.z-10{z-index:10}.z-50{z-index:50}.col-span-2{grid-column:span 2 / span 2}.-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-\[120px\]{width:120px}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[200px\]{min-width:200px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[100px\]{max-width:100px}.max-w-\[120px\]{max-width:120px}.max-w-\[150px\]{max-width:150px}.max-w-\[200px\]{max-width:200px}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(243 244 246 / var(--tw-divide-opacity, 1))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-amber-500{--tw-border-opacity: 1;border-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}.border-blue-700{--tw-border-opacity: 1;border-color:rgb(29 78 216 / var(--tw-border-opacity, 1))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.border-yellow-400{--tw-border-opacity: 1;border-color:rgb(250 204 21 / var(--tw-border-opacity, 1))}.border-yellow-700{--tw-border-opacity: 1;border-color:rgb(161 98 7 / var(--tw-border-opacity, 1))}.border-t-blue-600{--tw-border-opacity: 1;border-top-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}.border-t-transparent{border-top-color:transparent}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity, 1))}.bg-base-100{--tw-bg-opacity: 1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity, 1)))}.bg-base-200{--tw-bg-opacity: 1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity, 1)))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-cyan-50{--tw-bg-opacity: 1;background-color:rgb(236 254 255 / var(--tw-bg-opacity, 1))}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-emerald-50{--tw-bg-opacity: 1;background-color:rgb(236 253 245 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity, 1))}.bg-indigo-50{--tw-bg-opacity: 1;background-color:rgb(238 242 255 / var(--tw-bg-opacity, 1))}.bg-orange-100{--tw-bg-opacity: 1;background-color:rgb(255 237 213 / var(--tw-bg-opacity, 1))}.bg-orange-50{--tw-bg-opacity: 1;background-color:rgb(255 247 237 / var(--tw-bg-opacity, 1))}.bg-pink-100{--tw-bg-opacity: 1;background-color:rgb(252 231 243 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-600{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.bg-opacity-50{--tw-bg-opacity: .5}.stroke-current{stroke:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-10{padding-left:2.5rem}.pl-12{padding-left:3rem}.pl-16{padding-left:4rem}.pr-3{padding-right:.75rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-cyan-600{--tw-text-opacity: 1;color:rgb(8 145 178 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-error{--tw-text-opacity: 1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity, 1)))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity, 1))}.text-indigo-700{--tw-text-opacity: 1;color:rgb(67 56 202 / var(--tw-text-opacity, 1))}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-orange-700{--tw-text-opacity: 1;color:rgb(194 65 12 / var(--tw-text-opacity, 1))}.text-orange-800{--tw-text-opacity: 1;color:rgb(154 52 18 / var(--tw-text-opacity, 1))}.text-pink-700{--tw-text-opacity: 1;color:rgb(190 24 93 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity, 1)))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-purple-700{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.text-success{--tw-text-opacity: 1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity, 1)))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.opacity-60{opacity:.6}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.hover\:border-blue-300:hover{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.hover\:border-blue-500:hover{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.hover\:border-orange-300:hover{--tw-border-opacity: 1;border-color:rgb(253 186 116 / var(--tw-border-opacity, 1))}.hover\:border-purple-300:hover{--tw-border-opacity: 1;border-color:rgb(216 180 254 / var(--tw-border-opacity, 1))}.hover\:bg-amber-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.hover\:bg-amber-700:hover{--tw-bg-opacity: 1;background-color:rgb(180 83 9 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-100:hover{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-yellow-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.hover\:bg-yellow-700:hover{--tw-bg-opacity: 1;background-color:rgb(161 98 7 / var(--tw-bg-opacity, 1))}.hover\:text-blue-700:hover{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-indigo-700:hover{--tw-text-opacity: 1;color:rgb(67 56 202 / var(--tw-text-opacity, 1))}.hover\:text-red-800:hover{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.hover\:text-yellow-800:hover{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}} diff --git a/frontend/dist/assets/index-TpSq-kLx.js b/frontend/dist/assets/index-TpSq-kLx.js new file mode 100644 index 00000000..ad5095c9 --- /dev/null +++ b/frontend/dist/assets/index-TpSq-kLx.js @@ -0,0 +1,416 @@ +var Pk=Object.defineProperty;var _k=(e,t,r)=>t in e?Pk(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var ho=(e,t,r)=>_k(e,typeof t!="symbol"?t+"":t,r);function Ck(e,t){for(var r=0;rn[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function r(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=r(i);fetch(i.href,s)}})();function Tr(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Sv={exports:{}},kc={},Nv={exports:{}},ie={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ks=Symbol.for("react.element"),Ak=Symbol.for("react.portal"),Ok=Symbol.for("react.fragment"),Ek=Symbol.for("react.strict_mode"),Dk=Symbol.for("react.profiler"),Tk=Symbol.for("react.provider"),Mk=Symbol.for("react.context"),Ik=Symbol.for("react.forward_ref"),$k=Symbol.for("react.suspense"),Lk=Symbol.for("react.memo"),zk=Symbol.for("react.lazy"),dg=Symbol.iterator;function Rk(e){return e===null||typeof e!="object"?null:(e=dg&&e[dg]||e["@@iterator"],typeof e=="function"?e:null)}var kv={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Pv=Object.assign,_v={};function va(e,t,r){this.props=e,this.context=t,this.refs=_v,this.updater=r||kv}va.prototype.isReactComponent={};va.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};va.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Cv(){}Cv.prototype=va.prototype;function Pp(e,t,r){this.props=e,this.context=t,this.refs=_v,this.updater=r||kv}var _p=Pp.prototype=new Cv;_p.constructor=Pp;Pv(_p,va.prototype);_p.isPureReactComponent=!0;var fg=Array.isArray,Av=Object.prototype.hasOwnProperty,Cp={current:null},Ov={key:!0,ref:!0,__self:!0,__source:!0};function Ev(e,t,r){var n,i={},s=null,o=null;if(t!=null)for(n in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(s=""+t.key),t)Av.call(t,n)&&!Ov.hasOwnProperty(n)&&(i[n]=t[n]);var l=arguments.length-2;if(l===1)i.children=r;else if(1>>1,H=O[U];if(0>>1;Ui(we,L))Ai(J,we)?(O[U]=J,O[A]=L,U=A):(O[U]=we,O[re]=L,U=re);else if(Ai(J,L))O[U]=J,O[A]=L,U=A;else break e}}return k}function i(O,k){var L=O.sortIndex-k.sortIndex;return L!==0?L:O.id-k.id}if(typeof performance=="object"&&typeof performance.now=="function"){var s=performance;e.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();e.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,f=null,p=3,m=!1,x=!1,g=!1,b=typeof setTimeout=="function"?setTimeout:null,v=typeof clearTimeout=="function"?clearTimeout:null,j=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(O){for(var k=r(d);k!==null;){if(k.callback===null)n(d);else if(k.startTime<=O)n(d),k.sortIndex=k.expirationTime,t(c,k);else break;k=r(d)}}function w(O){if(g=!1,y(O),!x)if(r(c)!==null)x=!0,E(S);else{var k=r(d);k!==null&&D(w,k.startTime-O)}}function S(O,k){x=!1,g&&(g=!1,v(_),_=-1),m=!0;var L=p;try{for(y(k),f=r(c);f!==null&&(!(f.expirationTime>k)||O&&!M());){var U=f.callback;if(typeof U=="function"){f.callback=null,p=f.priorityLevel;var H=U(f.expirationTime<=k);k=e.unstable_now(),typeof H=="function"?f.callback=H:f===r(c)&&n(c),y(k)}else n(c);f=r(c)}if(f!==null)var te=!0;else{var re=r(d);re!==null&&D(w,re.startTime-k),te=!1}return te}finally{f=null,p=L,m=!1}}var N=!1,P=null,_=-1,T=5,$=-1;function M(){return!(e.unstable_now()-$O||125U?(O.sortIndex=L,t(d,O),r(c)===null&&O===r(d)&&(g?(v(_),_=-1):g=!0,D(w,L-U))):(O.sortIndex=H,t(c,O),x||m||(x=!0,E(S))),O},e.unstable_shouldYield=M,e.unstable_wrapCallback=function(O){var k=p;return function(){var L=p;p=k;try{return O.apply(this,arguments)}finally{p=L}}}})(Lv);$v.exports=Lv;var Zk=$v.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Xk=h,Ft=Zk;function F(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ed=Object.prototype.hasOwnProperty,Jk=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,hg={},mg={};function Qk(e){return Ed.call(mg,e)?!0:Ed.call(hg,e)?!1:Jk.test(e)?mg[e]=!0:(hg[e]=!0,!1)}function eP(e,t,r,n){if(r!==null&&r.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return n?!1:r!==null?!r.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function tP(e,t,r,n){if(t===null||typeof t>"u"||eP(e,t,r,n))return!0;if(n)return!1;if(r!==null)switch(r.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function yt(e,t,r,n,i,s,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=n,this.attributeNamespace=i,this.mustUseProperty=r,this.propertyName=e,this.type=t,this.sanitizeURL=s,this.removeEmptyString=o}var tt={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){tt[e]=new yt(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];tt[t]=new yt(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){tt[e]=new yt(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){tt[e]=new yt(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){tt[e]=new yt(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){tt[e]=new yt(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){tt[e]=new yt(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){tt[e]=new yt(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){tt[e]=new yt(e,5,!1,e.toLowerCase(),null,!1,!1)});var Op=/[\-:]([a-z])/g;function Ep(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Op,Ep);tt[t]=new yt(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Op,Ep);tt[t]=new yt(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Op,Ep);tt[t]=new yt(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){tt[e]=new yt(e,1,!1,e.toLowerCase(),null,!1,!1)});tt.xlinkHref=new yt("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){tt[e]=new yt(e,1,!1,e.toLowerCase(),null,!0,!0)});function Dp(e,t,r,n){var i=tt.hasOwnProperty(t)?tt[t]:null;(i!==null?i.type!==0:n||!(2l||i[o]!==s[l]){var c=` +`+i[o].replace(" at new "," at ");return e.displayName&&c.includes("")&&(c=c.replace("",e.displayName)),c}while(1<=o&&0<=l);break}}}finally{Lu=!1,Error.prepareStackTrace=r}return(e=e?e.displayName||e.name:"")?Ga(e):""}function rP(e){switch(e.tag){case 5:return Ga(e.type);case 16:return Ga("Lazy");case 13:return Ga("Suspense");case 19:return Ga("SuspenseList");case 0:case 2:case 15:return e=zu(e.type,!1),e;case 11:return e=zu(e.type.render,!1),e;case 1:return e=zu(e.type,!0),e;default:return""}}function Id(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Li:return"Fragment";case $i:return"Portal";case Dd:return"Profiler";case Tp:return"StrictMode";case Td:return"Suspense";case Md:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Bv:return(e.displayName||"Context")+".Consumer";case Rv:return(e._context.displayName||"Context")+".Provider";case Mp:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Ip:return t=e.displayName||null,t!==null?t:Id(e.type)||"Memo";case mn:t=e._payload,e=e._init;try{return Id(e(t))}catch{}}return null}function nP(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Id(t);case 8:return t===Tp?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function In(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Wv(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function iP(e){var t=Wv(e)?"checked":"value",r=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),n=""+e[t];if(!e.hasOwnProperty(t)&&typeof r<"u"&&typeof r.get=="function"&&typeof r.set=="function"){var i=r.get,s=r.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(o){n=""+o,s.call(this,o)}}),Object.defineProperty(e,t,{enumerable:r.enumerable}),{getValue:function(){return n},setValue:function(o){n=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function xo(e){e._valueTracker||(e._valueTracker=iP(e))}function Uv(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var r=t.getValue(),n="";return e&&(n=Wv(e)?e.checked?"true":"false":e.value),e=n,e!==r?(t.setValue(e),!0):!1}function ul(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function $d(e,t){var r=t.checked;return ke({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:r??e._wrapperState.initialChecked})}function xg(e,t){var r=t.defaultValue==null?"":t.defaultValue,n=t.checked!=null?t.checked:t.defaultChecked;r=In(t.value!=null?t.value:r),e._wrapperState={initialChecked:n,initialValue:r,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function qv(e,t){t=t.checked,t!=null&&Dp(e,"checked",t,!1)}function Ld(e,t){qv(e,t);var r=In(t.value),n=t.type;if(r!=null)n==="number"?(r===0&&e.value===""||e.value!=r)&&(e.value=""+r):e.value!==""+r&&(e.value=""+r);else if(n==="submit"||n==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?zd(e,t.type,r):t.hasOwnProperty("defaultValue")&&zd(e,t.type,In(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function yg(e,t,r){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var n=t.type;if(!(n!=="submit"&&n!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,r||t===e.value||(e.value=t),e.defaultValue=t}r=e.name,r!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,r!==""&&(e.name=r)}function zd(e,t,r){(t!=="number"||ul(e.ownerDocument)!==e)&&(r==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+r&&(e.defaultValue=""+r))}var Za=Array.isArray;function Xi(e,t,r,n){if(e=e.options,t){t={};for(var i=0;i"+t.valueOf().toString()+"",t=yo.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function hs(e,t){if(t){var r=e.firstChild;if(r&&r===e.lastChild&&r.nodeType===3){r.nodeValue=t;return}}e.textContent=t}var ts={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},aP=["Webkit","ms","Moz","O"];Object.keys(ts).forEach(function(e){aP.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),ts[t]=ts[e]})});function Yv(e,t,r){return t==null||typeof t=="boolean"||t===""?"":r||typeof t!="number"||t===0||ts.hasOwnProperty(e)&&ts[e]?(""+t).trim():t+"px"}function Gv(e,t){e=e.style;for(var r in t)if(t.hasOwnProperty(r)){var n=r.indexOf("--")===0,i=Yv(r,t[r],n);r==="float"&&(r="cssFloat"),n?e.setProperty(r,i):e[r]=i}}var sP=ke({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Fd(e,t){if(t){if(sP[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(F(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(F(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(F(61))}if(t.style!=null&&typeof t.style!="object")throw Error(F(62))}}function Wd(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ud=null;function $p(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var qd=null,Ji=null,Qi=null;function jg(e){if(e=Gs(e)){if(typeof qd!="function")throw Error(F(280));var t=e.stateNode;t&&(t=Oc(t),qd(e.stateNode,e.type,t))}}function Zv(e){Ji?Qi?Qi.push(e):Qi=[e]:Ji=e}function Xv(){if(Ji){var e=Ji,t=Qi;if(Qi=Ji=null,jg(e),t)for(e=0;e>>=0,e===0?32:31-(xP(e)/yP|0)|0}var vo=64,bo=4194304;function Xa(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function hl(e,t){var r=e.pendingLanes;if(r===0)return 0;var n=0,i=e.suspendedLanes,s=e.pingedLanes,o=r&268435455;if(o!==0){var l=o&~i;l!==0?n=Xa(l):(s&=o,s!==0&&(n=Xa(s)))}else o=r&~i,o!==0?n=Xa(o):s!==0&&(n=Xa(s));if(n===0)return 0;if(t!==0&&t!==n&&!(t&i)&&(i=n&-n,s=t&-t,i>=s||i===16&&(s&4194240)!==0))return t;if(n&4&&(n|=r&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=n;0r;r++)t.push(e);return t}function Vs(e,t,r){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-hr(t),e[t]=r}function wP(e,t){var r=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var n=e.eventTimes;for(e=e.expirationTimes;0=ns),Og=" ",Eg=!1;function xb(e,t){switch(e){case"keyup":return ZP.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function yb(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var zi=!1;function JP(e,t){switch(e){case"compositionend":return yb(t);case"keypress":return t.which!==32?null:(Eg=!0,Og);case"textInput":return e=t.data,e===Og&&Eg?null:e;default:return null}}function QP(e,t){if(zi)return e==="compositionend"||!qp&&xb(e,t)?(e=mb(),Xo=Fp=wn=null,zi=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=Ig(r)}}function wb(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?wb(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Sb(){for(var e=window,t=ul();t instanceof e.HTMLIFrameElement;){try{var r=typeof t.contentWindow.location.href=="string"}catch{r=!1}if(r)e=t.contentWindow;else break;t=ul(e.document)}return t}function Hp(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function l_(e){var t=Sb(),r=e.focusedElem,n=e.selectionRange;if(t!==r&&r&&r.ownerDocument&&wb(r.ownerDocument.documentElement,r)){if(n!==null&&Hp(r)){if(t=n.start,e=n.end,e===void 0&&(e=t),"selectionStart"in r)r.selectionStart=t,r.selectionEnd=Math.min(e,r.value.length);else if(e=(t=r.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=r.textContent.length,s=Math.min(n.start,i);n=n.end===void 0?s:Math.min(n.end,i),!e.extend&&s>n&&(i=n,n=s,s=i),i=$g(r,s);var o=$g(r,n);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),s>n?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=r;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof r.focus=="function"&&r.focus(),r=0;r=document.documentMode,Ri=null,Zd=null,as=null,Xd=!1;function Lg(e,t,r){var n=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;Xd||Ri==null||Ri!==ul(n)||(n=Ri,"selectionStart"in n&&Hp(n)?n={start:n.selectionStart,end:n.selectionEnd}:(n=(n.ownerDocument&&n.ownerDocument.defaultView||window).getSelection(),n={anchorNode:n.anchorNode,anchorOffset:n.anchorOffset,focusNode:n.focusNode,focusOffset:n.focusOffset}),as&&bs(as,n)||(as=n,n=xl(Zd,"onSelect"),0Wi||(e.current=nf[Wi],nf[Wi]=null,Wi--)}function me(e,t){Wi++,nf[Wi]=e.current,e.current=t}var $n={},ct=Fn($n),kt=Fn(!1),pi=$n;function sa(e,t){var r=e.type.contextTypes;if(!r)return $n;var n=e.stateNode;if(n&&n.__reactInternalMemoizedUnmaskedChildContext===t)return n.__reactInternalMemoizedMaskedChildContext;var i={},s;for(s in r)i[s]=t[s];return n&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Pt(e){return e=e.childContextTypes,e!=null}function vl(){ye(kt),ye(ct)}function qg(e,t,r){if(ct.current!==$n)throw Error(F(168));me(ct,t),me(kt,r)}function Db(e,t,r){var n=e.stateNode;if(t=t.childContextTypes,typeof n.getChildContext!="function")return r;n=n.getChildContext();for(var i in n)if(!(i in t))throw Error(F(108,nP(e)||"Unknown",i));return ke({},r,n)}function bl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||$n,pi=ct.current,me(ct,e),me(kt,kt.current),!0}function Hg(e,t,r){var n=e.stateNode;if(!n)throw Error(F(169));r?(e=Db(e,t,pi),n.__reactInternalMemoizedMergedChildContext=e,ye(kt),ye(ct),me(ct,e)):ye(kt),me(kt,r)}var Lr=null,Ec=!1,Ju=!1;function Tb(e){Lr===null?Lr=[e]:Lr.push(e)}function b_(e){Ec=!0,Tb(e)}function Wn(){if(!Ju&&Lr!==null){Ju=!0;var e=0,t=le;try{var r=Lr;for(le=1;e>=o,i-=o,Br=1<<32-hr(t)+i|r<_?(T=P,P=null):T=P.sibling;var $=p(v,P,y[_],w);if($===null){P===null&&(P=T);break}e&&P&&$.alternate===null&&t(v,P),j=s($,j,_),N===null?S=$:N.sibling=$,N=$,P=T}if(_===y.length)return r(v,P),be&&Gn(v,_),S;if(P===null){for(;__?(T=P,P=null):T=P.sibling;var M=p(v,P,$.value,w);if(M===null){P===null&&(P=T);break}e&&P&&M.alternate===null&&t(v,P),j=s(M,j,_),N===null?S=M:N.sibling=M,N=M,P=T}if($.done)return r(v,P),be&&Gn(v,_),S;if(P===null){for(;!$.done;_++,$=y.next())$=f(v,$.value,w),$!==null&&(j=s($,j,_),N===null?S=$:N.sibling=$,N=$);return be&&Gn(v,_),S}for(P=n(v,P);!$.done;_++,$=y.next())$=m(P,v,_,$.value,w),$!==null&&(e&&$.alternate!==null&&P.delete($.key===null?_:$.key),j=s($,j,_),N===null?S=$:N.sibling=$,N=$);return e&&P.forEach(function(C){return t(v,C)}),be&&Gn(v,_),S}function b(v,j,y,w){if(typeof y=="object"&&y!==null&&y.type===Li&&y.key===null&&(y=y.props.children),typeof y=="object"&&y!==null){switch(y.$$typeof){case go:e:{for(var S=y.key,N=j;N!==null;){if(N.key===S){if(S=y.type,S===Li){if(N.tag===7){r(v,N.sibling),j=i(N,y.props.children),j.return=v,v=j;break e}}else if(N.elementType===S||typeof S=="object"&&S!==null&&S.$$typeof===mn&&Yg(S)===N.type){r(v,N.sibling),j=i(N,y.props),j.ref=Ra(v,N,y),j.return=v,v=j;break e}r(v,N);break}else t(v,N);N=N.sibling}y.type===Li?(j=oi(y.props.children,v.mode,w,y.key),j.return=v,v=j):(w=al(y.type,y.key,y.props,null,v.mode,w),w.ref=Ra(v,j,y),w.return=v,v=w)}return o(v);case $i:e:{for(N=y.key;j!==null;){if(j.key===N)if(j.tag===4&&j.stateNode.containerInfo===y.containerInfo&&j.stateNode.implementation===y.implementation){r(v,j.sibling),j=i(j,y.children||[]),j.return=v,v=j;break e}else{r(v,j);break}else t(v,j);j=j.sibling}j=sd(y,v.mode,w),j.return=v,v=j}return o(v);case mn:return N=y._init,b(v,j,N(y._payload),w)}if(Za(y))return x(v,j,y,w);if(Ma(y))return g(v,j,y,w);_o(v,y)}return typeof y=="string"&&y!==""||typeof y=="number"?(y=""+y,j!==null&&j.tag===6?(r(v,j.sibling),j=i(j,y),j.return=v,v=j):(r(v,j),j=ad(y,v.mode,w),j.return=v,v=j),o(v)):r(v,j)}return b}var la=Lb(!0),zb=Lb(!1),Sl=Fn(null),Nl=null,Hi=null,Gp=null;function Zp(){Gp=Hi=Nl=null}function Xp(e){var t=Sl.current;ye(Sl),e._currentValue=t}function of(e,t,r){for(;e!==null;){var n=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,n!==null&&(n.childLanes|=t)):n!==null&&(n.childLanes&t)!==t&&(n.childLanes|=t),e===r)break;e=e.return}}function ta(e,t){Nl=e,Gp=Hi=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(St=!0),e.firstContext=null)}function tr(e){var t=e._currentValue;if(Gp!==e)if(e={context:e,memoizedValue:t,next:null},Hi===null){if(Nl===null)throw Error(F(308));Hi=e,Nl.dependencies={lanes:0,firstContext:e}}else Hi=Hi.next=e;return t}var ti=null;function Jp(e){ti===null?ti=[e]:ti.push(e)}function Rb(e,t,r,n){var i=t.interleaved;return i===null?(r.next=r,Jp(t)):(r.next=i.next,i.next=r),t.interleaved=r,Zr(e,n)}function Zr(e,t){e.lanes|=t;var r=e.alternate;for(r!==null&&(r.lanes|=t),r=e,e=e.return;e!==null;)e.childLanes|=t,r=e.alternate,r!==null&&(r.childLanes|=t),r=e,e=e.return;return r.tag===3?r.stateNode:null}var gn=!1;function Qp(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Bb(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function qr(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function An(e,t,r){var n=e.updateQueue;if(n===null)return null;if(n=n.shared,ae&2){var i=n.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),n.pending=t,Zr(e,r)}return i=n.interleaved,i===null?(t.next=t,Jp(n)):(t.next=i.next,i.next=t),n.interleaved=t,Zr(e,r)}function Qo(e,t,r){if(t=t.updateQueue,t!==null&&(t=t.shared,(r&4194240)!==0)){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,zp(e,r)}}function Gg(e,t){var r=e.updateQueue,n=e.alternate;if(n!==null&&(n=n.updateQueue,r===n)){var i=null,s=null;if(r=r.firstBaseUpdate,r!==null){do{var o={eventTime:r.eventTime,lane:r.lane,tag:r.tag,payload:r.payload,callback:r.callback,next:null};s===null?i=s=o:s=s.next=o,r=r.next}while(r!==null);s===null?i=s=t:s=s.next=t}else i=s=t;r={baseState:n.baseState,firstBaseUpdate:i,lastBaseUpdate:s,shared:n.shared,effects:n.effects},e.updateQueue=r;return}e=r.lastBaseUpdate,e===null?r.firstBaseUpdate=t:e.next=t,r.lastBaseUpdate=t}function kl(e,t,r,n){var i=e.updateQueue;gn=!1;var s=i.firstBaseUpdate,o=i.lastBaseUpdate,l=i.shared.pending;if(l!==null){i.shared.pending=null;var c=l,d=c.next;c.next=null,o===null?s=d:o.next=d,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,l=u.lastBaseUpdate,l!==o&&(l===null?u.firstBaseUpdate=d:l.next=d,u.lastBaseUpdate=c))}if(s!==null){var f=i.baseState;o=0,u=d=c=null,l=s;do{var p=l.lane,m=l.eventTime;if((n&p)===p){u!==null&&(u=u.next={eventTime:m,lane:0,tag:l.tag,payload:l.payload,callback:l.callback,next:null});e:{var x=e,g=l;switch(p=t,m=r,g.tag){case 1:if(x=g.payload,typeof x=="function"){f=x.call(m,f,p);break e}f=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=g.payload,p=typeof x=="function"?x.call(m,f,p):x,p==null)break e;f=ke({},f,p);break e;case 2:gn=!0}}l.callback!==null&&l.lane!==0&&(e.flags|=64,p=i.effects,p===null?i.effects=[l]:p.push(l))}else m={eventTime:m,lane:p,tag:l.tag,payload:l.payload,callback:l.callback,next:null},u===null?(d=u=m,c=f):u=u.next=m,o|=p;if(l=l.next,l===null){if(l=i.shared.pending,l===null)break;p=l,l=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(!0);if(u===null&&(c=f),i.baseState=c,i.firstBaseUpdate=d,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else s===null&&(i.shared.lanes=0);gi|=o,e.lanes=o,e.memoizedState=f}}function Zg(e,t,r){if(e=t.effects,t.effects=null,e!==null)for(t=0;tr?r:4,e(!0);var n=ed.transition;ed.transition={};try{e(!1),t()}finally{le=r,ed.transition=n}}function n1(){return rr().memoizedState}function N_(e,t,r){var n=En(e);if(r={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null},i1(e))a1(t,r);else if(r=Rb(e,t,r,n),r!==null){var i=gt();mr(r,e,n,i),s1(r,t,n)}}function k_(e,t,r){var n=En(e),i={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null};if(i1(e))a1(t,i);else{var s=e.alternate;if(e.lanes===0&&(s===null||s.lanes===0)&&(s=t.lastRenderedReducer,s!==null))try{var o=t.lastRenderedState,l=s(o,r);if(i.hasEagerState=!0,i.eagerState=l,gr(l,o)){var c=t.interleaved;c===null?(i.next=i,Jp(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}finally{}r=Rb(e,t,i,n),r!==null&&(i=gt(),mr(r,e,n,i),s1(r,t,n))}}function i1(e){var t=e.alternate;return e===Ne||t!==null&&t===Ne}function a1(e,t){ss=_l=!0;var r=e.pending;r===null?t.next=t:(t.next=r.next,r.next=t),e.pending=t}function s1(e,t,r){if(r&4194240){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,zp(e,r)}}var Cl={readContext:tr,useCallback:nt,useContext:nt,useEffect:nt,useImperativeHandle:nt,useInsertionEffect:nt,useLayoutEffect:nt,useMemo:nt,useReducer:nt,useRef:nt,useState:nt,useDebugValue:nt,useDeferredValue:nt,useTransition:nt,useMutableSource:nt,useSyncExternalStore:nt,useId:nt,unstable_isNewReconciler:!1},P_={readContext:tr,useCallback:function(e,t){return jr().memoizedState=[e,t===void 0?null:t],e},useContext:tr,useEffect:Jg,useImperativeHandle:function(e,t,r){return r=r!=null?r.concat([e]):null,tl(4194308,4,Jb.bind(null,t,e),r)},useLayoutEffect:function(e,t){return tl(4194308,4,e,t)},useInsertionEffect:function(e,t){return tl(4,2,e,t)},useMemo:function(e,t){var r=jr();return t=t===void 0?null:t,e=e(),r.memoizedState=[e,t],e},useReducer:function(e,t,r){var n=jr();return t=r!==void 0?r(t):t,n.memoizedState=n.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},n.queue=e,e=e.dispatch=N_.bind(null,Ne,e),[n.memoizedState,e]},useRef:function(e){var t=jr();return e={current:e},t.memoizedState=e},useState:Xg,useDebugValue:oh,useDeferredValue:function(e){return jr().memoizedState=e},useTransition:function(){var e=Xg(!1),t=e[0];return e=S_.bind(null,e[1]),jr().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,r){var n=Ne,i=jr();if(be){if(r===void 0)throw Error(F(407));r=r()}else{if(r=t(),Ke===null)throw Error(F(349));mi&30||qb(n,t,r)}i.memoizedState=r;var s={value:r,getSnapshot:t};return i.queue=s,Jg(Kb.bind(null,n,s,e),[e]),n.flags|=2048,Cs(9,Hb.bind(null,n,s,r,t),void 0,null),r},useId:function(){var e=jr(),t=Ke.identifierPrefix;if(be){var r=Fr,n=Br;r=(n&~(1<<32-hr(n)-1)).toString(32)+r,t=":"+t+"R"+r,r=Ps++,0<\/script>",e=e.removeChild(e.firstChild)):typeof n.is=="string"?e=o.createElement(r,{is:n.is}):(e=o.createElement(r),r==="select"&&(o=e,n.multiple?o.multiple=!0:n.size&&(o.size=n.size))):e=o.createElementNS(e,r),e[Sr]=t,e[Ss]=n,g1(e,t,!1,!1),t.stateNode=e;e:{switch(o=Wd(r,n),r){case"dialog":ge("cancel",e),ge("close",e),i=n;break;case"iframe":case"object":case"embed":ge("load",e),i=n;break;case"video":case"audio":for(i=0;ida&&(t.flags|=128,n=!0,Ba(s,!1),t.lanes=4194304)}else{if(!n)if(e=Pl(o),e!==null){if(t.flags|=128,n=!0,r=e.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),Ba(s,!0),s.tail===null&&s.tailMode==="hidden"&&!o.alternate&&!be)return it(t),null}else 2*Ae()-s.renderingStartTime>da&&r!==1073741824&&(t.flags|=128,n=!0,Ba(s,!1),t.lanes=4194304);s.isBackwards?(o.sibling=t.child,t.child=o):(r=s.last,r!==null?r.sibling=o:t.child=o,s.last=o)}return s.tail!==null?(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=Ae(),t.sibling=null,r=Se.current,me(Se,n?r&1|2:r&1),t):(it(t),null);case 22:case 23:return ph(),n=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==n&&(t.flags|=8192),n&&t.mode&1?It&1073741824&&(it(t),t.subtreeFlags&6&&(t.flags|=8192)):it(t),null;case 24:return null;case 25:return null}throw Error(F(156,t.tag))}function M_(e,t){switch(Vp(t),t.tag){case 1:return Pt(t.type)&&vl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ca(),ye(kt),ye(ct),rh(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return th(t),null;case 13:if(ye(Se),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(F(340));oa()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ye(Se),null;case 4:return ca(),null;case 10:return Xp(t.type._context),null;case 22:case 23:return ph(),null;case 24:return null;default:return null}}var Ao=!1,st=!1,I_=typeof WeakSet=="function"?WeakSet:Set,K=null;function Ki(e,t){var r=e.ref;if(r!==null)if(typeof r=="function")try{r(null)}catch(n){Pe(e,t,n)}else r.current=null}function gf(e,t,r){try{r()}catch(n){Pe(e,t,n)}}var cx=!1;function $_(e,t){if(Jd=ml,e=Sb(),Hp(e)){if("selectionStart"in e)var r={start:e.selectionStart,end:e.selectionEnd};else e:{r=(r=e.ownerDocument)&&r.defaultView||window;var n=r.getSelection&&r.getSelection();if(n&&n.rangeCount!==0){r=n.anchorNode;var i=n.anchorOffset,s=n.focusNode;n=n.focusOffset;try{r.nodeType,s.nodeType}catch{r=null;break e}var o=0,l=-1,c=-1,d=0,u=0,f=e,p=null;t:for(;;){for(var m;f!==r||i!==0&&f.nodeType!==3||(l=o+i),f!==s||n!==0&&f.nodeType!==3||(c=o+n),f.nodeType===3&&(o+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break t;if(p===r&&++d===i&&(l=o),p===s&&++u===n&&(c=o),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}r=l===-1||c===-1?null:{start:l,end:c}}else r=null}r=r||{start:0,end:0}}else r=null;for(Qd={focusedElem:e,selectionRange:r},ml=!1,K=t;K!==null;)if(t=K,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,K=e;else for(;K!==null;){t=K;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var g=x.memoizedProps,b=x.memoizedState,v=t.stateNode,j=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:cr(t.type,g),b);v.__reactInternalSnapshotBeforeUpdate=j}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent="":y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(F(163))}}catch(w){Pe(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,K=e;break}K=t.return}return x=cx,cx=!1,x}function os(e,t,r){var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var i=n=n.next;do{if((i.tag&e)===e){var s=i.destroy;i.destroy=void 0,s!==void 0&&gf(t,r,s)}i=i.next}while(i!==n)}}function Mc(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var r=t=t.next;do{if((r.tag&e)===e){var n=r.create;r.destroy=n()}r=r.next}while(r!==t)}}function xf(e){var t=e.ref;if(t!==null){var r=e.stateNode;switch(e.tag){case 5:e=r;break;default:e=r}typeof t=="function"?t(e):t.current=e}}function v1(e){var t=e.alternate;t!==null&&(e.alternate=null,v1(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Sr],delete t[Ss],delete t[rf],delete t[y_],delete t[v_])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function b1(e){return e.tag===5||e.tag===3||e.tag===4}function ux(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||b1(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function yf(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.nodeType===8?r.parentNode.insertBefore(e,t):r.insertBefore(e,t):(r.nodeType===8?(t=r.parentNode,t.insertBefore(e,r)):(t=r,t.appendChild(e)),r=r._reactRootContainer,r!=null||t.onclick!==null||(t.onclick=yl));else if(n!==4&&(e=e.child,e!==null))for(yf(e,t,r),e=e.sibling;e!==null;)yf(e,t,r),e=e.sibling}function vf(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.insertBefore(e,t):r.appendChild(e);else if(n!==4&&(e=e.child,e!==null))for(vf(e,t,r),e=e.sibling;e!==null;)vf(e,t,r),e=e.sibling}var Xe=null,ur=!1;function hn(e,t,r){for(r=r.child;r!==null;)j1(e,t,r),r=r.sibling}function j1(e,t,r){if(kr&&typeof kr.onCommitFiberUnmount=="function")try{kr.onCommitFiberUnmount(Pc,r)}catch{}switch(r.tag){case 5:st||Ki(r,t);case 6:var n=Xe,i=ur;Xe=null,hn(e,t,r),Xe=n,ur=i,Xe!==null&&(ur?(e=Xe,r=r.stateNode,e.nodeType===8?e.parentNode.removeChild(r):e.removeChild(r)):Xe.removeChild(r.stateNode));break;case 18:Xe!==null&&(ur?(e=Xe,r=r.stateNode,e.nodeType===8?Xu(e.parentNode,r):e.nodeType===1&&Xu(e,r),ys(e)):Xu(Xe,r.stateNode));break;case 4:n=Xe,i=ur,Xe=r.stateNode.containerInfo,ur=!0,hn(e,t,r),Xe=n,ur=i;break;case 0:case 11:case 14:case 15:if(!st&&(n=r.updateQueue,n!==null&&(n=n.lastEffect,n!==null))){i=n=n.next;do{var s=i,o=s.destroy;s=s.tag,o!==void 0&&(s&2||s&4)&&gf(r,t,o),i=i.next}while(i!==n)}hn(e,t,r);break;case 1:if(!st&&(Ki(r,t),n=r.stateNode,typeof n.componentWillUnmount=="function"))try{n.props=r.memoizedProps,n.state=r.memoizedState,n.componentWillUnmount()}catch(l){Pe(r,t,l)}hn(e,t,r);break;case 21:hn(e,t,r);break;case 22:r.mode&1?(st=(n=st)||r.memoizedState!==null,hn(e,t,r),st=n):hn(e,t,r);break;default:hn(e,t,r)}}function dx(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var r=e.stateNode;r===null&&(r=e.stateNode=new I_),t.forEach(function(n){var i=H_.bind(null,e,n);r.has(n)||(r.add(n),n.then(i,i))})}}function lr(e,t){var r=t.deletions;if(r!==null)for(var n=0;ni&&(i=o),n&=~s}if(n=i,n=Ae()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*z_(n/1960))-n,10e?16:e,Sn===null)var n=!1;else{if(e=Sn,Sn=null,El=0,ae&6)throw Error(F(331));var i=ae;for(ae|=4,K=e.current;K!==null;){var s=K,o=s.child;if(K.flags&16){var l=s.deletions;if(l!==null){for(var c=0;cAe()-dh?si(e,0):uh|=r),_t(e,t)}function A1(e,t){t===0&&(e.mode&1?(t=bo,bo<<=1,!(bo&130023424)&&(bo=4194304)):t=1);var r=gt();e=Zr(e,t),e!==null&&(Vs(e,t,r),_t(e,r))}function q_(e){var t=e.memoizedState,r=0;t!==null&&(r=t.retryLane),A1(e,r)}function H_(e,t){var r=0;switch(e.tag){case 13:var n=e.stateNode,i=e.memoizedState;i!==null&&(r=i.retryLane);break;case 19:n=e.stateNode;break;default:throw Error(F(314))}n!==null&&n.delete(t),A1(e,r)}var O1;O1=function(e,t,r){if(e!==null)if(e.memoizedProps!==t.pendingProps||kt.current)St=!0;else{if(!(e.lanes&r)&&!(t.flags&128))return St=!1,D_(e,t,r);St=!!(e.flags&131072)}else St=!1,be&&t.flags&1048576&&Mb(t,wl,t.index);switch(t.lanes=0,t.tag){case 2:var n=t.type;rl(e,t),e=t.pendingProps;var i=sa(t,ct.current);ta(t,r),i=ih(null,t,n,e,i,r);var s=ah();return t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Pt(n)?(s=!0,bl(t)):s=!1,t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,Qp(t),i.updater=Tc,t.stateNode=i,i._reactInternals=t,cf(t,n,e,r),t=ff(null,t,n,!0,s,r)):(t.tag=0,be&&s&&Kp(t),ht(null,t,i,r),t=t.child),t;case 16:n=t.elementType;e:{switch(rl(e,t),e=t.pendingProps,i=n._init,n=i(n._payload),t.type=n,i=t.tag=V_(n),e=cr(n,e),i){case 0:t=df(null,t,n,e,r);break e;case 1:t=sx(null,t,n,e,r);break e;case 11:t=ix(null,t,n,e,r);break e;case 14:t=ax(null,t,n,cr(n.type,e),r);break e}throw Error(F(306,n,""))}return t;case 0:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),df(e,t,n,i,r);case 1:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),sx(e,t,n,i,r);case 3:e:{if(p1(t),e===null)throw Error(F(387));n=t.pendingProps,s=t.memoizedState,i=s.element,Bb(e,t),kl(t,n,null,r);var o=t.memoizedState;if(n=o.element,s.isDehydrated)if(s={element:n,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=s,t.memoizedState=s,t.flags&256){i=ua(Error(F(423)),t),t=ox(e,t,n,r,i);break e}else if(n!==i){i=ua(Error(F(424)),t),t=ox(e,t,n,r,i);break e}else for(zt=Cn(t.stateNode.containerInfo.firstChild),Rt=t,be=!0,dr=null,r=zb(t,null,n,r),t.child=r;r;)r.flags=r.flags&-3|4096,r=r.sibling;else{if(oa(),n===i){t=Xr(e,t,r);break e}ht(e,t,n,r)}t=t.child}return t;case 5:return Fb(t),e===null&&sf(t),n=t.type,i=t.pendingProps,s=e!==null?e.memoizedProps:null,o=i.children,ef(n,i)?o=null:s!==null&&ef(n,s)&&(t.flags|=32),f1(e,t),ht(e,t,o,r),t.child;case 6:return e===null&&sf(t),null;case 13:return h1(e,t,r);case 4:return eh(t,t.stateNode.containerInfo),n=t.pendingProps,e===null?t.child=la(t,null,n,r):ht(e,t,n,r),t.child;case 11:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),ix(e,t,n,i,r);case 7:return ht(e,t,t.pendingProps,r),t.child;case 8:return ht(e,t,t.pendingProps.children,r),t.child;case 12:return ht(e,t,t.pendingProps.children,r),t.child;case 10:e:{if(n=t.type._context,i=t.pendingProps,s=t.memoizedProps,o=i.value,me(Sl,n._currentValue),n._currentValue=o,s!==null)if(gr(s.value,o)){if(s.children===i.children&&!kt.current){t=Xr(e,t,r);break e}}else for(s=t.child,s!==null&&(s.return=t);s!==null;){var l=s.dependencies;if(l!==null){o=s.child;for(var c=l.firstContext;c!==null;){if(c.context===n){if(s.tag===1){c=qr(-1,r&-r),c.tag=2;var d=s.updateQueue;if(d!==null){d=d.shared;var u=d.pending;u===null?c.next=c:(c.next=u.next,u.next=c),d.pending=c}}s.lanes|=r,c=s.alternate,c!==null&&(c.lanes|=r),of(s.return,r,t),l.lanes|=r;break}c=c.next}}else if(s.tag===10)o=s.type===t.type?null:s.child;else if(s.tag===18){if(o=s.return,o===null)throw Error(F(341));o.lanes|=r,l=o.alternate,l!==null&&(l.lanes|=r),of(o,r,t),o=s.sibling}else o=s.child;if(o!==null)o.return=s;else for(o=s;o!==null;){if(o===t){o=null;break}if(s=o.sibling,s!==null){s.return=o.return,o=s;break}o=o.return}s=o}ht(e,t,i.children,r),t=t.child}return t;case 9:return i=t.type,n=t.pendingProps.children,ta(t,r),i=tr(i),n=n(i),t.flags|=1,ht(e,t,n,r),t.child;case 14:return n=t.type,i=cr(n,t.pendingProps),i=cr(n.type,i),ax(e,t,n,i,r);case 15:return u1(e,t,t.type,t.pendingProps,r);case 17:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:cr(n,i),rl(e,t),t.tag=1,Pt(n)?(e=!0,bl(t)):e=!1,ta(t,r),o1(t,n,i),cf(t,n,i,r),ff(null,t,n,!0,e,r);case 19:return m1(e,t,r);case 22:return d1(e,t,r)}throw Error(F(156,t.tag))};function E1(e,t){return ib(e,t)}function K_(e,t,r,n){this.tag=e,this.key=r,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=n,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Zt(e,t,r,n){return new K_(e,t,r,n)}function mh(e){return e=e.prototype,!(!e||!e.isReactComponent)}function V_(e){if(typeof e=="function")return mh(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Mp)return 11;if(e===Ip)return 14}return 2}function Dn(e,t){var r=e.alternate;return r===null?(r=Zt(e.tag,t,e.key,e.mode),r.elementType=e.elementType,r.type=e.type,r.stateNode=e.stateNode,r.alternate=e,e.alternate=r):(r.pendingProps=t,r.type=e.type,r.flags=0,r.subtreeFlags=0,r.deletions=null),r.flags=e.flags&14680064,r.childLanes=e.childLanes,r.lanes=e.lanes,r.child=e.child,r.memoizedProps=e.memoizedProps,r.memoizedState=e.memoizedState,r.updateQueue=e.updateQueue,t=e.dependencies,r.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},r.sibling=e.sibling,r.index=e.index,r.ref=e.ref,r}function al(e,t,r,n,i,s){var o=2;if(n=e,typeof e=="function")mh(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case Li:return oi(r.children,i,s,t);case Tp:o=8,i|=8;break;case Dd:return e=Zt(12,r,t,i|2),e.elementType=Dd,e.lanes=s,e;case Td:return e=Zt(13,r,t,i),e.elementType=Td,e.lanes=s,e;case Md:return e=Zt(19,r,t,i),e.elementType=Md,e.lanes=s,e;case Fv:return $c(r,i,s,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Rv:o=10;break e;case Bv:o=9;break e;case Mp:o=11;break e;case Ip:o=14;break e;case mn:o=16,n=null;break e}throw Error(F(130,e==null?e:typeof e,""))}return t=Zt(o,r,t,i),t.elementType=e,t.type=n,t.lanes=s,t}function oi(e,t,r,n){return e=Zt(7,e,n,t),e.lanes=r,e}function $c(e,t,r,n){return e=Zt(22,e,n,t),e.elementType=Fv,e.lanes=r,e.stateNode={isHidden:!1},e}function ad(e,t,r){return e=Zt(6,e,null,t),e.lanes=r,e}function sd(e,t,r){return t=Zt(4,e.children!==null?e.children:[],e.key,t),t.lanes=r,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Y_(e,t,r,n,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Bu(0),this.expirationTimes=Bu(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Bu(0),this.identifierPrefix=n,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function gh(e,t,r,n,i,s,o,l,c){return e=new Y_(e,t,r,l,c),t===1?(t=1,s===!0&&(t|=8)):t=0,s=Zt(3,null,null,t),e.current=s,s.stateNode=e,s.memoizedState={element:n,isDehydrated:r,cache:null,transitions:null,pendingSuspenseBoundaries:null},Qp(s),e}function G_(e,t,r){var n=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(I1)}catch(e){console.error(e)}}I1(),Iv.exports=Ut;var bh=Iv.exports,vx=bh;Od.createRoot=vx.createRoot,Od.hydrateRoot=vx.hydrateRoot;/** + * @remix-run/router v1.23.1 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Os(){return Os=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function jh(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function tC(){return Math.random().toString(36).substr(2,8)}function jx(e,t){return{usr:e.state,key:e.key,idx:t}}function Nf(e,t,r,n){return r===void 0&&(r=null),Os({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?wa(t):t,{state:r,key:t&&t.key||n||tC()})}function Ml(e){let{pathname:t="/",search:r="",hash:n=""}=e;return r&&r!=="?"&&(t+=r.charAt(0)==="?"?r:"?"+r),n&&n!=="#"&&(t+=n.charAt(0)==="#"?n:"#"+n),t}function wa(e){let t={};if(e){let r=e.indexOf("#");r>=0&&(t.hash=e.substr(r),e=e.substr(0,r));let n=e.indexOf("?");n>=0&&(t.search=e.substr(n),e=e.substr(0,n)),e&&(t.pathname=e)}return t}function rC(e,t,r,n){n===void 0&&(n={});let{window:i=document.defaultView,v5Compat:s=!1}=n,o=i.history,l=Nn.Pop,c=null,d=u();d==null&&(d=0,o.replaceState(Os({},o.state,{idx:d}),""));function u(){return(o.state||{idx:null}).idx}function f(){l=Nn.Pop;let b=u(),v=b==null?null:b-d;d=b,c&&c({action:l,location:g.location,delta:v})}function p(b,v){l=Nn.Push;let j=Nf(g.location,b,v);d=u()+1;let y=jx(j,d),w=g.createHref(j);try{o.pushState(y,"",w)}catch(S){if(S instanceof DOMException&&S.name==="DataCloneError")throw S;i.location.assign(w)}s&&c&&c({action:l,location:g.location,delta:1})}function m(b,v){l=Nn.Replace;let j=Nf(g.location,b,v);d=u();let y=jx(j,d),w=g.createHref(j);o.replaceState(y,"",w),s&&c&&c({action:l,location:g.location,delta:0})}function x(b){let v=i.location.origin!=="null"?i.location.origin:i.location.href,j=typeof b=="string"?b:Ml(b);return j=j.replace(/ $/,"%20"),Oe(v,"No window.location.(origin|href) available to create URL for href: "+j),new URL(j,v)}let g={get action(){return l},get location(){return e(i,o)},listen(b){if(c)throw new Error("A history only accepts one active listener");return i.addEventListener(bx,f),c=b,()=>{i.removeEventListener(bx,f),c=null}},createHref(b){return t(i,b)},createURL:x,encodeLocation(b){let v=x(b);return{pathname:v.pathname,search:v.search,hash:v.hash}},push:p,replace:m,go(b){return o.go(b)}};return g}var wx;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(wx||(wx={}));function nC(e,t,r){return r===void 0&&(r="/"),iC(e,t,r)}function iC(e,t,r,n){let i=typeof t=="string"?wa(t):t,s=wh(i.pathname||"/",r);if(s==null)return null;let o=$1(e);aC(o);let l=null;for(let c=0;l==null&&c{let c={relativePath:l===void 0?s.path||"":l,caseSensitive:s.caseSensitive===!0,childrenIndex:o,route:s};c.relativePath.startsWith("/")&&(Oe(c.relativePath.startsWith(n),'Absolute route path "'+c.relativePath+'" nested under path '+('"'+n+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),c.relativePath=c.relativePath.slice(n.length));let d=Tn([n,c.relativePath]),u=r.concat(c);s.children&&s.children.length>0&&(Oe(s.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+d+'".')),$1(s.children,t,u,d)),!(s.path==null&&!s.index)&&t.push({path:d,score:fC(d,s.index),routesMeta:u})};return e.forEach((s,o)=>{var l;if(s.path===""||!((l=s.path)!=null&&l.includes("?")))i(s,o);else for(let c of L1(s.path))i(s,o,c)}),t}function L1(e){let t=e.split("/");if(t.length===0)return[];let[r,...n]=t,i=r.endsWith("?"),s=r.replace(/\?$/,"");if(n.length===0)return i?[s,""]:[s];let o=L1(n.join("/")),l=[];return l.push(...o.map(c=>c===""?s:[s,c].join("/"))),i&&l.push(...o),l.map(c=>e.startsWith("/")&&c===""?"/":c)}function aC(e){e.sort((t,r)=>t.score!==r.score?r.score-t.score:pC(t.routesMeta.map(n=>n.childrenIndex),r.routesMeta.map(n=>n.childrenIndex)))}const sC=/^:[\w-]+$/,oC=3,lC=2,cC=1,uC=10,dC=-2,Sx=e=>e==="*";function fC(e,t){let r=e.split("/"),n=r.length;return r.some(Sx)&&(n+=dC),t&&(n+=lC),r.filter(i=>!Sx(i)).reduce((i,s)=>i+(sC.test(s)?oC:s===""?cC:uC),n)}function pC(e,t){return e.length===t.length&&e.slice(0,-1).every((n,i)=>n===t[i])?e[e.length-1]-t[t.length-1]:0}function hC(e,t,r){let{routesMeta:n}=e,i={},s="/",o=[];for(let l=0;l{let{paramName:p,isOptional:m}=u;if(p==="*"){let g=l[f]||"";o=s.slice(0,s.length-g.length).replace(/(.)\/+$/,"$1")}const x=l[f];return m&&!x?d[p]=void 0:d[p]=(x||"").replace(/%2F/g,"/"),d},{}),pathname:s,pathnameBase:o,pattern:e}}function gC(e,t,r){t===void 0&&(t=!1),r===void 0&&(r=!0),jh(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let n=[],i="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(o,l,c)=>(n.push({paramName:l,isOptional:c!=null}),c?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(n.push({paramName:"*"}),i+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?i+="\\/*$":e!==""&&e!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,t?void 0:"i"),n]}function xC(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return jh(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function wh(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let r=t.endsWith("/")?t.length-1:t.length,n=e.charAt(r);return n&&n!=="/"?null:e.slice(r)||"/"}const yC=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,vC=e=>yC.test(e);function bC(e,t){t===void 0&&(t="/");let{pathname:r,search:n="",hash:i=""}=typeof e=="string"?wa(e):e,s;if(r)if(vC(r))s=r;else{if(r.includes("//")){let o=r;r=r.replace(/\/\/+/g,"/"),jh(!1,"Pathnames cannot have embedded double slashes - normalizing "+(o+" -> "+r))}r.startsWith("/")?s=Nx(r.substring(1),"/"):s=Nx(r,t)}else s=t;return{pathname:s,search:SC(n),hash:NC(i)}}function Nx(e,t){let r=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(i=>{i===".."?r.length>1&&r.pop():i!=="."&&r.push(i)}),r.length>1?r.join("/"):"/"}function od(e,t,r,n){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(n)+"]. Please separate it out to the ")+("`to."+r+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function jC(e){return e.filter((t,r)=>r===0||t.route.path&&t.route.path.length>0)}function Sh(e,t){let r=jC(e);return t?r.map((n,i)=>i===r.length-1?n.pathname:n.pathnameBase):r.map(n=>n.pathnameBase)}function Nh(e,t,r,n){n===void 0&&(n=!1);let i;typeof e=="string"?i=wa(e):(i=Os({},e),Oe(!i.pathname||!i.pathname.includes("?"),od("?","pathname","search",i)),Oe(!i.pathname||!i.pathname.includes("#"),od("#","pathname","hash",i)),Oe(!i.search||!i.search.includes("#"),od("#","search","hash",i)));let s=e===""||i.pathname==="",o=s?"/":i.pathname,l;if(o==null)l=r;else{let f=t.length-1;if(!n&&o.startsWith("..")){let p=o.split("/");for(;p[0]==="..";)p.shift(),f-=1;i.pathname=p.join("/")}l=f>=0?t[f]:"/"}let c=bC(i,l),d=o&&o!=="/"&&o.endsWith("/"),u=(s||o===".")&&r.endsWith("/");return!c.pathname.endsWith("/")&&(d||u)&&(c.pathname+="/"),c}const Tn=e=>e.join("/").replace(/\/\/+/g,"/"),wC=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),SC=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,NC=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function kC(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const z1=["post","put","patch","delete"];new Set(z1);const PC=["get",...z1];new Set(PC);/** + * React Router v6.30.2 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Es(){return Es=Object.assign?Object.assign.bind():function(e){for(var t=1;t{l.current=!0}),h.useCallback(function(d,u){if(u===void 0&&(u={}),!l.current)return;if(typeof d=="number"){n.go(d);return}let f=Nh(d,JSON.parse(o),s,u.relative==="path");e==null&&t!=="/"&&(f.pathname=f.pathname==="/"?t:Tn([t,f.pathname])),(u.replace?n.replace:n.push)(f,u.state,u)},[t,n,o,s,e])}function Na(){let{matches:e}=h.useContext(on),t=e[e.length-1];return t?t.params:{}}function F1(e,t){let{relative:r}=t===void 0?{}:t,{future:n}=h.useContext(Un),{matches:i}=h.useContext(on),{pathname:s}=_i(),o=JSON.stringify(Sh(i,n.v7_relativeSplatPath));return h.useMemo(()=>Nh(e,JSON.parse(o),s,r==="path"),[e,o,s,r])}function OC(e,t){return EC(e,t)}function EC(e,t,r,n){Sa()||Oe(!1);let{navigator:i}=h.useContext(Un),{matches:s}=h.useContext(on),o=s[s.length-1],l=o?o.params:{};o&&o.pathname;let c=o?o.pathnameBase:"/";o&&o.route;let d=_i(),u;if(t){var f;let b=typeof t=="string"?wa(t):t;c==="/"||(f=b.pathname)!=null&&f.startsWith(c)||Oe(!1),u=b}else u=d;let p=u.pathname||"/",m=p;if(c!=="/"){let b=c.replace(/^\//,"").split("/");m="/"+p.replace(/^\//,"").split("/").slice(b.length).join("/")}let x=nC(e,{pathname:m}),g=$C(x&&x.map(b=>Object.assign({},b,{params:Object.assign({},l,b.params),pathname:Tn([c,i.encodeLocation?i.encodeLocation(b.pathname).pathname:b.pathname]),pathnameBase:b.pathnameBase==="/"?c:Tn([c,i.encodeLocation?i.encodeLocation(b.pathnameBase).pathname:b.pathnameBase])})),s,r,n);return t&&g?h.createElement(Fc.Provider,{value:{location:Es({pathname:"/",search:"",hash:"",state:null,key:"default"},u),navigationType:Nn.Pop}},g):g}function DC(){let e=BC(),t=kC(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,i={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return h.createElement(h.Fragment,null,h.createElement("h2",null,"Unexpected Application Error!"),h.createElement("h3",{style:{fontStyle:"italic"}},t),r?h.createElement("pre",{style:i},r):null,null)}const TC=h.createElement(DC,null);class MC extends h.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,r){return r.location!==t.location||r.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:r.error,location:r.location,revalidation:t.revalidation||r.revalidation}}componentDidCatch(t,r){console.error("React Router caught the following error during render",t,r)}render(){return this.state.error!==void 0?h.createElement(on.Provider,{value:this.props.routeContext},h.createElement(R1.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function IC(e){let{routeContext:t,match:r,children:n}=e,i=h.useContext(kh);return i&&i.static&&i.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(i.staticContext._deepestRenderedBoundaryId=r.route.id),h.createElement(on.Provider,{value:t},n)}function $C(e,t,r,n){var i;if(t===void 0&&(t=[]),r===void 0&&(r=null),n===void 0&&(n=null),e==null){var s;if(!r)return null;if(r.errors)e=r.matches;else if((s=n)!=null&&s.v7_partialHydration&&t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let o=e,l=(i=r)==null?void 0:i.errors;if(l!=null){let u=o.findIndex(f=>f.route.id&&(l==null?void 0:l[f.route.id])!==void 0);u>=0||Oe(!1),o=o.slice(0,Math.min(o.length,u+1))}let c=!1,d=-1;if(r&&n&&n.v7_partialHydration)for(let u=0;u=0?o=o.slice(0,d+1):o=[o[0]];break}}}return o.reduceRight((u,f,p)=>{let m,x=!1,g=null,b=null;r&&(m=l&&f.route.id?l[f.route.id]:void 0,g=f.route.errorElement||TC,c&&(d<0&&p===0?(WC("route-fallback"),x=!0,b=null):d===p&&(x=!0,b=f.route.hydrateFallbackElement||null)));let v=t.concat(o.slice(0,p+1)),j=()=>{let y;return m?y=g:x?y=b:f.route.Component?y=h.createElement(f.route.Component,null):f.route.element?y=f.route.element:y=u,h.createElement(IC,{match:f,routeContext:{outlet:u,matches:v,isDataRoute:r!=null},children:y})};return r&&(f.route.ErrorBoundary||f.route.errorElement||p===0)?h.createElement(MC,{location:r.location,revalidation:r.revalidation,component:g,error:m,children:j(),routeContext:{outlet:null,matches:v,isDataRoute:!0}}):j()},null)}var W1=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(W1||{}),U1=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(U1||{});function LC(e){let t=h.useContext(kh);return t||Oe(!1),t}function zC(e){let t=h.useContext(_C);return t||Oe(!1),t}function RC(e){let t=h.useContext(on);return t||Oe(!1),t}function q1(e){let t=RC(),r=t.matches[t.matches.length-1];return r.route.id||Oe(!1),r.route.id}function BC(){var e;let t=h.useContext(R1),r=zC(),n=q1();return t!==void 0?t:(e=r.errors)==null?void 0:e[n]}function FC(){let{router:e}=LC(W1.UseNavigateStable),t=q1(U1.UseNavigateStable),r=h.useRef(!1);return B1(()=>{r.current=!0}),h.useCallback(function(i,s){s===void 0&&(s={}),r.current&&(typeof i=="number"?e.navigate(i):e.navigate(i,Es({fromRouteId:t},s)))},[e,t])}const kx={};function WC(e,t,r){kx[e]||(kx[e]=!0)}function UC(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function H1(e){let{to:t,replace:r,state:n,relative:i}=e;Sa()||Oe(!1);let{future:s,static:o}=h.useContext(Un),{matches:l}=h.useContext(on),{pathname:c}=_i(),d=dt(),u=Nh(t,Sh(l,s.v7_relativeSplatPath),c,i==="path"),f=JSON.stringify(u);return h.useEffect(()=>d(JSON.parse(f),{replace:r,state:n,relative:i}),[d,f,i,r,n]),null}function de(e){Oe(!1)}function qC(e){let{basename:t="/",children:r=null,location:n,navigationType:i=Nn.Pop,navigator:s,static:o=!1,future:l}=e;Sa()&&Oe(!1);let c=t.replace(/^\/*/,"/"),d=h.useMemo(()=>({basename:c,navigator:s,static:o,future:Es({v7_relativeSplatPath:!1},l)}),[c,l,s,o]);typeof n=="string"&&(n=wa(n));let{pathname:u="/",search:f="",hash:p="",state:m=null,key:x="default"}=n,g=h.useMemo(()=>{let b=wh(u,c);return b==null?null:{location:{pathname:b,search:f,hash:p,state:m,key:x},navigationType:i}},[c,u,f,p,m,x,i]);return g==null?null:h.createElement(Un.Provider,{value:d},h.createElement(Fc.Provider,{children:r,value:g}))}function HC(e){let{children:t,location:r}=e;return OC(kf(t),r)}new Promise(()=>{});function kf(e,t){t===void 0&&(t=[]);let r=[];return h.Children.forEach(e,(n,i)=>{if(!h.isValidElement(n))return;let s=[...t,i];if(n.type===h.Fragment){r.push.apply(r,kf(n.props.children,s));return}n.type!==de&&Oe(!1),!n.props.index||!n.props.children||Oe(!1);let o={id:n.props.id||s.join("-"),caseSensitive:n.props.caseSensitive,element:n.props.element,Component:n.props.Component,index:n.props.index,path:n.props.path,loader:n.props.loader,action:n.props.action,errorElement:n.props.errorElement,ErrorBoundary:n.props.ErrorBoundary,hasErrorBoundary:n.props.ErrorBoundary!=null||n.props.errorElement!=null,shouldRevalidate:n.props.shouldRevalidate,handle:n.props.handle,lazy:n.props.lazy};n.props.children&&(o.children=kf(n.props.children,s)),r.push(o)}),r}/** + * React Router DOM v6.30.2 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Pf(){return Pf=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(r[i]=e[i]);return r}function VC(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function YC(e,t){return e.button===0&&(!t||t==="_self")&&!VC(e)}function _f(e){return e===void 0&&(e=""),new URLSearchParams(typeof e=="string"||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,r)=>{let n=e[r];return t.concat(Array.isArray(n)?n.map(i=>[r,i]):[[r,n]])},[]))}function GC(e,t){let r=_f(e);return t&&t.forEach((n,i)=>{r.has(i)||t.getAll(i).forEach(s=>{r.append(i,s)})}),r}const ZC=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],XC="6";try{window.__reactRouterVersion=XC}catch{}const JC="startTransition",Px=Tv[JC];function QC(e){let{basename:t,children:r,future:n,window:i}=e,s=h.useRef();s.current==null&&(s.current=eC({window:i,v5Compat:!0}));let o=s.current,[l,c]=h.useState({action:o.action,location:o.location}),{v7_startTransition:d}=n||{},u=h.useCallback(f=>{d&&Px?Px(()=>c(f)):c(f)},[c,d]);return h.useLayoutEffect(()=>o.listen(u),[o,u]),h.useEffect(()=>UC(n),[n]),h.createElement(qC,{basename:t,children:r,location:l.location,navigationType:l.action,navigator:o,future:n})}const eA=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",tA=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,K1=h.forwardRef(function(t,r){let{onClick:n,relative:i,reloadDocument:s,replace:o,state:l,target:c,to:d,preventScrollReset:u,viewTransition:f}=t,p=KC(t,ZC),{basename:m}=h.useContext(Un),x,g=!1;if(typeof d=="string"&&tA.test(d)&&(x=d,eA))try{let y=new URL(window.location.href),w=d.startsWith("//")?new URL(y.protocol+d):new URL(d),S=wh(w.pathname,m);w.origin===y.origin&&S!=null?d=S+w.search+w.hash:g=!0}catch{}let b=CC(d,{relative:i}),v=rA(d,{replace:o,state:l,target:c,preventScrollReset:u,relative:i,viewTransition:f});function j(y){n&&n(y),y.defaultPrevented||v(y)}return h.createElement("a",Pf({},p,{href:x||b,onClick:g||s?n:j,ref:r,target:c}))});var _x;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(_x||(_x={}));var Cx;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(Cx||(Cx={}));function rA(e,t){let{target:r,replace:n,state:i,preventScrollReset:s,relative:o,viewTransition:l}=t===void 0?{}:t,c=dt(),d=_i(),u=F1(e,{relative:o});return h.useCallback(f=>{if(YC(f,r)){f.preventDefault();let p=n!==void 0?n:Ml(d)===Ml(u);c(e,{replace:p,state:i,preventScrollReset:s,relative:o,viewTransition:l})}},[d,c,u,n,i,r,e,s,o,l])}function nA(e){let t=h.useRef(_f(e)),r=h.useRef(!1),n=_i(),i=h.useMemo(()=>GC(n.search,r.current?null:t.current),[n.search]),s=dt(),o=h.useCallback((l,c)=>{const d=_f(typeof l=="function"?l(i):l);r.current=!0,s("?"+d,c)},[s,i]);return[i,o]}const iA={},Ax=e=>{let t;const r=new Set,n=(u,f)=>{const p=typeof u=="function"?u(t):u;if(!Object.is(p,t)){const m=t;t=f??(typeof p!="object"||p===null)?p:Object.assign({},t,p),r.forEach(x=>x(t,m))}},i=()=>t,c={setState:n,getState:i,getInitialState:()=>d,subscribe:u=>(r.add(u),()=>r.delete(u)),destroy:()=>{(iA?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),r.clear()}},d=t=e(n,i,c);return c},aA=e=>e?Ax(e):Ax;var V1={exports:{}},Y1={},G1={exports:{}},Z1={};/** + * @license React + * use-sync-external-store-shim.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var fa=h;function sA(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var oA=typeof Object.is=="function"?Object.is:sA,lA=fa.useState,cA=fa.useEffect,uA=fa.useLayoutEffect,dA=fa.useDebugValue;function fA(e,t){var r=t(),n=lA({inst:{value:r,getSnapshot:t}}),i=n[0].inst,s=n[1];return uA(function(){i.value=r,i.getSnapshot=t,ld(i)&&s({inst:i})},[e,r,t]),cA(function(){return ld(i)&&s({inst:i}),e(function(){ld(i)&&s({inst:i})})},[e]),dA(r),r}function ld(e){var t=e.getSnapshot;e=e.value;try{var r=t();return!oA(e,r)}catch{return!0}}function pA(e,t){return t()}var hA=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?pA:fA;Z1.useSyncExternalStore=fa.useSyncExternalStore!==void 0?fa.useSyncExternalStore:hA;G1.exports=Z1;var mA=G1.exports;/** + * @license React + * use-sync-external-store-shim/with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Wc=h,gA=mA;function xA(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var yA=typeof Object.is=="function"?Object.is:xA,vA=gA.useSyncExternalStore,bA=Wc.useRef,jA=Wc.useEffect,wA=Wc.useMemo,SA=Wc.useDebugValue;Y1.useSyncExternalStoreWithSelector=function(e,t,r,n,i){var s=bA(null);if(s.current===null){var o={hasValue:!1,value:null};s.current=o}else o=s.current;s=wA(function(){function c(m){if(!d){if(d=!0,u=m,m=n(m),i!==void 0&&o.hasValue){var x=o.value;if(i(x,m))return f=x}return f=m}if(x=f,yA(u,m))return x;var g=n(m);return i!==void 0&&i(x,g)?(u=m,x):(u=m,f=g)}var d=!1,u,f,p=r===void 0?null:r;return[function(){return c(t())},p===null?void 0:function(){return c(p())}]},[t,r,n,i]);var l=vA(e,s[0],s[1]);return jA(function(){o.hasValue=!0,o.value=l},[l]),SA(l),l};V1.exports=Y1;var X1=V1.exports;const NA=Tr(X1),J1={},{useDebugValue:kA}=fs,{useSyncExternalStoreWithSelector:PA}=NA;let Ox=!1;const _A=e=>e;function CA(e,t=_A,r){(J1?"production":void 0)!=="production"&&r&&!Ox&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),Ox=!0);const n=PA(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,r);return kA(n),n}const Ex=e=>{(J1?"production":void 0)!=="production"&&typeof e!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const t=typeof e=="function"?aA(e):e,r=(n,i)=>CA(t,n,i);return Object.assign(r,t),r},AA=e=>e?Ex(e):Ex,OA="https://dispos.crawlsy.com";class EA{constructor(t){ho(this,"baseUrl");this.baseUrl=t}getHeaders(){const t=localStorage.getItem("token");return{"Content-Type":"application/json",...t?{Authorization:`Bearer ${t}`}:{}}}async request(t,r){const n=`${this.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...this.getHeaders(),...r==null?void 0:r.headers}});if(!i.ok){const s=await i.json().catch(()=>({error:"Request failed"}));throw new Error(s.error||`HTTP ${i.status}`)}return i.json()}async login(t,r){return this.request("/api/auth/login",{method:"POST",body:JSON.stringify({email:t,password:r})})}async getMe(){return this.request("/api/auth/me")}async getDashboardStats(){return this.request("/api/dashboard/stats")}async getDashboardActivity(){return this.request("/api/dashboard/activity")}async getStores(){return this.request("/api/stores")}async getStore(t){return this.request(`/api/stores/${t}`)}async createStore(t){return this.request("/api/stores",{method:"POST",body:JSON.stringify(t)})}async updateStore(t,r){return this.request(`/api/stores/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getDispensaries(){return this.request("/api/dispensaries")}async getDispensary(t){return this.request(`/api/dispensaries/${t}`)}async updateDispensary(t,r){return this.request(`/api/dispensaries/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteStore(t){return this.request(`/api/stores/${t}`,{method:"DELETE"})}async scrapeStore(t,r,n){return this.request(`/api/stores/${t}/scrape`,{method:"POST",body:JSON.stringify({parallel:r,userAgent:n})})}async downloadStoreImages(t){return this.request(`/api/stores/${t}/download-images`,{method:"POST"})}async discoverStoreCategories(t){return this.request(`/api/stores/${t}/discover-categories`,{method:"POST"})}async debugScrapeStore(t){return this.request(`/api/stores/${t}/debug-scrape`,{method:"POST"})}async getStoreBrands(t){return this.request(`/api/stores/${t}/brands`)}async getStoreSpecials(t,r){const n=r?`?date=${r}`:"";return this.request(`/api/stores/${t}/specials${n}`)}async getCategories(t){const r=t?`?store_id=${t}`:"";return this.request(`/api/categories${r}`)}async getCategoryTree(t){return this.request(`/api/categories/tree?store_id=${t}`)}async getProducts(t){const r=new URLSearchParams(t).toString();return this.request(`/api/products${r?`?${r}`:""}`)}async getProduct(t){return this.request(`/api/products/${t}`)}async getCampaigns(){return this.request("/api/campaigns")}async getCampaign(t){return this.request(`/api/campaigns/${t}`)}async createCampaign(t){return this.request("/api/campaigns",{method:"POST",body:JSON.stringify(t)})}async updateCampaign(t,r){return this.request(`/api/campaigns/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteCampaign(t){return this.request(`/api/campaigns/${t}`,{method:"DELETE"})}async addProductToCampaign(t,r,n){return this.request(`/api/campaigns/${t}/products`,{method:"POST",body:JSON.stringify({product_id:r,display_order:n})})}async removeProductFromCampaign(t,r){return this.request(`/api/campaigns/${t}/products/${r}`,{method:"DELETE"})}async getAnalyticsOverview(t){return this.request(`/api/analytics/overview${t?`?days=${t}`:""}`)}async getProductAnalytics(t,r){return this.request(`/api/analytics/products/${t}${r?`?days=${r}`:""}`)}async getCampaignAnalytics(t,r){return this.request(`/api/analytics/campaigns/${t}${r?`?days=${r}`:""}`)}async getSettings(){return this.request("/api/settings")}async updateSetting(t,r){return this.request(`/api/settings/${t}`,{method:"PUT",body:JSON.stringify({value:r})})}async updateSettings(t){return this.request("/api/settings",{method:"PUT",body:JSON.stringify({settings:t})})}async getProxies(){return this.request("/api/proxies")}async getProxy(t){return this.request(`/api/proxies/${t}`)}async addProxy(t){return this.request("/api/proxies",{method:"POST",body:JSON.stringify(t)})}async addProxiesBulk(t){return this.request("/api/proxies/bulk",{method:"POST",body:JSON.stringify({proxies:t})})}async testProxy(t){return this.request(`/api/proxies/${t}/test`,{method:"POST"})}async testAllProxies(){return this.request("/api/proxies/test-all",{method:"POST"})}async getProxyTestJob(t){return this.request(`/api/proxies/test-job/${t}`)}async getActiveProxyTestJob(){return this.request("/api/proxies/test-job")}async cancelProxyTestJob(t){return this.request(`/api/proxies/test-job/${t}/cancel`,{method:"POST"})}async updateProxy(t,r){return this.request(`/api/proxies/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteProxy(t){return this.request(`/api/proxies/${t}`,{method:"DELETE"})}async updateProxyLocations(){return this.request("/api/proxies/update-locations",{method:"POST"})}async getLogs(t,r,n){const i=new URLSearchParams;return t&&i.append("limit",t.toString()),r&&i.append("level",r),n&&i.append("category",n),this.request(`/api/logs?${i.toString()}`)}async clearLogs(){return this.request("/api/logs",{method:"DELETE"})}async getActiveScrapers(){return this.request("/api/scraper-monitor/active")}async getScraperHistory(t){const r=t?`?store_id=${t}`:"";return this.request(`/api/scraper-monitor/history${r}`)}async getJobStats(t){const r=t?`?dispensary_id=${t}`:"";return this.request(`/api/scraper-monitor/jobs/stats${r}`)}async getActiveJobs(t){const r=t?`?dispensary_id=${t}`:"";return this.request(`/api/scraper-monitor/jobs/active${r}`)}async getRecentJobs(t){const r=new URLSearchParams;t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.dispensaryId&&r.append("dispensary_id",t.dispensaryId.toString()),t!=null&&t.status&&r.append("status",t.status);const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/scraper-monitor/jobs/recent${n}`)}async getWorkerStats(t){const r=t?`?dispensary_id=${t}`:"";return this.request(`/api/scraper-monitor/jobs/workers${r}`)}async getAZMonitorActiveJobs(){return this.request("/api/az/monitor/active-jobs")}async getAZMonitorRecentJobs(t){const r=t?`?limit=${t}`:"";return this.request(`/api/az/monitor/recent-jobs${r}`)}async getAZMonitorErrors(t){const r=new URLSearchParams;t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.hours&&r.append("hours",t.hours.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/monitor/errors${n}`)}async getAZMonitorSummary(){return this.request("/api/az/monitor/summary")}async getChanges(t){const r=t?`?status=${t}`:"";return this.request(`/api/changes${r}`)}async getChangeStats(){return this.request("/api/changes/stats")}async approveChange(t){return this.request(`/api/changes/${t}/approve`,{method:"POST"})}async rejectChange(t,r){return this.request(`/api/changes/${t}/reject`,{method:"POST",body:JSON.stringify({reason:r})})}async getDispensaryProducts(t,r){const n=r?`?category=${r}`:"";return this.request(`/api/dispensaries/${t}/products${n}`)}async getDispensaryBrands(t){return this.request(`/api/dispensaries/${t}/brands`)}async getDispensarySpecials(t){return this.request(`/api/dispensaries/${t}/specials`)}async getApiPermissions(){return this.request("/api/api-permissions")}async getApiPermissionDispensaries(){return this.request("/api/api-permissions/dispensaries")}async createApiPermission(t){return this.request("/api/api-permissions",{method:"POST",body:JSON.stringify(t)})}async updateApiPermission(t,r){return this.request(`/api/api-permissions/${t}`,{method:"PUT",body:JSON.stringify(r)})}async toggleApiPermission(t){return this.request(`/api/api-permissions/${t}/toggle`,{method:"PATCH"})}async deleteApiPermission(t){return this.request(`/api/api-permissions/${t}`,{method:"DELETE"})}async getGlobalSchedule(){return this.request("/api/schedule/global")}async updateGlobalSchedule(t,r){return this.request(`/api/schedule/global/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getStoreSchedules(){return this.request("/api/schedule/stores")}async getStoreSchedule(t){return this.request(`/api/schedule/stores/${t}`)}async updateStoreSchedule(t,r){return this.request(`/api/schedule/stores/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getDispensarySchedules(t){const r=new URLSearchParams;t!=null&&t.state&&r.append("state",t.state),t!=null&&t.search&&r.append("search",t.search);const n=r.toString();return this.request(`/api/schedule/dispensaries${n?`?${n}`:""}`)}async getDispensarySchedule(t){return this.request(`/api/schedule/dispensaries/${t}`)}async updateDispensarySchedule(t,r){return this.request(`/api/schedule/dispensaries/${t}`,{method:"PUT",body:JSON.stringify(r)})}async getDispensaryCrawlJobs(t){const r=t?`?limit=${t}`:"";return this.request(`/api/schedule/dispensary-jobs${r}`)}async triggerDispensaryCrawl(t){return this.request(`/api/schedule/trigger/dispensary/${t}`,{method:"POST"})}async resolvePlatformId(t){return this.request(`/api/schedule/dispensaries/${t}/resolve-platform-id`,{method:"POST"})}async detectMenuType(t){return this.request(`/api/schedule/dispensaries/${t}/detect-menu-type`,{method:"POST"})}async refreshDetection(t){return this.request(`/api/schedule/dispensaries/${t}/refresh-detection`,{method:"POST"})}async toggleDispensarySchedule(t,r){return this.request(`/api/schedule/dispensaries/${t}/toggle-active`,{method:"PUT",body:JSON.stringify({is_active:r})})}async deleteDispensarySchedule(t){return this.request(`/api/schedule/dispensaries/${t}/schedule`,{method:"DELETE"})}async getCrawlJobs(t){const r=t?`?limit=${t}`:"";return this.request(`/api/schedule/jobs${r}`)}async getStoreCrawlJobs(t,r){const n=r?`?limit=${r}`:"";return this.request(`/api/schedule/jobs/store/${t}${n}`)}async cancelCrawlJob(t){return this.request(`/api/schedule/jobs/${t}/cancel`,{method:"POST"})}async triggerStoreCrawl(t){return this.request(`/api/schedule/trigger/store/${t}`,{method:"POST"})}async triggerAllCrawls(){return this.request("/api/schedule/trigger/all",{method:"POST"})}async restartScheduler(){return this.request("/api/schedule/restart",{method:"POST"})}async getVersion(){return this.request("/api/version")}async getDutchieAZDashboard(){return this.request("/api/az/dashboard")}async getDutchieAZSchedules(){return this.request("/api/az/admin/schedules")}async getDutchieAZSchedule(t){return this.request(`/api/az/admin/schedules/${t}`)}async createDutchieAZSchedule(t){return this.request("/api/az/admin/schedules",{method:"POST",body:JSON.stringify(t)})}async updateDutchieAZSchedule(t,r){return this.request(`/api/az/admin/schedules/${t}`,{method:"PUT",body:JSON.stringify(r)})}async deleteDutchieAZSchedule(t){return this.request(`/api/az/admin/schedules/${t}`,{method:"DELETE"})}async triggerDutchieAZSchedule(t){return this.request(`/api/az/admin/schedules/${t}/trigger`,{method:"POST"})}async initDutchieAZSchedules(){return this.request("/api/az/admin/schedules/init",{method:"POST"})}async getDutchieAZScheduleLogs(t,r,n){const i=new URLSearchParams;r&&i.append("limit",r.toString()),n&&i.append("offset",n.toString());const s=i.toString()?`?${i.toString()}`:"";return this.request(`/api/az/admin/schedules/${t}/logs${s}`)}async getDutchieAZRunLogs(t){const r=new URLSearchParams;t!=null&&t.scheduleId&&r.append("scheduleId",t.scheduleId.toString()),t!=null&&t.jobName&&r.append("jobName",t.jobName),t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.offset&&r.append("offset",t.offset.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/admin/run-logs${n}`)}async getDutchieAZSchedulerStatus(){return this.request("/api/az/admin/scheduler/status")}async startDutchieAZScheduler(){return this.request("/api/az/admin/scheduler/start",{method:"POST"})}async stopDutchieAZScheduler(){return this.request("/api/az/admin/scheduler/stop",{method:"POST"})}async triggerDutchieAZImmediateCrawl(){return this.request("/api/az/admin/scheduler/trigger",{method:"POST"})}async getDutchieAZStores(t){const r=new URLSearchParams;t!=null&&t.city&&r.append("city",t.city),(t==null?void 0:t.hasPlatformId)!==void 0&&r.append("hasPlatformId",String(t.hasPlatformId)),t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.offset&&r.append("offset",t.offset.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/stores${n}`)}async getDutchieAZStore(t){return this.request(`/api/az/stores/${t}`)}async getDutchieAZStoreSummary(t){return this.request(`/api/az/stores/${t}/summary`)}async getDutchieAZStoreProducts(t,r){const n=new URLSearchParams;r!=null&&r.stockStatus&&n.append("stockStatus",r.stockStatus),r!=null&&r.type&&n.append("type",r.type),r!=null&&r.subcategory&&n.append("subcategory",r.subcategory),r!=null&&r.brandName&&n.append("brandName",r.brandName),r!=null&&r.search&&n.append("search",r.search),r!=null&&r.limit&&n.append("limit",r.limit.toString()),r!=null&&r.offset&&n.append("offset",r.offset.toString());const i=n.toString()?`?${n.toString()}`:"";return this.request(`/api/az/stores/${t}/products${i}`)}async getDutchieAZStoreBrands(t){return this.request(`/api/az/stores/${t}/brands`)}async getDutchieAZStoreCategories(t){return this.request(`/api/az/stores/${t}/categories`)}async getDutchieAZBrands(t){const r=new URLSearchParams;t!=null&&t.limit&&r.append("limit",t.limit.toString()),t!=null&&t.offset&&r.append("offset",t.offset.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/brands${n}`)}async getDutchieAZCategories(){return this.request("/api/az/categories")}async getDutchieAZDebugSummary(){return this.request("/api/az/debug/summary")}async getDutchieAZDebugStore(t){return this.request(`/api/az/debug/store/${t}`)}async triggerDutchieAZCrawl(t,r){return this.request(`/api/az/admin/crawl/${t}`,{method:"POST",body:JSON.stringify(r||{})})}async getDetectionStats(){return this.request("/api/az/admin/detection/stats")}async getDispensariesNeedingDetection(t){const r=new URLSearchParams;t!=null&&t.state&&r.append("state",t.state),t!=null&&t.limit&&r.append("limit",t.limit.toString());const n=r.toString()?`?${r.toString()}`:"";return this.request(`/api/az/admin/detection/pending${n}`)}async detectDispensary(t){return this.request(`/api/az/admin/detection/detect/${t}`,{method:"POST"})}async detectAllDispensaries(t){return this.request("/api/az/admin/detection/detect-all",{method:"POST",body:JSON.stringify(t||{})})}async triggerMenuDetectionJob(){return this.request("/api/az/admin/detection/trigger",{method:"POST"})}}const B=new EA(OA),Ph=AA(e=>({user:null,token:localStorage.getItem("token"),isAuthenticated:!!localStorage.getItem("token"),login:async(t,r)=>{const n=await B.login(t,r);localStorage.setItem("token",n.token),e({user:n.user,token:n.token,isAuthenticated:!0})},logout:()=>{localStorage.removeItem("token"),e({user:null,token:null,isAuthenticated:!1})},checkAuth:async()=>{try{const t=await B.getMe();e({user:t.user,isAuthenticated:!0})}catch{localStorage.removeItem("token"),e({user:null,token:null,isAuthenticated:!1})}}}));function DA(){const[e,t]=h.useState("admin@example.com"),[r,n]=h.useState("password"),[i,s]=h.useState(""),[o,l]=h.useState(!1),c=dt(),d=Ph(f=>f.login),u=async f=>{f.preventDefault(),s(""),l(!0);try{await d(e,r),c("/")}catch(p){s(p.message||"Login failed")}finally{l(!1)}};return a.jsx("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",minHeight:"100vh",background:"linear-gradient(135deg, #667eea 0%, #764ba2 100%)"},children:a.jsxs("div",{style:{background:"white",padding:"40px",borderRadius:"12px",boxShadow:"0 10px 40px rgba(0,0,0,0.2)",width:"100%",maxWidth:"400px"},children:[a.jsx("h1",{style:{marginBottom:"10px",fontSize:"28px"},children:"Dutchie Menus"}),a.jsx("p",{style:{color:"#666",marginBottom:"30px"},children:"Admin Dashboard"}),i&&a.jsx("div",{style:{background:"#fee",color:"#c33",padding:"12px",borderRadius:"6px",marginBottom:"20px",fontSize:"14px"},children:i}),a.jsxs("form",{onSubmit:u,children:[a.jsxs("div",{style:{marginBottom:"20px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"500"},children:"Email"}),a.jsx("input",{type:"email",value:e,onChange:f=>t(f.target.value),required:!0,style:{width:"100%",padding:"12px",border:"1px solid #ddd",borderRadius:"6px",fontSize:"14px"}})]}),a.jsxs("div",{style:{marginBottom:"25px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"500"},children:"Password"}),a.jsx("input",{type:"password",value:r,onChange:f=>n(f.target.value),required:!0,style:{width:"100%",padding:"12px",border:"1px solid #ddd",borderRadius:"6px",fontSize:"14px"}})]}),a.jsx("button",{type:"submit",disabled:o,style:{width:"100%",padding:"14px",background:o?"#999":"#667eea",color:"white",border:"none",borderRadius:"6px",cursor:o?"not-allowed":"pointer",fontSize:"16px",fontWeight:"500",transition:"background 0.2s"},children:o?"Logging in...":"Login"})]})]})})}/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const TA=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),MA=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),Dx=e=>{const t=MA(e);return t.charAt(0).toUpperCase()+t.slice(1)},Q1=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),IA=e=>{for(const t in e)if(t.startsWith("aria-")||t==="role"||t==="title")return!0};/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var $A={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const LA=h.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,className:i="",children:s,iconNode:o,...l},c)=>h.createElement("svg",{ref:c,...$A,width:t,height:t,stroke:e,strokeWidth:n?Number(r)*24/Number(t):r,className:Q1("lucide",i),...!s&&!IA(l)&&{"aria-hidden":"true"},...l},[...o.map(([d,u])=>h.createElement(d,u)),...Array.isArray(s)?s:[s]]));/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Q=(e,t)=>{const r=h.forwardRef(({className:n,...i},s)=>h.createElement(LA,{ref:s,iconNode:t,className:Q1(`lucide-${TA(Dx(e))}`,`lucide-${e}`,n),...i}));return r.displayName=Dx(e),r};/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zA=[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]],Ds=Q("activity",zA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const RA=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],_h=Q("arrow-left",RA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const BA=[["path",{d:"M10 12h4",key:"a56b0p"}],["path",{d:"M10 8h4",key:"1sr2af"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3",key:"1rgiei"}],["path",{d:"M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2",key:"secmi2"}],["path",{d:"M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16",key:"16ra0t"}]],Ln=Q("building-2",BA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const FA=[["path",{d:"M12 10h.01",key:"1nrarc"}],["path",{d:"M12 14h.01",key:"1etili"}],["path",{d:"M12 6h.01",key:"1vi96p"}],["path",{d:"M16 10h.01",key:"1m94wz"}],["path",{d:"M16 14h.01",key:"1gbofw"}],["path",{d:"M16 6h.01",key:"1x0f13"}],["path",{d:"M8 10h.01",key:"19clt8"}],["path",{d:"M8 14h.01",key:"6423bh"}],["path",{d:"M8 6h.01",key:"1dz90k"}],["path",{d:"M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3",key:"cabbwy"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2",key:"1uxh74"}]],WA=Q("building",FA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const UA=[["path",{d:"M8 2v4",key:"1cmpym"}],["path",{d:"M16 2v4",key:"4m81vk"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2",key:"1hopcy"}],["path",{d:"M3 10h18",key:"8toen8"}]],Ch=Q("calendar",UA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const qA=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16",key:"c24i48"}],["path",{d:"M18 17V9",key:"2bz60n"}],["path",{d:"M13 17V5",key:"1frdt8"}],["path",{d:"M8 17v-3",key:"17ska0"}]],HA=Q("chart-column",qA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const KA=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],ej=Q("chevron-down",KA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const VA=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],Cf=Q("chevron-right",VA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const YA=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],Uc=Q("circle-alert",YA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const GA=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]],_r=Q("circle-check-big",GA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ZA=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]],Hr=Q("circle-x",ZA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const XA=[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],xr=Q("clock",XA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const JA=[["line",{x1:"12",x2:"12",y1:"2",y2:"22",key:"7eqyqh"}],["path",{d:"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6",key:"1b0p4s"}]],QA=Q("dollar-sign",JA);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const eO=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],Jr=Q("external-link",eO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const tO=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],rO=Q("eye",tO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const nO=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]],iO=Q("file-text",nO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const aO=[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]],sO=Q("folder-open",aO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const oO=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2",key:"1m3agn"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}]],lO=Q("image",oO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const cO=[["path",{d:"m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4",key:"g0fldk"}],["path",{d:"m21 2-9.6 9.6",key:"1j0ho8"}],["circle",{cx:"7.5",cy:"15.5",r:"5.5",key:"yqb3hr"}]],uO=Q("key",cO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const dO=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",key:"zw3jo"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",key:"1wduqc"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",key:"kqbvx6"}]],Tx=Q("layers",dO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fO=[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]],pO=Q("layout-dashboard",fO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const hO=[["path",{d:"m16 17 5-5-5-5",key:"1bji2h"}],["path",{d:"M21 12H9",key:"dn1m92"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}]],mO=Q("log-out",hO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const gO=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7",key:"132q7q"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2",key:"izxlao"}]],tj=Q("mail",gO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xO=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0",key:"1r0f0z"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}]],yi=Q("map-pin",xO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const yO=[["path",{d:"M12.586 12.586 19 19",key:"ea5xo7"}],["path",{d:"M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z",key:"277e5u"}]],vO=Q("mouse-pointer",yO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const bO=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z",key:"1a0edw"}],["path",{d:"M12 22V12",key:"d0xqtd"}],["polyline",{points:"3.29 7 12 12 20.71 7",key:"ousv84"}],["path",{d:"m7.5 4.27 9 5.15",key:"1c824w"}]],Ct=Q("package",bO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const jO=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]],wO=Q("pencil",jO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const SO=[["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384",key:"9njp5v"}]],Ah=Q("phone",SO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const NO=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],Af=Q("plus",NO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const kO=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],Xt=Q("refresh-cw",kO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const PO=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]],_O=Q("save",PO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const CO=[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]],AO=Q("search",CO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const OO=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915",key:"1i5ecw"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],EO=Q("settings",OO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const DO=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]],sl=Q("shield",DO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const TO=[["path",{d:"M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5",key:"slp6dd"}],["path",{d:"M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244",key:"o0xfot"}],["path",{d:"M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05",key:"wn3emo"}]],Il=Q("store",TO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const MO=[["path",{d:"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z",key:"vktsd0"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor",key:"kqv944"}]],Cr=Q("tag",MO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const IO=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"6",key:"1vlfrh"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}]],rj=Q("target",IO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const $O=[["circle",{cx:"9",cy:"12",r:"3",key:"u3jwor"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7",key:"g7kal2"}]],Mx=Q("toggle-left",$O);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const LO=[["circle",{cx:"15",cy:"12",r:"3",key:"1afu0r"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7",key:"g7kal2"}]],Ix=Q("toggle-right",LO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zO=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],RO=Q("trash-2",zO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const BO=[["path",{d:"M16 17h6v-6",key:"t6n2it"}],["path",{d:"m22 17-8.5-8.5-5 5L2 7",key:"x473p"}]],FO=Q("trending-down",BO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const WO=[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]],zn=Q("trending-up",WO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const UO=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],nj=Q("triangle-alert",UO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const qO=[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]],HO=Q("upload",qO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const KO=[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",key:"1ngwbx"}]],VO=Q("wrench",KO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const YO=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],ij=Q("x",YO);/** + * @license lucide-react v0.553.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const GO=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],$x=Q("zap",GO);function Ge({to:e,icon:t,label:r,isActive:n}){return a.jsxs("a",{href:e,className:`flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${n?"bg-blue-50 text-blue-600":"text-gray-700 hover:bg-gray-50"}`,children:[a.jsx("span",{className:"flex-shrink-0",children:t}),a.jsx("span",{children:r})]})}function Do({title:e,children:t}){return a.jsxs("div",{className:"space-y-1",children:[a.jsx("div",{className:"px-3 mb-2",children:a.jsx("h3",{className:"text-xs font-semibold text-gray-400 uppercase tracking-wider",children:e})}),t]})}function X({children:e}){const t=dt(),r=_i(),{user:n,logout:i}=Ph(),[s,o]=h.useState(null);h.useEffect(()=>{(async()=>{try{const u=await B.getVersion();o(u)}catch(u){console.error("Failed to fetch version info:",u)}})()},[]);const l=()=>{i(),t("/login")},c=(d,u=!0)=>u?r.pathname===d:r.pathname.startsWith(d);return a.jsxs("div",{className:"flex min-h-screen bg-gray-50",children:[a.jsxs("div",{className:"w-64 bg-white border-r border-gray-200 flex flex-col",style:{position:"sticky",top:0,height:"100vh",overflowY:"auto"},children:[a.jsxs("div",{className:"px-6 py-5 border-b border-gray-200",children:[a.jsx("h1",{className:"text-lg font-semibold text-gray-900",children:"Dutchie Analytics"}),a.jsx("p",{className:"text-xs text-gray-500 mt-0.5",children:n==null?void 0:n.email})]}),a.jsxs("nav",{className:"flex-1 px-3 py-4 space-y-6",children:[a.jsxs(Do,{title:"Main",children:[a.jsx(Ge,{to:"/",icon:a.jsx(pO,{className:"w-4 h-4"}),label:"Dashboard",isActive:c("/",!0)}),a.jsx(Ge,{to:"/dispensaries",icon:a.jsx(Ln,{className:"w-4 h-4"}),label:"Dispensaries",isActive:c("/dispensaries")}),a.jsx(Ge,{to:"/categories",icon:a.jsx(sO,{className:"w-4 h-4"}),label:"Categories",isActive:c("/categories")}),a.jsx(Ge,{to:"/products",icon:a.jsx(Ct,{className:"w-4 h-4"}),label:"Products",isActive:c("/products")}),a.jsx(Ge,{to:"/campaigns",icon:a.jsx(rj,{className:"w-4 h-4"}),label:"Campaigns",isActive:c("/campaigns")}),a.jsx(Ge,{to:"/analytics",icon:a.jsx(zn,{className:"w-4 h-4"}),label:"Analytics",isActive:c("/analytics")})]}),a.jsxs(Do,{title:"AZ Data",children:[a.jsx(Ge,{to:"/wholesale-analytics",icon:a.jsx(zn,{className:"w-4 h-4"}),label:"Wholesale Analytics",isActive:c("/wholesale-analytics")}),a.jsx(Ge,{to:"/az",icon:a.jsx(Il,{className:"w-4 h-4"}),label:"AZ Stores",isActive:c("/az",!1)}),a.jsx(Ge,{to:"/az-schedule",icon:a.jsx(Ch,{className:"w-4 h-4"}),label:"AZ Schedule",isActive:c("/az-schedule")})]}),a.jsxs(Do,{title:"Scraper",children:[a.jsx(Ge,{to:"/scraper-tools",icon:a.jsx(VO,{className:"w-4 h-4"}),label:"Tools",isActive:c("/scraper-tools")}),a.jsx(Ge,{to:"/scraper-schedule",icon:a.jsx(xr,{className:"w-4 h-4"}),label:"Schedule",isActive:c("/scraper-schedule")}),a.jsx(Ge,{to:"/scraper-monitor",icon:a.jsx(Ds,{className:"w-4 h-4"}),label:"Monitor",isActive:c("/scraper-monitor")})]}),a.jsxs(Do,{title:"System",children:[a.jsx(Ge,{to:"/changes",icon:a.jsx(_r,{className:"w-4 h-4"}),label:"Change Approval",isActive:c("/changes")}),a.jsx(Ge,{to:"/api-permissions",icon:a.jsx(uO,{className:"w-4 h-4"}),label:"API Permissions",isActive:c("/api-permissions")}),a.jsx(Ge,{to:"/proxies",icon:a.jsx(sl,{className:"w-4 h-4"}),label:"Proxies",isActive:c("/proxies")}),a.jsx(Ge,{to:"/logs",icon:a.jsx(iO,{className:"w-4 h-4"}),label:"Logs",isActive:c("/logs")}),a.jsx(Ge,{to:"/settings",icon:a.jsx(EO,{className:"w-4 h-4"}),label:"Settings",isActive:c("/settings")})]})]}),a.jsx("div",{className:"px-3 py-4 border-t border-gray-200",children:a.jsxs("button",{onClick:l,className:"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-red-600 hover:bg-red-50 transition-colors",children:[a.jsx(mO,{className:"w-4 h-4"}),a.jsx("span",{children:"Logout"})]})}),s&&a.jsxs("div",{className:"px-3 py-2 border-t border-gray-200 bg-gray-50",children:[a.jsxs("p",{className:"text-xs text-gray-500 text-center",children:[s.build_version," (",s.git_sha.slice(0,7),")"]}),a.jsx("p",{className:"text-xs text-gray-400 text-center mt-0.5",children:s.image_tag})]})]}),a.jsx("div",{className:"flex-1 overflow-y-auto",children:a.jsx("div",{className:"max-w-7xl mx-auto px-8 py-8",children:e})})]})}function aj(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var i=e.length;for(t=0;t{var[n]=r;return sj(n)||oj(n)});return Object.fromEntries(t)}function qc(e){if(e==null)return null;if(h.isValidElement(e)&&typeof e.props=="object"&&e.props!==null){var t=e.props;return nr(t)}return typeof e=="object"&&!Array.isArray(e)?nr(e):null}function ut(e){var t=Object.entries(e).filter(r=>{var[n]=r;return sj(n)||oj(n)||Oh(n)});return Object.fromEntries(t)}function JO(e){return e==null?null:h.isValidElement(e)?ut(e.props):typeof e=="object"&&!Array.isArray(e)?ut(e):null}var QO=["children","width","height","viewBox","className","style","title","desc"];function Of(){return Of=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:r,width:n,height:i,viewBox:s,className:o,style:l,title:c,desc:d}=e,u=e6(e,QO),f=s||{width:n,height:i,x:0,y:0},p=ce("recharts-surface",o);return h.createElement("svg",Of({},ut(u),{className:p,width:n,height:i,style:l,viewBox:"".concat(f.x," ").concat(f.y," ").concat(f.width," ").concat(f.height),ref:t}),h.createElement("title",null,c),h.createElement("desc",null,d),r)}),r6=["children","className"];function Ef(){return Ef=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:r,className:n}=e,i=n6(e,r6),s=ce("recharts-layer",n);return h.createElement("g",Ef({className:s},ut(i),{ref:t}),r)}),a6=h.createContext(null);function he(e){return function(){return e}}const cj=Math.cos,$l=Math.sin,vr=Math.sqrt,Ll=Math.PI,Hc=2*Ll,Df=Math.PI,Tf=2*Df,Xn=1e-6,s6=Tf-Xn;function uj(e){this._+=e[0];for(let t=1,r=e.length;t=0))throw new Error(`invalid digits: ${e}`);if(t>15)return uj;const r=10**t;return function(n){this._+=n[0];for(let i=1,s=n.length;iXn)if(!(Math.abs(f*c-d*u)>Xn)||!s)this._append`L${this._x1=t},${this._y1=r}`;else{let m=n-o,x=i-l,g=c*c+d*d,b=m*m+x*x,v=Math.sqrt(g),j=Math.sqrt(p),y=s*Math.tan((Df-Math.acos((g+p-b)/(2*v*j)))/2),w=y/j,S=y/v;Math.abs(w-1)>Xn&&this._append`L${t+w*u},${r+w*f}`,this._append`A${s},${s},0,0,${+(f*m>u*x)},${this._x1=t+S*c},${this._y1=r+S*d}`}}arc(t,r,n,i,s,o){if(t=+t,r=+r,n=+n,o=!!o,n<0)throw new Error(`negative radius: ${n}`);let l=n*Math.cos(i),c=n*Math.sin(i),d=t+l,u=r+c,f=1^o,p=o?i-s:s-i;this._x1===null?this._append`M${d},${u}`:(Math.abs(this._x1-d)>Xn||Math.abs(this._y1-u)>Xn)&&this._append`L${d},${u}`,n&&(p<0&&(p=p%Tf+Tf),p>s6?this._append`A${n},${n},0,1,${f},${t-l},${r-c}A${n},${n},0,1,${f},${this._x1=d},${this._y1=u}`:p>Xn&&this._append`A${n},${n},0,${+(p>=Df)},${f},${this._x1=t+n*Math.cos(s)},${this._y1=r+n*Math.sin(s)}`)}rect(t,r,n,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+r}h${n=+n}v${+i}h${-n}Z`}toString(){return this._}}function Eh(e){let t=3;return e.digits=function(r){if(!arguments.length)return t;if(r==null)t=null;else{const n=Math.floor(r);if(!(n>=0))throw new RangeError(`invalid digits: ${r}`);t=n}return e},()=>new l6(t)}function Dh(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}function dj(e){this._context=e}dj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:this._context.lineTo(e,t);break}}};function Kc(e){return new dj(e)}function fj(e){return e[0]}function pj(e){return e[1]}function hj(e,t){var r=he(!0),n=null,i=Kc,s=null,o=Eh(l);e=typeof e=="function"?e:e===void 0?fj:he(e),t=typeof t=="function"?t:t===void 0?pj:he(t);function l(c){var d,u=(c=Dh(c)).length,f,p=!1,m;for(n==null&&(s=i(m=o())),d=0;d<=u;++d)!(d=m;--x)l.point(y[x],w[x]);l.lineEnd(),l.areaEnd()}v&&(y[p]=+e(b,p,f),w[p]=+t(b,p,f),l.point(n?+n(b,p,f):y[p],r?+r(b,p,f):w[p]))}if(j)return l=null,j+""||null}function u(){return hj().defined(i).curve(o).context(s)}return d.x=function(f){return arguments.length?(e=typeof f=="function"?f:he(+f),n=null,d):e},d.x0=function(f){return arguments.length?(e=typeof f=="function"?f:he(+f),d):e},d.x1=function(f){return arguments.length?(n=f==null?null:typeof f=="function"?f:he(+f),d):n},d.y=function(f){return arguments.length?(t=typeof f=="function"?f:he(+f),r=null,d):t},d.y0=function(f){return arguments.length?(t=typeof f=="function"?f:he(+f),d):t},d.y1=function(f){return arguments.length?(r=f==null?null:typeof f=="function"?f:he(+f),d):r},d.lineX0=d.lineY0=function(){return u().x(e).y(t)},d.lineY1=function(){return u().x(e).y(r)},d.lineX1=function(){return u().x(n).y(t)},d.defined=function(f){return arguments.length?(i=typeof f=="function"?f:he(!!f),d):i},d.curve=function(f){return arguments.length?(o=f,s!=null&&(l=o(s)),d):o},d.context=function(f){return arguments.length?(f==null?s=l=null:l=o(s=f),d):s},d}class mj{constructor(t,r){this._context=t,this._x=r}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line}point(t,r){switch(t=+t,r=+r,this._point){case 0:{this._point=1,this._line?this._context.lineTo(t,r):this._context.moveTo(t,r);break}case 1:this._point=2;default:{this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,r,t,r):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+r)/2,t,this._y0,t,r);break}}this._x0=t,this._y0=r}}function c6(e){return new mj(e,!0)}function u6(e){return new mj(e,!1)}const Th={draw(e,t){const r=vr(t/Ll);e.moveTo(r,0),e.arc(0,0,r,0,Hc)}},d6={draw(e,t){const r=vr(t/5)/2;e.moveTo(-3*r,-r),e.lineTo(-r,-r),e.lineTo(-r,-3*r),e.lineTo(r,-3*r),e.lineTo(r,-r),e.lineTo(3*r,-r),e.lineTo(3*r,r),e.lineTo(r,r),e.lineTo(r,3*r),e.lineTo(-r,3*r),e.lineTo(-r,r),e.lineTo(-3*r,r),e.closePath()}},gj=vr(1/3),f6=gj*2,p6={draw(e,t){const r=vr(t/f6),n=r*gj;e.moveTo(0,-r),e.lineTo(n,0),e.lineTo(0,r),e.lineTo(-n,0),e.closePath()}},h6={draw(e,t){const r=vr(t),n=-r/2;e.rect(n,n,r,r)}},m6=.8908130915292852,xj=$l(Ll/10)/$l(7*Ll/10),g6=$l(Hc/10)*xj,x6=-cj(Hc/10)*xj,y6={draw(e,t){const r=vr(t*m6),n=g6*r,i=x6*r;e.moveTo(0,-r),e.lineTo(n,i);for(let s=1;s<5;++s){const o=Hc*s/5,l=cj(o),c=$l(o);e.lineTo(c*r,-l*r),e.lineTo(l*n-c*i,c*n+l*i)}e.closePath()}},cd=vr(3),v6={draw(e,t){const r=-vr(t/(cd*3));e.moveTo(0,r*2),e.lineTo(-cd*r,-r),e.lineTo(cd*r,-r),e.closePath()}},Ht=-.5,Kt=vr(3)/2,Mf=1/vr(12),b6=(Mf/2+1)*3,j6={draw(e,t){const r=vr(t/b6),n=r/2,i=r*Mf,s=n,o=r*Mf+r,l=-s,c=o;e.moveTo(n,i),e.lineTo(s,o),e.lineTo(l,c),e.lineTo(Ht*n-Kt*i,Kt*n+Ht*i),e.lineTo(Ht*s-Kt*o,Kt*s+Ht*o),e.lineTo(Ht*l-Kt*c,Kt*l+Ht*c),e.lineTo(Ht*n+Kt*i,Ht*i-Kt*n),e.lineTo(Ht*s+Kt*o,Ht*o-Kt*s),e.lineTo(Ht*l+Kt*c,Ht*c-Kt*l),e.closePath()}};function w6(e,t){let r=null,n=Eh(i);e=typeof e=="function"?e:he(e||Th),t=typeof t=="function"?t:he(t===void 0?64:+t);function i(){let s;if(r||(r=s=n()),e.apply(this,arguments).draw(r,+t.apply(this,arguments)),s)return r=null,s+""||null}return i.type=function(s){return arguments.length?(e=typeof s=="function"?s:he(s),i):e},i.size=function(s){return arguments.length?(t=typeof s=="function"?s:he(+s),i):t},i.context=function(s){return arguments.length?(r=s??null,i):r},i}function zl(){}function Rl(e,t,r){e._context.bezierCurveTo((2*e._x0+e._x1)/3,(2*e._y0+e._y1)/3,(e._x0+2*e._x1)/3,(e._y0+2*e._y1)/3,(e._x0+4*e._x1+t)/6,(e._y0+4*e._y1+r)/6)}function yj(e){this._context=e}yj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Rl(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Rl(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function S6(e){return new yj(e)}function vj(e){this._context=e}vj.prototype={areaStart:zl,areaEnd:zl,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:{this._context.moveTo(this._x2,this._y2),this._context.closePath();break}case 2:{this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break}case 3:{this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4);break}}},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x2=e,this._y2=t;break;case 1:this._point=2,this._x3=e,this._y3=t;break;case 2:this._point=3,this._x4=e,this._y4=t,this._context.moveTo((this._x0+4*this._x1+e)/6,(this._y0+4*this._y1+t)/6);break;default:Rl(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function N6(e){return new vj(e)}function bj(e){this._context=e}bj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var r=(this._x0+4*this._x1+e)/6,n=(this._y0+4*this._y1+t)/6;this._line?this._context.lineTo(r,n):this._context.moveTo(r,n);break;case 3:this._point=4;default:Rl(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function k6(e){return new bj(e)}function jj(e){this._context=e}jj.prototype={areaStart:zl,areaEnd:zl,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(e,t){e=+e,t=+t,this._point?this._context.lineTo(e,t):(this._point=1,this._context.moveTo(e,t))}};function P6(e){return new jj(e)}function Lx(e){return e<0?-1:1}function zx(e,t,r){var n=e._x1-e._x0,i=t-e._x1,s=(e._y1-e._y0)/(n||i<0&&-0),o=(r-e._y1)/(i||n<0&&-0),l=(s*i+o*n)/(n+i);return(Lx(s)+Lx(o))*Math.min(Math.abs(s),Math.abs(o),.5*Math.abs(l))||0}function Rx(e,t){var r=e._x1-e._x0;return r?(3*(e._y1-e._y0)/r-t)/2:t}function ud(e,t,r){var n=e._x0,i=e._y0,s=e._x1,o=e._y1,l=(s-n)/3;e._context.bezierCurveTo(n+l,i+l*t,s-l,o-l*r,s,o)}function Bl(e){this._context=e}Bl.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:ud(this,this._t0,Rx(this,this._t0));break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){var r=NaN;if(e=+e,t=+t,!(e===this._x1&&t===this._y1)){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,ud(this,Rx(this,r=zx(this,e,t)),r);break;default:ud(this,this._t0,r=zx(this,e,t));break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t,this._t0=r}}};function wj(e){this._context=new Sj(e)}(wj.prototype=Object.create(Bl.prototype)).point=function(e,t){Bl.prototype.point.call(this,t,e)};function Sj(e){this._context=e}Sj.prototype={moveTo:function(e,t){this._context.moveTo(t,e)},closePath:function(){this._context.closePath()},lineTo:function(e,t){this._context.lineTo(t,e)},bezierCurveTo:function(e,t,r,n,i,s){this._context.bezierCurveTo(t,e,n,r,s,i)}};function _6(e){return new Bl(e)}function C6(e){return new wj(e)}function Nj(e){this._context=e}Nj.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var e=this._x,t=this._y,r=e.length;if(r)if(this._line?this._context.lineTo(e[0],t[0]):this._context.moveTo(e[0],t[0]),r===2)this._context.lineTo(e[1],t[1]);else for(var n=Bx(e),i=Bx(t),s=0,o=1;o=0;--t)i[t]=(o[t]-i[t+1])/s[t];for(s[r-1]=(e[r]+i[r-1])/2,t=0;t=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:{if(this._t<=0)this._context.lineTo(this._x,t),this._context.lineTo(e,t);else{var r=this._x*(1-this._t)+e*this._t;this._context.lineTo(r,this._y),this._context.lineTo(r,t)}break}}this._x=e,this._y=t}};function O6(e){return new Vc(e,.5)}function E6(e){return new Vc(e,0)}function D6(e){return new Vc(e,1)}function pa(e,t){if((o=e.length)>1)for(var r=1,n,i,s=e[t[0]],o,l=s.length;r=0;)r[t]=t;return r}function T6(e,t){return e[t]}function M6(e){const t=[];return t.key=e,t}function I6(){var e=he([]),t=If,r=pa,n=T6;function i(s){var o=Array.from(e.apply(this,arguments),M6),l,c=o.length,d=-1,u;for(const f of s)for(l=0,++d;l0){for(var r,n,i=0,s=e[0].length,o;i0){for(var r=0,n=e[t[0]],i,s=n.length;r0)||!((s=(i=e[t[0]]).length)>0))){for(var r=0,n=1,i,s,o;ne===0?0:e>0?1:-1,yr=e=>typeof e=="number"&&e!=+e,Qr=e=>typeof e=="string"&&e.indexOf("%")===e.length-1,Y=e=>(typeof e=="number"||e instanceof Number)&&!yr(e),Or=e=>Y(e)||typeof e=="string",B6=0,Ts=e=>{var t=++B6;return"".concat(e||"").concat(t)},Rn=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(!Y(t)&&typeof t!="string")return n;var s;if(Qr(t)){if(r==null)return n;var o=t.indexOf("%");s=r*parseFloat(t.slice(0,o))/100}else s=+t;return yr(s)&&(s=n),i&&r!=null&&s>r&&(s=r),s},_j=e=>{if(!Array.isArray(e))return!1;for(var t=e.length,r={},n=0;nn&&(typeof t=="function"?t(n):Xc(n,t))===r)}var Re=e=>e===null||typeof e>"u",Xs=e=>Re(e)?e:"".concat(e.charAt(0).toUpperCase()).concat(e.slice(1));function F6(e){return e!=null}function ka(){}var W6=["type","size","sizeType"];function $f(){return $f=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var t="symbol".concat(Xs(e));return Aj[t]||Th},Z6=(e,t,r)=>{if(t==="area")return e;switch(r){case"cross":return 5*e*e/9;case"diamond":return .5*e*e/Math.sqrt(3);case"square":return e*e;case"star":{var n=18*Y6;return 1.25*e*e*(Math.tan(n)-Math.tan(n*2)*Math.tan(n)**2)}case"triangle":return Math.sqrt(3)*e*e/4;case"wye":return(21-10*Math.sqrt(3))*e*e/8;default:return Math.PI*e*e/4}},X6=(e,t)=>{Aj["symbol".concat(Xs(e))]=t},Oj=e=>{var{type:t="circle",size:r=64,sizeType:n="area"}=e,i=K6(e,W6),s=Wx(Wx({},i),{},{type:t,size:r,sizeType:n}),o="circle";typeof t=="string"&&(o=t);var l=()=>{var p=G6(o),m=w6().type(p).size(Z6(r,n,o)),x=m();if(x!==null)return x},{className:c,cx:d,cy:u}=s,f=ut(s);return Y(d)&&Y(u)&&Y(r)?h.createElement("path",$f({},f,{className:ce("recharts-symbols",c),transform:"translate(".concat(d,", ").concat(u,")"),d:l()})):null};Oj.registerSymbol=X6;var Ej=e=>"radius"in e&&"startAngle"in e&&"endAngle"in e,Ih=(e,t)=>{if(!e||typeof e=="function"||typeof e=="boolean")return null;var r=e;if(h.isValidElement(e)&&(r=e.props),typeof r!="object"&&typeof r!="function")return null;var n={};return Object.keys(r).forEach(i=>{Oh(i)&&(n[i]=s=>r[i](r,s))}),n},J6=(e,t,r)=>n=>(e(t,r,n),null),Q6=(e,t,r)=>{if(e===null||typeof e!="object"&&typeof e!="function")return null;var n=null;return Object.keys(e).forEach(i=>{var s=e[i];Oh(i)&&typeof s=="function"&&(n||(n={}),n[i]=J6(s,t,r))}),n};function Ux(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function e4(e){for(var t=1;t(o[l]===void 0&&n[l]!==void 0&&(o[l]=n[l]),o),r);return s}var Dj={},Tj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n){const i=new Map;for(let s=0;s=0}e.isLength=t})(Ij);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Ij;function r(n){return n!=null&&typeof n!="function"&&t.isLength(n.length)}e.isArrayLike=r})(Jc);var $j={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return typeof r=="object"&&r!==null}e.isObjectLike=t})($j);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Jc,r=$j;function n(i){return r.isObjectLike(i)&&t.isArrayLike(i)}e.isArrayLikeObject=n})(Mj);var Lj={},zj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Yc;function r(n){return function(i){return t.get(i,n)}}e.property=r})(zj);var Rj={},Lh={},Bj={},zh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r!==null&&(typeof r=="object"||typeof r=="function")}e.isObject=t})(zh);var Rh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r==null||typeof r!="object"&&typeof r!="function"}e.isPrimitive=t})(Rh);var Bh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n){return r===n||Number.isNaN(r)&&Number.isNaN(n)}e.eq=t})(Bh);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=zh,r=Rh,n=Bh;function i(u,f,p){return typeof p!="function"?i(u,f,()=>{}):s(u,f,function m(x,g,b,v,j,y){const w=p(x,g,b,v,j,y);return w!==void 0?!!w:s(x,g,m,y)},new Map)}function s(u,f,p,m){if(f===u)return!0;switch(typeof f){case"object":return o(u,f,p,m);case"function":return Object.keys(f).length>0?s(u,{...f},p,m):n.eq(u,f);default:return t.isObject(u)?typeof f=="string"?f==="":!0:n.eq(u,f)}}function o(u,f,p,m){if(f==null)return!0;if(Array.isArray(f))return c(u,f,p,m);if(f instanceof Map)return l(u,f,p,m);if(f instanceof Set)return d(u,f,p,m);const x=Object.keys(f);if(u==null)return x.length===0;if(x.length===0)return!0;if(m!=null&&m.has(f))return m.get(f)===u;m==null||m.set(f,u);try{for(let g=0;g{})}e.isMatch=r})(Lh);var Fj={},Fh={},Wj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return Object.getOwnPropertySymbols(r).filter(n=>Object.prototype.propertyIsEnumerable.call(r,n))}e.getSymbols=t})(Wj);var Wh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r==null?r===void 0?"[object Undefined]":"[object Null]":Object.prototype.toString.call(r)}e.getTag=t})(Wh);var Uh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t="[object RegExp]",r="[object String]",n="[object Number]",i="[object Boolean]",s="[object Arguments]",o="[object Symbol]",l="[object Date]",c="[object Map]",d="[object Set]",u="[object Array]",f="[object Function]",p="[object ArrayBuffer]",m="[object Object]",x="[object Error]",g="[object DataView]",b="[object Uint8Array]",v="[object Uint8ClampedArray]",j="[object Uint16Array]",y="[object Uint32Array]",w="[object BigUint64Array]",S="[object Int8Array]",N="[object Int16Array]",P="[object Int32Array]",_="[object BigInt64Array]",T="[object Float32Array]",$="[object Float64Array]";e.argumentsTag=s,e.arrayBufferTag=p,e.arrayTag=u,e.bigInt64ArrayTag=_,e.bigUint64ArrayTag=w,e.booleanTag=i,e.dataViewTag=g,e.dateTag=l,e.errorTag=x,e.float32ArrayTag=T,e.float64ArrayTag=$,e.functionTag=f,e.int16ArrayTag=N,e.int32ArrayTag=P,e.int8ArrayTag=S,e.mapTag=c,e.numberTag=n,e.objectTag=m,e.regexpTag=t,e.setTag=d,e.stringTag=r,e.symbolTag=o,e.uint16ArrayTag=j,e.uint32ArrayTag=y,e.uint8ArrayTag=b,e.uint8ClampedArrayTag=v})(Uh);var Uj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return ArrayBuffer.isView(r)&&!(r instanceof DataView)}e.isTypedArray=t})(Uj);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Wj,r=Wh,n=Uh,i=Rh,s=Uj;function o(u,f){return l(u,void 0,u,new Map,f)}function l(u,f,p,m=new Map,x=void 0){const g=x==null?void 0:x(u,f,p,m);if(g!==void 0)return g;if(i.isPrimitive(u))return u;if(m.has(u))return m.get(u);if(Array.isArray(u)){const b=new Array(u.length);m.set(u,b);for(let v=0;vt.isMatch(s,i)}e.matches=n})(Rj);var qj={},Hj={},Kj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Fh,r=Uh;function n(i,s){return t.cloneDeepWith(i,(o,l,c,d)=>{const u=s==null?void 0:s(o,l,c,d);if(u!==void 0)return u;if(typeof i=="object")switch(Object.prototype.toString.call(i)){case r.numberTag:case r.stringTag:case r.booleanTag:{const f=new i.constructor(i==null?void 0:i.valueOf());return t.copyProperties(f,i),f}case r.argumentsTag:{const f={};return t.copyProperties(f,i),f.length=i.length,f[Symbol.iterator]=i[Symbol.iterator],f}default:return}})}e.cloneDeepWith=n})(Kj);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Kj;function r(n){return t.cloneDeepWith(n)}e.cloneDeep=r})(Hj);var Vj={},qh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=/^(?:0|[1-9]\d*)$/;function r(n,i=Number.MAX_SAFE_INTEGER){switch(typeof n){case"number":return Number.isInteger(n)&&n>=0&&ne,Ve=()=>{var e=h.useContext(Hh);return e?e.store.dispatch:s4},ol=()=>{},o4=()=>ol,l4=(e,t)=>e===t;function G(e){var t=h.useContext(Hh);return X1.useSyncExternalStoreWithSelector(t?t.subscription.addNestedSub:o4,t?t.store.getState:ol,t?t.store.getState:ol,t?e:ol,l4)}function c4(e,t=`expected a function, instead received ${typeof e}`){if(typeof e!="function")throw new TypeError(t)}function u4(e,t=`expected an object, instead received ${typeof e}`){if(typeof e!="object")throw new TypeError(t)}function d4(e,t="expected all items to be functions, instead received the following types: "){if(!e.every(r=>typeof r=="function")){const r=e.map(n=>typeof n=="function"?`function ${n.name||"unnamed"}()`:typeof n).join(", ");throw new TypeError(`${t}[${r}]`)}}var Hx=e=>Array.isArray(e)?e:[e];function f4(e){const t=Array.isArray(e[0])?e[0]:e;return d4(t,"createSelector expects all input-selectors to be functions, but received the following types: "),t}function p4(e,t){const r=[],{length:n}=e;for(let i=0;i{r=Mo(),o.resetResultsCount()},o.resultsCount=()=>s,o.resetResultsCount=()=>{s=0},o}function x4(e,...t){const r=typeof e=="function"?{memoize:e,memoizeOptions:t}:e,n=(...i)=>{let s=0,o=0,l,c={},d=i.pop();typeof d=="object"&&(c=d,d=i.pop()),c4(d,`createSelector expects an output function after the inputs, but received: [${typeof d}]`);const u={...r,...c},{memoize:f,memoizeOptions:p=[],argsMemoize:m=Gj,argsMemoizeOptions:x=[]}=u,g=Hx(p),b=Hx(x),v=f4(i),j=f(function(){return s++,d.apply(null,arguments)},...g),y=m(function(){o++;const S=p4(v,arguments);return l=j.apply(null,S),l},...b);return Object.assign(y,{resultFunc:d,memoizedResultFunc:j,dependencies:v,dependencyRecomputations:()=>o,resetDependencyRecomputations:()=>{o=0},lastResult:()=>l,recomputations:()=>s,resetRecomputations:()=>{s=0},memoize:f,argsMemoize:m})};return Object.assign(n,{withTypes:()=>n}),n}var I=x4(Gj),y4=Object.assign((e,t=I)=>{u4(e,`createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ${typeof e}`);const r=Object.keys(e),n=r.map(s=>e[s]);return t(n,(...s)=>s.reduce((o,l,c)=>(o[r[c]]=l,o),{}))},{withTypes:()=>y4}),Zj={},Xj={},Jj={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(n){return typeof n=="symbol"?1:n===null?2:n===void 0?3:n!==n?4:0}const r=(n,i,s)=>{if(n!==i){const o=t(n),l=t(i);if(o===l&&o===0){if(ni)return s==="desc"?-1:1}return s==="desc"?l-o:o-l}return 0};e.compareValues=r})(Jj);var Qj={},Kh={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return typeof r=="symbol"||r instanceof Symbol}e.isSymbol=t})(Kh);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Kh,r=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,n=/^\w*$/;function i(s,o){return Array.isArray(s)?!1:typeof s=="number"||typeof s=="boolean"||s==null||t.isSymbol(s)?!0:typeof s=="string"&&(n.test(s)||!r.test(s))||o!=null&&Object.hasOwn(o,s)}e.isKey=i})(Qj);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Jj,r=Qj,n=Zc;function i(s,o,l,c){if(s==null)return[];l=c?void 0:l,Array.isArray(s)||(s=Object.values(s)),Array.isArray(o)||(o=o==null?[null]:[o]),o.length===0&&(o=[null]),Array.isArray(l)||(l=l==null?[]:[l]),l=l.map(m=>String(m));const d=(m,x)=>{let g=m;for(let b=0;bx==null||m==null?x:typeof m=="object"&&"key"in m?Object.hasOwn(x,m.key)?x[m.key]:d(x,m.path):typeof m=="function"?m(x):Array.isArray(m)?d(x,m):typeof x=="object"?x[m]:x,f=o.map(m=>(Array.isArray(m)&&m.length===1&&(m=m[0]),m==null||typeof m=="function"||Array.isArray(m)||r.isKey(m)?m:{key:m,path:n.toPath(m)}));return s.map(m=>({original:m,criteria:f.map(x=>u(x,m))})).slice().sort((m,x)=>{for(let g=0;gm.original)}e.orderBy=i})(Xj);var ew={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n=1){const i=[],s=Math.floor(n),o=(l,c)=>{for(let d=0;d1&&n.isIterateeCall(s,o[0],o[1])?o=[]:l>2&&n.isIterateeCall(o[0],o[1],o[2])&&(o=[o[0]]),t.orderBy(s,r.flatten(o),["asc"])}e.sortBy=i})(Zj);var v4=Zj.sortBy;const Qc=Tr(v4);var tw=e=>e.legend.settings,b4=e=>e.legend.size,j4=e=>e.legend.payload;I([j4,tw],(e,t)=>{var{itemSorter:r}=t,n=e.flat(1);return r?Qc(n,r):n});var Io=1;function w4(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:[],[t,r]=h.useState({height:0,left:0,top:0,width:0}),n=h.useCallback(i=>{if(i!=null){var s=i.getBoundingClientRect(),o={height:s.height,left:s.left,top:s.top,width:s.width};(Math.abs(o.height-t.height)>Io||Math.abs(o.left-t.left)>Io||Math.abs(o.top-t.top)>Io||Math.abs(o.width-t.width)>Io)&&r({height:o.height,left:o.left,top:o.top,width:o.width})}},[t.width,t.height,t.top,t.left,...e]);return[t,n]}function Ze(e){return`Minified Redux error #${e}; visit https://redux.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}var S4=typeof Symbol=="function"&&Symbol.observable||"@@observable",Vx=S4,dd=()=>Math.random().toString(36).substring(7).split("").join("."),N4={INIT:`@@redux/INIT${dd()}`,REPLACE:`@@redux/REPLACE${dd()}`,PROBE_UNKNOWN_ACTION:()=>`@@redux/PROBE_UNKNOWN_ACTION${dd()}`},Fl=N4;function Yh(e){if(typeof e!="object"||e===null)return!1;let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t||Object.getPrototypeOf(e)===null}function rw(e,t,r){if(typeof e!="function")throw new Error(Ze(2));if(typeof t=="function"&&typeof r=="function"||typeof r=="function"&&typeof arguments[3]=="function")throw new Error(Ze(0));if(typeof t=="function"&&typeof r>"u"&&(r=t,t=void 0),typeof r<"u"){if(typeof r!="function")throw new Error(Ze(1));return r(rw)(e,t)}let n=e,i=t,s=new Map,o=s,l=0,c=!1;function d(){o===s&&(o=new Map,s.forEach((b,v)=>{o.set(v,b)}))}function u(){if(c)throw new Error(Ze(3));return i}function f(b){if(typeof b!="function")throw new Error(Ze(4));if(c)throw new Error(Ze(5));let v=!0;d();const j=l++;return o.set(j,b),function(){if(v){if(c)throw new Error(Ze(6));v=!1,d(),o.delete(j),s=null}}}function p(b){if(!Yh(b))throw new Error(Ze(7));if(typeof b.type>"u")throw new Error(Ze(8));if(typeof b.type!="string")throw new Error(Ze(17));if(c)throw new Error(Ze(9));try{c=!0,i=n(i,b)}finally{c=!1}return(s=o).forEach(j=>{j()}),b}function m(b){if(typeof b!="function")throw new Error(Ze(10));n=b,p({type:Fl.REPLACE})}function x(){const b=f;return{subscribe(v){if(typeof v!="object"||v===null)throw new Error(Ze(11));function j(){const w=v;w.next&&w.next(u())}return j(),{unsubscribe:b(j)}},[Vx](){return this}}}return p({type:Fl.INIT}),{dispatch:p,subscribe:f,getState:u,replaceReducer:m,[Vx]:x}}function k4(e){Object.keys(e).forEach(t=>{const r=e[t];if(typeof r(void 0,{type:Fl.INIT})>"u")throw new Error(Ze(12));if(typeof r(void 0,{type:Fl.PROBE_UNKNOWN_ACTION()})>"u")throw new Error(Ze(13))})}function nw(e){const t=Object.keys(e),r={};for(let s=0;s"u")throw l&&l.type,new Error(Ze(14));d[f]=x,c=c||x!==m}return c=c||n.length!==Object.keys(o).length,c?d:o}}function Wl(...e){return e.length===0?t=>t:e.length===1?e[0]:e.reduce((t,r)=>(...n)=>t(r(...n)))}function P4(...e){return t=>(r,n)=>{const i=t(r,n);let s=()=>{throw new Error(Ze(15))};const o={getState:i.getState,dispatch:(c,...d)=>s(c,...d)},l=e.map(c=>c(o));return s=Wl(...l)(i.dispatch),{...i,dispatch:s}}}function iw(e){return Yh(e)&&"type"in e&&typeof e.type=="string"}var aw=Symbol.for("immer-nothing"),Yx=Symbol.for("immer-draftable"),Wt=Symbol.for("immer-state");function fr(e,...t){throw new Error(`[Immer] minified error nr: ${e}. Full error at: https://bit.ly/3cXEKWf`)}var Ms=Object.getPrototypeOf;function vi(e){return!!e&&!!e[Wt]}function en(e){var t;return e?sw(e)||Array.isArray(e)||!!e[Yx]||!!((t=e.constructor)!=null&&t[Yx])||Js(e)||tu(e):!1}var _4=Object.prototype.constructor.toString(),Gx=new WeakMap;function sw(e){if(!e||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);if(t===null||t===Object.prototype)return!0;const r=Object.hasOwnProperty.call(t,"constructor")&&t.constructor;if(r===Object)return!0;if(typeof r!="function")return!1;let n=Gx.get(r);return n===void 0&&(n=Function.toString.call(r),Gx.set(r,n)),n===_4}function Ul(e,t,r=!0){eu(e)===0?(r?Reflect.ownKeys(e):Object.keys(e)).forEach(i=>{t(i,e[i],e)}):e.forEach((n,i)=>t(i,n,e))}function eu(e){const t=e[Wt];return t?t.type_:Array.isArray(e)?1:Js(e)?2:tu(e)?3:0}function Lf(e,t){return eu(e)===2?e.has(t):Object.prototype.hasOwnProperty.call(e,t)}function ow(e,t,r){const n=eu(e);n===2?e.set(t,r):n===3?e.add(r):e[t]=r}function C4(e,t){return e===t?e!==0||1/e===1/t:e!==e&&t!==t}function Js(e){return e instanceof Map}function tu(e){return e instanceof Set}function Jn(e){return e.copy_||e.base_}function zf(e,t){if(Js(e))return new Map(e);if(tu(e))return new Set(e);if(Array.isArray(e))return Array.prototype.slice.call(e);const r=sw(e);if(t===!0||t==="class_only"&&!r){const n=Object.getOwnPropertyDescriptors(e);delete n[Wt];let i=Reflect.ownKeys(n);for(let s=0;s1&&Object.defineProperties(e,{set:$o,add:$o,clear:$o,delete:$o}),Object.freeze(e),t&&Object.values(e).forEach(r=>Gh(r,!0))),e}function A4(){fr(2)}var $o={value:A4};function ru(e){return e===null||typeof e!="object"?!0:Object.isFrozen(e)}var O4={};function bi(e){const t=O4[e];return t||fr(0,e),t}var Is;function lw(){return Is}function E4(e,t){return{drafts_:[],parent_:e,immer_:t,canAutoFreeze_:!0,unfinalizedDrafts_:0}}function Zx(e,t){t&&(bi("Patches"),e.patches_=[],e.inversePatches_=[],e.patchListener_=t)}function Rf(e){Bf(e),e.drafts_.forEach(D4),e.drafts_=null}function Bf(e){e===Is&&(Is=e.parent_)}function Xx(e){return Is=E4(Is,e)}function D4(e){const t=e[Wt];t.type_===0||t.type_===1?t.revoke_():t.revoked_=!0}function Jx(e,t){t.unfinalizedDrafts_=t.drafts_.length;const r=t.drafts_[0];return e!==void 0&&e!==r?(r[Wt].modified_&&(Rf(t),fr(4)),en(e)&&(e=ql(t,e),t.parent_||Hl(t,e)),t.patches_&&bi("Patches").generateReplacementPatches_(r[Wt].base_,e,t.patches_,t.inversePatches_)):e=ql(t,r,[]),Rf(t),t.patches_&&t.patchListener_(t.patches_,t.inversePatches_),e!==aw?e:void 0}function ql(e,t,r){if(ru(t))return t;const n=e.immer_.shouldUseStrictIteration(),i=t[Wt];if(!i)return Ul(t,(s,o)=>Qx(e,i,t,s,o,r),n),t;if(i.scope_!==e)return t;if(!i.modified_)return Hl(e,i.base_,!0),i.base_;if(!i.finalized_){i.finalized_=!0,i.scope_.unfinalizedDrafts_--;const s=i.copy_;let o=s,l=!1;i.type_===3&&(o=new Set(s),s.clear(),l=!0),Ul(o,(c,d)=>Qx(e,i,s,c,d,r,l),n),Hl(e,s,!1),r&&e.patches_&&bi("Patches").generatePatches_(i,r,e.patches_,e.inversePatches_)}return i.copy_}function Qx(e,t,r,n,i,s,o){if(i==null||typeof i!="object"&&!o)return;const l=ru(i);if(!(l&&!o)){if(vi(i)){const c=s&&t&&t.type_!==3&&!Lf(t.assigned_,n)?s.concat(n):void 0,d=ql(e,i,c);if(ow(r,n,d),vi(d))e.canAutoFreeze_=!1;else return}else o&&r.add(i);if(en(i)&&!l){if(!e.immer_.autoFreeze_&&e.unfinalizedDrafts_<1||t&&t.base_&&t.base_[n]===i&&l)return;ql(e,i),(!t||!t.scope_.parent_)&&typeof n!="symbol"&&(Js(r)?r.has(n):Object.prototype.propertyIsEnumerable.call(r,n))&&Hl(e,i)}}}function Hl(e,t,r=!1){!e.parent_&&e.immer_.autoFreeze_&&e.canAutoFreeze_&&Gh(t,r)}function T4(e,t){const r=Array.isArray(e),n={type_:r?1:0,scope_:t?t.scope_:lw(),modified_:!1,finalized_:!1,assigned_:{},parent_:t,base_:e,draft_:null,copy_:null,revoke_:null,isManual_:!1};let i=n,s=Zh;r&&(i=[n],s=$s);const{revoke:o,proxy:l}=Proxy.revocable(i,s);return n.draft_=l,n.revoke_=o,l}var Zh={get(e,t){if(t===Wt)return e;const r=Jn(e);if(!Lf(r,t))return M4(e,r,t);const n=r[t];return e.finalized_||!en(n)?n:n===fd(e.base_,t)?(pd(e),e.copy_[t]=Wf(n,e)):n},has(e,t){return t in Jn(e)},ownKeys(e){return Reflect.ownKeys(Jn(e))},set(e,t,r){const n=cw(Jn(e),t);if(n!=null&&n.set)return n.set.call(e.draft_,r),!0;if(!e.modified_){const i=fd(Jn(e),t),s=i==null?void 0:i[Wt];if(s&&s.base_===r)return e.copy_[t]=r,e.assigned_[t]=!1,!0;if(C4(r,i)&&(r!==void 0||Lf(e.base_,t)))return!0;pd(e),Ff(e)}return e.copy_[t]===r&&(r!==void 0||t in e.copy_)||Number.isNaN(r)&&Number.isNaN(e.copy_[t])||(e.copy_[t]=r,e.assigned_[t]=!0),!0},deleteProperty(e,t){return fd(e.base_,t)!==void 0||t in e.base_?(e.assigned_[t]=!1,pd(e),Ff(e)):delete e.assigned_[t],e.copy_&&delete e.copy_[t],!0},getOwnPropertyDescriptor(e,t){const r=Jn(e),n=Reflect.getOwnPropertyDescriptor(r,t);return n&&{writable:!0,configurable:e.type_!==1||t!=="length",enumerable:n.enumerable,value:r[t]}},defineProperty(){fr(11)},getPrototypeOf(e){return Ms(e.base_)},setPrototypeOf(){fr(12)}},$s={};Ul(Zh,(e,t)=>{$s[e]=function(){return arguments[0]=arguments[0][0],t.apply(this,arguments)}});$s.deleteProperty=function(e,t){return $s.set.call(this,e,t,void 0)};$s.set=function(e,t,r){return Zh.set.call(this,e[0],t,r,e[0])};function fd(e,t){const r=e[Wt];return(r?Jn(r):e)[t]}function M4(e,t,r){var i;const n=cw(t,r);return n?"value"in n?n.value:(i=n.get)==null?void 0:i.call(e.draft_):void 0}function cw(e,t){if(!(t in e))return;let r=Ms(e);for(;r;){const n=Object.getOwnPropertyDescriptor(r,t);if(n)return n;r=Ms(r)}}function Ff(e){e.modified_||(e.modified_=!0,e.parent_&&Ff(e.parent_))}function pd(e){e.copy_||(e.copy_=zf(e.base_,e.scope_.immer_.useStrictShallowCopy_))}var I4=class{constructor(e){this.autoFreeze_=!0,this.useStrictShallowCopy_=!1,this.useStrictIteration_=!0,this.produce=(t,r,n)=>{if(typeof t=="function"&&typeof r!="function"){const s=r;r=t;const o=this;return function(c=s,...d){return o.produce(c,u=>r.call(this,u,...d))}}typeof r!="function"&&fr(6),n!==void 0&&typeof n!="function"&&fr(7);let i;if(en(t)){const s=Xx(this),o=Wf(t,void 0);let l=!0;try{i=r(o),l=!1}finally{l?Rf(s):Bf(s)}return Zx(s,n),Jx(i,s)}else if(!t||typeof t!="object"){if(i=r(t),i===void 0&&(i=t),i===aw&&(i=void 0),this.autoFreeze_&&Gh(i,!0),n){const s=[],o=[];bi("Patches").generateReplacementPatches_(t,i,s,o),n(s,o)}return i}else fr(1,t)},this.produceWithPatches=(t,r)=>{if(typeof t=="function")return(o,...l)=>this.produceWithPatches(o,c=>t(c,...l));let n,i;return[this.produce(t,r,(o,l)=>{n=o,i=l}),n,i]},typeof(e==null?void 0:e.autoFreeze)=="boolean"&&this.setAutoFreeze(e.autoFreeze),typeof(e==null?void 0:e.useStrictShallowCopy)=="boolean"&&this.setUseStrictShallowCopy(e.useStrictShallowCopy),typeof(e==null?void 0:e.useStrictIteration)=="boolean"&&this.setUseStrictIteration(e.useStrictIteration)}createDraft(e){en(e)||fr(8),vi(e)&&(e=Kr(e));const t=Xx(this),r=Wf(e,void 0);return r[Wt].isManual_=!0,Bf(t),r}finishDraft(e,t){const r=e&&e[Wt];(!r||!r.isManual_)&&fr(9);const{scope_:n}=r;return Zx(n,t),Jx(void 0,n)}setAutoFreeze(e){this.autoFreeze_=e}setUseStrictShallowCopy(e){this.useStrictShallowCopy_=e}setUseStrictIteration(e){this.useStrictIteration_=e}shouldUseStrictIteration(){return this.useStrictIteration_}applyPatches(e,t){let r;for(r=t.length-1;r>=0;r--){const i=t[r];if(i.path.length===0&&i.op==="replace"){e=i.value;break}}r>-1&&(t=t.slice(r+1));const n=bi("Patches").applyPatches_;return vi(e)?n(e,t):this.produce(e,i=>n(i,t))}};function Wf(e,t){const r=Js(e)?bi("MapSet").proxyMap_(e,t):tu(e)?bi("MapSet").proxySet_(e,t):T4(e,t);return(t?t.scope_:lw()).drafts_.push(r),r}function Kr(e){return vi(e)||fr(10,e),uw(e)}function uw(e){if(!en(e)||ru(e))return e;const t=e[Wt];let r,n=!0;if(t){if(!t.modified_)return t.base_;t.finalized_=!0,r=zf(e,t.scope_.immer_.useStrictShallowCopy_),n=t.scope_.immer_.shouldUseStrictIteration()}else r=zf(e,!0);return Ul(r,(i,s)=>{ow(r,i,uw(s))},n),t&&(t.finalized_=!1),r}var Uf=new I4,dw=Uf.produce,$4=Uf.setUseStrictIteration.bind(Uf);function fw(e){return({dispatch:r,getState:n})=>i=>s=>typeof s=="function"?s(r,n,e):i(s)}var L4=fw(),z4=fw,R4=typeof window<"u"&&window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__:function(){if(arguments.length!==0)return typeof arguments[0]=="object"?Wl:Wl.apply(null,arguments)};function ar(e,t){function r(...n){if(t){let i=t(...n);if(!i)throw new Error(Bt(0));return{type:e,payload:i.payload,..."meta"in i&&{meta:i.meta},..."error"in i&&{error:i.error}}}return{type:e,payload:n[0]}}return r.toString=()=>`${e}`,r.type=e,r.match=n=>iw(n)&&n.type===e,r}var pw=class Qa extends Array{constructor(...t){super(...t),Object.setPrototypeOf(this,Qa.prototype)}static get[Symbol.species](){return Qa}concat(...t){return super.concat.apply(this,t)}prepend(...t){return t.length===1&&Array.isArray(t[0])?new Qa(...t[0].concat(this)):new Qa(...t.concat(this))}};function e0(e){return en(e)?dw(e,()=>{}):e}function Lo(e,t,r){return e.has(t)?e.get(t):e.set(t,r(t)).get(t)}function B4(e){return typeof e=="boolean"}var F4=()=>function(t){const{thunk:r=!0,immutableCheck:n=!0,serializableCheck:i=!0,actionCreatorCheck:s=!0}=t??{};let o=new pw;return r&&(B4(r)?o.push(L4):o.push(z4(r.extraArgument))),o},hw="RTK_autoBatch",Le=()=>e=>({payload:e,meta:{[hw]:!0}}),t0=e=>t=>{setTimeout(t,e)},mw=(e={type:"raf"})=>t=>(...r)=>{const n=t(...r);let i=!0,s=!1,o=!1;const l=new Set,c=e.type==="tick"?queueMicrotask:e.type==="raf"?typeof window<"u"&&window.requestAnimationFrame?window.requestAnimationFrame:t0(10):e.type==="callback"?e.queueNotification:t0(e.timeout),d=()=>{o=!1,s&&(s=!1,l.forEach(u=>u()))};return Object.assign({},n,{subscribe(u){const f=()=>i&&u(),p=n.subscribe(f);return l.add(u),()=>{p(),l.delete(u)}},dispatch(u){var f;try{return i=!((f=u==null?void 0:u.meta)!=null&&f[hw]),s=!i,s&&(o||(o=!0,c(d))),n.dispatch(u)}finally{i=!0}}})},W4=e=>function(r){const{autoBatch:n=!0}=r??{};let i=new pw(e);return n&&i.push(mw(typeof n=="object"?n:void 0)),i};function U4(e){const t=F4(),{reducer:r=void 0,middleware:n,devTools:i=!0,preloadedState:s=void 0,enhancers:o=void 0}=e||{};let l;if(typeof r=="function")l=r;else if(Yh(r))l=nw(r);else throw new Error(Bt(1));let c;typeof n=="function"?c=n(t):c=t();let d=Wl;i&&(d=R4({trace:!1,...typeof i=="object"&&i}));const u=P4(...c),f=W4(u);let p=typeof o=="function"?o(f):f();const m=d(...p);return rw(l,s,m)}function gw(e){const t={},r=[];let n;const i={addCase(s,o){const l=typeof s=="string"?s:s.type;if(!l)throw new Error(Bt(28));if(l in t)throw new Error(Bt(29));return t[l]=o,i},addAsyncThunk(s,o){return o.pending&&(t[s.pending.type]=o.pending),o.rejected&&(t[s.rejected.type]=o.rejected),o.fulfilled&&(t[s.fulfilled.type]=o.fulfilled),o.settled&&r.push({matcher:s.settled,reducer:o.settled}),i},addMatcher(s,o){return r.push({matcher:s,reducer:o}),i},addDefaultCase(s){return n=s,i}};return e(i),[t,r,n]}$4(!1);function q4(e){return typeof e=="function"}function H4(e,t){let[r,n,i]=gw(t),s;if(q4(e))s=()=>e0(e());else{const l=e0(e);s=()=>l}function o(l=s(),c){let d=[r[c.type],...n.filter(({matcher:u})=>u(c)).map(({reducer:u})=>u)];return d.filter(u=>!!u).length===0&&(d=[i]),d.reduce((u,f)=>{if(f)if(vi(u)){const m=f(u,c);return m===void 0?u:m}else{if(en(u))return dw(u,p=>f(p,c));{const p=f(u,c);if(p===void 0){if(u===null)return u;throw Error("A case reducer on a non-draftable value must not return undefined")}return p}}return u},l)}return o.getInitialState=s,o}var K4="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW",V4=(e=21)=>{let t="",r=e;for(;r--;)t+=K4[Math.random()*64|0];return t},Y4=Symbol.for("rtk-slice-createasyncthunk");function G4(e,t){return`${e}/${t}`}function Z4({creators:e}={}){var r;const t=(r=e==null?void 0:e.asyncThunk)==null?void 0:r[Y4];return function(i){const{name:s,reducerPath:o=s}=i;if(!s)throw new Error(Bt(11));const l=(typeof i.reducers=="function"?i.reducers(J4()):i.reducers)||{},c=Object.keys(l),d={sliceCaseReducersByName:{},sliceCaseReducersByType:{},actionCreators:{},sliceMatchers:[]},u={addCase(w,S){const N=typeof w=="string"?w:w.type;if(!N)throw new Error(Bt(12));if(N in d.sliceCaseReducersByType)throw new Error(Bt(13));return d.sliceCaseReducersByType[N]=S,u},addMatcher(w,S){return d.sliceMatchers.push({matcher:w,reducer:S}),u},exposeAction(w,S){return d.actionCreators[w]=S,u},exposeCaseReducer(w,S){return d.sliceCaseReducersByName[w]=S,u}};c.forEach(w=>{const S=l[w],N={reducerName:w,type:G4(s,w),createNotation:typeof i.reducers=="function"};eE(S)?rE(N,S,u,t):Q4(N,S,u)});function f(){const[w={},S=[],N=void 0]=typeof i.extraReducers=="function"?gw(i.extraReducers):[i.extraReducers],P={...w,...d.sliceCaseReducersByType};return H4(i.initialState,_=>{for(let T in P)_.addCase(T,P[T]);for(let T of d.sliceMatchers)_.addMatcher(T.matcher,T.reducer);for(let T of S)_.addMatcher(T.matcher,T.reducer);N&&_.addDefaultCase(N)})}const p=w=>w,m=new Map,x=new WeakMap;let g;function b(w,S){return g||(g=f()),g(w,S)}function v(){return g||(g=f()),g.getInitialState()}function j(w,S=!1){function N(_){let T=_[w];return typeof T>"u"&&S&&(T=Lo(x,N,v)),T}function P(_=p){const T=Lo(m,S,()=>new WeakMap);return Lo(T,_,()=>{const $={};for(const[M,C]of Object.entries(i.selectors??{}))$[M]=X4(C,_,()=>Lo(x,_,v),S);return $})}return{reducerPath:w,getSelectors:P,get selectors(){return P(N)},selectSlice:N}}const y={name:s,reducer:b,actions:d.actionCreators,caseReducers:d.sliceCaseReducersByName,getInitialState:v,...j(o),injectInto(w,{reducerPath:S,...N}={}){const P=S??o;return w.inject({reducerPath:P,reducer:b},N),{...y,...j(P,!0)}}};return y}}function X4(e,t,r,n){function i(s,...o){let l=t(s);return typeof l>"u"&&n&&(l=r()),e(l,...o)}return i.unwrapped=e,i}var At=Z4();function J4(){function e(t,r){return{_reducerDefinitionType:"asyncThunk",payloadCreator:t,...r}}return e.withTypes=()=>e,{reducer(t){return Object.assign({[t.name](...r){return t(...r)}}[t.name],{_reducerDefinitionType:"reducer"})},preparedReducer(t,r){return{_reducerDefinitionType:"reducerWithPrepare",prepare:t,reducer:r}},asyncThunk:e}}function Q4({type:e,reducerName:t,createNotation:r},n,i){let s,o;if("reducer"in n){if(r&&!tE(n))throw new Error(Bt(17));s=n.reducer,o=n.prepare}else s=n;i.addCase(e,s).exposeCaseReducer(t,s).exposeAction(t,o?ar(e,o):ar(e))}function eE(e){return e._reducerDefinitionType==="asyncThunk"}function tE(e){return e._reducerDefinitionType==="reducerWithPrepare"}function rE({type:e,reducerName:t},r,n,i){if(!i)throw new Error(Bt(18));const{payloadCreator:s,fulfilled:o,pending:l,rejected:c,settled:d,options:u}=r,f=i(e,s,u);n.exposeAction(t,f),o&&n.addCase(f.fulfilled,o),l&&n.addCase(f.pending,l),c&&n.addCase(f.rejected,c),d&&n.addMatcher(f.settled,d),n.exposeCaseReducer(t,{fulfilled:o||zo,pending:l||zo,rejected:c||zo,settled:d||zo})}function zo(){}var nE="task",xw="listener",yw="completed",Xh="cancelled",iE=`task-${Xh}`,aE=`task-${yw}`,qf=`${xw}-${Xh}`,sE=`${xw}-${yw}`,nu=class{constructor(e){ho(this,"name","TaskAbortError");ho(this,"message");this.code=e,this.message=`${nE} ${Xh} (reason: ${e})`}},Jh=(e,t)=>{if(typeof e!="function")throw new TypeError(Bt(32))},Kl=()=>{},vw=(e,t=Kl)=>(e.catch(t),e),bw=(e,t)=>(e.addEventListener("abort",t,{once:!0}),()=>e.removeEventListener("abort",t)),li=(e,t)=>{const r=e.signal;r.aborted||("reason"in r||Object.defineProperty(r,"reason",{enumerable:!0,value:t,configurable:!0,writable:!0}),e.abort(t))},ci=e=>{if(e.aborted){const{reason:t}=e;throw new nu(t)}};function jw(e,t){let r=Kl;return new Promise((n,i)=>{const s=()=>i(new nu(e.reason));if(e.aborted){s();return}r=bw(e,s),t.finally(()=>r()).then(n,i)}).finally(()=>{r=Kl})}var oE=async(e,t)=>{try{return await Promise.resolve(),{status:"ok",value:await e()}}catch(r){return{status:r instanceof nu?"cancelled":"rejected",error:r}}finally{t==null||t()}},Vl=e=>t=>vw(jw(e,t).then(r=>(ci(e),r))),ww=e=>{const t=Vl(e);return r=>t(new Promise(n=>setTimeout(n,r)))},{assign:na}=Object,r0={},iu="listenerMiddleware",lE=(e,t)=>{const r=n=>bw(e,()=>li(n,e.reason));return(n,i)=>{Jh(n);const s=new AbortController;r(s);const o=oE(async()=>{ci(e),ci(s.signal);const l=await n({pause:Vl(s.signal),delay:ww(s.signal),signal:s.signal});return ci(s.signal),l},()=>li(s,aE));return i!=null&&i.autoJoin&&t.push(o.catch(Kl)),{result:Vl(e)(o),cancel(){li(s,iE)}}}},cE=(e,t)=>{const r=async(n,i)=>{ci(t);let s=()=>{};const l=[new Promise((c,d)=>{let u=e({predicate:n,effect:(f,p)=>{p.unsubscribe(),c([f,p.getState(),p.getOriginalState()])}});s=()=>{u(),d()}})];i!=null&&l.push(new Promise(c=>setTimeout(c,i,null)));try{const c=await jw(t,Promise.race(l));return ci(t),c}finally{s()}};return(n,i)=>vw(r(n,i))},Sw=e=>{let{type:t,actionCreator:r,matcher:n,predicate:i,effect:s}=e;if(t)i=ar(t).match;else if(r)t=r.type,i=r.match;else if(n)i=n;else if(!i)throw new Error(Bt(21));return Jh(s),{predicate:i,type:t,effect:s}},Nw=na(e=>{const{type:t,predicate:r,effect:n}=Sw(e);return{id:V4(),effect:n,type:t,predicate:r,pending:new Set,unsubscribe:()=>{throw new Error(Bt(22))}}},{withTypes:()=>Nw}),n0=(e,t)=>{const{type:r,effect:n,predicate:i}=Sw(t);return Array.from(e.values()).find(s=>(typeof r=="string"?s.type===r:s.predicate===i)&&s.effect===n)},Hf=e=>{e.pending.forEach(t=>{li(t,qf)})},uE=(e,t)=>()=>{for(const r of t.keys())Hf(r);e.clear()},i0=(e,t,r)=>{try{e(t,r)}catch(n){setTimeout(()=>{throw n},0)}},kw=na(ar(`${iu}/add`),{withTypes:()=>kw}),dE=ar(`${iu}/removeAll`),Pw=na(ar(`${iu}/remove`),{withTypes:()=>Pw}),fE=(...e)=>{console.error(`${iu}/error`,...e)},Qs=(e={})=>{const t=new Map,r=new Map,n=m=>{const x=r.get(m)??0;r.set(m,x+1)},i=m=>{const x=r.get(m)??1;x===1?r.delete(m):r.set(m,x-1)},{extra:s,onError:o=fE}=e;Jh(o);const l=m=>(m.unsubscribe=()=>t.delete(m.id),t.set(m.id,m),x=>{m.unsubscribe(),x!=null&&x.cancelActive&&Hf(m)}),c=m=>{const x=n0(t,m)??Nw(m);return l(x)};na(c,{withTypes:()=>c});const d=m=>{const x=n0(t,m);return x&&(x.unsubscribe(),m.cancelActive&&Hf(x)),!!x};na(d,{withTypes:()=>d});const u=async(m,x,g,b)=>{const v=new AbortController,j=cE(c,v.signal),y=[];try{m.pending.add(v),n(m),await Promise.resolve(m.effect(x,na({},g,{getOriginalState:b,condition:(w,S)=>j(w,S).then(Boolean),take:j,delay:ww(v.signal),pause:Vl(v.signal),extra:s,signal:v.signal,fork:lE(v.signal,y),unsubscribe:m.unsubscribe,subscribe:()=>{t.set(m.id,m)},cancelActiveListeners:()=>{m.pending.forEach((w,S,N)=>{w!==v&&(li(w,qf),N.delete(w))})},cancel:()=>{li(v,qf),m.pending.delete(v)},throwIfCancelled:()=>{ci(v.signal)}})))}catch(w){w instanceof nu||i0(o,w,{raisedBy:"effect"})}finally{await Promise.all(y),li(v,sE),i(m),m.pending.delete(v)}},f=uE(t,r);return{middleware:m=>x=>g=>{if(!iw(g))return x(g);if(kw.match(g))return c(g.payload);if(dE.match(g)){f();return}if(Pw.match(g))return d(g.payload);let b=m.getState();const v=()=>{if(b===r0)throw new Error(Bt(23));return b};let j;try{if(j=x(g),t.size>0){const y=m.getState(),w=Array.from(t.values());for(const S of w){let N=!1;try{N=S.predicate(g,y,b)}catch(P){N=!1,i0(o,P,{raisedBy:"predicate"})}N&&u(S,g,m,v)}}}finally{b=r0}return j},startListening:c,stopListening:d,clearListeners:f}};function Bt(e){return`Minified Redux Toolkit error #${e}; visit https://redux-toolkit.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}var pE={layoutType:"horizontal",width:0,height:0,margin:{top:5,right:5,bottom:5,left:5},scale:1},_w=At({name:"chartLayout",initialState:pE,reducers:{setLayout(e,t){e.layoutType=t.payload},setChartSize(e,t){e.width=t.payload.width,e.height=t.payload.height},setMargin(e,t){var r,n,i,s;e.margin.top=(r=t.payload.top)!==null&&r!==void 0?r:0,e.margin.right=(n=t.payload.right)!==null&&n!==void 0?n:0,e.margin.bottom=(i=t.payload.bottom)!==null&&i!==void 0?i:0,e.margin.left=(s=t.payload.left)!==null&&s!==void 0?s:0},setScale(e,t){e.scale=t.payload}}}),{setMargin:hE,setLayout:mE,setChartSize:gE,setScale:xE}=_w.actions,yE=_w.reducer;function Cw(e,t,r){return Array.isArray(e)&&e&&t+r!==0?e.slice(t,r+1):e}function a0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Yi(e){for(var t=1;t{if(t&&r){var{width:n,height:i}=r,{align:s,verticalAlign:o,layout:l}=t;if((l==="vertical"||l==="horizontal"&&o==="middle")&&s!=="center"&&Y(e[s]))return Yi(Yi({},e),{},{[s]:e[s]+(n||0)});if((l==="horizontal"||l==="vertical"&&s==="center")&&o!=="middle"&&Y(e[o]))return Yi(Yi({},e),{},{[o]:e[o]+(i||0)})}return e},Mr=(e,t)=>e==="horizontal"&&t==="xAxis"||e==="vertical"&&t==="yAxis"||e==="centric"&&t==="angleAxis"||e==="radial"&&t==="radiusAxis",Aw=(e,t,r,n)=>{if(n)return e.map(l=>l.coordinate);var i,s,o=e.map(l=>(l.coordinate===t&&(i=!0),l.coordinate===r&&(s=!0),l.coordinate));return i||o.push(t),s||o.push(r),o},Ow=(e,t,r)=>{if(!e)return null;var{duplicateDomain:n,type:i,range:s,scale:o,realScaleType:l,isCategorical:c,categoricalDomain:d,tickCount:u,ticks:f,niceTicks:p,axisType:m}=e;if(!o)return null;var x=l==="scaleBand"&&o.bandwidth?o.bandwidth()/2:2,g=i==="category"&&o.bandwidth?o.bandwidth()/x:0;if(g=m==="angleAxis"&&s&&s.length>=2?Jt(s[0]-s[1])*2*g:g,f||p){var b=(f||p||[]).map((v,j)=>{var y=n?n.indexOf(v):v;return{coordinate:o(y)+g,value:v,offset:g,index:j}});return b.filter(v=>!yr(v.coordinate))}return c&&d?d.map((v,j)=>({coordinate:o(v)+g,value:v,index:j,offset:g})):o.ticks&&u!=null?o.ticks(u).map((v,j)=>({coordinate:o(v)+g,value:v,offset:g,index:j})):o.domain().map((v,j)=>({coordinate:o(v)+g,value:n?n[v]:v,index:j,offset:g}))},s0=1e-4,SE=e=>{var t=e.domain();if(!(!t||t.length<=2)){var r=t.length,n=e.range(),i=Math.min(n[0],n[1])-s0,s=Math.max(n[0],n[1])+s0,o=e(t[0]),l=e(t[r-1]);(os||ls)&&e.domain([t[0],t[r-1]])}},NE=e=>{var t=e.length;if(!(t<=0))for(var r=0,n=e[0].length;r=0?(e[o][r][0]=i,e[o][r][1]=i+l,i=e[o][r][1]):(e[o][r][0]=s,e[o][r][1]=s+l,s=e[o][r][1])}},kE=e=>{var t=e.length;if(!(t<=0))for(var r=0,n=e[0].length;r=0?(e[s][r][0]=i,e[s][r][1]=i+o,i=e[s][r][1]):(e[s][r][0]=0,e[s][r][1]=0)}},PE={sign:NE,expand:$6,none:pa,silhouette:L6,wiggle:z6,positive:kE},_E=(e,t,r)=>{var n=PE[r],i=I6().keys(t).value((s,o)=>Number(et(s,o,0))).order(If).offset(n);return i(e)};function CE(e){return e==null?void 0:String(e)}function Yl(e){var{axis:t,ticks:r,bandSize:n,entry:i,index:s,dataKey:o}=e;if(t.type==="category"){if(!t.allowDuplicatedCategory&&t.dataKey&&!Re(i[t.dataKey])){var l=Cj(r,"value",i[t.dataKey]);if(l)return l.coordinate+n/2}return r[s]?r[s].coordinate+n/2:null}var c=et(i,Re(o)?t.dataKey:o);return Re(c)?null:t.scale(c)}var AE=e=>{var t=e.flat(2).filter(Y);return[Math.min(...t),Math.max(...t)]},OE=e=>[e[0]===1/0?0:e[0],e[1]===-1/0?0:e[1]],EE=(e,t,r)=>{if(e!=null)return OE(Object.keys(e).reduce((n,i)=>{var s=e[i],{stackedData:o}=s,l=o.reduce((c,d)=>{var u=Cw(d,t,r),f=AE(u);return[Math.min(c[0],f[0]),Math.max(c[1],f[1])]},[1/0,-1/0]);return[Math.min(l[0],n[0]),Math.max(l[1],n[1])]},[1/0,-1/0]))},o0=/^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,l0=/^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,ha=(e,t,r)=>{if(e&&e.scale&&e.scale.bandwidth){var n=e.scale.bandwidth();if(!r||n>0)return n}if(e&&t&&t.length>=2){for(var i=Qc(t,u=>u.coordinate),s=1/0,o=1,l=i.length;o{if(t==="horizontal")return e.chartX;if(t==="vertical")return e.chartY},TE=(e,t)=>t==="centric"?e.angle:e.radius,ln=e=>e.layout.width,cn=e=>e.layout.height,ME=e=>e.layout.scale,Ew=e=>e.layout.margin,su=I(e=>e.cartesianAxis.xAxis,e=>Object.values(e)),ou=I(e=>e.cartesianAxis.yAxis,e=>Object.values(e)),IE="data-recharts-item-index",$E="data-recharts-item-data-key",eo=60;function u0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ro(e){for(var t=1;te.brush.height;function FE(e){var t=ou(e);return t.reduce((r,n)=>{if(n.orientation==="left"&&!n.mirror&&!n.hide){var i=typeof n.width=="number"?n.width:eo;return r+i}return r},0)}function WE(e){var t=ou(e);return t.reduce((r,n)=>{if(n.orientation==="right"&&!n.mirror&&!n.hide){var i=typeof n.width=="number"?n.width:eo;return r+i}return r},0)}function UE(e){var t=su(e);return t.reduce((r,n)=>n.orientation==="top"&&!n.mirror&&!n.hide?r+n.height:r,0)}function qE(e){var t=su(e);return t.reduce((r,n)=>n.orientation==="bottom"&&!n.mirror&&!n.hide?r+n.height:r,0)}var rt=I([ln,cn,Ew,BE,FE,WE,UE,qE,tw,b4],(e,t,r,n,i,s,o,l,c,d)=>{var u={left:(r.left||0)+i,right:(r.right||0)+s},f={top:(r.top||0)+o,bottom:(r.bottom||0)+l},p=Ro(Ro({},f),u),m=p.bottom;p.bottom+=n,p=wE(p,c,d);var x=e-p.left-p.right,g=t-p.top-p.bottom;return Ro(Ro({brushBottom:m},p),{},{width:Math.max(x,0),height:Math.max(g,0)})}),HE=I(rt,e=>({x:e.left,y:e.top,width:e.width,height:e.height})),Dw=I(ln,cn,(e,t)=>({x:0,y:0,width:e,height:t})),KE=h.createContext(null),pt=()=>h.useContext(KE)!=null,lu=e=>e.brush,cu=I([lu,rt,Ew],(e,t,r)=>({height:e.height,x:Y(e.x)?e.x:t.left,y:Y(e.y)?e.y:t.top+t.height+t.brushBottom-((r==null?void 0:r.bottom)||0),width:Y(e.width)?e.width:t.width})),Tw={},Mw={},Iw={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r,n,{signal:i,edges:s}={}){let o,l=null;const c=s!=null&&s.includes("leading"),d=s==null||s.includes("trailing"),u=()=>{l!==null&&(r.apply(o,l),o=void 0,l=null)},f=()=>{d&&u(),g()};let p=null;const m=()=>{p!=null&&clearTimeout(p),p=setTimeout(()=>{p=null,f()},n)},x=()=>{p!==null&&(clearTimeout(p),p=null)},g=()=>{x(),o=void 0,l=null},b=()=>{u()},v=function(...j){if(i!=null&&i.aborted)return;o=this,l=j;const y=p==null;m(),c&&y&&u()};return v.schedule=m,v.cancel=g,v.flush=b,i==null||i.addEventListener("abort",g,{once:!0}),v}e.debounce=t})(Iw);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Iw;function r(n,i=0,s={}){typeof s!="object"&&(s={});const{leading:o=!1,trailing:l=!0,maxWait:c}=s,d=Array(2);o&&(d[0]="leading"),l&&(d[1]="trailing");let u,f=null;const p=t.debounce(function(...g){u=n.apply(this,g),f=null},i,{edges:d}),m=function(...g){return c!=null&&(f===null&&(f=Date.now()),Date.now()-f>=c)?(u=n.apply(this,g),f=Date.now(),p.cancel(),p.schedule(),u):(p.apply(this,g),u)},x=()=>(p.flush(),u);return m.cancel=p.cancel,m.flush=x,m}e.debounce=r})(Mw);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Mw;function r(n,i=0,s={}){const{leading:o=!0,trailing:l=!0}=s;return t.debounce(n,i,{leading:o,maxWait:i,trailing:l})}e.throttle=r})(Tw);var VE=Tw.throttle;const YE=Tr(VE);var Gl=function(t,r){for(var n=arguments.length,i=new Array(n>2?n-2:0),s=2;s{var{width:n="100%",height:i="100%",aspect:s,maxHeight:o}=r,l=Qr(n)?e:Number(n),c=Qr(i)?t:Number(i);return s&&s>0&&(l?c=l/s:c&&(l=c*s),o&&c!=null&&c>o&&(c=o)),{calculatedWidth:l,calculatedHeight:c}},GE={width:0,height:0,overflow:"visible"},ZE={width:0,overflowX:"visible"},XE={height:0,overflowY:"visible"},JE={},QE=e=>{var{width:t,height:r}=e,n=Qr(t),i=Qr(r);return n&&i?GE:n?ZE:i?XE:JE};function e3(e){var{width:t,height:r,aspect:n}=e,i=t,s=r;return i===void 0&&s===void 0?(i="100%",s="100%"):i===void 0?i=n&&n>0?void 0:"100%":s===void 0&&(s=n&&n>0?void 0:"100%"),{width:i,height:s}}function _e(e){return Number.isFinite(e)}function Er(e){return typeof e=="number"&&e>0&&Number.isFinite(e)}function Kf(){return Kf=Object.assign?Object.assign.bind():function(e){for(var t=1;t({width:r,height:n}),[r,n]);return i3(i)?h.createElement(Lw.Provider,{value:i},t):null}var Qh=()=>h.useContext(Lw),a3=h.forwardRef((e,t)=>{var{aspect:r,initialDimension:n={width:-1,height:-1},width:i,height:s,minWidth:o=0,minHeight:l,maxHeight:c,children:d,debounce:u=0,id:f,className:p,onResize:m,style:x={}}=e,g=h.useRef(null),b=h.useRef();b.current=m,h.useImperativeHandle(t,()=>g.current);var[v,j]=h.useState({containerWidth:n.width,containerHeight:n.height}),y=h.useCallback((_,T)=>{j($=>{var M=Math.round(_),C=Math.round(T);return $.containerWidth===M&&$.containerHeight===C?$:{containerWidth:M,containerHeight:C}})},[]);h.useEffect(()=>{if(g.current==null||typeof ResizeObserver>"u")return ka;var _=C=>{var R,{width:q,height:Z}=C[0].contentRect;y(q,Z),(R=b.current)===null||R===void 0||R.call(b,q,Z)};u>0&&(_=YE(_,u,{trailing:!0,leading:!1}));var T=new ResizeObserver(_),{width:$,height:M}=g.current.getBoundingClientRect();return y($,M),T.observe(g.current),()=>{T.disconnect()}},[y,u]);var{containerWidth:w,containerHeight:S}=v;Gl(!r||r>0,"The aspect(%s) must be greater than zero.",r);var{calculatedWidth:N,calculatedHeight:P}=$w(w,S,{width:i,height:s,aspect:r,maxHeight:c});return Gl(N!=null&&N>0||P!=null&&P>0,`The width(%s) and height(%s) of chart should be greater than 0, + please check the style of container, or the props width(%s) and height(%s), + or add a minWidth(%s) or minHeight(%s) or use aspect(%s) to control the + height and width.`,N,P,i,s,o,l,r),h.createElement("div",{id:f?"".concat(f):void 0,className:ce("recharts-responsive-container",p),style:f0(f0({},x),{},{width:i,height:s,minWidth:o,minHeight:l,maxHeight:c}),ref:g},h.createElement("div",{style:QE({width:i,height:s})},h.createElement(zw,{width:N,height:P},d)))}),p0=h.forwardRef((e,t)=>{var r=Qh();if(Er(r.width)&&Er(r.height))return e.children;var{width:n,height:i}=e3({width:e.width,height:e.height,aspect:e.aspect}),{calculatedWidth:s,calculatedHeight:o}=$w(void 0,void 0,{width:n,height:i,aspect:e.aspect,maxHeight:e.maxHeight});return Y(s)&&Y(o)?h.createElement(zw,{width:s,height:o},e.children):h.createElement(a3,Kf({},e,{width:n,height:i,ref:t}))});function Rw(e){if(e)return{x:e.x,y:e.y,upperWidth:"upperWidth"in e?e.upperWidth:e.width,lowerWidth:"lowerWidth"in e?e.lowerWidth:e.width,width:e.width,height:e.height}}var uu=()=>{var e,t=pt(),r=G(HE),n=G(cu),i=(e=G(lu))===null||e===void 0?void 0:e.padding;return!t||!n||!i?r:{width:n.width-i.left-i.right,height:n.height-i.top-i.bottom,x:i.left,y:i.top}},s3={top:0,bottom:0,left:0,right:0,width:0,height:0,brushBottom:0},Bw=()=>{var e;return(e=G(rt))!==null&&e!==void 0?e:s3},Fw=()=>G(ln),Ww=()=>G(cn),ue=e=>e.layout.layoutType,to=()=>G(ue),o3=()=>{var e=to();return e!==void 0},du=e=>{var t=Ve(),r=pt(),{width:n,height:i}=e,s=Qh(),o=n,l=i;return s&&(o=s.width>0?s.width:n,l=s.height>0?s.height:i),h.useEffect(()=>{!r&&Er(o)&&Er(l)&&t(gE({width:o,height:l}))},[t,r,o,l]),null},l3={settings:{layout:"horizontal",align:"center",verticalAlign:"middle",itemSorter:"value"},size:{width:0,height:0},payload:[]},Uw=At({name:"legend",initialState:l3,reducers:{setLegendSize(e,t){e.size.width=t.payload.width,e.size.height=t.payload.height},setLegendSettings(e,t){e.settings.align=t.payload.align,e.settings.layout=t.payload.layout,e.settings.verticalAlign=t.payload.verticalAlign,e.settings.itemSorter=t.payload.itemSorter},addLegendPayload:{reducer(e,t){e.payload.push(t.payload)},prepare:Le()},removeLegendPayload:{reducer(e,t){var r=Kr(e).payload.indexOf(t.payload);r>-1&&e.payload.splice(r,1)},prepare:Le()}}}),{setLegendSize:_7,setLegendSettings:C7,addLegendPayload:c3,removeLegendPayload:u3}=Uw.actions,d3=Uw.reducer;function Vf(){return Vf=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{separator:t=" : ",contentStyle:r={},itemStyle:n={},labelStyle:i={},payload:s,formatter:o,itemSorter:l,wrapperClassName:c,labelClassName:d,label:u,labelFormatter:f,accessibilityLayer:p=!1}=e,m=()=>{if(s&&s.length){var S={padding:0,margin:0},N=(l?Qc(s,l):s).map((P,_)=>{if(P.type==="none")return null;var T=P.formatter||o||m3,{value:$,name:M}=P,C=$,R=M;if(T){var q=T($,M,P,_,s);if(Array.isArray(q))[C,R]=q;else if(q!=null)C=q;else return null}var Z=hd({display:"block",paddingTop:4,paddingBottom:4,color:P.color||"#000"},n);return h.createElement("li",{className:"recharts-tooltip-item",key:"tooltip-item-".concat(_),style:Z},Or(R)?h.createElement("span",{className:"recharts-tooltip-item-name"},R):null,Or(R)?h.createElement("span",{className:"recharts-tooltip-item-separator"},t):null,h.createElement("span",{className:"recharts-tooltip-item-value"},C),h.createElement("span",{className:"recharts-tooltip-item-unit"},P.unit||""))});return h.createElement("ul",{className:"recharts-tooltip-item-list",style:S},N)}return null},x=hd({margin:0,padding:10,backgroundColor:"#fff",border:"1px solid #ccc",whiteSpace:"nowrap"},r),g=hd({margin:0},i),b=!Re(u),v=b?u:"",j=ce("recharts-default-tooltip",c),y=ce("recharts-tooltip-label",d);b&&f&&s!==void 0&&s!==null&&(v=f(u,s));var w=p?{role:"status","aria-live":"assertive"}:{};return h.createElement("div",Vf({className:j,style:x},w),h.createElement("p",{className:y,style:g},h.isValidElement(v)?v:"".concat(v)),m())},Wa="recharts-tooltip-wrapper",x3={visibility:"hidden"};function y3(e){var{coordinate:t,translateX:r,translateY:n}=e;return ce(Wa,{["".concat(Wa,"-right")]:Y(r)&&t&&Y(t.x)&&r>=t.x,["".concat(Wa,"-left")]:Y(r)&&t&&Y(t.x)&&r=t.y,["".concat(Wa,"-top")]:Y(n)&&t&&Y(t.y)&&n0?i:0),f=r[n]+i;if(t[n])return o[n]?u:f;var p=c[n];if(p==null)return 0;if(o[n]){var m=u,x=p;return mb?Math.max(u,p):Math.max(f,p)}function v3(e){var{translateX:t,translateY:r,useTranslate3d:n}=e;return{transform:n?"translate3d(".concat(t,"px, ").concat(r,"px, 0)"):"translate(".concat(t,"px, ").concat(r,"px)")}}function b3(e){var{allowEscapeViewBox:t,coordinate:r,offsetTopLeft:n,position:i,reverseDirection:s,tooltipBox:o,useTranslate3d:l,viewBox:c}=e,d,u,f;return o.height>0&&o.width>0&&r?(u=m0({allowEscapeViewBox:t,coordinate:r,key:"x",offsetTopLeft:n,position:i,reverseDirection:s,tooltipDimension:o.width,viewBox:c,viewBoxDimension:c.width}),f=m0({allowEscapeViewBox:t,coordinate:r,key:"y",offsetTopLeft:n,position:i,reverseDirection:s,tooltipDimension:o.height,viewBox:c,viewBoxDimension:c.height}),d=v3({translateX:u,translateY:f,useTranslate3d:l})):d=x3,{cssProperties:d,cssClasses:y3({translateX:u,translateY:f,coordinate:r})}}function g0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Bo(e){for(var t=1;t{if(t.key==="Escape"){var r,n,i,s;this.setState({dismissed:!0,dismissedAtCoordinate:{x:(r=(n=this.props.coordinate)===null||n===void 0?void 0:n.x)!==null&&r!==void 0?r:0,y:(i=(s=this.props.coordinate)===null||s===void 0?void 0:s.y)!==null&&i!==void 0?i:0}})}})}componentDidMount(){document.addEventListener("keydown",this.handleKeyDown)}componentWillUnmount(){document.removeEventListener("keydown",this.handleKeyDown)}componentDidUpdate(){var t,r;this.state.dismissed&&(((t=this.props.coordinate)===null||t===void 0?void 0:t.x)!==this.state.dismissedAtCoordinate.x||((r=this.props.coordinate)===null||r===void 0?void 0:r.y)!==this.state.dismissedAtCoordinate.y)&&(this.state.dismissed=!1)}render(){var{active:t,allowEscapeViewBox:r,animationDuration:n,animationEasing:i,children:s,coordinate:o,hasPayload:l,isAnimationActive:c,offset:d,position:u,reverseDirection:f,useTranslate3d:p,viewBox:m,wrapperStyle:x,lastBoundingBox:g,innerRef:b,hasPortalFromProps:v}=this.props,{cssClasses:j,cssProperties:y}=b3({allowEscapeViewBox:r,coordinate:o,offsetTopLeft:d,position:u,reverseDirection:f,tooltipBox:{height:g.height,width:g.width},useTranslate3d:p,viewBox:m}),w=v?{}:Bo(Bo({transition:c&&t?"transform ".concat(n,"ms ").concat(i):void 0},y),{},{pointerEvents:"none",visibility:!this.state.dismissed&&t&&l?"visible":"hidden",position:"absolute",top:0,left:0}),S=Bo(Bo({},w),{},{visibility:!this.state.dismissed&&t&&l?"visible":"hidden"},x);return h.createElement("div",{xmlns:"http://www.w3.org/1999/xhtml",tabIndex:-1,className:j,style:S,ref:b},s)}}var N3=()=>!(typeof window<"u"&&window.document&&window.document.createElement&&window.setTimeout),Ci={devToolsEnabled:!1,isSsr:N3()},qw=()=>{var e;return(e=G(t=>t.rootProps.accessibilityLayer))!==null&&e!==void 0?e:!0};function Gf(){return Gf=Object.assign?Object.assign.bind():function(e){for(var t=1;t_e(e.x)&&_e(e.y),b0=e=>e.base!=null&&Zl(e.base)&&Zl(e),Ua=e=>e.x,qa=e=>e.y,C3=(e,t)=>{if(typeof e=="function")return e;var r="curve".concat(Xs(e));return(r==="curveMonotone"||r==="curveBump")&&t?v0["".concat(r).concat(t==="vertical"?"Y":"X")]:v0[r]||Kc},A3=e=>{var{type:t="linear",points:r=[],baseLine:n,layout:i,connectNulls:s=!1}=e,o=C3(t,i),l=s?r.filter(Zl):r,c;if(Array.isArray(n)){var d=r.map((m,x)=>y0(y0({},m),{},{base:n[x]}));i==="vertical"?c=To().y(qa).x1(Ua).x0(m=>m.base.x):c=To().x(Ua).y1(qa).y0(m=>m.base.y);var u=c.defined(b0).curve(o),f=s?d.filter(b0):d;return u(f)}i==="vertical"&&Y(n)?c=To().y(qa).x1(Ua).x0(n):Y(n)?c=To().x(Ua).y1(qa).y0(n):c=hj().x(Ua).y(qa);var p=c.defined(Zl).curve(o);return p(l)},us=e=>{var{className:t,points:r,path:n,pathRef:i}=e;if((!r||!r.length)&&!n)return null;var s=r&&r.length?A3(e):n;return h.createElement("path",Gf({},nr(e),Ih(e),{className:ce("recharts-curve",t),d:s===null?void 0:s,ref:i}))},O3=["x","y","top","left","width","height","className"];function Zf(){return Zf=Object.assign?Object.assign.bind():function(e){for(var t=1;t"M".concat(e,",").concat(i,"v").concat(n,"M").concat(s,",").concat(t,"h").concat(r),z3=e=>{var{x:t=0,y:r=0,top:n=0,left:i=0,width:s=0,height:o=0,className:l}=e,c=I3(e,O3),d=E3({x:t,y:r,top:n,left:i,width:s,height:o},c);return!Y(t)||!Y(r)||!Y(s)||!Y(o)||!Y(n)||!Y(i)?null:h.createElement("path",Zf({},ut(d),{className:ce("recharts-cross",l),d:L3(t,r,s,o,n,i)}))};function R3(e,t,r,n){var i=n/2;return{stroke:"none",fill:"#ccc",x:e==="horizontal"?t.x-i:r.left+.5,y:e==="horizontal"?r.top+.5:t.y-i,width:e==="horizontal"?n:r.width-1,height:e==="horizontal"?r.height-1:n}}function w0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function S0(e){for(var t=1;te.replace(/([A-Z])/g,t=>"-".concat(t.toLowerCase())),Hw=(e,t,r)=>e.map(n=>"".concat(U3(n)," ").concat(t,"ms ").concat(r)).join(","),q3=(e,t)=>[Object.keys(e),Object.keys(t)].reduce((r,n)=>r.filter(i=>n.includes(i))),Ls=(e,t)=>Object.keys(t).reduce((r,n)=>S0(S0({},r),{},{[n]:e(n,t[n])}),{});function N0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function $e(e){for(var t=1;te+(t-e)*r,Xf=e=>{var{from:t,to:r}=e;return t!==r},Kw=(e,t,r)=>{var n=Ls((i,s)=>{if(Xf(s)){var[o,l]=e(s.from,s.to,s.velocity);return $e($e({},s),{},{from:o,velocity:l})}return s},t);return r<1?Ls((i,s)=>Xf(s)?$e($e({},s),{},{velocity:Xl(s.velocity,n[i].velocity,r),from:Xl(s.from,n[i].from,r)}):s,t):Kw(e,n,r-1)};function Y3(e,t,r,n,i,s){var o,l=n.reduce((p,m)=>$e($e({},p),{},{[m]:{from:e[m],velocity:0,to:t[m]}}),{}),c=()=>Ls((p,m)=>m.from,l),d=()=>!Object.values(l).filter(Xf).length,u=null,f=p=>{o||(o=p);var m=p-o,x=m/r.dt;l=Kw(r,l,x),i($e($e($e({},e),t),c())),o=p,d()||(u=s.setTimeout(f))};return()=>(u=s.setTimeout(f),()=>{var p;(p=u)===null||p===void 0||p()})}function G3(e,t,r,n,i,s,o){var l=null,c=i.reduce((f,p)=>$e($e({},f),{},{[p]:[e[p],t[p]]}),{}),d,u=f=>{d||(d=f);var p=(f-d)/n,m=Ls((g,b)=>Xl(...b,r(p)),c);if(s($e($e($e({},e),t),m)),p<1)l=o.setTimeout(u);else{var x=Ls((g,b)=>Xl(...b,r(1)),c);s($e($e($e({},e),t),x))}};return()=>(l=o.setTimeout(u),()=>{var f;(f=l)===null||f===void 0||f()})}const Z3=(e,t,r,n,i,s)=>{var o=q3(e,t);return r==null?()=>(i($e($e({},e),t)),()=>{}):r.isStepper===!0?Y3(e,t,r,o,i,s):G3(e,t,r,n,o,i,s)};var Jl=1e-4,Vw=(e,t)=>[0,3*e,3*t-6*e,3*e-3*t+1],Yw=(e,t)=>e.map((r,n)=>r*t**n).reduce((r,n)=>r+n),k0=(e,t)=>r=>{var n=Vw(e,t);return Yw(n,r)},X3=(e,t)=>r=>{var n=Vw(e,t),i=[...n.map((s,o)=>s*o).slice(1),0];return Yw(i,r)},J3=function(){for(var t=arguments.length,r=new Array(t),n=0;nparseFloat(l));return[o[0],o[1],o[2],o[3]]}}}return r.length===4?r:[0,0,1,1]},Q3=(e,t,r,n)=>{var i=k0(e,r),s=k0(t,n),o=X3(e,r),l=d=>d>1?1:d<0?0:d,c=d=>{for(var u=d>1?1:d,f=u,p=0;p<8;++p){var m=i(f)-u,x=o(f);if(Math.abs(m-u)0&&arguments[0]!==void 0?arguments[0]:{},{stiff:r=100,damping:n=8,dt:i=17}=t,s=(o,l,c)=>{var d=-(o-l)*r,u=c*n,f=c+(d-u)*i/1e3,p=c*i/1e3+o;return Math.abs(p-l){if(typeof e=="string")switch(e){case"ease":case"ease-in-out":case"ease-out":case"ease-in":case"linear":return P0(e);case"spring":return eD();default:if(e.split("(")[0]==="cubic-bezier")return P0(e)}return typeof e=="function"?e:null};function rD(e){var t,r=()=>null,n=!1,i=null,s=o=>{if(!n){if(Array.isArray(o)){if(!o.length)return;var l=o,[c,...d]=l;if(typeof c=="number"){i=e.setTimeout(s.bind(null,d),c);return}s(c),i=e.setTimeout(s.bind(null,d));return}typeof o=="string"&&(t=o,r(t)),typeof o=="object"&&(t=o,r(t)),typeof o=="function"&&o()}};return{stop:()=>{n=!0},start:o=>{n=!1,i&&(i(),i=null),s(o)},subscribe:o=>(r=o,()=>{r=()=>null}),getTimeoutController:()=>e}}class nD{setTimeout(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,n=performance.now(),i=null,s=o=>{o-n>=r?t(o):typeof requestAnimationFrame=="function"&&(i=requestAnimationFrame(s))};return i=requestAnimationFrame(s),()=>{i!=null&&cancelAnimationFrame(i)}}}function iD(){return rD(new nD)}var aD=h.createContext(iD);function sD(e,t){var r=h.useContext(aD);return h.useMemo(()=>t??r(e),[e,t,r])}var oD={begin:0,duration:1e3,easing:"ease",isActive:!0,canBegin:!0,onAnimationEnd:()=>{},onAnimationStart:()=>{}},_0={t:0},md={t:1};function fu(e){var t=ft(e,oD),{isActive:r,canBegin:n,duration:i,easing:s,begin:o,onAnimationEnd:l,onAnimationStart:c,children:d}=t,u=sD(t.animationId,t.animationManager),[f,p]=h.useState(r?_0:md),m=h.useRef(null);return h.useEffect(()=>{r||p(md)},[r]),h.useEffect(()=>{if(!r||!n)return ka;var x=Z3(_0,md,tD(s),i,p,u.getTimeoutController()),g=()=>{m.current=x()};return u.start([c,o,g,i,l]),()=>{u.stop(),m.current&&m.current(),l()}},[r,n,i,s,o,c,l,u]),d(f.t)}function pu(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"animation-",r=h.useRef(Ts(t)),n=h.useRef(e);return n.current!==e&&(r.current=Ts(t),n.current=e),r.current}var lD=["radius"],cD=["radius"];function C0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function A0(e){for(var t=1;t{var s=Math.min(Math.abs(r)/2,Math.abs(n)/2),o=n>=0?1:-1,l=r>=0?1:-1,c=n>=0&&r>=0||n<0&&r<0?1:0,d;if(s>0&&i instanceof Array){for(var u=[0,0,0,0],f=0,p=4;fs?s:i[f];d="M".concat(e,",").concat(t+o*u[0]),u[0]>0&&(d+="A ".concat(u[0],",").concat(u[0],",0,0,").concat(c,",").concat(e+l*u[0],",").concat(t)),d+="L ".concat(e+r-l*u[1],",").concat(t),u[1]>0&&(d+="A ".concat(u[1],",").concat(u[1],",0,0,").concat(c,`, + `).concat(e+r,",").concat(t+o*u[1])),d+="L ".concat(e+r,",").concat(t+n-o*u[2]),u[2]>0&&(d+="A ".concat(u[2],",").concat(u[2],",0,0,").concat(c,`, + `).concat(e+r-l*u[2],",").concat(t+n)),d+="L ".concat(e+l*u[3],",").concat(t+n),u[3]>0&&(d+="A ".concat(u[3],",").concat(u[3],",0,0,").concat(c,`, + `).concat(e,",").concat(t+n-o*u[3])),d+="Z"}else if(s>0&&i===+i&&i>0){var m=Math.min(s,i);d="M ".concat(e,",").concat(t+o*m,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e+l*m,",").concat(t,` + L `).concat(e+r-l*m,",").concat(t,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e+r,",").concat(t+o*m,` + L `).concat(e+r,",").concat(t+n-o*m,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e+r-l*m,",").concat(t+n,` + L `).concat(e+l*m,",").concat(t+n,` + A `).concat(m,",").concat(m,",0,0,").concat(c,",").concat(e,",").concat(t+n-o*m," Z")}else d="M ".concat(e,",").concat(t," h ").concat(r," v ").concat(n," h ").concat(-r," Z");return d},D0={x:0,y:0,width:0,height:0,radius:0,isAnimationActive:!1,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},Gw=e=>{var t=ft(e,D0),r=h.useRef(null),[n,i]=h.useState(-1);h.useEffect(()=>{if(r.current&&r.current.getTotalLength)try{var D=r.current.getTotalLength();D&&i(D)}catch{}},[]);var{x:s,y:o,width:l,height:c,radius:d,className:u}=t,{animationEasing:f,animationDuration:p,animationBegin:m,isAnimationActive:x,isUpdateAnimationActive:g}=t,b=h.useRef(l),v=h.useRef(c),j=h.useRef(s),y=h.useRef(o),w=h.useMemo(()=>({x:s,y:o,width:l,height:c,radius:d}),[s,o,l,c,d]),S=pu(w,"rectangle-");if(s!==+s||o!==+o||l!==+l||c!==+c||l===0||c===0)return null;var N=ce("recharts-rectangle",u);if(!g){var P=ut(t),{radius:_}=P,T=O0(P,lD);return h.createElement("path",Ql({},T,{radius:typeof d=="number"?d:void 0,className:N,d:E0(s,o,l,c,d)}))}var $=b.current,M=v.current,C=j.current,R=y.current,q="0px ".concat(n===-1?1:n,"px"),Z="".concat(n,"px 0px"),E=Hw(["strokeDasharray"],p,typeof f=="string"?f:D0.animationEasing);return h.createElement(fu,{animationId:S,key:S,canBegin:n>0,duration:p,easing:f,isActive:g,begin:m},D=>{var O=De($,l,D),k=De(M,c,D),L=De(C,s,D),U=De(R,o,D);r.current&&(b.current=O,v.current=k,j.current=L,y.current=U);var H;x?D>0?H={transition:E,strokeDasharray:Z}:H={strokeDasharray:q}:H={strokeDasharray:Z};var te=ut(t),{radius:re}=te,we=O0(te,cD);return h.createElement("path",Ql({},we,{radius:typeof d=="number"?d:void 0,className:N,d:E0(L,U,O,k,d),ref:r,style:A0(A0({},H),t.style)}))})};function T0(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function M0(e){for(var t=1;te*180/Math.PI,Je=(e,t,r,n)=>({x:e+Math.cos(-ec*n)*r,y:t+Math.sin(-ec*n)*r}),yD=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{top:0,right:0,bottom:0,left:0};return Math.min(Math.abs(t-(n.left||0)-(n.right||0)),Math.abs(r-(n.top||0)-(n.bottom||0)))/2},vD=(e,t)=>{var{x:r,y:n}=e,{x:i,y:s}=t;return Math.sqrt((r-i)**2+(n-s)**2)},bD=(e,t)=>{var{x:r,y:n}=e,{cx:i,cy:s}=t,o=vD({x:r,y:n},{x:i,y:s});if(o<=0)return{radius:o,angle:0};var l=(r-i)/o,c=Math.acos(l);return n>s&&(c=2*Math.PI-c),{radius:o,angle:xD(c),angleInRadian:c}},jD=e=>{var{startAngle:t,endAngle:r}=e,n=Math.floor(t/360),i=Math.floor(r/360),s=Math.min(n,i);return{startAngle:t-s*360,endAngle:r-s*360}},wD=(e,t)=>{var{startAngle:r,endAngle:n}=t,i=Math.floor(r/360),s=Math.floor(n/360),o=Math.min(i,s);return e+o*360},SD=(e,t)=>{var{chartX:r,chartY:n}=e,{radius:i,angle:s}=bD({x:r,y:n},t),{innerRadius:o,outerRadius:l}=t;if(il||i===0)return null;var{startAngle:c,endAngle:d}=jD(t),u=s,f;if(c<=d){for(;u>d;)u-=360;for(;u=c&&u<=d}else{for(;u>c;)u-=360;for(;u=d&&u<=c}return f?M0(M0({},t),{},{radius:i,angle:wD(u,t)}):null};function Zw(e){var{cx:t,cy:r,radius:n,startAngle:i,endAngle:s}=e,o=Je(t,r,n,i),l=Je(t,r,n,s);return{points:[o,l],cx:t,cy:r,radius:n,startAngle:i,endAngle:s}}function Jf(){return Jf=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var r=Jt(t-e),n=Math.min(Math.abs(t-e),359.999);return r*n},Fo=e=>{var{cx:t,cy:r,radius:n,angle:i,sign:s,isExternal:o,cornerRadius:l,cornerIsExternal:c}=e,d=l*(o?1:-1)+n,u=Math.asin(l/d)/ec,f=c?i:i+s*u,p=Je(t,r,d,f),m=Je(t,r,n,f),x=c?i-s*u:i,g=Je(t,r,d*Math.cos(u*ec),x);return{center:p,circleTangency:m,lineTangency:g,theta:u}},Xw=e=>{var{cx:t,cy:r,innerRadius:n,outerRadius:i,startAngle:s,endAngle:o}=e,l=ND(s,o),c=s+l,d=Je(t,r,i,s),u=Je(t,r,i,c),f="M ".concat(d.x,",").concat(d.y,` + A `).concat(i,",").concat(i,`,0, + `).concat(+(Math.abs(l)>180),",").concat(+(s>c),`, + `).concat(u.x,",").concat(u.y,` + `);if(n>0){var p=Je(t,r,n,s),m=Je(t,r,n,c);f+="L ".concat(m.x,",").concat(m.y,` + A `).concat(n,",").concat(n,`,0, + `).concat(+(Math.abs(l)>180),",").concat(+(s<=c),`, + `).concat(p.x,",").concat(p.y," Z")}else f+="L ".concat(t,",").concat(r," Z");return f},kD=e=>{var{cx:t,cy:r,innerRadius:n,outerRadius:i,cornerRadius:s,forceCornerRadius:o,cornerIsExternal:l,startAngle:c,endAngle:d}=e,u=Jt(d-c),{circleTangency:f,lineTangency:p,theta:m}=Fo({cx:t,cy:r,radius:i,angle:c,sign:u,cornerRadius:s,cornerIsExternal:l}),{circleTangency:x,lineTangency:g,theta:b}=Fo({cx:t,cy:r,radius:i,angle:d,sign:-u,cornerRadius:s,cornerIsExternal:l}),v=l?Math.abs(c-d):Math.abs(c-d)-m-b;if(v<0)return o?"M ".concat(p.x,",").concat(p.y,` + a`).concat(s,",").concat(s,",0,0,1,").concat(s*2,`,0 + a`).concat(s,",").concat(s,",0,0,1,").concat(-s*2,`,0 + `):Xw({cx:t,cy:r,innerRadius:n,outerRadius:i,startAngle:c,endAngle:d});var j="M ".concat(p.x,",").concat(p.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(f.x,",").concat(f.y,` + A`).concat(i,",").concat(i,",0,").concat(+(v>180),",").concat(+(u<0),",").concat(x.x,",").concat(x.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(g.x,",").concat(g.y,` + `);if(n>0){var{circleTangency:y,lineTangency:w,theta:S}=Fo({cx:t,cy:r,radius:n,angle:c,sign:u,isExternal:!0,cornerRadius:s,cornerIsExternal:l}),{circleTangency:N,lineTangency:P,theta:_}=Fo({cx:t,cy:r,radius:n,angle:d,sign:-u,isExternal:!0,cornerRadius:s,cornerIsExternal:l}),T=l?Math.abs(c-d):Math.abs(c-d)-S-_;if(T<0&&s===0)return"".concat(j,"L").concat(t,",").concat(r,"Z");j+="L".concat(P.x,",").concat(P.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(N.x,",").concat(N.y,` + A`).concat(n,",").concat(n,",0,").concat(+(T>180),",").concat(+(u>0),",").concat(y.x,",").concat(y.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(u<0),",").concat(w.x,",").concat(w.y,"Z")}else j+="L".concat(t,",").concat(r,"Z");return j},PD={cx:0,cy:0,innerRadius:0,outerRadius:0,startAngle:0,endAngle:0,cornerRadius:0,forceCornerRadius:!1,cornerIsExternal:!1},Jw=e=>{var t=ft(e,PD),{cx:r,cy:n,innerRadius:i,outerRadius:s,cornerRadius:o,forceCornerRadius:l,cornerIsExternal:c,startAngle:d,endAngle:u,className:f}=t;if(s0&&Math.abs(d-u)<360?g=kD({cx:r,cy:n,innerRadius:i,outerRadius:s,cornerRadius:Math.min(x,m/2),forceCornerRadius:l,cornerIsExternal:c,startAngle:d,endAngle:u}):g=Xw({cx:r,cy:n,innerRadius:i,outerRadius:s,startAngle:d,endAngle:u}),h.createElement("path",Jf({},ut(t),{className:p,d:g}))};function _D(e,t,r){if(e==="horizontal")return[{x:t.x,y:r.top},{x:t.x,y:r.top+r.height}];if(e==="vertical")return[{x:r.left,y:t.y},{x:r.left+r.width,y:t.y}];if(Ej(t)){if(e==="centric"){var{cx:n,cy:i,innerRadius:s,outerRadius:o,angle:l}=t,c=Je(n,i,s,l),d=Je(n,i,o,l);return[{x:c.x,y:c.y},{x:d.x,y:d.y}]}return Zw(t)}}var Qw={},eS={},tS={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Kh;function r(n){return t.isSymbol(n)?NaN:Number(n)}e.toNumber=r})(tS);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=tS;function r(n){return n?(n=t.toNumber(n),n===1/0||n===-1/0?(n<0?-1:1)*Number.MAX_VALUE:n===n?n:0):n===0?n:0}e.toFinite=r})(eS);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=Vh,r=eS;function n(i,s,o){o&&typeof o!="number"&&t.isIterateeCall(i,s,o)&&(s=o=void 0),i=r.toFinite(i),s===void 0?(s=i,i=0):s=r.toFinite(s),o=o===void 0?it?1:e>=t?0:NaN}function AD(e,t){return e==null||t==null?NaN:te?1:t>=e?0:NaN}function em(e){let t,r,n;e.length!==2?(t=Mn,r=(l,c)=>Mn(e(l),c),n=(l,c)=>e(l)-c):(t=e===Mn||e===AD?e:OD,r=e,n=e);function i(l,c,d=0,u=l.length){if(d>>1;r(l[f],c)<0?d=f+1:u=f}while(d>>1;r(l[f],c)<=0?d=f+1:u=f}while(dd&&n(l[f-1],c)>-n(l[f],c)?f-1:f}return{left:i,center:o,right:s}}function OD(){return 0}function nS(e){return e===null?NaN:+e}function*ED(e,t){for(let r of e)r!=null&&(r=+r)>=r&&(yield r)}const DD=em(Mn),ro=DD.right;em(nS).center;class I0 extends Map{constructor(t,r=ID){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:r}}),t!=null)for(const[n,i]of t)this.set(n,i)}get(t){return super.get($0(this,t))}has(t){return super.has($0(this,t))}set(t,r){return super.set(TD(this,t),r)}delete(t){return super.delete(MD(this,t))}}function $0({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):r}function TD({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):(e.set(n,r),r)}function MD({_intern:e,_key:t},r){const n=t(r);return e.has(n)&&(r=e.get(n),e.delete(n)),r}function ID(e){return e!==null&&typeof e=="object"?e.valueOf():e}function $D(e=Mn){if(e===Mn)return iS;if(typeof e!="function")throw new TypeError("compare is not a function");return(t,r)=>{const n=e(t,r);return n||n===0?n:(e(r,r)===0)-(e(t,t)===0)}}function iS(e,t){return(e==null||!(e>=e))-(t==null||!(t>=t))||(et?1:0)}const LD=Math.sqrt(50),zD=Math.sqrt(10),RD=Math.sqrt(2);function tc(e,t,r){const n=(t-e)/Math.max(0,r),i=Math.floor(Math.log10(n)),s=n/Math.pow(10,i),o=s>=LD?10:s>=zD?5:s>=RD?2:1;let l,c,d;return i<0?(d=Math.pow(10,-i)/o,l=Math.round(e*d),c=Math.round(t*d),l/dt&&--c,d=-d):(d=Math.pow(10,i)*o,l=Math.round(e/d),c=Math.round(t/d),l*dt&&--c),c0))return[];if(e===t)return[e];const n=t=i))return[];const l=s-i+1,c=new Array(l);if(n)if(o<0)for(let d=0;d=n)&&(r=n);return r}function z0(e,t){let r;for(const n of e)n!=null&&(r>n||r===void 0&&n>=n)&&(r=n);return r}function aS(e,t,r=0,n=1/0,i){if(t=Math.floor(t),r=Math.floor(Math.max(0,r)),n=Math.floor(Math.min(e.length-1,n)),!(r<=t&&t<=n))return e;for(i=i===void 0?iS:$D(i);n>r;){if(n-r>600){const c=n-r+1,d=t-r+1,u=Math.log(c),f=.5*Math.exp(2*u/3),p=.5*Math.sqrt(u*f*(c-f)/c)*(d-c/2<0?-1:1),m=Math.max(r,Math.floor(t-d*f/c+p)),x=Math.min(n,Math.floor(t+(c-d)*f/c+p));aS(e,t,m,x,i)}const s=e[t];let o=r,l=n;for(Ha(e,r,t),i(e[n],s)>0&&Ha(e,r,n);o0;)--l}i(e[r],s)===0?Ha(e,r,l):(++l,Ha(e,l,n)),l<=t&&(r=l+1),t<=l&&(n=l-1)}return e}function Ha(e,t,r){const n=e[t];e[t]=e[r],e[r]=n}function BD(e,t,r){if(e=Float64Array.from(ED(e)),!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return z0(e);if(t>=1)return L0(e);var n,i=(n-1)*t,s=Math.floor(i),o=L0(aS(e,s).subarray(0,s+1)),l=z0(e.subarray(s+1));return o+(l-o)*(i-s)}}function FD(e,t,r=nS){if(!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return+r(e[0],0,e);if(t>=1)return+r(e[n-1],n-1,e);var n,i=(n-1)*t,s=Math.floor(i),o=+r(e[s],s,e),l=+r(e[s+1],s+1,e);return o+(l-o)*(i-s)}}function WD(e,t,r){e=+e,t=+t,r=(i=arguments.length)<2?(t=e,e=0,1):i<3?1:+r;for(var n=-1,i=Math.max(0,Math.ceil((t-e)/r))|0,s=new Array(i);++n>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):r===8?Wo(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):r===4?Wo(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=HD.exec(e))?new Nt(t[1],t[2],t[3],1):(t=KD.exec(e))?new Nt(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=VD.exec(e))?Wo(t[1],t[2],t[3],t[4]):(t=YD.exec(e))?Wo(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=GD.exec(e))?H0(t[1],t[2]/100,t[3]/100,1):(t=ZD.exec(e))?H0(t[1],t[2]/100,t[3]/100,t[4]):R0.hasOwnProperty(e)?W0(R0[e]):e==="transparent"?new Nt(NaN,NaN,NaN,0):null}function W0(e){return new Nt(e>>16&255,e>>8&255,e&255,1)}function Wo(e,t,r,n){return n<=0&&(e=t=r=NaN),new Nt(e,t,r,n)}function QD(e){return e instanceof no||(e=Bs(e)),e?(e=e.rgb(),new Nt(e.r,e.g,e.b,e.opacity)):new Nt}function np(e,t,r,n){return arguments.length===1?QD(e):new Nt(e,t,r,n??1)}function Nt(e,t,r,n){this.r=+e,this.g=+t,this.b=+r,this.opacity=+n}nm(Nt,np,oS(no,{brighter(e){return e=e==null?rc:Math.pow(rc,e),new Nt(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?zs:Math.pow(zs,e),new Nt(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new Nt(ui(this.r),ui(this.g),ui(this.b),nc(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:U0,formatHex:U0,formatHex8:e5,formatRgb:q0,toString:q0}));function U0(){return`#${ni(this.r)}${ni(this.g)}${ni(this.b)}`}function e5(){return`#${ni(this.r)}${ni(this.g)}${ni(this.b)}${ni((isNaN(this.opacity)?1:this.opacity)*255)}`}function q0(){const e=nc(this.opacity);return`${e===1?"rgb(":"rgba("}${ui(this.r)}, ${ui(this.g)}, ${ui(this.b)}${e===1?")":`, ${e})`}`}function nc(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function ui(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function ni(e){return e=ui(e),(e<16?"0":"")+e.toString(16)}function H0(e,t,r,n){return n<=0?e=t=r=NaN:r<=0||r>=1?e=t=NaN:t<=0&&(e=NaN),new pr(e,t,r,n)}function lS(e){if(e instanceof pr)return new pr(e.h,e.s,e.l,e.opacity);if(e instanceof no||(e=Bs(e)),!e)return new pr;if(e instanceof pr)return e;e=e.rgb();var t=e.r/255,r=e.g/255,n=e.b/255,i=Math.min(t,r,n),s=Math.max(t,r,n),o=NaN,l=s-i,c=(s+i)/2;return l?(t===s?o=(r-n)/l+(r0&&c<1?0:o,new pr(o,l,c,e.opacity)}function t5(e,t,r,n){return arguments.length===1?lS(e):new pr(e,t,r,n??1)}function pr(e,t,r,n){this.h=+e,this.s=+t,this.l=+r,this.opacity=+n}nm(pr,t5,oS(no,{brighter(e){return e=e==null?rc:Math.pow(rc,e),new pr(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?zs:Math.pow(zs,e),new pr(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,r=this.l,n=r+(r<.5?r:1-r)*t,i=2*r-n;return new Nt(gd(e>=240?e-240:e+120,i,n),gd(e,i,n),gd(e<120?e+240:e-120,i,n),this.opacity)},clamp(){return new pr(K0(this.h),Uo(this.s),Uo(this.l),nc(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const e=nc(this.opacity);return`${e===1?"hsl(":"hsla("}${K0(this.h)}, ${Uo(this.s)*100}%, ${Uo(this.l)*100}%${e===1?")":`, ${e})`}`}}));function K0(e){return e=(e||0)%360,e<0?e+360:e}function Uo(e){return Math.max(0,Math.min(1,e||0))}function gd(e,t,r){return(e<60?t+(r-t)*e/60:e<180?r:e<240?t+(r-t)*(240-e)/60:t)*255}const im=e=>()=>e;function r5(e,t){return function(r){return e+r*t}}function n5(e,t,r){return e=Math.pow(e,r),t=Math.pow(t,r)-e,r=1/r,function(n){return Math.pow(e+n*t,r)}}function i5(e){return(e=+e)==1?cS:function(t,r){return r-t?n5(t,r,e):im(isNaN(t)?r:t)}}function cS(e,t){var r=t-e;return r?r5(e,r):im(isNaN(e)?t:e)}const V0=function e(t){var r=i5(t);function n(i,s){var o=r((i=np(i)).r,(s=np(s)).r),l=r(i.g,s.g),c=r(i.b,s.b),d=cS(i.opacity,s.opacity);return function(u){return i.r=o(u),i.g=l(u),i.b=c(u),i.opacity=d(u),i+""}}return n.gamma=e,n}(1);function a5(e,t){t||(t=[]);var r=e?Math.min(t.length,e.length):0,n=t.slice(),i;return function(s){for(i=0;ir&&(s=t.slice(r,s),l[o]?l[o]+=s:l[++o]=s),(n=n[0])===(i=i[0])?l[o]?l[o]+=i:l[++o]=i:(l[++o]=null,c.push({i:o,x:ic(n,i)})),r=xd.lastIndex;return rt&&(r=e,e=t,t=r),function(n){return Math.max(e,Math.min(t,n))}}function g5(e,t,r){var n=e[0],i=e[1],s=t[0],o=t[1];return i2?x5:g5,c=d=null,f}function f(p){return p==null||isNaN(p=+p)?s:(c||(c=l(e.map(n),t,r)))(n(o(p)))}return f.invert=function(p){return o(i((d||(d=l(t,e.map(n),ic)))(p)))},f.domain=function(p){return arguments.length?(e=Array.from(p,ac),u()):e.slice()},f.range=function(p){return arguments.length?(t=Array.from(p),u()):t.slice()},f.rangeRound=function(p){return t=Array.from(p),r=am,u()},f.clamp=function(p){return arguments.length?(o=p?!0:mt,u()):o!==mt},f.interpolate=function(p){return arguments.length?(r=p,u()):r},f.unknown=function(p){return arguments.length?(s=p,f):s},function(p,m){return n=p,i=m,u()}}function sm(){return hu()(mt,mt)}function y5(e){return Math.abs(e=Math.round(e))>=1e21?e.toLocaleString("en").replace(/,/g,""):e.toString(10)}function sc(e,t){if((r=(e=t?e.toExponential(t-1):e.toExponential()).indexOf("e"))<0)return null;var r,n=e.slice(0,r);return[n.length>1?n[0]+n.slice(2):n,+e.slice(r+1)]}function ma(e){return e=sc(Math.abs(e)),e?e[1]:NaN}function v5(e,t){return function(r,n){for(var i=r.length,s=[],o=0,l=e[0],c=0;i>0&&l>0&&(c+l+1>n&&(l=Math.max(1,n-c)),s.push(r.substring(i-=l,i+l)),!((c+=l+1)>n));)l=e[o=(o+1)%e.length];return s.reverse().join(t)}}function b5(e){return function(t){return t.replace(/[0-9]/g,function(r){return e[+r]})}}var j5=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Fs(e){if(!(t=j5.exec(e)))throw new Error("invalid format: "+e);var t;return new om({fill:t[1],align:t[2],sign:t[3],symbol:t[4],zero:t[5],width:t[6],comma:t[7],precision:t[8]&&t[8].slice(1),trim:t[9],type:t[10]})}Fs.prototype=om.prototype;function om(e){this.fill=e.fill===void 0?" ":e.fill+"",this.align=e.align===void 0?">":e.align+"",this.sign=e.sign===void 0?"-":e.sign+"",this.symbol=e.symbol===void 0?"":e.symbol+"",this.zero=!!e.zero,this.width=e.width===void 0?void 0:+e.width,this.comma=!!e.comma,this.precision=e.precision===void 0?void 0:+e.precision,this.trim=!!e.trim,this.type=e.type===void 0?"":e.type+""}om.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(this.width===void 0?"":Math.max(1,this.width|0))+(this.comma?",":"")+(this.precision===void 0?"":"."+Math.max(0,this.precision|0))+(this.trim?"~":"")+this.type};function w5(e){e:for(var t=e.length,r=1,n=-1,i;r0&&(n=0);break}return n>0?e.slice(0,n)+e.slice(i+1):e}var uS;function S5(e,t){var r=sc(e,t);if(!r)return e+"";var n=r[0],i=r[1],s=i-(uS=Math.max(-8,Math.min(8,Math.floor(i/3)))*3)+1,o=n.length;return s===o?n:s>o?n+new Array(s-o+1).join("0"):s>0?n.slice(0,s)+"."+n.slice(s):"0."+new Array(1-s).join("0")+sc(e,Math.max(0,t+s-1))[0]}function G0(e,t){var r=sc(e,t);if(!r)return e+"";var n=r[0],i=r[1];return i<0?"0."+new Array(-i).join("0")+n:n.length>i+1?n.slice(0,i+1)+"."+n.slice(i+1):n+new Array(i-n.length+2).join("0")}const Z0={"%":(e,t)=>(e*100).toFixed(t),b:e=>Math.round(e).toString(2),c:e=>e+"",d:y5,e:(e,t)=>e.toExponential(t),f:(e,t)=>e.toFixed(t),g:(e,t)=>e.toPrecision(t),o:e=>Math.round(e).toString(8),p:(e,t)=>G0(e*100,t),r:G0,s:S5,X:e=>Math.round(e).toString(16).toUpperCase(),x:e=>Math.round(e).toString(16)};function X0(e){return e}var J0=Array.prototype.map,Q0=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function N5(e){var t=e.grouping===void 0||e.thousands===void 0?X0:v5(J0.call(e.grouping,Number),e.thousands+""),r=e.currency===void 0?"":e.currency[0]+"",n=e.currency===void 0?"":e.currency[1]+"",i=e.decimal===void 0?".":e.decimal+"",s=e.numerals===void 0?X0:b5(J0.call(e.numerals,String)),o=e.percent===void 0?"%":e.percent+"",l=e.minus===void 0?"−":e.minus+"",c=e.nan===void 0?"NaN":e.nan+"";function d(f){f=Fs(f);var p=f.fill,m=f.align,x=f.sign,g=f.symbol,b=f.zero,v=f.width,j=f.comma,y=f.precision,w=f.trim,S=f.type;S==="n"?(j=!0,S="g"):Z0[S]||(y===void 0&&(y=12),w=!0,S="g"),(b||p==="0"&&m==="=")&&(b=!0,p="0",m="=");var N=g==="$"?r:g==="#"&&/[boxX]/.test(S)?"0"+S.toLowerCase():"",P=g==="$"?n:/[%p]/.test(S)?o:"",_=Z0[S],T=/[defgprs%]/.test(S);y=y===void 0?6:/[gprs]/.test(S)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y));function $(M){var C=N,R=P,q,Z,E;if(S==="c")R=_(M)+R,M="";else{M=+M;var D=M<0||1/M<0;if(M=isNaN(M)?c:_(Math.abs(M),y),w&&(M=w5(M)),D&&+M==0&&x!=="+"&&(D=!1),C=(D?x==="("?x:l:x==="-"||x==="("?"":x)+C,R=(S==="s"?Q0[8+uS/3]:"")+R+(D&&x==="("?")":""),T){for(q=-1,Z=M.length;++qE||E>57){R=(E===46?i+M.slice(q+1):M.slice(q))+R,M=M.slice(0,q);break}}}j&&!b&&(M=t(M,1/0));var O=C.length+M.length+R.length,k=O>1)+C+M+R+k.slice(O);break;default:M=k+C+M+R;break}return s(M)}return $.toString=function(){return f+""},$}function u(f,p){var m=d((f=Fs(f),f.type="f",f)),x=Math.max(-8,Math.min(8,Math.floor(ma(p)/3)))*3,g=Math.pow(10,-x),b=Q0[8+x/3];return function(v){return m(g*v)+b}}return{format:d,formatPrefix:u}}var qo,lm,dS;k5({thousands:",",grouping:[3],currency:["$",""]});function k5(e){return qo=N5(e),lm=qo.format,dS=qo.formatPrefix,qo}function P5(e){return Math.max(0,-ma(Math.abs(e)))}function _5(e,t){return Math.max(0,Math.max(-8,Math.min(8,Math.floor(ma(t)/3)))*3-ma(Math.abs(e)))}function C5(e,t){return e=Math.abs(e),t=Math.abs(t)-e,Math.max(0,ma(t)-ma(e))+1}function fS(e,t,r,n){var i=tp(e,t,r),s;switch(n=Fs(n??",f"),n.type){case"s":{var o=Math.max(Math.abs(e),Math.abs(t));return n.precision==null&&!isNaN(s=_5(i,o))&&(n.precision=s),dS(n,o)}case"":case"e":case"g":case"p":case"r":{n.precision==null&&!isNaN(s=C5(i,Math.max(Math.abs(e),Math.abs(t))))&&(n.precision=s-(n.type==="e"));break}case"f":case"%":{n.precision==null&&!isNaN(s=P5(i))&&(n.precision=s-(n.type==="%")*2);break}}return lm(n)}function qn(e){var t=e.domain;return e.ticks=function(r){var n=t();return Qf(n[0],n[n.length-1],r??10)},e.tickFormat=function(r,n){var i=t();return fS(i[0],i[i.length-1],r??10,n)},e.nice=function(r){r==null&&(r=10);var n=t(),i=0,s=n.length-1,o=n[i],l=n[s],c,d,u=10;for(l0;){if(d=ep(o,l,r),d===c)return n[i]=o,n[s]=l,t(n);if(d>0)o=Math.floor(o/d)*d,l=Math.ceil(l/d)*d;else if(d<0)o=Math.ceil(o*d)/d,l=Math.floor(l*d)/d;else break;c=d}return e},e}function pS(){var e=sm();return e.copy=function(){return io(e,pS())},or.apply(e,arguments),qn(e)}function hS(e){var t;function r(n){return n==null||isNaN(n=+n)?t:n}return r.invert=r,r.domain=r.range=function(n){return arguments.length?(e=Array.from(n,ac),r):e.slice()},r.unknown=function(n){return arguments.length?(t=n,r):t},r.copy=function(){return hS(e).unknown(t)},e=arguments.length?Array.from(e,ac):[0,1],qn(r)}function mS(e,t){e=e.slice();var r=0,n=e.length-1,i=e[r],s=e[n],o;return sMath.pow(e,t)}function T5(e){return e===Math.E?Math.log:e===10&&Math.log10||e===2&&Math.log2||(e=Math.log(e),t=>Math.log(t)/e)}function ry(e){return(t,r)=>-e(-t,r)}function cm(e){const t=e(ey,ty),r=t.domain;let n=10,i,s;function o(){return i=T5(n),s=D5(n),r()[0]<0?(i=ry(i),s=ry(s),e(A5,O5)):e(ey,ty),t}return t.base=function(l){return arguments.length?(n=+l,o()):n},t.domain=function(l){return arguments.length?(r(l),o()):r()},t.ticks=l=>{const c=r();let d=c[0],u=c[c.length-1];const f=u0){for(;p<=m;++p)for(x=1;xu)break;v.push(g)}}else for(;p<=m;++p)for(x=n-1;x>=1;--x)if(g=p>0?x/s(-p):x*s(p),!(gu)break;v.push(g)}v.length*2{if(l==null&&(l=10),c==null&&(c=n===10?"s":","),typeof c!="function"&&(!(n%1)&&(c=Fs(c)).precision==null&&(c.trim=!0),c=lm(c)),l===1/0)return c;const d=Math.max(1,n*l/t.ticks().length);return u=>{let f=u/s(Math.round(i(u)));return f*nr(mS(r(),{floor:l=>s(Math.floor(i(l))),ceil:l=>s(Math.ceil(i(l)))})),t}function gS(){const e=cm(hu()).domain([1,10]);return e.copy=()=>io(e,gS()).base(e.base()),or.apply(e,arguments),e}function ny(e){return function(t){return Math.sign(t)*Math.log1p(Math.abs(t/e))}}function iy(e){return function(t){return Math.sign(t)*Math.expm1(Math.abs(t))*e}}function um(e){var t=1,r=e(ny(t),iy(t));return r.constant=function(n){return arguments.length?e(ny(t=+n),iy(t)):t},qn(r)}function xS(){var e=um(hu());return e.copy=function(){return io(e,xS()).constant(e.constant())},or.apply(e,arguments)}function ay(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function M5(e){return e<0?-Math.sqrt(-e):Math.sqrt(e)}function I5(e){return e<0?-e*e:e*e}function dm(e){var t=e(mt,mt),r=1;function n(){return r===1?e(mt,mt):r===.5?e(M5,I5):e(ay(r),ay(1/r))}return t.exponent=function(i){return arguments.length?(r=+i,n()):r},qn(t)}function fm(){var e=dm(hu());return e.copy=function(){return io(e,fm()).exponent(e.exponent())},or.apply(e,arguments),e}function $5(){return fm.apply(null,arguments).exponent(.5)}function sy(e){return Math.sign(e)*e*e}function L5(e){return Math.sign(e)*Math.sqrt(Math.abs(e))}function yS(){var e=sm(),t=[0,1],r=!1,n;function i(s){var o=L5(e(s));return isNaN(o)?n:r?Math.round(o):o}return i.invert=function(s){return e.invert(sy(s))},i.domain=function(s){return arguments.length?(e.domain(s),i):e.domain()},i.range=function(s){return arguments.length?(e.range((t=Array.from(s,ac)).map(sy)),i):t.slice()},i.rangeRound=function(s){return i.range(s).round(!0)},i.round=function(s){return arguments.length?(r=!!s,i):r},i.clamp=function(s){return arguments.length?(e.clamp(s),i):e.clamp()},i.unknown=function(s){return arguments.length?(n=s,i):n},i.copy=function(){return yS(e.domain(),t).round(r).clamp(e.clamp()).unknown(n)},or.apply(i,arguments),qn(i)}function vS(){var e=[],t=[],r=[],n;function i(){var o=0,l=Math.max(1,t.length);for(r=new Array(l-1);++o0?r[l-1]:e[0],l=r?[n[r-1],t]:[n[d-1],n[d]]},o.unknown=function(c){return arguments.length&&(s=c),o},o.thresholds=function(){return n.slice()},o.copy=function(){return bS().domain([e,t]).range(i).unknown(s)},or.apply(qn(o),arguments)}function jS(){var e=[.5],t=[0,1],r,n=1;function i(s){return s!=null&&s<=s?t[ro(e,s,0,n)]:r}return i.domain=function(s){return arguments.length?(e=Array.from(s),n=Math.min(e.length,t.length-1),i):e.slice()},i.range=function(s){return arguments.length?(t=Array.from(s),n=Math.min(e.length,t.length-1),i):t.slice()},i.invertExtent=function(s){var o=t.indexOf(s);return[e[o-1],e[o]]},i.unknown=function(s){return arguments.length?(r=s,i):r},i.copy=function(){return jS().domain(e).range(t).unknown(r)},or.apply(i,arguments)}const yd=new Date,vd=new Date;function Be(e,t,r,n){function i(s){return e(s=arguments.length===0?new Date:new Date(+s)),s}return i.floor=s=>(e(s=new Date(+s)),s),i.ceil=s=>(e(s=new Date(s-1)),t(s,1),e(s),s),i.round=s=>{const o=i(s),l=i.ceil(s);return s-o(t(s=new Date(+s),o==null?1:Math.floor(o)),s),i.range=(s,o,l)=>{const c=[];if(s=i.ceil(s),l=l==null?1:Math.floor(l),!(s0))return c;let d;do c.push(d=new Date(+s)),t(s,l),e(s);while(dBe(o=>{if(o>=o)for(;e(o),!s(o);)o.setTime(o-1)},(o,l)=>{if(o>=o)if(l<0)for(;++l<=0;)for(;t(o,-1),!s(o););else for(;--l>=0;)for(;t(o,1),!s(o););}),r&&(i.count=(s,o)=>(yd.setTime(+s),vd.setTime(+o),e(yd),e(vd),Math.floor(r(yd,vd))),i.every=s=>(s=Math.floor(s),!isFinite(s)||!(s>0)?null:s>1?i.filter(n?o=>n(o)%s===0:o=>i.count(0,o)%s===0):i)),i}const oc=Be(()=>{},(e,t)=>{e.setTime(+e+t)},(e,t)=>t-e);oc.every=e=>(e=Math.floor(e),!isFinite(e)||!(e>0)?null:e>1?Be(t=>{t.setTime(Math.floor(t/e)*e)},(t,r)=>{t.setTime(+t+r*e)},(t,r)=>(r-t)/e):oc);oc.range;const Wr=1e3,Qt=Wr*60,Ur=Qt*60,tn=Ur*24,pm=tn*7,oy=tn*30,bd=tn*365,ii=Be(e=>{e.setTime(e-e.getMilliseconds())},(e,t)=>{e.setTime(+e+t*Wr)},(e,t)=>(t-e)/Wr,e=>e.getUTCSeconds());ii.range;const hm=Be(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*Wr)},(e,t)=>{e.setTime(+e+t*Qt)},(e,t)=>(t-e)/Qt,e=>e.getMinutes());hm.range;const mm=Be(e=>{e.setUTCSeconds(0,0)},(e,t)=>{e.setTime(+e+t*Qt)},(e,t)=>(t-e)/Qt,e=>e.getUTCMinutes());mm.range;const gm=Be(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*Wr-e.getMinutes()*Qt)},(e,t)=>{e.setTime(+e+t*Ur)},(e,t)=>(t-e)/Ur,e=>e.getHours());gm.range;const xm=Be(e=>{e.setUTCMinutes(0,0,0)},(e,t)=>{e.setTime(+e+t*Ur)},(e,t)=>(t-e)/Ur,e=>e.getUTCHours());xm.range;const ao=Be(e=>e.setHours(0,0,0,0),(e,t)=>e.setDate(e.getDate()+t),(e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*Qt)/tn,e=>e.getDate()-1);ao.range;const mu=Be(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/tn,e=>e.getUTCDate()-1);mu.range;const wS=Be(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/tn,e=>Math.floor(e/tn));wS.range;function Ai(e){return Be(t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7),t.setHours(0,0,0,0)},(t,r)=>{t.setDate(t.getDate()+r*7)},(t,r)=>(r-t-(r.getTimezoneOffset()-t.getTimezoneOffset())*Qt)/pm)}const gu=Ai(0),lc=Ai(1),z5=Ai(2),R5=Ai(3),ga=Ai(4),B5=Ai(5),F5=Ai(6);gu.range;lc.range;z5.range;R5.range;ga.range;B5.range;F5.range;function Oi(e){return Be(t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCDate(t.getUTCDate()+r*7)},(t,r)=>(r-t)/pm)}const xu=Oi(0),cc=Oi(1),W5=Oi(2),U5=Oi(3),xa=Oi(4),q5=Oi(5),H5=Oi(6);xu.range;cc.range;W5.range;U5.range;xa.range;q5.range;H5.range;const ym=Be(e=>{e.setDate(1),e.setHours(0,0,0,0)},(e,t)=>{e.setMonth(e.getMonth()+t)},(e,t)=>t.getMonth()-e.getMonth()+(t.getFullYear()-e.getFullYear())*12,e=>e.getMonth());ym.range;const vm=Be(e=>{e.setUTCDate(1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)},(e,t)=>t.getUTCMonth()-e.getUTCMonth()+(t.getUTCFullYear()-e.getUTCFullYear())*12,e=>e.getUTCMonth());vm.range;const rn=Be(e=>{e.setMonth(0,1),e.setHours(0,0,0,0)},(e,t)=>{e.setFullYear(e.getFullYear()+t)},(e,t)=>t.getFullYear()-e.getFullYear(),e=>e.getFullYear());rn.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Be(t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e),t.setMonth(0,1),t.setHours(0,0,0,0)},(t,r)=>{t.setFullYear(t.getFullYear()+r*e)});rn.range;const nn=Be(e=>{e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)},(e,t)=>t.getUTCFullYear()-e.getUTCFullYear(),e=>e.getUTCFullYear());nn.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Be(t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e),t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCFullYear(t.getUTCFullYear()+r*e)});nn.range;function SS(e,t,r,n,i,s){const o=[[ii,1,Wr],[ii,5,5*Wr],[ii,15,15*Wr],[ii,30,30*Wr],[s,1,Qt],[s,5,5*Qt],[s,15,15*Qt],[s,30,30*Qt],[i,1,Ur],[i,3,3*Ur],[i,6,6*Ur],[i,12,12*Ur],[n,1,tn],[n,2,2*tn],[r,1,pm],[t,1,oy],[t,3,3*oy],[e,1,bd]];function l(d,u,f){const p=ub).right(o,p);if(m===o.length)return e.every(tp(d/bd,u/bd,f));if(m===0)return oc.every(Math.max(tp(d,u,f),1));const[x,g]=o[p/o[m-1][2]53)return null;"w"in W||(W.w=1),"Z"in W?(pe=wd(Ka(W.y,0,1)),Et=pe.getUTCDay(),pe=Et>4||Et===0?cc.ceil(pe):cc(pe),pe=mu.offset(pe,(W.V-1)*7),W.y=pe.getUTCFullYear(),W.m=pe.getUTCMonth(),W.d=pe.getUTCDate()+(W.w+6)%7):(pe=jd(Ka(W.y,0,1)),Et=pe.getDay(),pe=Et>4||Et===0?lc.ceil(pe):lc(pe),pe=ao.offset(pe,(W.V-1)*7),W.y=pe.getFullYear(),W.m=pe.getMonth(),W.d=pe.getDate()+(W.w+6)%7)}else("W"in W||"U"in W)&&("w"in W||(W.w="u"in W?W.u%7:"W"in W?1:0),Et="Z"in W?wd(Ka(W.y,0,1)).getUTCDay():jd(Ka(W.y,0,1)).getDay(),W.m=0,W.d="W"in W?(W.w+6)%7+W.W*7-(Et+5)%7:W.w+W.U*7-(Et+6)%7);return"Z"in W?(W.H+=W.Z/100|0,W.M+=W.Z%100,wd(W)):jd(W)}}function _(z,ee,ne,W){for(var bt=0,pe=ee.length,Et=ne.length,Dt,Yn;bt=Et)return-1;if(Dt=ee.charCodeAt(bt++),Dt===37){if(Dt=ee.charAt(bt++),Yn=S[Dt in ly?ee.charAt(bt++):Dt],!Yn||(W=Yn(z,ne,W))<0)return-1}else if(Dt!=ne.charCodeAt(W++))return-1}return W}function T(z,ee,ne){var W=d.exec(ee.slice(ne));return W?(z.p=u.get(W[0].toLowerCase()),ne+W[0].length):-1}function $(z,ee,ne){var W=m.exec(ee.slice(ne));return W?(z.w=x.get(W[0].toLowerCase()),ne+W[0].length):-1}function M(z,ee,ne){var W=f.exec(ee.slice(ne));return W?(z.w=p.get(W[0].toLowerCase()),ne+W[0].length):-1}function C(z,ee,ne){var W=v.exec(ee.slice(ne));return W?(z.m=j.get(W[0].toLowerCase()),ne+W[0].length):-1}function R(z,ee,ne){var W=g.exec(ee.slice(ne));return W?(z.m=b.get(W[0].toLowerCase()),ne+W[0].length):-1}function q(z,ee,ne){return _(z,t,ee,ne)}function Z(z,ee,ne){return _(z,r,ee,ne)}function E(z,ee,ne){return _(z,n,ee,ne)}function D(z){return o[z.getDay()]}function O(z){return s[z.getDay()]}function k(z){return c[z.getMonth()]}function L(z){return l[z.getMonth()]}function U(z){return i[+(z.getHours()>=12)]}function H(z){return 1+~~(z.getMonth()/3)}function te(z){return o[z.getUTCDay()]}function re(z){return s[z.getUTCDay()]}function we(z){return c[z.getUTCMonth()]}function A(z){return l[z.getUTCMonth()]}function J(z){return i[+(z.getUTCHours()>=12)]}function Ot(z){return 1+~~(z.getUTCMonth()/3)}return{format:function(z){var ee=N(z+="",y);return ee.toString=function(){return z},ee},parse:function(z){var ee=P(z+="",!1);return ee.toString=function(){return z},ee},utcFormat:function(z){var ee=N(z+="",w);return ee.toString=function(){return z},ee},utcParse:function(z){var ee=P(z+="",!0);return ee.toString=function(){return z},ee}}}var ly={"-":"",_:" ",0:"0"},Ye=/^\s*\d+/,X5=/^%/,J5=/[\\^$*+?|[\]().{}]/g;function se(e,t,r){var n=e<0?"-":"",i=(n?-e:e)+"",s=i.length;return n+(s[t.toLowerCase(),r]))}function eT(e,t,r){var n=Ye.exec(t.slice(r,r+1));return n?(e.w=+n[0],r+n[0].length):-1}function tT(e,t,r){var n=Ye.exec(t.slice(r,r+1));return n?(e.u=+n[0],r+n[0].length):-1}function rT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.U=+n[0],r+n[0].length):-1}function nT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.V=+n[0],r+n[0].length):-1}function iT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.W=+n[0],r+n[0].length):-1}function cy(e,t,r){var n=Ye.exec(t.slice(r,r+4));return n?(e.y=+n[0],r+n[0].length):-1}function uy(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.y=+n[0]+(+n[0]>68?1900:2e3),r+n[0].length):-1}function aT(e,t,r){var n=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(t.slice(r,r+6));return n?(e.Z=n[1]?0:-(n[2]+(n[3]||"00")),r+n[0].length):-1}function sT(e,t,r){var n=Ye.exec(t.slice(r,r+1));return n?(e.q=n[0]*3-3,r+n[0].length):-1}function oT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.m=n[0]-1,r+n[0].length):-1}function dy(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.d=+n[0],r+n[0].length):-1}function lT(e,t,r){var n=Ye.exec(t.slice(r,r+3));return n?(e.m=0,e.d=+n[0],r+n[0].length):-1}function fy(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.H=+n[0],r+n[0].length):-1}function cT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.M=+n[0],r+n[0].length):-1}function uT(e,t,r){var n=Ye.exec(t.slice(r,r+2));return n?(e.S=+n[0],r+n[0].length):-1}function dT(e,t,r){var n=Ye.exec(t.slice(r,r+3));return n?(e.L=+n[0],r+n[0].length):-1}function fT(e,t,r){var n=Ye.exec(t.slice(r,r+6));return n?(e.L=Math.floor(n[0]/1e3),r+n[0].length):-1}function pT(e,t,r){var n=X5.exec(t.slice(r,r+1));return n?r+n[0].length:-1}function hT(e,t,r){var n=Ye.exec(t.slice(r));return n?(e.Q=+n[0],r+n[0].length):-1}function mT(e,t,r){var n=Ye.exec(t.slice(r));return n?(e.s=+n[0],r+n[0].length):-1}function py(e,t){return se(e.getDate(),t,2)}function gT(e,t){return se(e.getHours(),t,2)}function xT(e,t){return se(e.getHours()%12||12,t,2)}function yT(e,t){return se(1+ao.count(rn(e),e),t,3)}function NS(e,t){return se(e.getMilliseconds(),t,3)}function vT(e,t){return NS(e,t)+"000"}function bT(e,t){return se(e.getMonth()+1,t,2)}function jT(e,t){return se(e.getMinutes(),t,2)}function wT(e,t){return se(e.getSeconds(),t,2)}function ST(e){var t=e.getDay();return t===0?7:t}function NT(e,t){return se(gu.count(rn(e)-1,e),t,2)}function kS(e){var t=e.getDay();return t>=4||t===0?ga(e):ga.ceil(e)}function kT(e,t){return e=kS(e),se(ga.count(rn(e),e)+(rn(e).getDay()===4),t,2)}function PT(e){return e.getDay()}function _T(e,t){return se(lc.count(rn(e)-1,e),t,2)}function CT(e,t){return se(e.getFullYear()%100,t,2)}function AT(e,t){return e=kS(e),se(e.getFullYear()%100,t,2)}function OT(e,t){return se(e.getFullYear()%1e4,t,4)}function ET(e,t){var r=e.getDay();return e=r>=4||r===0?ga(e):ga.ceil(e),se(e.getFullYear()%1e4,t,4)}function DT(e){var t=e.getTimezoneOffset();return(t>0?"-":(t*=-1,"+"))+se(t/60|0,"0",2)+se(t%60,"0",2)}function hy(e,t){return se(e.getUTCDate(),t,2)}function TT(e,t){return se(e.getUTCHours(),t,2)}function MT(e,t){return se(e.getUTCHours()%12||12,t,2)}function IT(e,t){return se(1+mu.count(nn(e),e),t,3)}function PS(e,t){return se(e.getUTCMilliseconds(),t,3)}function $T(e,t){return PS(e,t)+"000"}function LT(e,t){return se(e.getUTCMonth()+1,t,2)}function zT(e,t){return se(e.getUTCMinutes(),t,2)}function RT(e,t){return se(e.getUTCSeconds(),t,2)}function BT(e){var t=e.getUTCDay();return t===0?7:t}function FT(e,t){return se(xu.count(nn(e)-1,e),t,2)}function _S(e){var t=e.getUTCDay();return t>=4||t===0?xa(e):xa.ceil(e)}function WT(e,t){return e=_S(e),se(xa.count(nn(e),e)+(nn(e).getUTCDay()===4),t,2)}function UT(e){return e.getUTCDay()}function qT(e,t){return se(cc.count(nn(e)-1,e),t,2)}function HT(e,t){return se(e.getUTCFullYear()%100,t,2)}function KT(e,t){return e=_S(e),se(e.getUTCFullYear()%100,t,2)}function VT(e,t){return se(e.getUTCFullYear()%1e4,t,4)}function YT(e,t){var r=e.getUTCDay();return e=r>=4||r===0?xa(e):xa.ceil(e),se(e.getUTCFullYear()%1e4,t,4)}function GT(){return"+0000"}function my(){return"%"}function gy(e){return+e}function xy(e){return Math.floor(+e/1e3)}var Di,CS,AS;ZT({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});function ZT(e){return Di=Z5(e),CS=Di.format,Di.parse,AS=Di.utcFormat,Di.utcParse,Di}function XT(e){return new Date(e)}function JT(e){return e instanceof Date?+e:+new Date(+e)}function bm(e,t,r,n,i,s,o,l,c,d){var u=sm(),f=u.invert,p=u.domain,m=d(".%L"),x=d(":%S"),g=d("%I:%M"),b=d("%I %p"),v=d("%a %d"),j=d("%b %d"),y=d("%B"),w=d("%Y");function S(N){return(c(N)t(i/(e.length-1)))},r.quantiles=function(n){return Array.from({length:n+1},(i,s)=>BD(e,s/n))},r.copy=function(){return TS(t).domain(e)},un.apply(r,arguments)}function vu(){var e=0,t=.5,r=1,n=1,i,s,o,l,c,d=mt,u,f=!1,p;function m(g){return isNaN(g=+g)?p:(g=.5+((g=+u(g))-s)*(n*ge.chartData,nM=I([Kn],e=>{var t=e.chartData!=null?e.chartData.length-1:0;return{chartData:e.chartData,computedData:e.computedData,dataEndIndex:t,dataStartIndex:0}}),bu=(e,t,r,n)=>n?nM(e):Kn(e);function ji(e){if(Array.isArray(e)&&e.length===2){var[t,r]=e;if(_e(t)&&_e(r))return!0}return!1}function yy(e,t,r){return r?e:[Math.min(e[0],t[0]),Math.max(e[1],t[1])]}function LS(e,t){if(t&&typeof e!="function"&&Array.isArray(e)&&e.length===2){var[r,n]=e,i,s;if(_e(r))i=r;else if(typeof r=="function")return;if(_e(n))s=n;else if(typeof n=="function")return;var o=[i,s];if(ji(o))return o}}function iM(e,t,r){if(!(!r&&t==null)){if(typeof e=="function"&&t!=null)try{var n=e(t,r);if(ji(n))return yy(n,t,r)}catch{}if(Array.isArray(e)&&e.length===2){var[i,s]=e,o,l;if(i==="auto")t!=null&&(o=Math.min(...t));else if(Y(i))o=i;else if(typeof i=="function")try{t!=null&&(o=i(t==null?void 0:t[0]))}catch{}else if(typeof i=="string"&&o0.test(i)){var c=o0.exec(i);if(c==null||t==null)o=void 0;else{var d=+c[1];o=t[0]-d}}else o=t==null?void 0:t[0];if(s==="auto")t!=null&&(l=Math.max(...t));else if(Y(s))l=s;else if(typeof s=="function")try{t!=null&&(l=s(t==null?void 0:t[1]))}catch{}else if(typeof s=="string"&&l0.test(s)){var u=l0.exec(s);if(u==null||t==null)l=void 0;else{var f=+u[1];l=t[1]+f}}else l=t==null?void 0:t[1];var p=[o,l];if(ji(p))return t==null?p:yy(p,t,r)}}}var _a=1e9,aM={precision:20,rounding:4,toExpNeg:-7,toExpPos:21,LN10:"2.302585092994045684017991454684364207601101488628772976033327900967572609677352480235997205089598298341967784042286"},Nm,je=!0,sr="[DecimalError] ",di=sr+"Invalid argument: ",Sm=sr+"Exponent out of range: ",Ca=Math.floor,Qn=Math.pow,sM=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,Lt,qe=1e7,xe=7,zS=9007199254740991,uc=Ca(zS/xe),V={};V.absoluteValue=V.abs=function(){var e=new this.constructor(this);return e.s&&(e.s=1),e};V.comparedTo=V.cmp=function(e){var t,r,n,i,s=this;if(e=new s.constructor(e),s.s!==e.s)return s.s||-e.s;if(s.e!==e.e)return s.e>e.e^s.s<0?1:-1;for(n=s.d.length,i=e.d.length,t=0,r=ne.d[t]^s.s<0?1:-1;return n===i?0:n>i^s.s<0?1:-1};V.decimalPlaces=V.dp=function(){var e=this,t=e.d.length-1,r=(t-e.e)*xe;if(t=e.d[t],t)for(;t%10==0;t/=10)r--;return r<0?0:r};V.dividedBy=V.div=function(e){return Vr(this,new this.constructor(e))};V.dividedToIntegerBy=V.idiv=function(e){var t=this,r=t.constructor;return fe(Vr(t,new r(e),0,1),r.precision)};V.equals=V.eq=function(e){return!this.cmp(e)};V.exponent=function(){return Me(this)};V.greaterThan=V.gt=function(e){return this.cmp(e)>0};V.greaterThanOrEqualTo=V.gte=function(e){return this.cmp(e)>=0};V.isInteger=V.isint=function(){return this.e>this.d.length-2};V.isNegative=V.isneg=function(){return this.s<0};V.isPositive=V.ispos=function(){return this.s>0};V.isZero=function(){return this.s===0};V.lessThan=V.lt=function(e){return this.cmp(e)<0};V.lessThanOrEqualTo=V.lte=function(e){return this.cmp(e)<1};V.logarithm=V.log=function(e){var t,r=this,n=r.constructor,i=n.precision,s=i+5;if(e===void 0)e=new n(10);else if(e=new n(e),e.s<1||e.eq(Lt))throw Error(sr+"NaN");if(r.s<1)throw Error(sr+(r.s?"NaN":"-Infinity"));return r.eq(Lt)?new n(0):(je=!1,t=Vr(Ws(r,s),Ws(e,s),s),je=!0,fe(t,i))};V.minus=V.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?FS(t,e):RS(t,(e.s=-e.s,e))};V.modulo=V.mod=function(e){var t,r=this,n=r.constructor,i=n.precision;if(e=new n(e),!e.s)throw Error(sr+"NaN");return r.s?(je=!1,t=Vr(r,e,0,1).times(e),je=!0,r.minus(t)):fe(new n(r),i)};V.naturalExponential=V.exp=function(){return BS(this)};V.naturalLogarithm=V.ln=function(){return Ws(this)};V.negated=V.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e};V.plus=V.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?RS(t,e):FS(t,(e.s=-e.s,e))};V.precision=V.sd=function(e){var t,r,n,i=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(di+e);if(t=Me(i)+1,n=i.d.length-1,r=n*xe+1,n=i.d[n],n){for(;n%10==0;n/=10)r--;for(n=i.d[0];n>=10;n/=10)r++}return e&&t>r?t:r};V.squareRoot=V.sqrt=function(){var e,t,r,n,i,s,o,l=this,c=l.constructor;if(l.s<1){if(!l.s)return new c(0);throw Error(sr+"NaN")}for(e=Me(l),je=!1,i=Math.sqrt(+l),i==0||i==1/0?(t=Nr(l.d),(t.length+e)%2==0&&(t+="0"),i=Math.sqrt(t),e=Ca((e+1)/2)-(e<0||e%2),i==1/0?t="5e"+e:(t=i.toExponential(),t=t.slice(0,t.indexOf("e")+1)+e),n=new c(t)):n=new c(i.toString()),r=c.precision,i=o=r+3;;)if(s=n,n=s.plus(Vr(l,s,o+2)).times(.5),Nr(s.d).slice(0,o)===(t=Nr(n.d)).slice(0,o)){if(t=t.slice(o-3,o+1),i==o&&t=="4999"){if(fe(s,r+1,0),s.times(s).eq(l)){n=s;break}}else if(t!="9999")break;o+=4}return je=!0,fe(n,r)};V.times=V.mul=function(e){var t,r,n,i,s,o,l,c,d,u=this,f=u.constructor,p=u.d,m=(e=new f(e)).d;if(!u.s||!e.s)return new f(0);for(e.s*=u.s,r=u.e+e.e,c=p.length,d=m.length,c=0;){for(t=0,i=c+n;i>n;)l=s[i]+m[n]*p[i-n-1]+t,s[i--]=l%qe|0,t=l/qe|0;s[i]=(s[i]+t)%qe|0}for(;!s[--o];)s.pop();return t?++r:s.shift(),e.d=s,e.e=r,je?fe(e,f.precision):e};V.toDecimalPlaces=V.todp=function(e,t){var r=this,n=r.constructor;return r=new n(r),e===void 0?r:(Dr(e,0,_a),t===void 0?t=n.rounding:Dr(t,0,8),fe(r,e+Me(r)+1,t))};V.toExponential=function(e,t){var r,n=this,i=n.constructor;return e===void 0?r=wi(n,!0):(Dr(e,0,_a),t===void 0?t=i.rounding:Dr(t,0,8),n=fe(new i(n),e+1,t),r=wi(n,!0,e+1)),r};V.toFixed=function(e,t){var r,n,i=this,s=i.constructor;return e===void 0?wi(i):(Dr(e,0,_a),t===void 0?t=s.rounding:Dr(t,0,8),n=fe(new s(i),e+Me(i)+1,t),r=wi(n.abs(),!1,e+Me(n)+1),i.isneg()&&!i.isZero()?"-"+r:r)};V.toInteger=V.toint=function(){var e=this,t=e.constructor;return fe(new t(e),Me(e)+1,t.rounding)};V.toNumber=function(){return+this};V.toPower=V.pow=function(e){var t,r,n,i,s,o,l=this,c=l.constructor,d=12,u=+(e=new c(e));if(!e.s)return new c(Lt);if(l=new c(l),!l.s){if(e.s<1)throw Error(sr+"Infinity");return l}if(l.eq(Lt))return l;if(n=c.precision,e.eq(Lt))return fe(l,n);if(t=e.e,r=e.d.length-1,o=t>=r,s=l.s,o){if((r=u<0?-u:u)<=zS){for(i=new c(Lt),t=Math.ceil(n/xe+4),je=!1;r%2&&(i=i.times(l),by(i.d,t)),r=Ca(r/2),r!==0;)l=l.times(l),by(l.d,t);return je=!0,e.s<0?new c(Lt).div(i):fe(i,n)}}else if(s<0)throw Error(sr+"NaN");return s=s<0&&e.d[Math.max(t,r)]&1?-1:1,l.s=1,je=!1,i=e.times(Ws(l,n+d)),je=!0,i=BS(i),i.s=s,i};V.toPrecision=function(e,t){var r,n,i=this,s=i.constructor;return e===void 0?(r=Me(i),n=wi(i,r<=s.toExpNeg||r>=s.toExpPos)):(Dr(e,1,_a),t===void 0?t=s.rounding:Dr(t,0,8),i=fe(new s(i),e,t),r=Me(i),n=wi(i,e<=r||r<=s.toExpNeg,e)),n};V.toSignificantDigits=V.tosd=function(e,t){var r=this,n=r.constructor;return e===void 0?(e=n.precision,t=n.rounding):(Dr(e,1,_a),t===void 0?t=n.rounding:Dr(t,0,8)),fe(new n(r),e,t)};V.toString=V.valueOf=V.val=V.toJSON=V[Symbol.for("nodejs.util.inspect.custom")]=function(){var e=this,t=Me(e),r=e.constructor;return wi(e,t<=r.toExpNeg||t>=r.toExpPos)};function RS(e,t){var r,n,i,s,o,l,c,d,u=e.constructor,f=u.precision;if(!e.s||!t.s)return t.s||(t=new u(e)),je?fe(t,f):t;if(c=e.d,d=t.d,o=e.e,i=t.e,c=c.slice(),s=o-i,s){for(s<0?(n=c,s=-s,l=d.length):(n=d,i=o,l=c.length),o=Math.ceil(f/xe),l=o>l?o+1:l+1,s>l&&(s=l,n.length=1),n.reverse();s--;)n.push(0);n.reverse()}for(l=c.length,s=d.length,l-s<0&&(s=l,n=d,d=c,c=n),r=0;s;)r=(c[--s]=c[s]+d[s]+r)/qe|0,c[s]%=qe;for(r&&(c.unshift(r),++i),l=c.length;c[--l]==0;)c.pop();return t.d=c,t.e=i,je?fe(t,f):t}function Dr(e,t,r){if(e!==~~e||er)throw Error(di+e)}function Nr(e){var t,r,n,i=e.length-1,s="",o=e[0];if(i>0){for(s+=o,t=1;to?1:-1;else for(l=c=0;li[l]?1:-1;break}return c}function r(n,i,s){for(var o=0;s--;)n[s]-=o,o=n[s]1;)n.shift()}return function(n,i,s,o){var l,c,d,u,f,p,m,x,g,b,v,j,y,w,S,N,P,_,T=n.constructor,$=n.s==i.s?1:-1,M=n.d,C=i.d;if(!n.s)return new T(n);if(!i.s)throw Error(sr+"Division by zero");for(c=n.e-i.e,P=C.length,S=M.length,m=new T($),x=m.d=[],d=0;C[d]==(M[d]||0);)++d;if(C[d]>(M[d]||0)&&--c,s==null?j=s=T.precision:o?j=s+(Me(n)-Me(i))+1:j=s,j<0)return new T(0);if(j=j/xe+2|0,d=0,P==1)for(u=0,C=C[0],j++;(d1&&(C=e(C,u),M=e(M,u),P=C.length,S=M.length),w=P,g=M.slice(0,P),b=g.length;b=qe/2&&++N;do u=0,l=t(C,g,P,b),l<0?(v=g[0],P!=b&&(v=v*qe+(g[1]||0)),u=v/N|0,u>1?(u>=qe&&(u=qe-1),f=e(C,u),p=f.length,b=g.length,l=t(f,g,p,b),l==1&&(u--,r(f,P16)throw Error(Sm+Me(e));if(!e.s)return new u(Lt);for(je=!1,l=f,o=new u(.03125);e.abs().gte(.1);)e=e.times(o),d+=5;for(n=Math.log(Qn(2,d))/Math.LN10*2+5|0,l+=n,r=i=s=new u(Lt),u.precision=l;;){if(i=fe(i.times(e),l),r=r.times(++c),o=s.plus(Vr(i,r,l)),Nr(o.d).slice(0,l)===Nr(s.d).slice(0,l)){for(;d--;)s=fe(s.times(s),l);return u.precision=f,t==null?(je=!0,fe(s,f)):s}s=o}}function Me(e){for(var t=e.e*xe,r=e.d[0];r>=10;r/=10)t++;return t}function Sd(e,t,r){if(t>e.LN10.sd())throw je=!0,r&&(e.precision=r),Error(sr+"LN10 precision limit exceeded");return fe(new e(e.LN10),t)}function xn(e){for(var t="";e--;)t+="0";return t}function Ws(e,t){var r,n,i,s,o,l,c,d,u,f=1,p=10,m=e,x=m.d,g=m.constructor,b=g.precision;if(m.s<1)throw Error(sr+(m.s?"NaN":"-Infinity"));if(m.eq(Lt))return new g(0);if(t==null?(je=!1,d=b):d=t,m.eq(10))return t==null&&(je=!0),Sd(g,d);if(d+=p,g.precision=d,r=Nr(x),n=r.charAt(0),s=Me(m),Math.abs(s)<15e14){for(;n<7&&n!=1||n==1&&r.charAt(1)>3;)m=m.times(e),r=Nr(m.d),n=r.charAt(0),f++;s=Me(m),n>1?(m=new g("0."+r),s++):m=new g(n+"."+r.slice(1))}else return c=Sd(g,d+2,b).times(s+""),m=Ws(new g(n+"."+r.slice(1)),d-p).plus(c),g.precision=b,t==null?(je=!0,fe(m,b)):m;for(l=o=m=Vr(m.minus(Lt),m.plus(Lt),d),u=fe(m.times(m),d),i=3;;){if(o=fe(o.times(u),d),c=l.plus(Vr(o,new g(i),d)),Nr(c.d).slice(0,d)===Nr(l.d).slice(0,d))return l=l.times(2),s!==0&&(l=l.plus(Sd(g,d+2,b).times(s+""))),l=Vr(l,new g(f),d),g.precision=b,t==null?(je=!0,fe(l,b)):l;l=c,i+=2}}function vy(e,t){var r,n,i;for((r=t.indexOf("."))>-1&&(t=t.replace(".","")),(n=t.search(/e/i))>0?(r<0&&(r=n),r+=+t.slice(n+1),t=t.substring(0,n)):r<0&&(r=t.length),n=0;t.charCodeAt(n)===48;)++n;for(i=t.length;t.charCodeAt(i-1)===48;)--i;if(t=t.slice(n,i),t){if(i-=n,r=r-n-1,e.e=Ca(r/xe),e.d=[],n=(r+1)%xe,r<0&&(n+=xe),nuc||e.e<-uc))throw Error(Sm+r)}else e.s=0,e.e=0,e.d=[0];return e}function fe(e,t,r){var n,i,s,o,l,c,d,u,f=e.d;for(o=1,s=f[0];s>=10;s/=10)o++;if(n=t-o,n<0)n+=xe,i=t,d=f[u=0];else{if(u=Math.ceil((n+1)/xe),s=f.length,u>=s)return e;for(d=s=f[u],o=1;s>=10;s/=10)o++;n%=xe,i=n-xe+o}if(r!==void 0&&(s=Qn(10,o-i-1),l=d/s%10|0,c=t<0||f[u+1]!==void 0||d%s,c=r<4?(l||c)&&(r==0||r==(e.s<0?3:2)):l>5||l==5&&(r==4||c||r==6&&(n>0?i>0?d/Qn(10,o-i):0:f[u-1])%10&1||r==(e.s<0?8:7))),t<1||!f[0])return c?(s=Me(e),f.length=1,t=t-s-1,f[0]=Qn(10,(xe-t%xe)%xe),e.e=Ca(-t/xe)||0):(f.length=1,f[0]=e.e=e.s=0),e;if(n==0?(f.length=u,s=1,u--):(f.length=u+1,s=Qn(10,xe-n),f[u]=i>0?(d/Qn(10,o-i)%Qn(10,i)|0)*s:0),c)for(;;)if(u==0){(f[0]+=s)==qe&&(f[0]=1,++e.e);break}else{if(f[u]+=s,f[u]!=qe)break;f[u--]=0,s=1}for(n=f.length;f[--n]===0;)f.pop();if(je&&(e.e>uc||e.e<-uc))throw Error(Sm+Me(e));return e}function FS(e,t){var r,n,i,s,o,l,c,d,u,f,p=e.constructor,m=p.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new p(e),je?fe(t,m):t;if(c=e.d,f=t.d,n=t.e,d=e.e,c=c.slice(),o=d-n,o){for(u=o<0,u?(r=c,o=-o,l=f.length):(r=f,n=d,l=c.length),i=Math.max(Math.ceil(m/xe),l)+2,o>i&&(o=i,r.length=1),r.reverse(),i=o;i--;)r.push(0);r.reverse()}else{for(i=c.length,l=f.length,u=i0;--i)c[l++]=0;for(i=f.length;i>o;){if(c[--i]0?s=s.charAt(0)+"."+s.slice(1)+xn(n):o>1&&(s=s.charAt(0)+"."+s.slice(1)),s=s+(i<0?"e":"e+")+i):i<0?(s="0."+xn(-i-1)+s,r&&(n=r-o)>0&&(s+=xn(n))):i>=o?(s+=xn(i+1-o),r&&(n=r-i-1)>0&&(s=s+"."+xn(n))):((n=i+1)0&&(i+1===o&&(s+="."),s+=xn(n))),e.s<0?"-"+s:s}function by(e,t){if(e.length>t)return e.length=t,!0}function WS(e){var t,r,n;function i(s){var o=this;if(!(o instanceof i))return new i(s);if(o.constructor=i,s instanceof i){o.s=s.s,o.e=s.e,o.d=(s=s.d)?s.slice():s;return}if(typeof s=="number"){if(s*0!==0)throw Error(di+s);if(s>0)o.s=1;else if(s<0)s=-s,o.s=-1;else{o.s=0,o.e=0,o.d=[0];return}if(s===~~s&&s<1e7){o.e=0,o.d=[s];return}return vy(o,s.toString())}else if(typeof s!="string")throw Error(di+s);if(s.charCodeAt(0)===45?(s=s.slice(1),o.s=-1):o.s=1,sM.test(s))vy(o,s);else throw Error(di+s)}if(i.prototype=V,i.ROUND_UP=0,i.ROUND_DOWN=1,i.ROUND_CEIL=2,i.ROUND_FLOOR=3,i.ROUND_HALF_UP=4,i.ROUND_HALF_DOWN=5,i.ROUND_HALF_EVEN=6,i.ROUND_HALF_CEIL=7,i.ROUND_HALF_FLOOR=8,i.clone=WS,i.config=i.set=oM,e===void 0&&(e={}),e)for(n=["precision","rounding","toExpNeg","toExpPos","LN10"],t=0;t=i[t+1]&&n<=i[t+2])this[r]=n;else throw Error(di+r+": "+n);if((n=e[r="LN10"])!==void 0)if(n==Math.LN10)this[r]=new this(n);else throw Error(di+r+": "+n);return this}var Nm=WS(aM);Lt=new Nm(1);const oe=Nm;var lM=e=>e,US={},qS=e=>e===US,jy=e=>function t(){return arguments.length===0||arguments.length===1&&qS(arguments.length<=0?void 0:arguments[0])?t:e(...arguments)},HS=(e,t)=>e===1?t:jy(function(){for(var r=arguments.length,n=new Array(r),i=0;io!==US).length;return s>=e?t(...n):HS(e-s,jy(function(){for(var o=arguments.length,l=new Array(o),c=0;cqS(u)?l.shift():u);return t(...d,...l)}))}),ju=e=>HS(e.length,e),sp=(e,t)=>{for(var r=[],n=e;nArray.isArray(t)?t.map(e):Object.keys(t).map(r=>t[r]).map(e)),uM=function(){for(var t=arguments.length,r=new Array(t),n=0;nc(l),s(...arguments))}},op=e=>Array.isArray(e)?e.reverse():e.split("").reverse().join(""),KS=e=>{var t=null,r=null;return function(){for(var n=arguments.length,i=new Array(n),s=0;s{var c;return o===((c=t)===null||c===void 0?void 0:c[l])})||(t=i,r=e(...i)),r}};function VS(e){var t;return e===0?t=1:t=Math.floor(new oe(e).abs().log(10).toNumber())+1,t}function YS(e,t,r){for(var n=new oe(e),i=0,s=[];n.lt(t)&&i<1e5;)s.push(n.toNumber()),n=n.add(r),i++;return s}ju((e,t,r)=>{var n=+e,i=+t;return n+r*(i-n)});ju((e,t,r)=>{var n=t-+e;return n=n||1/0,(r-e)/n});ju((e,t,r)=>{var n=t-+e;return n=n||1/0,Math.max(0,Math.min(1,(r-e)/n))});var GS=e=>{var[t,r]=e,[n,i]=[t,r];return t>r&&([n,i]=[r,t]),[n,i]},ZS=(e,t,r)=>{if(e.lte(0))return new oe(0);var n=VS(e.toNumber()),i=new oe(10).pow(n),s=e.div(i),o=n!==1?.05:.1,l=new oe(Math.ceil(s.div(o).toNumber())).add(r).mul(o),c=l.mul(i);return t?new oe(c.toNumber()):new oe(Math.ceil(c.toNumber()))},dM=(e,t,r)=>{var n=new oe(1),i=new oe(e);if(!i.isint()&&r){var s=Math.abs(e);s<1?(n=new oe(10).pow(VS(e)-1),i=new oe(Math.floor(i.div(n).toNumber())).mul(n)):s>1&&(i=new oe(Math.floor(e)))}else e===0?i=new oe(Math.floor((t-1)/2)):r||(i=new oe(Math.floor(e)));var o=Math.floor((t-1)/2),l=uM(cM(c=>i.add(new oe(c-o).mul(n)).toNumber()),sp);return l(0,t)},XS=function(t,r,n,i){var s=arguments.length>4&&arguments[4]!==void 0?arguments[4]:0;if(!Number.isFinite((r-t)/(n-1)))return{step:new oe(0),tickMin:new oe(0),tickMax:new oe(0)};var o=ZS(new oe(r).sub(t).div(n-1),i,s),l;t<=0&&r>=0?l=new oe(0):(l=new oe(t).add(r).div(2),l=l.sub(new oe(l).mod(o)));var c=Math.ceil(l.sub(t).div(o).toNumber()),d=Math.ceil(new oe(r).sub(l).div(o).toNumber()),u=c+d+1;return u>n?XS(t,r,n,i,s+1):(u0?d+(n-u):d,c=r>0?c:c+(n-u)),{step:o,tickMin:l.sub(new oe(c).mul(o)),tickMax:l.add(new oe(d).mul(o))})};function fM(e){var[t,r]=e,n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:6,i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,s=Math.max(n,2),[o,l]=GS([t,r]);if(o===-1/0||l===1/0){var c=l===1/0?[o,...sp(0,n-1).map(()=>1/0)]:[...sp(0,n-1).map(()=>-1/0),l];return t>r?op(c):c}if(o===l)return dM(o,n,i);var{step:d,tickMin:u,tickMax:f}=XS(o,l,s,i,0),p=YS(u,f.add(new oe(.1).mul(d)),d);return t>r?op(p):p}function pM(e,t){var[r,n]=e,i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,[s,o]=GS([r,n]);if(s===-1/0||o===1/0)return[r,n];if(s===o)return[s];var l=Math.max(t,2),c=ZS(new oe(o).sub(s).div(l-1),i,0),d=[...YS(new oe(s),new oe(o),c),o];return i===!1&&(d=d.map(u=>Math.round(u))),r>n?op(d):d}var hM=KS(fM),mM=KS(pM),gM=e=>e.rootProps.barCategoryGap,wu=e=>e.rootProps.stackOffset,km=e=>e.options.chartName,Pm=e=>e.rootProps.syncId,JS=e=>e.rootProps.syncMethod,_m=e=>e.options.eventEmitter,xM=e=>e.rootProps.baseValue,lt={grid:-100,barBackground:-50,area:100,cursorRectangle:200,bar:300,line:400,axis:500,scatter:600,activeBar:1e3,cursorLine:1100,activeDot:1200,label:2e3},zr={allowDuplicatedCategory:!0,angleAxisId:0,reversed:!1,scale:"auto",tick:!0,type:"category"},$t={allowDataOverflow:!1,allowDuplicatedCategory:!0,radiusAxisId:0,scale:"auto",tick:!0,tickCount:5,type:"number"},Su=(e,t)=>{if(!(!e||!t))return e!=null&&e.reversed?[t[1],t[0]]:t},yM={allowDataOverflow:!1,allowDecimals:!1,allowDuplicatedCategory:!1,dataKey:void 0,domain:void 0,id:zr.angleAxisId,includeHidden:!1,name:void 0,reversed:zr.reversed,scale:zr.scale,tick:zr.tick,tickCount:void 0,ticks:void 0,type:zr.type,unit:void 0},vM={allowDataOverflow:$t.allowDataOverflow,allowDecimals:!1,allowDuplicatedCategory:$t.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:$t.radiusAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:$t.scale,tick:$t.tick,tickCount:$t.tickCount,ticks:void 0,type:$t.type,unit:void 0},bM={allowDataOverflow:!1,allowDecimals:!1,allowDuplicatedCategory:zr.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:zr.angleAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:zr.scale,tick:zr.tick,tickCount:void 0,ticks:void 0,type:"number",unit:void 0},jM={allowDataOverflow:$t.allowDataOverflow,allowDecimals:!1,allowDuplicatedCategory:$t.allowDuplicatedCategory,dataKey:void 0,domain:void 0,id:$t.radiusAxisId,includeHidden:!1,name:void 0,reversed:!1,scale:$t.scale,tick:$t.tick,tickCount:$t.tickCount,ticks:void 0,type:"category",unit:void 0},Cm=(e,t)=>e.polarAxis.angleAxis[t]!=null?e.polarAxis.angleAxis[t]:e.layout.layoutType==="radial"?bM:yM,Am=(e,t)=>e.polarAxis.radiusAxis[t]!=null?e.polarAxis.radiusAxis[t]:e.layout.layoutType==="radial"?jM:vM,Nu=e=>e.polarOptions,Om=I([ln,cn,rt],yD),QS=I([Nu,Om],(e,t)=>{if(e!=null)return Rn(e.innerRadius,t,0)}),e2=I([Nu,Om],(e,t)=>{if(e!=null)return Rn(e.outerRadius,t,t*.8)}),wM=e=>{if(e==null)return[0,0];var{startAngle:t,endAngle:r}=e;return[t,r]},t2=I([Nu],wM);I([Cm,t2],Su);var r2=I([Om,QS,e2],(e,t,r)=>{if(!(e==null||t==null||r==null))return[t,r]});I([Am,r2],Su);var n2=I([ue,Nu,QS,e2,ln,cn],(e,t,r,n,i,s)=>{if(!(e!=="centric"&&e!=="radial"||t==null||r==null||n==null)){var{cx:o,cy:l,startAngle:c,endAngle:d}=t;return{cx:Rn(o,i,i/2),cy:Rn(l,s,s/2),innerRadius:r,outerRadius:n,startAngle:c,endAngle:d,clockWise:!1}}}),Fe=(e,t)=>t,ku=(e,t,r)=>r;function Em(e){return e==null?void 0:e.id}function i2(e,t,r){var{chartData:n=[]}=t,{allowDuplicatedCategory:i,dataKey:s}=r,o=new Map;return e.forEach(l=>{var c,d=(c=l.data)!==null&&c!==void 0?c:n;if(!(d==null||d.length===0)){var u=Em(l);d.forEach((f,p)=>{var m=s==null||i?p:String(et(f,s,null)),x=et(f,l.dataKey,0),g;o.has(m)?g=o.get(m):g={},Object.assign(g,{[u]:x}),o.set(m,g)})}}),Array.from(o.values())}function Dm(e){return e.stackId!=null&&e.dataKey!=null}var Pu=(e,t)=>e===t?!0:e==null||t==null?!1:e[0]===t[0]&&e[1]===t[1];function _u(e,t){return Array.isArray(e)&&Array.isArray(t)&&e.length===0&&t.length===0?!0:e===t}function SM(e,t){if(e.length===t.length){for(var r=0;r{var t=ue(e);return t==="horizontal"?"xAxis":t==="vertical"?"yAxis":t==="centric"?"angleAxis":"radiusAxis"},Aa=e=>e.tooltip.settings.axisId;function wy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function dc(e){for(var t=1;te.cartesianAxis.xAxis[t],dn=(e,t)=>{var r=a2(e,t);return r??Tt},Mt={allowDataOverflow:!1,allowDecimals:!0,allowDuplicatedCategory:!0,angle:0,dataKey:void 0,domain:lp,hide:!0,id:0,includeHidden:!1,interval:"preserveEnd",minTickGap:5,mirror:!1,name:void 0,orientation:"left",padding:{top:0,bottom:0},reversed:!1,scale:"auto",tick:!0,tickCount:5,tickFormatter:void 0,ticks:void 0,type:"number",unit:void 0,width:eo},s2=(e,t)=>e.cartesianAxis.yAxis[t],fn=(e,t)=>{var r=s2(e,t);return r??Mt},_M={domain:[0,"auto"],includeHidden:!1,reversed:!1,allowDataOverflow:!1,allowDuplicatedCategory:!1,dataKey:void 0,id:0,name:"",range:[64,64],scale:"auto",type:"number",unit:""},Tm=(e,t)=>{var r=e.cartesianAxis.zAxis[t];return r??_M},vt=(e,t,r)=>{switch(t){case"xAxis":return dn(e,r);case"yAxis":return fn(e,r);case"zAxis":return Tm(e,r);case"angleAxis":return Cm(e,r);case"radiusAxis":return Am(e,r);default:throw new Error("Unexpected axis type: ".concat(t))}},CM=(e,t,r)=>{switch(t){case"xAxis":return dn(e,r);case"yAxis":return fn(e,r);default:throw new Error("Unexpected axis type: ".concat(t))}},so=(e,t,r)=>{switch(t){case"xAxis":return dn(e,r);case"yAxis":return fn(e,r);case"angleAxis":return Cm(e,r);case"radiusAxis":return Am(e,r);default:throw new Error("Unexpected axis type: ".concat(t))}},o2=e=>e.graphicalItems.cartesianItems.some(t=>t.type==="bar")||e.graphicalItems.polarItems.some(t=>t.type==="radialBar");function l2(e,t){return r=>{switch(e){case"xAxis":return"xAxisId"in r&&r.xAxisId===t;case"yAxis":return"yAxisId"in r&&r.yAxisId===t;case"zAxis":return"zAxisId"in r&&r.zAxisId===t;case"angleAxis":return"angleAxisId"in r&&r.angleAxisId===t;case"radiusAxis":return"radiusAxisId"in r&&r.radiusAxisId===t;default:return!1}}}var Mm=e=>e.graphicalItems.cartesianItems,AM=I([Fe,ku],l2),c2=(e,t,r)=>e.filter(r).filter(n=>(t==null?void 0:t.includeHidden)===!0?!0:!n.hide),oo=I([Mm,vt,AM],c2,{memoizeOptions:{resultEqualityCheck:_u}}),u2=I([oo],e=>e.filter(t=>t.type==="area"||t.type==="bar").filter(Dm)),d2=e=>e.filter(t=>!("stackId"in t)||t.stackId===void 0),OM=I([oo],d2),f2=e=>e.map(t=>t.data).filter(Boolean).flat(1),EM=I([oo],f2,{memoizeOptions:{resultEqualityCheck:_u}}),p2=(e,t)=>{var{chartData:r=[],dataStartIndex:n,dataEndIndex:i}=t;return e.length>0?e:r.slice(n,i+1)},Im=I([EM,bu],p2),h2=(e,t,r)=>(t==null?void 0:t.dataKey)!=null?e.map(n=>({value:et(n,t.dataKey)})):r.length>0?r.map(n=>n.dataKey).flatMap(n=>e.map(i=>({value:et(i,n)}))):e.map(n=>({value:n})),Cu=I([Im,vt,oo],h2);function m2(e,t){switch(e){case"xAxis":return t.direction==="x";case"yAxis":return t.direction==="y";default:return!1}}function ll(e){if(Or(e)||e instanceof Date){var t=Number(e);if(_e(t))return t}}function Sy(e){if(Array.isArray(e)){var t=[ll(e[0]),ll(e[1])];return ji(t)?t:void 0}var r=ll(e);if(r!=null)return[r,r]}function an(e){return e.map(ll).filter(F6)}function DM(e,t,r){return!r||typeof t!="number"||yr(t)?[]:r.length?an(r.flatMap(n=>{var i=et(e,n.dataKey),s,o;if(Array.isArray(i)?[s,o]=i:s=o=i,!(!_e(s)||!_e(o)))return[t-s,t+o]})):[]}var Ue=e=>{var t=We(e),r=Aa(e);return so(e,t,r)},g2=I([Ue],e=>e==null?void 0:e.dataKey),TM=I([u2,bu,Ue],i2),x2=(e,t,r)=>{var n={},i=t.reduce((s,o)=>(o.stackId==null||(s[o.stackId]==null&&(s[o.stackId]=[]),s[o.stackId].push(o)),s),n);return Object.fromEntries(Object.entries(i).map(s=>{var[o,l]=s,c=l.map(Em);return[o,{stackedData:_E(e,c,r),graphicalItems:l}]}))},cp=I([TM,u2,wu],x2),y2=(e,t,r,n)=>{var{dataStartIndex:i,dataEndIndex:s}=t;if(n==null&&r!=="zAxis"){var o=EE(e,i,s);if(!(o!=null&&o[0]===0&&o[1]===0))return o}},MM=I([vt],e=>e.allowDataOverflow),$m=e=>{var t;if(e==null||!("domain"in e))return lp;if(e.domain!=null)return e.domain;if(e.ticks!=null){if(e.type==="number"){var r=an(e.ticks);return[Math.min(...r),Math.max(...r)]}if(e.type==="category")return e.ticks.map(String)}return(t=e==null?void 0:e.domain)!==null&&t!==void 0?t:lp},v2=I([vt],$m),b2=I([v2,MM],LS),IM=I([cp,Kn,Fe,b2],y2,{memoizeOptions:{resultEqualityCheck:Pu}}),Lm=e=>e.errorBars,$M=(e,t,r)=>e.flatMap(n=>t[n.id]).filter(Boolean).filter(n=>m2(r,n)),fc=function(){for(var t=arguments.length,r=new Array(t),n=0;n{var s,o;if(r.length>0&&e.forEach(l=>{r.forEach(c=>{var d,u,f=(d=n[c.id])===null||d===void 0?void 0:d.filter(v=>m2(i,v)),p=et(l,(u=t.dataKey)!==null&&u!==void 0?u:c.dataKey),m=DM(l,p,f);if(m.length>=2){var x=Math.min(...m),g=Math.max(...m);(s==null||xo)&&(o=g)}var b=Sy(p);b!=null&&(s=s==null?b[0]:Math.min(s,b[0]),o=o==null?b[1]:Math.max(o,b[1]))})}),(t==null?void 0:t.dataKey)!=null&&e.forEach(l=>{var c=Sy(et(l,t.dataKey));c!=null&&(s=s==null?c[0]:Math.min(s,c[0]),o=o==null?c[1]:Math.max(o,c[1]))}),_e(s)&&_e(o))return[s,o]},LM=I([Im,vt,OM,Lm,Fe],j2,{memoizeOptions:{resultEqualityCheck:Pu}});function zM(e){var{value:t}=e;if(Or(t)||t instanceof Date)return t}var RM=(e,t,r)=>{var n=e.map(zM).filter(i=>i!=null);return r&&(t.dataKey==null||t.allowDuplicatedCategory&&_j(n))?rS(0,e.length):t.allowDuplicatedCategory?n:Array.from(new Set(n))},w2=e=>e.referenceElements.dots,Oa=(e,t,r)=>e.filter(n=>n.ifOverflow==="extendDomain").filter(n=>t==="xAxis"?n.xAxisId===r:n.yAxisId===r),BM=I([w2,Fe,ku],Oa),S2=e=>e.referenceElements.areas,FM=I([S2,Fe,ku],Oa),N2=e=>e.referenceElements.lines,WM=I([N2,Fe,ku],Oa),k2=(e,t)=>{var r=an(e.map(n=>t==="xAxis"?n.x:n.y));if(r.length!==0)return[Math.min(...r),Math.max(...r)]},UM=I(BM,Fe,k2),P2=(e,t)=>{var r=an(e.flatMap(n=>[t==="xAxis"?n.x1:n.y1,t==="xAxis"?n.x2:n.y2]));if(r.length!==0)return[Math.min(...r),Math.max(...r)]},qM=I([FM,Fe],P2);function HM(e){var t;if(e.x!=null)return an([e.x]);var r=(t=e.segment)===null||t===void 0?void 0:t.map(n=>n.x);return r==null||r.length===0?[]:an(r)}function KM(e){var t;if(e.y!=null)return an([e.y]);var r=(t=e.segment)===null||t===void 0?void 0:t.map(n=>n.y);return r==null||r.length===0?[]:an(r)}var _2=(e,t)=>{var r=e.flatMap(n=>t==="xAxis"?HM(n):KM(n));if(r.length!==0)return[Math.min(...r),Math.max(...r)]},VM=I([WM,Fe],_2),YM=I(UM,VM,qM,(e,t,r)=>fc(e,r,t)),C2=(e,t,r,n,i,s,o,l)=>{if(r!=null)return r;var c=o==="vertical"&&l==="xAxis"||o==="horizontal"&&l==="yAxis",d=c?fc(n,s,i):fc(s,i);return iM(t,d,e.allowDataOverflow)},GM=I([vt,v2,b2,IM,LM,YM,ue,Fe],C2,{memoizeOptions:{resultEqualityCheck:Pu}}),ZM=[0,1],A2=(e,t,r,n,i,s,o)=>{if(!((e==null||r==null||r.length===0)&&o===void 0)){var{dataKey:l,type:c}=e,d=Mr(t,s);if(d&&l==null){var u;return rS(0,(u=r==null?void 0:r.length)!==null&&u!==void 0?u:0)}return c==="category"?RM(n,e,d):i==="expand"?ZM:o}},zm=I([vt,ue,Im,Cu,wu,Fe,GM],A2),O2=(e,t,r,n,i)=>{if(e!=null){var{scale:s,type:o}=e;if(s==="auto")return t==="radial"&&i==="radiusAxis"?"band":t==="radial"&&i==="angleAxis"?"linear":o==="category"&&n&&(n.indexOf("LineChart")>=0||n.indexOf("AreaChart")>=0||n.indexOf("ComposedChart")>=0&&!r)?"point":o==="category"?"band":"linear";if(typeof s=="string"){var l="scale".concat(Xs(s));return l in es?l:"point"}}},lo=I([vt,ue,o2,km,Fe],O2);function XM(e){if(e!=null){if(e in es)return es[e]();var t="scale".concat(Xs(e));if(t in es)return es[t]()}}function Rm(e,t,r,n){if(!(r==null||n==null)){if(typeof e.scale=="function")return e.scale.copy().domain(r).range(n);var i=XM(t);if(i!=null){var s=i.domain(r).range(n);return SE(s),s}}}var E2=(e,t,r)=>{var n=$m(t);if(!(r!=="auto"&&r!=="linear")){if(t!=null&&t.tickCount&&Array.isArray(n)&&(n[0]==="auto"||n[1]==="auto")&&ji(e))return hM(e,t.tickCount,t.allowDecimals);if(t!=null&&t.tickCount&&t.type==="number"&&ji(e))return mM(e,t.tickCount,t.allowDecimals)}},Bm=I([zm,so,lo],E2),D2=(e,t,r,n)=>{if(n!=="angleAxis"&&(e==null?void 0:e.type)==="number"&&ji(t)&&Array.isArray(r)&&r.length>0){var i=t[0],s=r[0],o=t[1],l=r[r.length-1];return[Math.min(i,s),Math.max(o,l)]}return t},JM=I([vt,zm,Bm,Fe],D2),QM=I(Cu,vt,(e,t)=>{if(!(!t||t.type!=="number")){var r=1/0,n=Array.from(an(e.map(l=>l.value))).sort((l,c)=>l-c);if(n.length<2)return 1/0;var i=n[n.length-1]-n[0];if(i===0)return 1/0;for(var s=0;sn,(e,t,r,n,i)=>{if(!_e(e))return 0;var s=t==="vertical"?n.height:n.width;if(i==="gap")return e*s/2;if(i==="no-gap"){var o=Rn(r,e*s),l=e*s/2;return l-o-(l-o)/s*o}return 0}),eI=(e,t)=>{var r=dn(e,t);return r==null||typeof r.padding!="string"?0:T2(e,"xAxis",t,r.padding)},tI=(e,t)=>{var r=fn(e,t);return r==null||typeof r.padding!="string"?0:T2(e,"yAxis",t,r.padding)},rI=I(dn,eI,(e,t)=>{var r,n;if(e==null)return{left:0,right:0};var{padding:i}=e;return typeof i=="string"?{left:t,right:t}:{left:((r=i.left)!==null&&r!==void 0?r:0)+t,right:((n=i.right)!==null&&n!==void 0?n:0)+t}}),nI=I(fn,tI,(e,t)=>{var r,n;if(e==null)return{top:0,bottom:0};var{padding:i}=e;return typeof i=="string"?{top:t,bottom:t}:{top:((r=i.top)!==null&&r!==void 0?r:0)+t,bottom:((n=i.bottom)!==null&&n!==void 0?n:0)+t}}),iI=I([rt,rI,cu,lu,(e,t,r)=>r],(e,t,r,n,i)=>{var{padding:s}=n;return i?[s.left,r.width-s.right]:[e.left+t.left,e.left+e.width-t.right]}),aI=I([rt,ue,nI,cu,lu,(e,t,r)=>r],(e,t,r,n,i,s)=>{var{padding:o}=i;return s?[n.height-o.bottom,o.top]:t==="horizontal"?[e.top+e.height-r.bottom,e.top+r.top]:[e.top+r.top,e.top+e.height-r.bottom]}),co=(e,t,r,n)=>{var i;switch(t){case"xAxis":return iI(e,r,n);case"yAxis":return aI(e,r,n);case"zAxis":return(i=Tm(e,r))===null||i===void 0?void 0:i.range;case"angleAxis":return t2(e);case"radiusAxis":return r2(e,r);default:return}},M2=I([vt,co],Su),Ea=I([vt,lo,JM,M2],Rm);I([oo,Lm,Fe],$M);function I2(e,t){return e.idt.id?1:0}var Au=(e,t)=>t,Ou=(e,t,r)=>r,sI=I(su,Au,Ou,(e,t,r)=>e.filter(n=>n.orientation===t).filter(n=>n.mirror===r).sort(I2)),oI=I(ou,Au,Ou,(e,t,r)=>e.filter(n=>n.orientation===t).filter(n=>n.mirror===r).sort(I2)),$2=(e,t)=>({width:e.width,height:t.height}),lI=(e,t)=>{var r=typeof t.width=="number"?t.width:eo;return{width:r,height:e.height}},cI=I(rt,dn,$2),uI=(e,t,r)=>{switch(t){case"top":return e.top;case"bottom":return r-e.bottom;default:return 0}},dI=(e,t,r)=>{switch(t){case"left":return e.left;case"right":return r-e.right;default:return 0}},fI=I(cn,rt,sI,Au,Ou,(e,t,r,n,i)=>{var s={},o;return r.forEach(l=>{var c=$2(t,l);o==null&&(o=uI(t,n,e));var d=n==="top"&&!i||n==="bottom"&&i;s[l.id]=o-Number(d)*c.height,o+=(d?-1:1)*c.height}),s}),pI=I(ln,rt,oI,Au,Ou,(e,t,r,n,i)=>{var s={},o;return r.forEach(l=>{var c=lI(t,l);o==null&&(o=dI(t,n,e));var d=n==="left"&&!i||n==="right"&&i;s[l.id]=o-Number(d)*c.width,o+=(d?-1:1)*c.width}),s}),hI=(e,t)=>{var r=dn(e,t);if(r!=null)return fI(e,r.orientation,r.mirror)},mI=I([rt,dn,hI,(e,t)=>t],(e,t,r,n)=>{if(t!=null){var i=r==null?void 0:r[n];return i==null?{x:e.left,y:0}:{x:e.left,y:i}}}),gI=(e,t)=>{var r=fn(e,t);if(r!=null)return pI(e,r.orientation,r.mirror)},xI=I([rt,fn,gI,(e,t)=>t],(e,t,r,n)=>{if(t!=null){var i=r==null?void 0:r[n];return i==null?{x:0,y:e.top}:{x:i,y:e.top}}}),yI=I(rt,fn,(e,t)=>{var r=typeof t.width=="number"?t.width:eo;return{width:r,height:e.height}}),L2=(e,t,r,n)=>{if(r!=null){var{allowDuplicatedCategory:i,type:s,dataKey:o}=r,l=Mr(e,n),c=t.map(d=>d.value);if(o&&l&&s==="category"&&i&&_j(c))return c}},Fm=I([ue,Cu,vt,Fe],L2),z2=(e,t,r,n)=>{if(!(r==null||r.dataKey==null)){var{type:i,scale:s}=r,o=Mr(e,n);if(o&&(i==="number"||s!=="auto"))return t.map(l=>l.value)}},Wm=I([ue,Cu,so,Fe],z2),Ny=I([ue,CM,lo,Ea,Fm,Wm,co,Bm,Fe],(e,t,r,n,i,s,o,l,c)=>{if(t!=null){var d=Mr(e,c);return{angle:t.angle,interval:t.interval,minTickGap:t.minTickGap,orientation:t.orientation,tick:t.tick,tickCount:t.tickCount,tickFormatter:t.tickFormatter,ticks:t.ticks,type:t.type,unit:t.unit,axisType:c,categoricalDomain:s,duplicateDomain:i,isCategorical:d,niceTicks:l,range:o,realScaleType:r,scale:n}}}),vI=(e,t,r,n,i,s,o,l,c)=>{if(!(t==null||n==null)){var d=Mr(e,c),{type:u,ticks:f,tickCount:p}=t,m=r==="scaleBand"&&typeof n.bandwidth=="function"?n.bandwidth()/2:2,x=u==="category"&&n.bandwidth?n.bandwidth()/m:0;x=c==="angleAxis"&&s!=null&&s.length>=2?Jt(s[0]-s[1])*2*x:x;var g=f||i;if(g){var b=g.map((v,j)=>{var y=o?o.indexOf(v):v;return{index:j,coordinate:n(y)+x,value:v,offset:x}});return b.filter(v=>_e(v.coordinate))}return d&&l?l.map((v,j)=>({coordinate:n(v)+x,value:v,index:j,offset:x})).filter(v=>_e(v.coordinate)):n.ticks?n.ticks(p).map(v=>({coordinate:n(v)+x,value:v,offset:x})):n.domain().map((v,j)=>({coordinate:n(v)+x,value:o?o[v]:v,index:j,offset:x}))}},R2=I([ue,so,lo,Ea,Bm,co,Fm,Wm,Fe],vI),bI=(e,t,r,n,i,s,o)=>{if(!(t==null||r==null||n==null||n[0]===n[1])){var l=Mr(e,o),{tickCount:c}=t,d=0;return d=o==="angleAxis"&&(n==null?void 0:n.length)>=2?Jt(n[0]-n[1])*2*d:d,l&&s?s.map((u,f)=>({coordinate:r(u)+d,value:u,index:f,offset:d})):r.ticks?r.ticks(c).map(u=>({coordinate:r(u)+d,value:u,offset:d})):r.domain().map((u,f)=>({coordinate:r(u)+d,value:i?i[u]:u,index:f,offset:d}))}},Eu=I([ue,so,Ea,co,Fm,Wm,Fe],bI),Du=I(vt,Ea,(e,t)=>{if(!(e==null||t==null))return dc(dc({},e),{},{scale:t})}),jI=I([vt,lo,zm,M2],Rm);I((e,t,r)=>Tm(e,r),jI,(e,t)=>{if(!(e==null||t==null))return dc(dc({},e),{},{scale:t})});var wI=I([ue,su,ou],(e,t,r)=>{switch(e){case"horizontal":return t.some(n=>n.reversed)?"right-to-left":"left-to-right";case"vertical":return r.some(n=>n.reversed)?"bottom-to-top":"top-to-bottom";case"centric":case"radial":return"left-to-right";default:return}}),B2=e=>e.options.defaultTooltipEventType,F2=e=>e.options.validateTooltipEventTypes;function W2(e,t,r){if(e==null)return t;var n=e?"axis":"item";return r==null?t:r.includes(n)?n:t}function Um(e,t){var r=B2(e),n=F2(e);return W2(t,r,n)}function SI(e){return G(t=>Um(t,e))}var U2=(e,t)=>{var r,n=Number(t);if(!(yr(n)||t==null))return n>=0?e==null||(r=e[n])===null||r===void 0?void 0:r.value:void 0},NI=e=>e.tooltip.settings,jn={active:!1,index:null,dataKey:void 0,coordinate:void 0},kI={itemInteraction:{click:jn,hover:jn},axisInteraction:{click:jn,hover:jn},keyboardInteraction:jn,syncInteraction:{active:!1,index:null,dataKey:void 0,label:void 0,coordinate:void 0,sourceViewBox:void 0},tooltipItemPayloads:[],settings:{shared:void 0,trigger:"hover",axisId:0,active:!1,defaultIndex:void 0}},q2=At({name:"tooltip",initialState:kI,reducers:{addTooltipEntrySettings:{reducer(e,t){e.tooltipItemPayloads.push(t.payload)},prepare:Le()},removeTooltipEntrySettings:{reducer(e,t){var r=Kr(e).tooltipItemPayloads.indexOf(t.payload);r>-1&&e.tooltipItemPayloads.splice(r,1)},prepare:Le()},setTooltipSettingsState(e,t){e.settings=t.payload},setActiveMouseOverItemIndex(e,t){e.syncInteraction.active=!1,e.keyboardInteraction.active=!1,e.itemInteraction.hover.active=!0,e.itemInteraction.hover.index=t.payload.activeIndex,e.itemInteraction.hover.dataKey=t.payload.activeDataKey,e.itemInteraction.hover.coordinate=t.payload.activeCoordinate},mouseLeaveChart(e){e.itemInteraction.hover.active=!1,e.axisInteraction.hover.active=!1},mouseLeaveItem(e){e.itemInteraction.hover.active=!1},setActiveClickItemIndex(e,t){e.syncInteraction.active=!1,e.itemInteraction.click.active=!0,e.keyboardInteraction.active=!1,e.itemInteraction.click.index=t.payload.activeIndex,e.itemInteraction.click.dataKey=t.payload.activeDataKey,e.itemInteraction.click.coordinate=t.payload.activeCoordinate},setMouseOverAxisIndex(e,t){e.syncInteraction.active=!1,e.axisInteraction.hover.active=!0,e.keyboardInteraction.active=!1,e.axisInteraction.hover.index=t.payload.activeIndex,e.axisInteraction.hover.dataKey=t.payload.activeDataKey,e.axisInteraction.hover.coordinate=t.payload.activeCoordinate},setMouseClickAxisIndex(e,t){e.syncInteraction.active=!1,e.keyboardInteraction.active=!1,e.axisInteraction.click.active=!0,e.axisInteraction.click.index=t.payload.activeIndex,e.axisInteraction.click.dataKey=t.payload.activeDataKey,e.axisInteraction.click.coordinate=t.payload.activeCoordinate},setSyncInteraction(e,t){e.syncInteraction=t.payload},setKeyboardInteraction(e,t){e.keyboardInteraction.active=t.payload.active,e.keyboardInteraction.index=t.payload.activeIndex,e.keyboardInteraction.coordinate=t.payload.activeCoordinate,e.keyboardInteraction.dataKey=t.payload.activeDataKey}}}),{addTooltipEntrySettings:PI,removeTooltipEntrySettings:_I,setTooltipSettingsState:CI,setActiveMouseOverItemIndex:AI,mouseLeaveItem:A7,mouseLeaveChart:H2,setActiveClickItemIndex:O7,setMouseOverAxisIndex:K2,setMouseClickAxisIndex:OI,setSyncInteraction:up,setKeyboardInteraction:dp}=q2.actions,EI=q2.reducer;function ky(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ho(e){for(var t=1;t{if(t==null)return jn;var i=II(e,t,r);if(i==null)return jn;if(i.active)return i;if(e.keyboardInteraction.active)return e.keyboardInteraction;if(e.syncInteraction.active&&e.syncInteraction.index!=null)return e.syncInteraction;var s=e.settings.active===!0;if($I(i)){if(s)return Ho(Ho({},i),{},{active:!0})}else if(n!=null)return{active:!0,coordinate:void 0,dataKey:void 0,index:n};return Ho(Ho({},jn),{},{coordinate:i.coordinate})},qm=(e,t)=>{var r=e==null?void 0:e.index;if(r==null)return null;var n=Number(r);if(!_e(n))return r;var i=0,s=1/0;return t.length>0&&(s=t.length-1),String(Math.max(i,Math.min(n,s)))},Y2=(e,t,r,n,i,s,o,l)=>{if(!(s==null||l==null)){var c=o[0],d=c==null?void 0:l(c.positions,s);if(d!=null)return d;var u=i==null?void 0:i[Number(s)];if(u)switch(r){case"horizontal":return{x:u.coordinate,y:(n.top+t)/2};default:return{x:(n.left+e)/2,y:u.coordinate}}}},G2=(e,t,r,n)=>{if(t==="axis")return e.tooltipItemPayloads;if(e.tooltipItemPayloads.length===0)return[];var i;return r==="hover"?i=e.itemInteraction.hover.dataKey:i=e.itemInteraction.click.dataKey,i==null&&n!=null?[e.tooltipItemPayloads[0]]:e.tooltipItemPayloads.filter(s=>{var o;return((o=s.settings)===null||o===void 0?void 0:o.dataKey)===i})},uo=e=>e.options.tooltipPayloadSearcher,Da=e=>e.tooltip;function Py(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function _y(e){for(var t=1;t{if(!(t==null||s==null)){var{chartData:l,computedData:c,dataStartIndex:d,dataEndIndex:u}=r,f=[];return e.reduce((p,m)=>{var x,{dataDefinedOnItem:g,settings:b}=m,v=BI(g,l),j=Array.isArray(v)?Cw(v,d,u):v,y=(x=b==null?void 0:b.dataKey)!==null&&x!==void 0?x:n,w=b==null?void 0:b.nameKey,S;if(n&&Array.isArray(j)&&!Array.isArray(j[0])&&o==="axis"?S=Cj(j,n,i):S=s(j,t,c,w),Array.isArray(S))S.forEach(P=>{var _=_y(_y({},b),{},{name:P.name,unit:P.unit,color:void 0,fill:void 0});p.push(c0({tooltipEntrySettings:_,dataKey:P.dataKey,payload:P.payload,value:et(P.payload,P.dataKey),name:P.name}))});else{var N;p.push(c0({tooltipEntrySettings:b,dataKey:y,payload:S,value:et(S,y),name:(N=et(S,w))!==null&&N!==void 0?N:b==null?void 0:b.name}))}return p},f)}},Hm=I([Ue,ue,o2,km,We],O2),FI=I([e=>e.graphicalItems.cartesianItems,e=>e.graphicalItems.polarItems],(e,t)=>[...e,...t]),WI=I([We,Aa],l2),fo=I([FI,Ue,WI],c2,{memoizeOptions:{resultEqualityCheck:_u}}),UI=I([fo],e=>e.filter(Dm)),qI=I([fo],f2,{memoizeOptions:{resultEqualityCheck:_u}}),Ta=I([qI,Kn],p2),HI=I([UI,Kn,Ue],i2),Km=I([Ta,Ue,fo],h2),X2=I([Ue],$m),KI=I([Ue],e=>e.allowDataOverflow),J2=I([X2,KI],LS),VI=I([fo],e=>e.filter(Dm)),YI=I([HI,VI,wu],x2),GI=I([YI,Kn,We,J2],y2),ZI=I([fo],d2),XI=I([Ta,Ue,ZI,Lm,We],j2,{memoizeOptions:{resultEqualityCheck:Pu}}),JI=I([w2,We,Aa],Oa),QI=I([JI,We],k2),e$=I([S2,We,Aa],Oa),t$=I([e$,We],P2),r$=I([N2,We,Aa],Oa),n$=I([r$,We],_2),i$=I([QI,n$,t$],fc),a$=I([Ue,X2,J2,GI,XI,i$,ue,We],C2),Q2=I([Ue,ue,Ta,Km,wu,We,a$],A2),s$=I([Q2,Ue,Hm],E2),o$=I([Ue,Q2,s$,We],D2),eN=e=>{var t=We(e),r=Aa(e),n=!1;return co(e,t,r,n)},tN=I([Ue,eN],Su),rN=I([Ue,Hm,o$,tN],Rm),l$=I([ue,Km,Ue,We],L2),c$=I([ue,Km,Ue,We],z2),u$=(e,t,r,n,i,s,o,l)=>{if(t){var{type:c}=t,d=Mr(e,l);if(n){var u=r==="scaleBand"&&n.bandwidth?n.bandwidth()/2:2,f=c==="category"&&n.bandwidth?n.bandwidth()/u:0;return f=l==="angleAxis"&&i!=null&&(i==null?void 0:i.length)>=2?Jt(i[0]-i[1])*2*f:f,d&&o?o.map((p,m)=>({coordinate:n(p)+f,value:p,index:m,offset:f})):n.domain().map((p,m)=>({coordinate:n(p)+f,value:s?s[p]:p,index:m,offset:f}))}}},pn=I([ue,Ue,Hm,rN,eN,l$,c$,We],u$),Vm=I([B2,F2,NI],(e,t,r)=>W2(r.shared,e,t)),nN=e=>e.tooltip.settings.trigger,Ym=e=>e.tooltip.settings.defaultIndex,Tu=I([Da,Vm,nN,Ym],V2),Us=I([Tu,Ta],qm),iN=I([pn,Us],U2),d$=I([Tu],e=>{if(e)return e.dataKey}),aN=I([Da,Vm,nN,Ym],G2),f$=I([ln,cn,ue,rt,pn,Ym,aN,uo],Y2),p$=I([Tu,f$],(e,t)=>e!=null&&e.coordinate?e.coordinate:t),h$=I([Tu],e=>e.active),m$=I([aN,Us,Kn,g2,iN,uo,Vm],Z2),g$=I([m$],e=>{if(e!=null){var t=e.map(r=>r.payload).filter(r=>r!=null);return Array.from(new Set(t))}});function Cy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ay(e){for(var t=1;tG(Ue),j$=()=>{var e=b$(),t=G(pn),r=G(rN);return ha(!e||!r?void 0:Ay(Ay({},e),{},{scale:r}),t)};function Oy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ti(e){for(var t=1;t{var i=t.find(s=>s&&s.index===r);if(i){if(e==="horizontal")return{x:i.coordinate,y:n.chartY};if(e==="vertical")return{x:n.chartX,y:i.coordinate}}return{x:0,y:0}},P$=(e,t,r,n)=>{var i=t.find(d=>d&&d.index===r);if(i){if(e==="centric"){var s=i.coordinate,{radius:o}=n;return Ti(Ti(Ti({},n),Je(n.cx,n.cy,o,s)),{},{angle:s,radius:o})}var l=i.coordinate,{angle:c}=n;return Ti(Ti(Ti({},n),Je(n.cx,n.cy,l,c)),{},{angle:c,radius:l})}return{angle:0,clockWise:!1,cx:0,cy:0,endAngle:0,innerRadius:0,outerRadius:0,radius:0,startAngle:0,x:0,y:0}};function _$(e,t){var{chartX:r,chartY:n}=e;return r>=t.left&&r<=t.left+t.width&&n>=t.top&&n<=t.top+t.height}var sN=(e,t,r,n,i)=>{var s,o=-1,l=(s=t==null?void 0:t.length)!==null&&s!==void 0?s:0;if(l<=1||e==null)return 0;if(n==="angleAxis"&&i!=null&&Math.abs(Math.abs(i[1]-i[0])-360)<=1e-6)for(var c=0;c0?r[c-1].coordinate:r[l-1].coordinate,u=r[c].coordinate,f=c>=l-1?r[0].coordinate:r[c+1].coordinate,p=void 0;if(Jt(u-d)!==Jt(f-u)){var m=[];if(Jt(f-u)===Jt(i[1]-i[0])){p=f;var x=u+i[1]-i[0];m[0]=Math.min(x,(x+d)/2),m[1]=Math.max(x,(x+d)/2)}else{p=d;var g=f+i[1]-i[0];m[0]=Math.min(u,(g+u)/2),m[1]=Math.max(u,(g+u)/2)}var b=[Math.min(u,(p+u)/2),Math.max(u,(p+u)/2)];if(e>b[0]&&e<=b[1]||e>=m[0]&&e<=m[1]){({index:o}=r[c]);break}}else{var v=Math.min(d,f),j=Math.max(d,f);if(e>(v+u)/2&&e<=(j+u)/2){({index:o}=r[c]);break}}}else if(t){for(var y=0;y0&&y(t[y].coordinate+t[y-1].coordinate)/2&&e<=(t[y].coordinate+t[y+1].coordinate)/2||y===l-1&&e>(t[y].coordinate+t[y-1].coordinate)/2){({index:o}=t[y]);break}}return o},oN=()=>G(km),Gm=(e,t)=>t,lN=(e,t,r)=>r,Zm=(e,t,r,n)=>n,C$=I(pn,e=>Qc(e,t=>t.coordinate)),Xm=I([Da,Gm,lN,Zm],V2),cN=I([Xm,Ta],qm),A$=(e,t,r)=>{if(t!=null){var n=Da(e);return t==="axis"?r==="hover"?n.axisInteraction.hover.dataKey:n.axisInteraction.click.dataKey:r==="hover"?n.itemInteraction.hover.dataKey:n.itemInteraction.click.dataKey}},uN=I([Da,Gm,lN,Zm],G2),pc=I([ln,cn,ue,rt,pn,Zm,uN,uo],Y2),O$=I([Xm,pc],(e,t)=>{var r;return(r=e.coordinate)!==null&&r!==void 0?r:t}),dN=I([pn,cN],U2),E$=I([uN,cN,Kn,g2,dN,uo,Gm],Z2),D$=I([Xm],e=>({isActive:e.active,activeIndex:e.index})),T$=(e,t,r,n,i,s,o)=>{if(!(!e||!r||!n||!i)&&_$(e,o)){var l=DE(e,t),c=sN(l,s,i,r,n),d=k$(t,i,c,e);return{activeIndex:String(c),activeCoordinate:d}}},M$=(e,t,r,n,i,s,o)=>{if(!(!e||!n||!i||!s||!r)){var l=SD(e,r);if(l){var c=TE(l,t),d=sN(c,o,s,n,i),u=P$(t,s,d,l);return{activeIndex:String(d),activeCoordinate:u}}}},I$=(e,t,r,n,i,s,o,l)=>{if(!(!e||!t||!n||!i||!s))return t==="horizontal"||t==="vertical"?T$(e,t,n,i,s,o,l):M$(e,t,r,n,i,s,o)},$$=I(e=>e.zIndex.zIndexMap,(e,t)=>t,(e,t,r)=>r,(e,t,r)=>{if(t!=null){var n=e[t];if(n!=null)return r?n.panoramaElementId:n.elementId}}),L$=I(e=>e.zIndex.zIndexMap,e=>{var t=Object.keys(e).map(n=>parseInt(n,10)).concat(Object.values(lt)),r=Array.from(new Set(t));return r.sort((n,i)=>n-i)},{memoizeOptions:{resultEqualityCheck:SM}});function Ey(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Dy(e){for(var t=1;tDy(Dy({},e),{},{[t]:{elementId:void 0,panoramaElementId:void 0,consumers:0}}),F$)},U$=new Set(Object.values(lt));function q$(e){return U$.has(e)}var fN=At({name:"zIndex",initialState:W$,reducers:{registerZIndexPortal:{reducer:(e,t)=>{var{zIndex:r}=t.payload;e.zIndexMap[r]?e.zIndexMap[r].consumers+=1:e.zIndexMap[r]={consumers:1,elementId:void 0,panoramaElementId:void 0}},prepare:Le()},unregisterZIndexPortal:{reducer:(e,t)=>{var{zIndex:r}=t.payload;e.zIndexMap[r]&&(e.zIndexMap[r].consumers-=1,e.zIndexMap[r].consumers<=0&&!q$(r)&&delete e.zIndexMap[r])},prepare:Le()},registerZIndexPortalId:{reducer:(e,t)=>{var{zIndex:r,elementId:n,isPanorama:i}=t.payload;e.zIndexMap[r]?i?e.zIndexMap[r].panoramaElementId=n:e.zIndexMap[r].elementId=n:e.zIndexMap[r]={consumers:0,elementId:i?void 0:n,panoramaElementId:i?n:void 0}},prepare:Le()},unregisterZIndexPortalId:{reducer:(e,t)=>{var{zIndex:r}=t.payload;e.zIndexMap[r]&&(t.payload.isPanorama?e.zIndexMap[r].panoramaElementId=void 0:e.zIndexMap[r].elementId=void 0)},prepare:Le()}}}),{registerZIndexPortal:H$,unregisterZIndexPortal:K$,registerZIndexPortalId:V$,unregisterZIndexPortalId:Y$}=fN.actions,G$=fN.reducer;function Ir(e){var{zIndex:t,children:r}=e,n=o3(),i=n&&t!==void 0&&t!==0,s=pt(),o=Ve();h.useLayoutEffect(()=>i?(o(H$({zIndex:t})),()=>{o(K$({zIndex:t}))}):ka,[o,t,i]);var l=G(d=>$$(d,t,s));if(!i)return r;if(!l)return null;var c=document.getElementById(l);return c?bh.createPortal(r,c):null}function fp(){return fp=Object.assign?Object.assign.bind():function(e){for(var t=1;th.useContext(pN),hN={exports:{}};(function(e){var t=Object.prototype.hasOwnProperty,r="~";function n(){}Object.create&&(n.prototype=Object.create(null),new n().__proto__||(r=!1));function i(c,d,u){this.fn=c,this.context=d,this.once=u||!1}function s(c,d,u,f,p){if(typeof u!="function")throw new TypeError("The listener must be a function");var m=new i(u,f||c,p),x=r?r+d:d;return c._events[x]?c._events[x].fn?c._events[x]=[c._events[x],m]:c._events[x].push(m):(c._events[x]=m,c._eventsCount++),c}function o(c,d){--c._eventsCount===0?c._events=new n:delete c._events[d]}function l(){this._events=new n,this._eventsCount=0}l.prototype.eventNames=function(){var d=[],u,f;if(this._eventsCount===0)return d;for(f in u=this._events)t.call(u,f)&&d.push(r?f.slice(1):f);return Object.getOwnPropertySymbols?d.concat(Object.getOwnPropertySymbols(u)):d},l.prototype.listeners=function(d){var u=r?r+d:d,f=this._events[u];if(!f)return[];if(f.fn)return[f.fn];for(var p=0,m=f.length,x=new Array(m);p{e.eventEmitter==null&&(e.eventEmitter=Symbol("rechartsEventEmitter"))}}}),sL=gN.reducer,{createEventEmitter:oL}=gN.actions;function lL(e){return e.tooltip.syncInteraction}var cL={chartData:void 0,computedData:void 0,dataStartIndex:0,dataEndIndex:0},xN=At({name:"chartData",initialState:cL,reducers:{setChartData(e,t){if(e.chartData=t.payload,t.payload==null){e.dataStartIndex=0,e.dataEndIndex=0;return}t.payload.length>0&&e.dataEndIndex!==t.payload.length-1&&(e.dataEndIndex=t.payload.length-1)},setComputedData(e,t){e.computedData=t.payload},setDataStartEndIndexes(e,t){var{startIndex:r,endIndex:n}=t.payload;r!=null&&(e.dataStartIndex=r),n!=null&&(e.dataEndIndex=n)}}}),{setChartData:Iy,setDataStartEndIndexes:uL,setComputedData:E7}=xN.actions,dL=xN.reducer,fL=["x","y"];function $y(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Mi(e){for(var t=1;tc.rootProps.className);h.useEffect(()=>{if(e==null)return ka;var c=(d,u,f)=>{if(t!==f&&e===d){if(n==="index"){var p;if(o&&u!==null&&u!==void 0&&(p=u.payload)!==null&&p!==void 0&&p.coordinate&&u.payload.sourceViewBox){var m=u.payload.coordinate,{x,y:g}=m,b=gL(m,fL),{x:v,y:j,width:y,height:w}=u.payload.sourceViewBox,S=Mi(Mi({},b),{},{x:o.x+(y?(x-v)/y:0)*o.width,y:o.y+(w?(g-j)/w:0)*o.height});r(Mi(Mi({},u),{},{payload:Mi(Mi({},u.payload),{},{coordinate:S})}))}else r(u);return}if(i!=null){var N;if(typeof n=="function"){var P={activeTooltipIndex:u.payload.index==null?void 0:Number(u.payload.index),isTooltipActive:u.payload.active,activeIndex:u.payload.index==null?void 0:Number(u.payload.index),activeLabel:u.payload.label,activeDataKey:u.payload.dataKey,activeCoordinate:u.payload.coordinate},_=n(i,P);N=i[_]}else n==="value"&&(N=i.find(E=>String(E.value)===u.payload.label));var{coordinate:T}=u.payload;if(N==null||u.payload.active===!1||T==null||o==null){r(up({active:!1,coordinate:void 0,dataKey:void 0,index:null,label:void 0,sourceViewBox:void 0}));return}var{x:$,y:M}=T,C=Math.min($,o.x+o.width),R=Math.min(M,o.y+o.height),q={x:s==="horizontal"?N.coordinate:C,y:s==="horizontal"?R:N.coordinate},Z=up({active:u.payload.active,coordinate:q,dataKey:u.payload.dataKey,index:String(N.index),label:u.payload.label,sourceViewBox:u.payload.sourceViewBox});r(Z)}}};return qs.on(pp,c),()=>{qs.off(pp,c)}},[l,r,t,e,n,i,s,o])}function vL(){var e=G(Pm),t=G(_m),r=Ve();h.useEffect(()=>{if(e==null)return ka;var n=(i,s,o)=>{t!==o&&e===i&&r(uL(s))};return qs.on(My,n),()=>{qs.off(My,n)}},[r,t,e])}function bL(){var e=Ve();h.useEffect(()=>{e(oL())},[e]),yL(),vL()}function jL(e,t,r,n,i,s){var o=G(m=>A$(m,e,t)),l=G(_m),c=G(Pm),d=G(JS),u=G(lL),f=u==null?void 0:u.active,p=uu();h.useEffect(()=>{if(!f&&c!=null&&l!=null){var m=up({active:s,coordinate:r,dataKey:o,index:i,label:typeof n=="number"?String(n):n,sourceViewBox:p});qs.emit(pp,c,m,l)}},[f,r,o,i,n,l,c,d,s,p])}function Ly(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function zy(e){for(var t=1;t{P(CI({shared:j,trigger:y,axisId:N,active:i,defaultIndex:_}))},[P,j,y,N,i,_]);var T=uu(),$=qw(),M=SI(j),{activeIndex:C,isActive:R}=(t=G(J=>D$(J,M,y,_)))!==null&&t!==void 0?t:{},q=G(J=>E$(J,M,y,_)),Z=G(J=>dN(J,M,y,_)),E=G(J=>O$(J,M,y,_)),D=q,O=rL(),k=(r=i??R)!==null&&r!==void 0?r:!1,[L,U]=w4([D,k]),H=M==="axis"?Z:void 0;jL(M,y,E,H,C,k);var te=S??O;if(te==null||T==null||M==null)return null;var re=D??Ry;k||(re=Ry),d&&re.length&&(re=a4(re.filter(J=>J.value!=null&&(J.hide!==!0||n.includeHidden)),p,kL));var we=re.length>0,A=h.createElement(S3,{allowEscapeViewBox:s,animationDuration:o,animationEasing:l,isAnimationActive:u,active:k,coordinate:E,hasPayload:we,offset:f,position:m,reverseDirection:x,useTranslate3d:g,viewBox:T,wrapperStyle:b,lastBoundingBox:L,innerRef:U,hasPortalFromProps:!!S},PL(c,zy(zy({},n),{},{payload:re,label:H,active:k,activeIndex:C,coordinate:E,accessibilityLayer:$})));return h.createElement(h.Fragment,null,bh.createPortal(A,te),k&&h.createElement(tL,{cursor:v,tooltipEventType:M,coordinate:E,payload:re,index:C}))}function CL(e,t,r){return(t=AL(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function AL(e){var t=OL(e,"string");return typeof t=="symbol"?t:t+""}function OL(e,t){if(typeof e!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(typeof n!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}class EL{constructor(t){CL(this,"cache",new Map),this.maxSize=t}get(t){var r=this.cache.get(t);return r!==void 0&&(this.cache.delete(t),this.cache.set(t,r)),r}set(t,r){if(this.cache.has(t))this.cache.delete(t);else if(this.cache.size>=this.maxSize){var n=this.cache.keys().next().value;this.cache.delete(n)}this.cache.set(t,r)}clear(){this.cache.clear()}size(){return this.cache.size}}function Fy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function DL(e){for(var t=1;t{try{var r=document.getElementById(Uy);r||(r=document.createElement("span"),r.setAttribute("id",Uy),r.setAttribute("aria-hidden","true"),document.body.appendChild(r)),Object.assign(r.style,LL,t),r.textContent="".concat(e);var n=r.getBoundingClientRect();return{width:n.width,height:n.height}}catch{return{width:0,height:0}}},ds=function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(t==null||Ci.isSsr)return{width:0,height:0};if(!yN.enableCache)return qy(t,r);var n=zL(t,r),i=Wy.get(n);if(i)return i;var s=qy(t,r);return Wy.set(n,s),s},Hy=/(-?\d+(?:\.\d+)?[a-zA-Z%]*)([*/])(-?\d+(?:\.\d+)?[a-zA-Z%]*)/,Ky=/(-?\d+(?:\.\d+)?[a-zA-Z%]*)([+-])(-?\d+(?:\.\d+)?[a-zA-Z%]*)/,RL=/^px|cm|vh|vw|em|rem|%|mm|in|pt|pc|ex|ch|vmin|vmax|Q$/,BL=/(-?\d+(?:\.\d+)?)([a-zA-Z%]+)?/,vN={cm:96/2.54,mm:96/25.4,pt:96/72,pc:96/6,in:96,Q:96/(2.54*40),px:1},FL=Object.keys(vN),Gi="NaN";function WL(e,t){return e*vN[t]}class jt{static parse(t){var r,[,n,i]=(r=BL.exec(t))!==null&&r!==void 0?r:[];return new jt(parseFloat(n),i??"")}constructor(t,r){this.num=t,this.unit=r,this.num=t,this.unit=r,yr(t)&&(this.unit=""),r!==""&&!RL.test(r)&&(this.num=NaN,this.unit=""),FL.includes(r)&&(this.num=WL(t,r),this.unit="px")}add(t){return this.unit!==t.unit?new jt(NaN,""):new jt(this.num+t.num,this.unit)}subtract(t){return this.unit!==t.unit?new jt(NaN,""):new jt(this.num-t.num,this.unit)}multiply(t){return this.unit!==""&&t.unit!==""&&this.unit!==t.unit?new jt(NaN,""):new jt(this.num*t.num,this.unit||t.unit)}divide(t){return this.unit!==""&&t.unit!==""&&this.unit!==t.unit?new jt(NaN,""):new jt(this.num/t.num,this.unit||t.unit)}toString(){return"".concat(this.num).concat(this.unit)}isNaN(){return yr(this.num)}}function bN(e){if(e.includes(Gi))return Gi;for(var t=e;t.includes("*")||t.includes("/");){var r,[,n,i,s]=(r=Hy.exec(t))!==null&&r!==void 0?r:[],o=jt.parse(n??""),l=jt.parse(s??""),c=i==="*"?o.multiply(l):o.divide(l);if(c.isNaN())return Gi;t=t.replace(Hy,c.toString())}for(;t.includes("+")||/.-\d+(?:\.\d+)?/.test(t);){var d,[,u,f,p]=(d=Ky.exec(t))!==null&&d!==void 0?d:[],m=jt.parse(u??""),x=jt.parse(p??""),g=f==="+"?m.add(x):m.subtract(x);if(g.isNaN())return Gi;t=t.replace(Ky,g.toString())}return t}var Vy=/\(([^()]*)\)/;function UL(e){for(var t=e,r;(r=Vy.exec(t))!=null;){var[,n]=r;t=t.replace(Vy,bN(n))}return t}function qL(e){var t=e.replace(/\s+/g,"");return t=UL(t),t=bN(t),t}function HL(e){try{return qL(e)}catch{return Gi}}function Nd(e){var t=HL(e.slice(5,-1));return t===Gi?"":t}var KL=["x","y","lineHeight","capHeight","fill","scaleToFit","textAnchor","verticalAnchor"],VL=["dx","dy","angle","className","breakAll"];function hp(){return hp=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{children:t,breakAll:r,style:n}=e;try{var i=[];Re(t)||(r?i=t.toString().split(""):i=t.toString().split(jN));var s=i.map(l=>({word:l,width:ds(l,n).width})),o=r?0:ds(" ",n).width;return{wordsWithComputedWidth:s,spaceWidth:o}}catch{return null}};function GL(e){return e==="start"||e==="middle"||e==="end"||e==="inherit"}var SN=(e,t,r,n)=>e.reduce((i,s)=>{var{word:o,width:l}=s,c=i[i.length-1];if(c&&l!=null&&(t==null||n||c.width+l+re.reduce((t,r)=>t.width>r.width?t:r),ZL="…",Gy=(e,t,r,n,i,s,o,l)=>{var c=e.slice(0,t),d=wN({breakAll:r,style:n,children:c+ZL});if(!d)return[!1,[]];var u=SN(d.wordsWithComputedWidth,s,o,l),f=u.length>i||NN(u).width>Number(s);return[f,u]},XL=(e,t,r,n,i)=>{var{maxLines:s,children:o,style:l,breakAll:c}=e,d=Y(s),u=String(o),f=SN(t,n,r,i);if(!d||i)return f;var p=f.length>s||NN(f).width>Number(n);if(!p)return f;for(var m=0,x=u.length-1,g=0,b;m<=x&&g<=u.length-1;){var v=Math.floor((m+x)/2),j=v-1,[y,w]=Gy(u,j,c,l,s,n,r,i),[S]=Gy(u,v,c,l,s,n,r,i);if(!y&&!S&&(m=v+1),y&&S&&(x=v-1),!y&&S){b=w;break}g++}return b||f},Zy=e=>{var t=Re(e)?[]:e.toString().split(jN);return[{words:t,width:void 0}]},JL=e=>{var{width:t,scaleToFit:r,children:n,style:i,breakAll:s,maxLines:o}=e;if((t||r)&&!Ci.isSsr){var l,c,d=wN({breakAll:s,children:n,style:i});if(d){var{wordsWithComputedWidth:u,spaceWidth:f}=d;l=u,c=f}else return Zy(n);return XL({breakAll:s,children:n,maxLines:o,style:i},l,c,t,!!r)}return Zy(n)},kN="#808080",QL={breakAll:!1,capHeight:"0.71em",fill:kN,lineHeight:"1em",scaleToFit:!1,textAnchor:"start",verticalAnchor:"end",x:0,y:0},Jm=h.forwardRef((e,t)=>{var r=ft(e,QL),{x:n,y:i,lineHeight:s,capHeight:o,fill:l,scaleToFit:c,textAnchor:d,verticalAnchor:u}=r,f=Yy(r,KL),p=h.useMemo(()=>JL({breakAll:f.breakAll,children:f.children,maxLines:f.maxLines,scaleToFit:c,style:f.style,width:f.width}),[f.breakAll,f.children,f.maxLines,c,f.style,f.width]),{dx:m,dy:x,angle:g,className:b,breakAll:v}=f,j=Yy(f,VL);if(!Or(n)||!Or(i)||p.length===0)return null;var y=Number(n)+(Y(m)?m:0),w=Number(i)+(Y(x)?x:0);if(!_e(y)||!_e(w))return null;var S;switch(u){case"start":S=Nd("calc(".concat(o,")"));break;case"middle":S=Nd("calc(".concat((p.length-1)/2," * -").concat(s," + (").concat(o," / 2))"));break;default:S=Nd("calc(".concat(p.length-1," * -").concat(s,")"));break}var N=[];if(c){var P=p[0].width,{width:_}=f;N.push("scale(".concat(Y(_)&&Y(P)?_/P:1,")"))}return g&&N.push("rotate(".concat(g,", ").concat(y,", ").concat(w,")")),N.length&&(j.transform=N.join(" ")),h.createElement("text",hp({},ut(j),{ref:t,x:y,y:w,className:ce("recharts-text",b),textAnchor:d,fill:l.includes("url")?kN:l}),p.map((T,$)=>{var M=T.words.join(v?"":" ");return h.createElement("tspan",{x:y,dy:$===0?S:s,key:"".concat(M,"-").concat($)},M)}))});Jm.displayName="Text";var e8=["labelRef"];function t8(e,t){if(e==null)return{};var r,n,i=r8(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var{x:t,y:r,upperWidth:n,lowerWidth:i,width:s,height:o,children:l}=e,c=h.useMemo(()=>({x:t,y:r,upperWidth:n,lowerWidth:i,width:s,height:o}),[t,r,n,i,s,o]);return h.createElement(PN.Provider,{value:c},l)},_N=()=>{var e=h.useContext(PN),t=uu();return e||Rw(t)},o8=h.createContext(null),l8=()=>{var e=h.useContext(o8),t=G(n2);return e||t},c8=e=>{var{value:t,formatter:r}=e,n=Re(e.children)?t:e.children;return typeof r=="function"?r(n):n},Qm=e=>e!=null&&typeof e=="function",u8=(e,t)=>{var r=Jt(t-e),n=Math.min(Math.abs(t-e),360);return r*n},d8=(e,t,r,n,i)=>{var{offset:s,className:o}=e,{cx:l,cy:c,innerRadius:d,outerRadius:u,startAngle:f,endAngle:p,clockWise:m}=i,x=(d+u)/2,g=u8(f,p),b=g>=0?1:-1,v,j;switch(t){case"insideStart":v=f+b*s,j=m;break;case"insideEnd":v=p-b*s,j=!m;break;case"end":v=p+b*s,j=m;break;default:throw new Error("Unsupported position ".concat(t))}j=g<=0?j:!j;var y=Je(l,c,x,v),w=Je(l,c,x,v+(j?1:-1)*359),S="M".concat(y.x,",").concat(y.y,` + A`).concat(x,",").concat(x,",0,1,").concat(j?0:1,`, + `).concat(w.x,",").concat(w.y),N=Re(e.id)?Ts("recharts-radial-line-"):e.id;return h.createElement("text",Rr({},n,{dominantBaseline:"central",className:ce("recharts-radial-bar-label",o)}),h.createElement("defs",null,h.createElement("path",{id:N,d:S})),h.createElement("textPath",{xlinkHref:"#".concat(N)},r))},f8=(e,t,r)=>{var{cx:n,cy:i,innerRadius:s,outerRadius:o,startAngle:l,endAngle:c}=e,d=(l+c)/2;if(r==="outside"){var{x:u,y:f}=Je(n,i,o+t,d);return{x:u,y:f,textAnchor:u>=n?"start":"end",verticalAnchor:"middle"}}if(r==="center")return{x:n,y:i,textAnchor:"middle",verticalAnchor:"middle"};if(r==="centerTop")return{x:n,y:i,textAnchor:"middle",verticalAnchor:"start"};if(r==="centerBottom")return{x:n,y:i,textAnchor:"middle",verticalAnchor:"end"};var p=(s+o)/2,{x:m,y:x}=Je(n,i,p,d);return{x:m,y:x,textAnchor:"middle",verticalAnchor:"middle"}},mp=e=>"cx"in e&&Y(e.cx),p8=(e,t)=>{var{parentViewBox:r,offset:n,position:i}=e,s;r!=null&&!mp(r)&&(s=r);var{x:o,y:l,upperWidth:c,lowerWidth:d,height:u}=t,f=o,p=o+(c-d)/2,m=(f+p)/2,x=(c+d)/2,g=f+c/2,b=u>=0?1:-1,v=b*n,j=b>0?"end":"start",y=b>0?"start":"end",w=c>=0?1:-1,S=w*n,N=w>0?"end":"start",P=w>0?"start":"end";if(i==="top"){var _={x:f+c/2,y:l-v,textAnchor:"middle",verticalAnchor:j};return Ce(Ce({},_),s?{height:Math.max(l-s.y,0),width:c}:{})}if(i==="bottom"){var T={x:p+d/2,y:l+u+v,textAnchor:"middle",verticalAnchor:y};return Ce(Ce({},T),s?{height:Math.max(s.y+s.height-(l+u),0),width:d}:{})}if(i==="left"){var $={x:m-S,y:l+u/2,textAnchor:N,verticalAnchor:"middle"};return Ce(Ce({},$),s?{width:Math.max($.x-s.x,0),height:u}:{})}if(i==="right"){var M={x:m+x+S,y:l+u/2,textAnchor:P,verticalAnchor:"middle"};return Ce(Ce({},M),s?{width:Math.max(s.x+s.width-M.x,0),height:u}:{})}var C=s?{width:x,height:u}:{};return i==="insideLeft"?Ce({x:m+S,y:l+u/2,textAnchor:P,verticalAnchor:"middle"},C):i==="insideRight"?Ce({x:m+x-S,y:l+u/2,textAnchor:N,verticalAnchor:"middle"},C):i==="insideTop"?Ce({x:f+c/2,y:l+v,textAnchor:"middle",verticalAnchor:y},C):i==="insideBottom"?Ce({x:p+d/2,y:l+u-v,textAnchor:"middle",verticalAnchor:j},C):i==="insideTopLeft"?Ce({x:f+S,y:l+v,textAnchor:P,verticalAnchor:y},C):i==="insideTopRight"?Ce({x:f+c-S,y:l+v,textAnchor:N,verticalAnchor:y},C):i==="insideBottomLeft"?Ce({x:p+S,y:l+u-v,textAnchor:P,verticalAnchor:j},C):i==="insideBottomRight"?Ce({x:p+d-S,y:l+u-v,textAnchor:N,verticalAnchor:j},C):i&&typeof i=="object"&&(Y(i.x)||Qr(i.x))&&(Y(i.y)||Qr(i.y))?Ce({x:o+Rn(i.x,x),y:l+Rn(i.y,u),textAnchor:"end",verticalAnchor:"end"},C):Ce({x:g,y:l+u/2,textAnchor:"middle",verticalAnchor:"middle"},C)},h8={offset:5,zIndex:lt.label};function yn(e){var t=ft(e,h8),{viewBox:r,position:n,value:i,children:s,content:o,className:l="",textBreakAll:c,labelRef:d}=t,u=l8(),f=_N(),p=n==="center"?f:u??f,m,x,g;if(r==null?m=p:mp(r)?m=r:m=Rw(r),!m||Re(i)&&Re(s)&&!h.isValidElement(o)&&typeof o!="function")return null;var b=Ce(Ce({},t),{},{viewBox:m});if(h.isValidElement(o)){var{labelRef:v}=b,j=t8(b,e8);return h.cloneElement(o,j)}if(typeof o=="function"){if(x=h.createElement(o,b),h.isValidElement(x))return x}else x=c8(t);var y=ut(t);if(mp(m)){if(n==="insideStart"||n==="insideEnd"||n==="end")return d8(t,n,x,y,m);g=f8(m,t.offset,t.position)}else g=p8(t,m);return h.createElement(Ir,{zIndex:t.zIndex},h.createElement(Jm,Rr({ref:d,className:ce("recharts-label",l)},y,g,{textAnchor:GL(y.textAnchor)?y.textAnchor:g.textAnchor,breakAll:c}),x))}yn.displayName="Label";var m8=(e,t,r)=>{if(!e)return null;var n={viewBox:t,labelRef:r};return e===!0?h.createElement(yn,Rr({key:"label-implicit"},n)):Or(e)?h.createElement(yn,Rr({key:"label-implicit",value:e},n)):h.isValidElement(e)?e.type===yn?h.cloneElement(e,Ce({key:"label-implicit"},n)):h.createElement(yn,Rr({key:"label-implicit",content:e},n)):Qm(e)?h.createElement(yn,Rr({key:"label-implicit",content:e},n)):e&&typeof e=="object"?h.createElement(yn,Rr({},e,{key:"label-implicit"},n)):null};function g8(e){var{label:t,labelRef:r}=e,n=_N();return m8(t,n,r)||null}var CN={},AN={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return r[r.length-1]}e.last=t})(AN);var ON={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){return Array.isArray(r)?r:Array.from(r)}e.toArray=t})(ON);(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});const t=AN,r=ON,n=Jc;function i(s){if(n.isArrayLike(s))return t.last(r.toArray(s))}e.last=i})(CN);var x8=CN.last;const y8=Tr(x8);var v8=["valueAccessor"],b8=["dataKey","clockWise","id","textBreakAll","zIndex"];function hc(){return hc=Object.assign?Object.assign.bind():function(e){for(var t=1;tArray.isArray(e.value)?y8(e.value):e.value,EN=h.createContext(void 0),DN=EN.Provider,TN=h.createContext(void 0);TN.Provider;function S8(){return h.useContext(EN)}function N8(){return h.useContext(TN)}function cl(e){var{valueAccessor:t=w8}=e,r=Jy(e,v8),{dataKey:n,clockWise:i,id:s,textBreakAll:o,zIndex:l}=r,c=Jy(r,b8),d=S8(),u=N8(),f=d||u;return!f||!f.length?null:h.createElement(Ir,{zIndex:l??lt.label},h.createElement(ir,{className:"recharts-label-list"},f.map((p,m)=>{var x,g=Re(n)?t(p,m):et(p&&p.payload,n),b=Re(s)?{}:{id:"".concat(s,"-").concat(m)};return h.createElement(yn,hc({key:"label-".concat(m)},ut(p),c,b,{fill:(x=r.fill)!==null&&x!==void 0?x:p.fill,parentViewBox:p.parentViewBox,value:g,textBreakAll:o,viewBox:p.viewBox,index:m,zIndex:0}))})))}cl.displayName="LabelList";function MN(e){var{label:t}=e;return t?t===!0?h.createElement(cl,{key:"labelList-implicit"}):h.isValidElement(t)||Qm(t)?h.createElement(cl,{key:"labelList-implicit",content:t}):typeof t=="object"?h.createElement(cl,hc({key:"labelList-implicit"},t,{type:String(t.type)})):null:null}function gp(){return gp=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{cx:t,cy:r,r:n,className:i}=e,s=ce("recharts-dot",i);return Y(t)&&Y(r)&&Y(n)?h.createElement("circle",gp({},nr(e),Ih(e),{className:s,cx:t,cy:r,r:n})):null},k8={radiusAxis:{},angleAxis:{}},$N=At({name:"polarAxis",initialState:k8,reducers:{addRadiusAxis(e,t){e.radiusAxis[t.payload.id]=t.payload},removeRadiusAxis(e,t){delete e.radiusAxis[t.payload.id]},addAngleAxis(e,t){e.angleAxis[t.payload.id]=t.payload},removeAngleAxis(e,t){delete e.angleAxis[t.payload.id]}}}),{addRadiusAxis:D7,removeRadiusAxis:T7,addAngleAxis:M7,removeAngleAxis:I7}=$N.actions,P8=$N.reducer,eg=e=>e&&typeof e=="object"&&"clipDot"in e?!!e.clipDot:!0,LN={};(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});function t(r){var i;if(typeof r!="object"||r==null)return!1;if(Object.getPrototypeOf(r)===null)return!0;if(Object.prototype.toString.call(r)!=="[object Object]"){const s=r[Symbol.toStringTag];return s==null||!((i=Object.getOwnPropertyDescriptor(r,Symbol.toStringTag))!=null&&i.writable)?!1:r.toString()===`[object ${s}]`}let n=r;for(;Object.getPrototypeOf(n)!==null;)n=Object.getPrototypeOf(n);return Object.getPrototypeOf(r)===n}e.isPlainObject=t})(LN);var _8=LN.isPlainObject;const C8=Tr(_8);function Qy(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function ev(e){for(var t=1;t{var s=r-n,o;return o="M ".concat(e,",").concat(t),o+="L ".concat(e+r,",").concat(t),o+="L ".concat(e+r-s/2,",").concat(t+i),o+="L ".concat(e+r-s/2-n,",").concat(t+i),o+="L ".concat(e,",").concat(t," Z"),o},D8={x:0,y:0,upperWidth:0,lowerWidth:0,height:0,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},T8=e=>{var t=ft(e,D8),{x:r,y:n,upperWidth:i,lowerWidth:s,height:o,className:l}=t,{animationEasing:c,animationDuration:d,animationBegin:u,isUpdateAnimationActive:f}=t,p=h.useRef(null),[m,x]=h.useState(-1),g=h.useRef(i),b=h.useRef(s),v=h.useRef(o),j=h.useRef(r),y=h.useRef(n),w=pu(e,"trapezoid-");if(h.useEffect(()=>{if(p.current&&p.current.getTotalLength)try{var q=p.current.getTotalLength();q&&x(q)}catch{}},[]),r!==+r||n!==+n||i!==+i||s!==+s||o!==+o||i===0&&s===0||o===0)return null;var S=ce("recharts-trapezoid",l);if(!f)return h.createElement("g",null,h.createElement("path",mc({},ut(t),{className:S,d:tv(r,n,i,s,o)})));var N=g.current,P=b.current,_=v.current,T=j.current,$=y.current,M="0px ".concat(m===-1?1:m,"px"),C="".concat(m,"px 0px"),R=Hw(["strokeDasharray"],d,c);return h.createElement(fu,{animationId:w,key:w,canBegin:m>0,duration:d,easing:c,isActive:f,begin:u},q=>{var Z=De(N,i,q),E=De(P,s,q),D=De(_,o,q),O=De(T,r,q),k=De($,n,q);p.current&&(g.current=Z,b.current=E,v.current=D,j.current=O,y.current=k);var L=q>0?{transition:R,strokeDasharray:C}:{strokeDasharray:M};return h.createElement("path",mc({},ut(t),{className:S,d:tv(O,k,Z,E,D),ref:p,style:ev(ev({},L),t.style)}))})},M8=["option","shapeType","propTransformer","activeClassName","isActive"];function I8(e,t){if(e==null)return{};var r,n,i=$8(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{if(!i){var s=t(r);return n(PI(s)),()=>{n(_I(s))}}},[t,r,n,i]),null}function RN(e){var{legendPayload:t}=e,r=Ve(),n=pt();return h.useLayoutEffect(()=>n?ka:(r(c3(t)),()=>{r(u3(t))}),[r,n,t]),null}var kd,q8=()=>{var[e]=h.useState(()=>Ts("uid-"));return e},H8=(kd=Tv.useId)!==null&&kd!==void 0?kd:q8;function BN(e,t){var r=H8();return t||(e?"".concat(e,"-").concat(r):r)}var K8=h.createContext(void 0),FN=e=>{var{id:t,type:r,children:n}=e,i=BN("recharts-".concat(r),t);return h.createElement(K8.Provider,{value:i},n(i))},V8={cartesianItems:[],polarItems:[]},WN=At({name:"graphicalItems",initialState:V8,reducers:{addCartesianGraphicalItem:{reducer(e,t){e.cartesianItems.push(t.payload)},prepare:Le()},replaceCartesianGraphicalItem:{reducer(e,t){var{prev:r,next:n}=t.payload,i=Kr(e).cartesianItems.indexOf(r);i>-1&&(e.cartesianItems[i]=n)},prepare:Le()},removeCartesianGraphicalItem:{reducer(e,t){var r=Kr(e).cartesianItems.indexOf(t.payload);r>-1&&e.cartesianItems.splice(r,1)},prepare:Le()},addPolarGraphicalItem:{reducer(e,t){e.polarItems.push(t.payload)},prepare:Le()},removePolarGraphicalItem:{reducer(e,t){var r=Kr(e).polarItems.indexOf(t.payload);r>-1&&e.polarItems.splice(r,1)},prepare:Le()}}}),{addCartesianGraphicalItem:Y8,replaceCartesianGraphicalItem:G8,removeCartesianGraphicalItem:Z8,addPolarGraphicalItem:$7,removePolarGraphicalItem:L7}=WN.actions,X8=WN.reducer;function UN(e){var t=Ve(),r=h.useRef(null);return h.useLayoutEffect(()=>{r.current===null?t(Y8(e)):r.current!==e&&t(G8({prev:r.current,next:e})),r.current=e},[t,e]),h.useLayoutEffect(()=>()=>{r.current&&(t(Z8(r.current)),r.current=null)},[t]),null}var J8=["points"];function iv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Pd(e){for(var t=1;t{var b,v,j=Pd(Pd(Pd({r:3},o),f),{},{index:g,cx:(b=x.x)!==null&&b!==void 0?b:void 0,cy:(v=x.y)!==null&&v!==void 0?v:void 0,dataKey:s,value:x.value,payload:x.payload,points:t});return h.createElement(iz,{key:"dot-".concat(g),option:r,dotProps:j,className:i})}),m={};return l&&c!=null&&(m.clipPath="url(#clipPath-".concat(u?"":"dots-").concat(c,")")),h.createElement(Ir,{zIndex:d},h.createElement(ir,xc({className:n},m),p))}function av(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function sv(e){for(var t=1;t({top:e.top,bottom:e.bottom,left:e.left,right:e.right})),xz=I([gz,ln,cn],(e,t,r)=>{if(!(!e||t==null||r==null))return{x:e.left,y:e.top,width:Math.max(0,t-e.left-e.right),height:Math.max(0,r-e.top-e.bottom)}}),Mu=()=>G(xz),yz=()=>G(g$);function ov(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function _d(e){for(var t=1;t{var{point:t,childIndex:r,mainColor:n,activeDot:i,dataKey:s}=e;if(i===!1||t.x==null||t.y==null)return null;var o={index:r,dataKey:s,cx:t.x,cy:t.y,r:4,fill:n??"none",strokeWidth:2,stroke:"#fff",payload:t.payload,value:t.value},l=_d(_d(_d({},o),qc(i)),Ih(i)),c;return h.isValidElement(i)?c=h.cloneElement(i,l):typeof i=="function"?c=i(l):c=h.createElement(IN,l),h.createElement(ir,{className:"recharts-active-dot"},c)};function xp(e){var{points:t,mainColor:r,activeDot:n,itemDataKey:i,zIndex:s=lt.activeDot}=e,o=G(Us),l=yz();if(t==null||l==null)return null;var c=t.find(d=>l.includes(d.payload));return Re(c)?null:h.createElement(Ir,{zIndex:s},h.createElement(wz,{point:c,childIndex:Number(o),mainColor:r,dataKey:i,activeDot:n}))}var Sz={},KN=At({name:"errorBars",initialState:Sz,reducers:{addErrorBar:(e,t)=>{var{itemId:r,errorBar:n}=t.payload;e[r]||(e[r]=[]),e[r].push(n)},replaceErrorBar:(e,t)=>{var{itemId:r,prev:n,next:i}=t.payload;e[r]&&(e[r]=e[r].map(s=>s.dataKey===n.dataKey&&s.direction===n.direction?i:s))},removeErrorBar:(e,t)=>{var{itemId:r,errorBar:n}=t.payload;e[r]&&(e[r]=e[r].filter(i=>i.dataKey!==n.dataKey||i.direction!==n.direction))}}}),{addErrorBar:B7,replaceErrorBar:F7,removeErrorBar:W7}=KN.actions,Nz=KN.reducer,kz=["children"];function Pz(e,t){if(e==null)return{};var r,n,i=_z(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n({x:0,y:0,value:0}),errorBarOffset:0},Az=h.createContext(Cz);function Oz(e){var{children:t}=e,r=Pz(e,kz);return h.createElement(Az.Provider,{value:r},t)}function tg(e,t){var r,n,i=G(d=>dn(d,e)),s=G(d=>fn(d,t)),o=(r=i==null?void 0:i.allowDataOverflow)!==null&&r!==void 0?r:Tt.allowDataOverflow,l=(n=s==null?void 0:s.allowDataOverflow)!==null&&n!==void 0?n:Mt.allowDataOverflow,c=o||l;return{needClip:c,needClipX:o,needClipY:l}}function VN(e){var{xAxisId:t,yAxisId:r,clipPathId:n}=e,i=Mu(),{needClipX:s,needClipY:o,needClip:l}=tg(t,r);if(!l||!i)return null;var{x:c,y:d,width:u,height:f}=i;return h.createElement("clipPath",{id:"clipPath-".concat(n)},h.createElement("rect",{x:s?c:c-u/2,y:o?d:d-f/2,width:s?u:u*2,height:o?f:f*2}))}var Ez=e=>{var{chartData:t}=e,r=Ve(),n=pt();return h.useEffect(()=>n?()=>{}:(r(Iy(t)),()=>{r(Iy(void 0))}),[t,r,n]),null},lv={x:0,y:0,width:0,height:0,padding:{top:0,right:0,bottom:0,left:0}},YN=At({name:"brush",initialState:lv,reducers:{setBrushSettings(e,t){return t.payload==null?lv:t.payload}}}),{setBrushSettings:U7}=YN.actions,Dz=YN.reducer;function Tz(e,t,r){return(t=Mz(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function Mz(e){var t=Iz(e,"string");return typeof t=="symbol"?t:t+""}function Iz(e,t){if(typeof e!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(typeof n!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}class rg{static create(t){return new rg(t)}constructor(t){this.scale=t}get domain(){return this.scale.domain}get range(){return this.scale.range}get rangeMin(){return this.range()[0]}get rangeMax(){return this.range()[1]}get bandwidth(){return this.scale.bandwidth}apply(t){var{bandAware:r,position:n}=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(t!==void 0){if(n)switch(n){case"start":return this.scale(t);case"middle":{var i=this.bandwidth?this.bandwidth()/2:0;return this.scale(t)+i}case"end":{var s=this.bandwidth?this.bandwidth():0;return this.scale(t)+s}default:return this.scale(t)}if(r){var o=this.bandwidth?this.bandwidth()/2:0;return this.scale(t)+o}return this.scale(t)}}isInRange(t){var r=this.range(),n=r[0],i=r[r.length-1];return n<=i?t>=n&&t<=i:t>=i&&t<=n}}Tz(rg,"EPS",1e-4);function $z(e){return(e%180+180)%180}var Lz=function(t){var{width:r,height:n}=t,i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,s=$z(i),o=s*Math.PI/180,l=Math.atan(n/r),c=o>l&&o{e.dots.push(t.payload)},removeDot:(e,t)=>{var r=Kr(e).dots.findIndex(n=>n===t.payload);r!==-1&&e.dots.splice(r,1)},addArea:(e,t)=>{e.areas.push(t.payload)},removeArea:(e,t)=>{var r=Kr(e).areas.findIndex(n=>n===t.payload);r!==-1&&e.areas.splice(r,1)},addLine:(e,t)=>{e.lines.push(t.payload)},removeLine:(e,t)=>{var r=Kr(e).lines.findIndex(n=>n===t.payload);r!==-1&&e.lines.splice(r,1)}}}),{addDot:q7,removeDot:H7,addArea:K7,removeArea:V7,addLine:Y7,removeLine:G7}=GN.actions,Rz=GN.reducer,Bz=h.createContext(void 0),Fz=e=>{var{children:t}=e,[r]=h.useState("".concat(Ts("recharts"),"-clip")),n=Mu();if(n==null)return null;var{x:i,y:s,width:o,height:l}=n;return h.createElement(Bz.Provider,{value:r},h.createElement("defs",null,h.createElement("clipPath",{id:r},h.createElement("rect",{x:i,y:s,height:l,width:o}))),t)};function ya(e,t){for(var r in e)if({}.hasOwnProperty.call(e,r)&&(!{}.hasOwnProperty.call(t,r)||e[r]!==t[r]))return!1;for(var n in t)if({}.hasOwnProperty.call(t,n)&&!{}.hasOwnProperty.call(e,n))return!1;return!0}function ZN(e,t){if(t<1)return[];if(t===1)return e;for(var r=[],n=0;ne*i)return!1;var s=r();return e*(t-e*s/2-n)>=0&&e*(t+e*s/2-i)<=0}function qz(e,t){return ZN(e,t+1)}function Hz(e,t,r,n,i){for(var s=(n||[]).slice(),{start:o,end:l}=t,c=0,d=1,u=o,f=function(){var x=n==null?void 0:n[c];if(x===void 0)return{v:ZN(n,d)};var g=c,b,v=()=>(b===void 0&&(b=r(x,g)),b),j=x.coordinate,y=c===0||yc(e,j,v,u,l);y||(c=0,u=o,d+=1),y&&(u=j+e*(v()/2+i),c+=d)},p;d<=s.length;)if(p=f(),p)return p.v;return[]}function cv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function at(e){for(var t=1;t(x===void 0&&(x=r(m,p)),x);if(p===o-1){var b=e*(m.coordinate+e*g()/2-c);s[p]=m=at(at({},m),{},{tickCoord:b>0?m.coordinate-b*e:m.coordinate})}else s[p]=m=at(at({},m),{},{tickCoord:m.coordinate});if(m.tickCoord!=null){var v=yc(e,m.tickCoord,g,l,c);v&&(c=m.tickCoord-e*(g()/2+i),s[p]=at(at({},m),{},{isShow:!0}))}},u=o-1;u>=0;u--)d(u);return s}function Zz(e,t,r,n,i,s){var o=(n||[]).slice(),l=o.length,{start:c,end:d}=t;if(s){var u=n[l-1],f=r(u,l-1),p=e*(u.coordinate+e*f/2-d);if(o[l-1]=u=at(at({},u),{},{tickCoord:p>0?u.coordinate-p*e:u.coordinate}),u.tickCoord!=null){var m=yc(e,u.tickCoord,()=>f,c,d);m&&(d=u.tickCoord-e*(f/2+i),o[l-1]=at(at({},u),{},{isShow:!0}))}}for(var x=s?l-1:l,g=function(j){var y=o[j],w,S=()=>(w===void 0&&(w=r(y,j)),w);if(j===0){var N=e*(y.coordinate-e*S()/2-c);o[j]=y=at(at({},y),{},{tickCoord:N<0?y.coordinate-N*e:y.coordinate})}else o[j]=y=at(at({},y),{},{tickCoord:y.coordinate});if(y.tickCoord!=null){var P=yc(e,y.tickCoord,S,c,d);P&&(c=y.tickCoord+e*(S()/2+i),o[j]=at(at({},y),{},{isShow:!0}))}},b=0;b{var S=typeof d=="function"?d(y.value,w):y.value;return x==="width"?Wz(ds(S,{fontSize:t,letterSpacing:r}),g,f):ds(S,{fontSize:t,letterSpacing:r})[x]},v=i.length>=2?Jt(i[1].coordinate-i[0].coordinate):1,j=Uz(s,v,x);return c==="equidistantPreserveStart"?Hz(v,j,b,i,o):(c==="preserveStart"||c==="preserveStartEnd"?m=Zz(v,j,b,i,o,c==="preserveStartEnd"):m=Gz(v,j,b,i,o),m.filter(y=>y.isShow))}var Xz=e=>{var{ticks:t,label:r,labelGapWithTick:n=5,tickSize:i=0,tickMargin:s=0}=e,o=0;if(t){Array.from(t).forEach(u=>{if(u){var f=u.getBoundingClientRect();f.width>o&&(o=f.width)}});var l=r?r.getBoundingClientRect().width:0,c=i+s,d=o+c+l+(r?n:0);return Math.round(d)}return 0},Jz=["axisLine","width","height","className","hide","ticks","axisType"],Qz=["viewBox"],eR=["viewBox"];function yp(e,t){if(e==null)return{};var r,n,i=tR(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var{ticks:r=[],tick:n,tickLine:i,stroke:s,tickFormatter:o,unit:l,padding:c,tickTextProps:d,orientation:u,mirror:f,x:p,y:m,width:x,height:g,tickSize:b,tickMargin:v,fontSize:j,letterSpacing:y,getTicksConfig:w,events:S,axisType:N}=e,P=ng(Ee(Ee({},w),{},{ticks:r}),j,y),_=oR(u,f),T=lR(u,f),$=nr(w),M=qc(n),C={};typeof i=="object"&&(C=i);var R=Ee(Ee({},$),{},{fill:"none"},C),q=P.map(D=>Ee({entry:D},sR(D,p,m,x,g,u,b,f,v))),Z=q.map(D=>{var{entry:O,line:k}=D;return h.createElement(ir,{className:"recharts-cartesian-axis-tick",key:"tick-".concat(O.value,"-").concat(O.coordinate,"-").concat(O.tickCoord)},i&&h.createElement("line",Si({},R,k,{className:ce("recharts-cartesian-axis-tick-line",Xc(i,"className"))})))}),E=q.map((D,O)=>{var{entry:k,tick:L}=D,U=Ee(Ee(Ee(Ee({textAnchor:_,verticalAnchor:T},$),{},{stroke:"none",fill:s},M),L),{},{index:O,payload:k,visibleTicksCount:P.length,tickFormatter:o,padding:c},d);return h.createElement(ir,Si({className:"recharts-cartesian-axis-tick-label",key:"tick-label-".concat(k.value,"-").concat(k.coordinate,"-").concat(k.tickCoord)},Q6(S,k,O)),n&&h.createElement(cR,{option:n,tickProps:U,value:"".concat(typeof o=="function"?o(k.value,O):k.value).concat(l||"")}))});return h.createElement("g",{className:"recharts-cartesian-axis-ticks recharts-".concat(N,"-ticks")},E.length>0&&h.createElement(Ir,{zIndex:lt.label},h.createElement("g",{className:"recharts-cartesian-axis-tick-labels recharts-".concat(N,"-tick-labels"),ref:t},E)),Z.length>0&&h.createElement("g",{className:"recharts-cartesian-axis-tick-lines recharts-".concat(N,"-tick-lines")},Z))}),dR=h.forwardRef((e,t)=>{var{axisLine:r,width:n,height:i,className:s,hide:o,ticks:l,axisType:c}=e,d=yp(e,Jz),[u,f]=h.useState(""),[p,m]=h.useState(""),x=h.useRef(null);h.useImperativeHandle(t,()=>({getCalculatedWidth:()=>{var b;return Xz({ticks:x.current,label:(b=e.labelRef)===null||b===void 0?void 0:b.current,labelGapWithTick:5,tickSize:e.tickSize,tickMargin:e.tickMargin})}}));var g=h.useCallback(b=>{if(b){var v=b.getElementsByClassName("recharts-cartesian-axis-tick-value");x.current=v;var j=v[0];if(j){var y=window.getComputedStyle(j),w=y.fontSize,S=y.letterSpacing;(w!==u||S!==p)&&(f(w),m(S))}}},[u,p]);return o||n!=null&&n<=0||i!=null&&i<=0?null:h.createElement(Ir,{zIndex:e.zIndex},h.createElement(ir,{className:ce("recharts-cartesian-axis",s)},h.createElement(aR,{x:e.x,y:e.y,width:n,height:i,orientation:e.orientation,mirror:e.mirror,axisLine:r,otherSvgProps:nr(e)}),h.createElement(uR,{ref:g,axisType:c,events:d,fontSize:u,getTicksConfig:e,height:e.height,letterSpacing:p,mirror:e.mirror,orientation:e.orientation,padding:e.padding,stroke:e.stroke,tick:e.tick,tickFormatter:e.tickFormatter,tickLine:e.tickLine,tickMargin:e.tickMargin,tickSize:e.tickSize,tickTextProps:e.tickTextProps,ticks:l,unit:e.unit,width:e.width,x:e.x,y:e.y}),h.createElement(s8,{x:e.x,y:e.y,width:e.width,height:e.height,lowerWidth:e.width,upperWidth:e.width},h.createElement(g8,{label:e.label,labelRef:e.labelRef}),e.children)))}),fR=h.memo(dR,(e,t)=>{var{viewBox:r}=e,n=yp(e,Qz),{viewBox:i}=t,s=yp(t,eR);return ya(r,i)&&ya(n,s)}),ag=h.forwardRef((e,t)=>{var r=ft(e,ig);return h.createElement(fR,Si({},r,{ref:t}))});ag.displayName="CartesianAxis";var pR=["x1","y1","x2","y2","key"],hR=["offset"],mR=["xAxisId","yAxisId"],gR=["xAxisId","yAxisId"];function dv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function ot(e){for(var t=1;t{var{fill:t}=e;if(!t||t==="none")return null;var{fillOpacity:r,x:n,y:i,width:s,height:o,ry:l}=e;return h.createElement("rect",{x:n,y:i,ry:l,width:s,height:o,stroke:"none",fill:t,fillOpacity:r,className:"recharts-cartesian-grid-bg"})};function XN(e){var{option:t,lineItemProps:r}=e,n;if(h.isValidElement(t))n=h.cloneElement(t,r);else if(typeof t=="function")n=t(r);else{var i,{x1:s,y1:o,x2:l,y2:c,key:d}=r,u=vc(r,pR),f=(i=nr(u))!==null&&i!==void 0?i:{},{offset:p}=f,m=vc(f,hR);n=h.createElement("line",ai({},m,{x1:s,y1:o,x2:l,y2:c,fill:"none",key:d}))}return n}function wR(e){var{x:t,width:r,horizontal:n=!0,horizontalPoints:i}=e;if(!n||!i||!i.length)return null;var{xAxisId:s,yAxisId:o}=e,l=vc(e,mR),c=i.map((d,u)=>{var f=ot(ot({},l),{},{x1:t,y1:d,x2:t+r,y2:d,key:"line-".concat(u),index:u});return h.createElement(XN,{key:"line-".concat(u),option:n,lineItemProps:f})});return h.createElement("g",{className:"recharts-cartesian-grid-horizontal"},c)}function SR(e){var{y:t,height:r,vertical:n=!0,verticalPoints:i}=e;if(!n||!i||!i.length)return null;var{xAxisId:s,yAxisId:o}=e,l=vc(e,gR),c=i.map((d,u)=>{var f=ot(ot({},l),{},{x1:d,y1:t,x2:d,y2:t+r,key:"line-".concat(u),index:u});return h.createElement(XN,{option:n,lineItemProps:f,key:"line-".concat(u)})});return h.createElement("g",{className:"recharts-cartesian-grid-vertical"},c)}function NR(e){var{horizontalFill:t,fillOpacity:r,x:n,y:i,width:s,height:o,horizontalPoints:l,horizontal:c=!0}=e;if(!c||!t||!t.length||l==null)return null;var d=l.map(f=>Math.round(f+i-i)).sort((f,p)=>f-p);i!==d[0]&&d.unshift(0);var u=d.map((f,p)=>{var m=!d[p+1],x=m?i+o-f:d[p+1]-f;if(x<=0)return null;var g=p%t.length;return h.createElement("rect",{key:"react-".concat(p),y:f,x:n,height:x,width:s,stroke:"none",fill:t[g],fillOpacity:r,className:"recharts-cartesian-grid-bg"})});return h.createElement("g",{className:"recharts-cartesian-gridstripes-horizontal"},u)}function kR(e){var{vertical:t=!0,verticalFill:r,fillOpacity:n,x:i,y:s,width:o,height:l,verticalPoints:c}=e;if(!t||!r||!r.length)return null;var d=c.map(f=>Math.round(f+i-i)).sort((f,p)=>f-p);i!==d[0]&&d.unshift(0);var u=d.map((f,p)=>{var m=!d[p+1],x=m?i+o-f:d[p+1]-f;if(x<=0)return null;var g=p%r.length;return h.createElement("rect",{key:"react-".concat(p),x:f,y:s,width:x,height:l,stroke:"none",fill:r[g],fillOpacity:n,className:"recharts-cartesian-grid-bg"})});return h.createElement("g",{className:"recharts-cartesian-gridstripes-vertical"},u)}var PR=(e,t)=>{var{xAxis:r,width:n,height:i,offset:s}=e;return Aw(ng(ot(ot(ot({},ig),r),{},{ticks:Ow(r),viewBox:{x:0,y:0,width:n,height:i}})),s.left,s.left+s.width,t)},_R=(e,t)=>{var{yAxis:r,width:n,height:i,offset:s}=e;return Aw(ng(ot(ot(ot({},ig),r),{},{ticks:Ow(r),viewBox:{x:0,y:0,width:n,height:i}})),s.top,s.top+s.height,t)},CR={horizontal:!0,vertical:!0,horizontalPoints:[],verticalPoints:[],stroke:"#ccc",fill:"none",verticalFill:[],horizontalFill:[],xAxisId:0,yAxisId:0,syncWithTicks:!1,zIndex:lt.grid};function vp(e){var t=Fw(),r=Ww(),n=Bw(),i=ot(ot({},ft(e,CR)),{},{x:Y(e.x)?e.x:n.left,y:Y(e.y)?e.y:n.top,width:Y(e.width)?e.width:n.width,height:Y(e.height)?e.height:n.height}),{xAxisId:s,yAxisId:o,x:l,y:c,width:d,height:u,syncWithTicks:f,horizontalValues:p,verticalValues:m}=i,x=pt(),g=G(T=>Ny(T,"xAxis",s,x)),b=G(T=>Ny(T,"yAxis",o,x));if(!Er(d)||!Er(u)||!Y(l)||!Y(c))return null;var v=i.verticalCoordinatesGenerator||PR,j=i.horizontalCoordinatesGenerator||_R,{horizontalPoints:y,verticalPoints:w}=i;if((!y||!y.length)&&typeof j=="function"){var S=p&&p.length,N=j({yAxis:b?ot(ot({},b),{},{ticks:S?p:b.ticks}):void 0,width:t??d,height:r??u,offset:n},S?!0:f);Gl(Array.isArray(N),"horizontalCoordinatesGenerator should return Array but instead it returned [".concat(typeof N,"]")),Array.isArray(N)&&(y=N)}if((!w||!w.length)&&typeof v=="function"){var P=m&&m.length,_=v({xAxis:g?ot(ot({},g),{},{ticks:P?m:g.ticks}):void 0,width:t??d,height:r??u,offset:n},P?!0:f);Gl(Array.isArray(_),"verticalCoordinatesGenerator should return Array but instead it returned [".concat(typeof _,"]")),Array.isArray(_)&&(w=_)}return h.createElement(Ir,{zIndex:i.zIndex},h.createElement("g",{className:"recharts-cartesian-grid"},h.createElement(jR,{fill:i.fill,fillOpacity:i.fillOpacity,x:i.x,y:i.y,width:i.width,height:i.height,ry:i.ry}),h.createElement(NR,ai({},i,{horizontalPoints:y})),h.createElement(kR,ai({},i,{verticalPoints:w})),h.createElement(wR,ai({},i,{offset:n,horizontalPoints:y,xAxis:g,yAxis:b})),h.createElement(SR,ai({},i,{offset:n,verticalPoints:w,xAxis:g,yAxis:b}))))}vp.displayName="CartesianGrid";var JN=(e,t,r,n)=>Du(e,"xAxis",t,n),QN=(e,t,r,n)=>Eu(e,"xAxis",t,n),ek=(e,t,r,n)=>Du(e,"yAxis",r,n),tk=(e,t,r,n)=>Eu(e,"yAxis",r,n),AR=I([ue,JN,ek,QN,tk],(e,t,r,n,i)=>Mr(e,"xAxis")?ha(t,n,!1):ha(r,i,!1)),OR=(e,t,r,n,i)=>i;function ER(e){return e.type==="line"}var DR=I([Mm,OR],(e,t)=>e.filter(ER).find(r=>r.id===t)),TR=I([ue,JN,ek,QN,tk,DR,AR,bu],(e,t,r,n,i,s,o,l)=>{var{chartData:c,dataStartIndex:d,dataEndIndex:u}=l;if(!(s==null||t==null||r==null||n==null||i==null||n.length===0||i.length===0||o==null)){var{dataKey:f,data:p}=s,m;if(p!=null&&p.length>0?m=p:m=c==null?void 0:c.slice(d,u+1),m!=null)return QR({layout:e,xAxis:t,yAxis:r,xAxisTicks:n,yAxisTicks:i,dataKey:f,bandSize:o,displayedData:m})}});function rk(e){var t=qc(e),r=3,n=2;if(t!=null){var{r:i,strokeWidth:s}=t,o=Number(i),l=Number(s);return(Number.isNaN(o)||o<0)&&(o=r),(Number.isNaN(l)||l<0)&&(l=n),{r:o,strokeWidth:l}}return{r,strokeWidth:n}}var MR=["id"],IR=["type","layout","connectNulls","needClip","shape"],$R=["activeDot","animateNewValues","animationBegin","animationDuration","animationEasing","connectNulls","dot","hide","isAnimationActive","label","legendType","xAxisId","yAxisId","id"];function Hs(){return Hs=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{dataKey:t,name:r,stroke:n,legendType:i,hide:s}=e;return[{inactive:s,dataKey:t,type:i,color:n,value:au(r,t),payload:e}]};function WR(e){var{dataKey:t,data:r,stroke:n,strokeWidth:i,fill:s,name:o,hide:l,unit:c}=e;return{dataDefinedOnItem:r,positions:void 0,settings:{stroke:n,strokeWidth:i,fill:s,dataKey:t,nameKey:void 0,name:au(o,t),hide:l,type:e.tooltipType,color:e.stroke,unit:c}}}var nk=(e,t)=>"".concat(t,"px ").concat(e-t,"px");function UR(e,t){for(var r=e.length%2!==0?[...e,0]:e,n=[],i=0;i{var n=r.reduce((f,p)=>f+p);if(!n)return nk(t,e);for(var i=Math.floor(e/n),s=e%n,o=t-e,l=[],c=0,d=0;cs){l=[...r.slice(0,c),s-d];break}var u=l.length%2===0?[0,o]:[o];return[...UR(r,i),...l,...u].map(f=>"".concat(f,"px")).join(", ")};function HR(e){var{clipPathId:t,points:r,props:n}=e,{dot:i,dataKey:s,needClip:o}=n,{id:l}=n,c=sg(n,MR),d=nr(c);return h.createElement(qN,{points:r,dot:i,className:"recharts-line-dots",dotClassName:"recharts-line-dot",dataKey:s,baseProps:d,needClip:o,clipPathId:t})}function KR(e){var{showLabels:t,children:r,points:n}=e,i=h.useMemo(()=>n==null?void 0:n.map(s=>{var o,l,c={x:(o=s.x)!==null&&o!==void 0?o:0,y:(l=s.y)!==null&&l!==void 0?l:0,width:0,lowerWidth:0,upperWidth:0,height:0};return wr(wr({},c),{},{value:s.value,payload:s.payload,viewBox:c,parentViewBox:void 0,fill:void 0})}),[n]);return h.createElement(DN,{value:t?i:void 0},r)}function pv(e){var{clipPathId:t,pathRef:r,points:n,strokeDasharray:i,props:s}=e,{type:o,layout:l,connectNulls:c,needClip:d,shape:u}=s,f=sg(s,IR),p=wr(wr({},ut(f)),{},{fill:"none",className:"recharts-line-curve",clipPath:d?"url(#clipPath-".concat(t,")"):void 0,points:n,type:o,layout:l,connectNulls:c,strokeDasharray:i??s.strokeDasharray});return h.createElement(h.Fragment,null,(n==null?void 0:n.length)>1&&h.createElement(U8,Hs({shapeType:"curve",option:u},p,{pathRef:r})),h.createElement(HR,{points:n,clipPathId:t,props:s}))}function VR(e){try{return e&&e.getTotalLength&&e.getTotalLength()||0}catch{return 0}}function YR(e){var{clipPathId:t,props:r,pathRef:n,previousPointsRef:i,longestAnimatedLengthRef:s}=e,{points:o,strokeDasharray:l,isAnimationActive:c,animationBegin:d,animationDuration:u,animationEasing:f,animateNewValues:p,width:m,height:x,onAnimationEnd:g,onAnimationStart:b}=r,v=i.current,j=pu(r,"recharts-line-"),[y,w]=h.useState(!1),S=!y,N=h.useCallback(()=>{typeof g=="function"&&g(),w(!1)},[g]),P=h.useCallback(()=>{typeof b=="function"&&b(),w(!0)},[b]),_=VR(n.current),T=s.current;return h.createElement(KR,{points:o,showLabels:S},r.children,h.createElement(fu,{animationId:j,begin:d,duration:u,isActive:c,easing:f,onAnimationEnd:N,onAnimationStart:P,key:j},$=>{var M=De(T,_+T,$),C=Math.min(M,_),R;if(c)if(l){var q="".concat(l).split(/[,\s]+/gim).map(D=>parseFloat(D));R=qR(C,_,q)}else R=nk(_,C);else R=l==null?void 0:String(l);if(v){var Z=v.length/o.length,E=$===1?o:o.map((D,O)=>{var k=Math.floor(O*Z);if(v[k]){var L=v[k];return wr(wr({},D),{},{x:De(L.x,D.x,$),y:De(L.y,D.y,$)})}return p?wr(wr({},D),{},{x:De(m*2,D.x,$),y:De(x/2,D.y,$)}):wr(wr({},D),{},{x:D.x,y:D.y})});return i.current=E,h.createElement(pv,{props:r,points:E,clipPathId:t,pathRef:n,strokeDasharray:R})}return $>0&&_>0&&(i.current=o,s.current=C),h.createElement(pv,{props:r,points:o,clipPathId:t,pathRef:n,strokeDasharray:R})}),h.createElement(MN,{label:r.label}))}function GR(e){var{clipPathId:t,props:r}=e,n=h.useRef(null),i=h.useRef(0),s=h.useRef(null);return h.createElement(YR,{props:r,clipPathId:t,previousPointsRef:n,longestAnimatedLengthRef:i,pathRef:s})}var ZR=(e,t)=>{var r,n;return{x:(r=e.x)!==null&&r!==void 0?r:void 0,y:(n=e.y)!==null&&n!==void 0?n:void 0,value:e.value,errorVal:et(e.payload,t)}};class XR extends h.Component{render(){var{hide:t,dot:r,points:n,className:i,xAxisId:s,yAxisId:o,top:l,left:c,width:d,height:u,id:f,needClip:p,zIndex:m}=this.props;if(t)return null;var x=ce("recharts-line",i),g=f,{r:b,strokeWidth:v}=rk(r),j=eg(r),y=b*2+v;return h.createElement(Ir,{zIndex:m},h.createElement(ir,{className:x},p&&h.createElement("defs",null,h.createElement(VN,{clipPathId:g,xAxisId:s,yAxisId:o}),!j&&h.createElement("clipPath",{id:"clipPath-dots-".concat(g)},h.createElement("rect",{x:c-y/2,y:l-y/2,width:d+y,height:u+y}))),h.createElement(Oz,{xAxisId:s,yAxisId:o,data:n,dataPointFormatter:ZR,errorBarOffset:0},h.createElement(GR,{props:this.props,clipPathId:g}))),h.createElement(xp,{activeDot:this.props.activeDot,points:n,mainColor:this.props.stroke,itemDataKey:this.props.dataKey}))}}var ik={activeDot:!0,animateNewValues:!0,animationBegin:0,animationDuration:1500,animationEasing:"ease",connectNulls:!1,dot:!0,fill:"#fff",hide:!1,isAnimationActive:!Ci.isSsr,label:!1,legendType:"line",stroke:"#3182bd",strokeWidth:1,xAxisId:0,yAxisId:0,zIndex:lt.line};function JR(e){var t=ft(e,ik),{activeDot:r,animateNewValues:n,animationBegin:i,animationDuration:s,animationEasing:o,connectNulls:l,dot:c,hide:d,isAnimationActive:u,label:f,legendType:p,xAxisId:m,yAxisId:x,id:g}=t,b=sg(t,$R),{needClip:v}=tg(m,x),j=Mu(),y=to(),w=pt(),S=G($=>TR($,m,x,w,g));if(y!=="horizontal"&&y!=="vertical"||S==null||j==null)return null;var{height:N,width:P,x:_,y:T}=j;return h.createElement(XR,Hs({},b,{id:g,connectNulls:l,dot:c,activeDot:r,animateNewValues:n,animationBegin:i,animationDuration:s,animationEasing:o,isAnimationActive:u,hide:d,label:f,legendType:p,xAxisId:m,yAxisId:x,points:S,layout:y,height:N,width:P,left:_,top:T,needClip:v}))}function QR(e){var{layout:t,xAxis:r,yAxis:n,xAxisTicks:i,yAxisTicks:s,dataKey:o,bandSize:l,displayedData:c}=e;return c.map((d,u)=>{var f=et(d,o);if(t==="horizontal"){var p=Yl({axis:r,ticks:i,bandSize:l,entry:d,index:u}),m=Re(f)?null:n.scale(f);return{x:p,y:m,value:f,payload:d}}var x=Re(f)?null:r.scale(f),g=Yl({axis:n,ticks:s,bandSize:l,entry:d,index:u});return x==null||g==null?null:{x,y:g,value:f,payload:d}}).filter(Boolean)}function e9(e){var t=ft(e,ik),r=pt();return h.createElement(FN,{id:t.id,type:"line"},n=>h.createElement(h.Fragment,null,h.createElement(RN,{legendPayload:FR(t)}),h.createElement(zN,{fn:WR,args:t}),h.createElement(UN,{type:"line",id:n,data:t.data,xAxisId:t.xAxisId,yAxisId:t.yAxisId,zAxisId:0,dataKey:t.dataKey,hide:t.hide,isPanorama:r}),h.createElement(JR,Hs({},t,{id:n}))))}var ak=h.memo(e9);ak.displayName="Line";var sk=(e,t,r,n)=>Du(e,"xAxis",t,n),ok=(e,t,r,n)=>Eu(e,"xAxis",t,n),lk=(e,t,r,n)=>Du(e,"yAxis",r,n),ck=(e,t,r,n)=>Eu(e,"yAxis",r,n),t9=I([ue,sk,lk,ok,ck],(e,t,r,n,i)=>Mr(e,"xAxis")?ha(t,n,!1):ha(r,i,!1)),r9=(e,t,r,n,i)=>i,uk=I([Mm,r9],(e,t)=>e.filter(r=>r.type==="area").find(r=>r.id===t)),n9=(e,t,r,n,i)=>{var s,o=uk(e,t,r,n,i);if(o!=null){var l=ue(e),c=Mr(l,"xAxis"),d;if(c?d=cp(e,"yAxis",r,n):d=cp(e,"xAxis",t,n),d!=null){var{stackId:u}=o,f=Em(o);if(!(u==null||f==null)){var p=(s=d[u])===null||s===void 0?void 0:s.stackedData;return p==null?void 0:p.find(m=>m.key===f)}}}},i9=I([ue,sk,lk,ok,ck,n9,bu,t9,uk,xM],(e,t,r,n,i,s,o,l,c,d)=>{var{chartData:u,dataStartIndex:f,dataEndIndex:p}=o;if(!(c==null||e!=="horizontal"&&e!=="vertical"||t==null||r==null||n==null||i==null||n.length===0||i.length===0||l==null)){var{data:m}=c,x;if(m&&m.length>0?x=m:x=u==null?void 0:u.slice(f,p+1),x!=null)return S9({layout:e,xAxis:t,yAxis:r,xAxisTicks:n,yAxisTicks:i,dataStartIndex:f,areaSettings:c,stackedData:s,displayedData:x,chartBaseValue:d,bandSize:l})}}),a9=["id"],s9=["activeDot","animationBegin","animationDuration","animationEasing","connectNulls","dot","fill","fillOpacity","hide","isAnimationActive","legendType","stroke","xAxisId","yAxisId"];function fi(){return fi=Object.assign?Object.assign.bind():function(e){for(var t=1;t{var{dataKey:t,name:r,stroke:n,fill:i,legendType:s,hide:o}=e;return[{inactive:o,dataKey:t,type:s,color:bc(n,i),value:au(r,t),payload:e}]};function f9(e){var{dataKey:t,data:r,stroke:n,strokeWidth:i,fill:s,name:o,hide:l,unit:c}=e;return{dataDefinedOnItem:r,positions:void 0,settings:{stroke:n,strokeWidth:i,fill:s,dataKey:t,nameKey:void 0,name:au(o,t),hide:l,type:e.tooltipType,color:bc(n,s),unit:c}}}function p9(e){var{clipPathId:t,points:r,props:n}=e,{needClip:i,dot:s,dataKey:o}=n,l=nr(n);return h.createElement(qN,{points:r,dot:s,className:"recharts-area-dots",dotClassName:"recharts-area-dot",dataKey:o,baseProps:l,needClip:i,clipPathId:t})}function h9(e){var{showLabels:t,children:r,points:n}=e,i=n.map(s=>{var o,l,c={x:(o=s.x)!==null&&o!==void 0?o:0,y:(l=s.y)!==null&&l!==void 0?l:0,width:0,lowerWidth:0,upperWidth:0,height:0};return Zi(Zi({},c),{},{value:s.value,payload:s.payload,parentViewBox:void 0,viewBox:c,fill:void 0})});return h.createElement(DN,{value:t?i:void 0},r)}function mv(e){var{points:t,baseLine:r,needClip:n,clipPathId:i,props:s}=e,{layout:o,type:l,stroke:c,connectNulls:d,isRange:u}=s,{id:f}=s,p=dk(s,a9),m=nr(p),x=ut(p);return h.createElement(h.Fragment,null,(t==null?void 0:t.length)>1&&h.createElement(ir,{clipPath:n?"url(#clipPath-".concat(i,")"):void 0},h.createElement(us,fi({},x,{id:f,points:t,connectNulls:d,type:l,baseLine:r,layout:o,stroke:"none",className:"recharts-area-area"})),c!=="none"&&h.createElement(us,fi({},m,{className:"recharts-area-curve",layout:o,type:l,connectNulls:d,fill:"none",points:t})),c!=="none"&&u&&h.createElement(us,fi({},m,{className:"recharts-area-curve",layout:o,type:l,connectNulls:d,fill:"none",points:r}))),h.createElement(p9,{points:t,props:p,clipPathId:i}))}function m9(e){var{alpha:t,baseLine:r,points:n,strokeWidth:i}=e,s=n[0].y,o=n[n.length-1].y;if(!_e(s)||!_e(o))return null;var l=t*Math.abs(s-o),c=Math.max(...n.map(d=>d.x||0));return Y(r)?c=Math.max(r,c):r&&Array.isArray(r)&&r.length&&(c=Math.max(...r.map(d=>d.x||0),c)),Y(c)?h.createElement("rect",{x:0,y:sd.y||0));return Y(r)?c=Math.max(r,c):r&&Array.isArray(r)&&r.length&&(c=Math.max(...r.map(d=>d.y||0),c)),Y(c)?h.createElement("rect",{x:s{typeof m=="function"&&m(),b(!1)},[m]),y=h.useCallback(()=>{typeof p=="function"&&p(),b(!0)},[p]),w=i.current,S=s.current;return h.createElement(h9,{showLabels:v,points:o},n.children,h.createElement(fu,{animationId:x,begin:d,duration:u,isActive:c,easing:f,onAnimationEnd:j,onAnimationStart:y,key:x},N=>{if(w){var P=w.length/o.length,_=N===1?o:o.map(($,M)=>{var C=Math.floor(M*P);if(w[C]){var R=w[C];return Zi(Zi({},$),{},{x:De(R.x,$.x,N),y:De(R.y,$.y,N)})}return $}),T;return Y(l)?T=De(S,l,N):Re(l)||yr(l)?T=De(S,0,N):T=l.map(($,M)=>{var C=Math.floor(M*P);if(Array.isArray(S)&&S[C]){var R=S[C];return Zi(Zi({},$),{},{x:De(R.x,$.x,N),y:De(R.y,$.y,N)})}return $}),N>0&&(i.current=_,s.current=T),h.createElement(mv,{points:_,baseLine:T,needClip:t,clipPathId:r,props:n})}return N>0&&(i.current=o,s.current=l),h.createElement(ir,null,c&&h.createElement("defs",null,h.createElement("clipPath",{id:"animationClipPath-".concat(r)},h.createElement(x9,{alpha:N,points:o,baseLine:l,layout:n.layout,strokeWidth:n.strokeWidth}))),h.createElement(ir,{clipPath:"url(#animationClipPath-".concat(r,")")},h.createElement(mv,{points:o,baseLine:l,needClip:t,clipPathId:r,props:n})))}),h.createElement(MN,{label:n.label}))}function v9(e){var{needClip:t,clipPathId:r,props:n}=e,i=h.useRef(null),s=h.useRef();return h.createElement(y9,{needClip:t,clipPathId:r,props:n,previousPointsRef:i,previousBaselineRef:s})}class b9 extends h.PureComponent{render(){var{hide:t,dot:r,points:n,className:i,top:s,left:o,needClip:l,xAxisId:c,yAxisId:d,width:u,height:f,id:p,baseLine:m,zIndex:x}=this.props;if(t)return null;var g=ce("recharts-area",i),b=p,{r:v,strokeWidth:j}=rk(r),y=eg(r),w=v*2+j;return h.createElement(Ir,{zIndex:x},h.createElement(ir,{className:g},l&&h.createElement("defs",null,h.createElement(VN,{clipPathId:b,xAxisId:c,yAxisId:d}),!y&&h.createElement("clipPath",{id:"clipPath-dots-".concat(b)},h.createElement("rect",{x:o-w/2,y:s-w/2,width:u+w,height:f+w}))),h.createElement(v9,{needClip:l,clipPathId:b,props:this.props})),h.createElement(xp,{points:n,mainColor:bc(this.props.stroke,this.props.fill),itemDataKey:this.props.dataKey,activeDot:this.props.activeDot}),this.props.isRange&&Array.isArray(m)&&h.createElement(xp,{points:m,mainColor:bc(this.props.stroke,this.props.fill),itemDataKey:this.props.dataKey,activeDot:this.props.activeDot}))}}var fk={activeDot:!0,animationBegin:0,animationDuration:1500,animationEasing:"ease",connectNulls:!1,dot:!1,fill:"#3182bd",fillOpacity:.6,hide:!1,isAnimationActive:!Ci.isSsr,legendType:"line",stroke:"#3182bd",xAxisId:0,yAxisId:0,zIndex:lt.area};function j9(e){var t,r=ft(e,fk),{activeDot:n,animationBegin:i,animationDuration:s,animationEasing:o,connectNulls:l,dot:c,fill:d,fillOpacity:u,hide:f,isAnimationActive:p,legendType:m,stroke:x,xAxisId:g,yAxisId:b}=r,v=dk(r,s9),j=to(),y=oN(),{needClip:w}=tg(g,b),S=pt(),{points:N,isRange:P,baseLine:_}=(t=G(q=>i9(q,g,b,S,e.id)))!==null&&t!==void 0?t:{},T=Mu();if(j!=="horizontal"&&j!=="vertical"||T==null||y!=="AreaChart"&&y!=="ComposedChart")return null;var{height:$,width:M,x:C,y:R}=T;return!N||!N.length?null:h.createElement(b9,fi({},v,{activeDot:n,animationBegin:i,animationDuration:s,animationEasing:o,baseLine:_,connectNulls:l,dot:c,fill:d,fillOpacity:u,height:$,hide:f,layout:j,isAnimationActive:p,isRange:P,legendType:m,needClip:w,points:N,stroke:x,width:M,left:C,top:R,xAxisId:g,yAxisId:b}))}var w9=(e,t,r,n,i)=>{var s=r??t;if(Y(s))return s;var o=e==="horizontal"?i:n,l=o.scale.domain();if(o.type==="number"){var c=Math.max(l[0],l[1]),d=Math.min(l[0],l[1]);return s==="dataMin"?d:s==="dataMax"||c<0?c:Math.max(Math.min(l[0],l[1]),0)}return s==="dataMin"?l[0]:s==="dataMax"?l[1]:l[0]};function S9(e){var{areaSettings:{connectNulls:t,baseValue:r,dataKey:n},stackedData:i,layout:s,chartBaseValue:o,xAxis:l,yAxis:c,displayedData:d,dataStartIndex:u,xAxisTicks:f,yAxisTicks:p,bandSize:m}=e,x=i&&i.length,g=w9(s,o,r,l,c),b=s==="horizontal",v=!1,j=d.map((w,S)=>{var N;x?N=i[u+S]:(N=et(w,n),Array.isArray(N)?v=!0:N=[g,N]);var P=N[1]==null||x&&!t&&et(w,n)==null;return b?{x:Yl({axis:l,ticks:f,bandSize:m,entry:w,index:S}),y:P?null:c.scale(N[1]),value:N,payload:w}:{x:P?null:l.scale(N[1]),y:Yl({axis:c,ticks:p,bandSize:m,entry:w,index:S}),value:N,payload:w}}),y;return x||v?y=j.map(w=>{var S=Array.isArray(w.value)?w.value[0]:null;return b?{x:w.x,y:S!=null&&w.y!=null?c.scale(S):null,payload:w.payload}:{x:S!=null?l.scale(S):null,y:w.y,payload:w.payload}}):y=b?c.scale(g):l.scale(g),{points:j,baseLine:y,isRange:v}}function N9(e){var t=ft(e,fk),r=pt();return h.createElement(FN,{id:t.id,type:"area"},n=>h.createElement(h.Fragment,null,h.createElement(RN,{legendPayload:d9(t)}),h.createElement(zN,{fn:f9,args:t}),h.createElement(UN,{type:"area",id:n,data:t.data,dataKey:t.dataKey,xAxisId:t.xAxisId,yAxisId:t.yAxisId,zAxisId:0,stackId:CE(t.stackId),hide:t.hide,barSize:void 0,baseValue:t.baseValue,isPanorama:r,connectNulls:t.connectNulls}),h.createElement(j9,fi({},t,{id:n}))))}var pk=h.memo(N9);pk.displayName="Area";var k9=["dangerouslySetInnerHTML","ticks"],P9=["id"],_9=["domain"],C9=["domain"];function bp(){return bp=Object.assign?Object.assign.bind():function(e){for(var t=1;t(t(uz(e)),()=>{t(dz(e))}),[e,t]),null}var E9=e=>{var{xAxisId:t,className:r}=e,n=G(Dw),i=pt(),s="xAxis",o=G(b=>Ea(b,s,t,i)),l=G(b=>R2(b,s,t,i)),c=G(b=>cI(b,t)),d=G(b=>mI(b,t)),u=G(b=>a2(b,t));if(c==null||d==null||u==null)return null;var{dangerouslySetInnerHTML:f,ticks:p}=e,m=jc(e,k9),{id:x}=u,g=jc(u,P9);return h.createElement(ag,bp({},m,g,{scale:o,x:d.x,y:d.y,width:c.width,height:c.height,className:ce("recharts-".concat(s," ").concat(s),r),viewBox:n,ticks:l,axisType:s}))},D9={allowDataOverflow:Tt.allowDataOverflow,allowDecimals:Tt.allowDecimals,allowDuplicatedCategory:Tt.allowDuplicatedCategory,height:Tt.height,hide:!1,mirror:Tt.mirror,orientation:Tt.orientation,padding:Tt.padding,reversed:Tt.reversed,scale:Tt.scale,tickCount:Tt.tickCount,type:Tt.type,xAxisId:0},T9=e=>{var t,r,n,i,s,o=ft(e,D9);return h.createElement(h.Fragment,null,h.createElement(O9,{interval:(t=o.interval)!==null&&t!==void 0?t:"preserveEnd",id:o.xAxisId,scale:o.scale,type:o.type,padding:o.padding,allowDataOverflow:o.allowDataOverflow,domain:o.domain,dataKey:o.dataKey,allowDuplicatedCategory:o.allowDuplicatedCategory,allowDecimals:o.allowDecimals,tickCount:o.tickCount,includeHidden:(r=o.includeHidden)!==null&&r!==void 0?r:!1,reversed:o.reversed,ticks:o.ticks,height:o.height,orientation:o.orientation,mirror:o.mirror,hide:o.hide,unit:o.unit,name:o.name,angle:(n=o.angle)!==null&&n!==void 0?n:0,minTickGap:(i=o.minTickGap)!==null&&i!==void 0?i:5,tick:(s=o.tick)!==null&&s!==void 0?s:!0,tickFormatter:o.tickFormatter}),h.createElement(E9,o))},M9=(e,t)=>{var{domain:r}=e,n=jc(e,_9),{domain:i}=t,s=jc(t,C9);return ya(n,s)?Array.isArray(r)&&r.length===2&&Array.isArray(i)&&i.length===2?r[0]===i[0]&&r[1]===i[1]:ya({domain:r},{domain:i}):!1},jp=h.memo(T9,M9);jp.displayName="XAxis";var I9=["dangerouslySetInnerHTML","ticks"],$9=["id"],L9=["domain"],z9=["domain"];function wp(){return wp=Object.assign?Object.assign.bind():function(e){for(var t=1;t(t(fz(e)),()=>{t(pz(e))}),[e,t]),null}var F9=e=>{var{yAxisId:t,className:r,width:n,label:i}=e,s=h.useRef(null),o=h.useRef(null),l=G(Dw),c=pt(),d=Ve(),u="yAxis",f=G(S=>Ea(S,u,t,c)),p=G(S=>yI(S,t)),m=G(S=>xI(S,t)),x=G(S=>R2(S,u,t,c)),g=G(S=>s2(S,t));if(h.useLayoutEffect(()=>{if(!(n!=="auto"||!p||Qm(i)||h.isValidElement(i)||g==null)){var S=s.current;if(S){var N=S.getCalculatedWidth();Math.round(p.width)!==Math.round(N)&&d(hz({id:t,width:N}))}}},[x,p,d,i,t,n,g]),p==null||m==null||g==null)return null;var{dangerouslySetInnerHTML:b,ticks:v}=e,j=wc(e,I9),{id:y}=g,w=wc(g,$9);return h.createElement(ag,wp({},j,w,{ref:s,labelRef:o,scale:f,x:m.x,y:m.y,tickTextProps:n==="auto"?{width:void 0}:{width:n},width:p.width,height:p.height,className:ce("recharts-".concat(u," ").concat(u),r),viewBox:l,ticks:x,axisType:u}))},W9={allowDataOverflow:Mt.allowDataOverflow,allowDecimals:Mt.allowDecimals,allowDuplicatedCategory:Mt.allowDuplicatedCategory,hide:!1,mirror:Mt.mirror,orientation:Mt.orientation,padding:Mt.padding,reversed:Mt.reversed,scale:Mt.scale,tickCount:Mt.tickCount,type:Mt.type,width:Mt.width,yAxisId:0},U9=e=>{var t,r,n,i,s,o=ft(e,W9);return h.createElement(h.Fragment,null,h.createElement(B9,{interval:(t=o.interval)!==null&&t!==void 0?t:"preserveEnd",id:o.yAxisId,scale:o.scale,type:o.type,domain:o.domain,allowDataOverflow:o.allowDataOverflow,dataKey:o.dataKey,allowDuplicatedCategory:o.allowDuplicatedCategory,allowDecimals:o.allowDecimals,tickCount:o.tickCount,padding:o.padding,includeHidden:(r=o.includeHidden)!==null&&r!==void 0?r:!1,reversed:o.reversed,ticks:o.ticks,width:o.width,orientation:o.orientation,mirror:o.mirror,hide:o.hide,unit:o.unit,name:o.name,angle:(n=o.angle)!==null&&n!==void 0?n:0,minTickGap:(i=o.minTickGap)!==null&&i!==void 0?i:5,tick:(s=o.tick)!==null&&s!==void 0?s:!0,tickFormatter:o.tickFormatter}),h.createElement(F9,o))},q9=(e,t)=>{var{domain:r}=e,n=wc(e,L9),{domain:i}=t,s=wc(t,z9);return ya(n,s)?Array.isArray(r)&&r.length===2&&Array.isArray(i)&&i.length===2?r[0]===i[0]&&r[1]===i[1]:ya({domain:r},{domain:i}):!1},Sp=h.memo(U9,q9);Sp.displayName="YAxis";var H9={};/** + * @license React + * use-sync-external-store-with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var po=h;function K9(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var V9=typeof Object.is=="function"?Object.is:K9,Y9=po.useSyncExternalStore,G9=po.useRef,Z9=po.useEffect,X9=po.useMemo,J9=po.useDebugValue;H9.useSyncExternalStoreWithSelector=function(e,t,r,n,i){var s=G9(null);if(s.current===null){var o={hasValue:!1,value:null};s.current=o}else o=s.current;s=X9(function(){function c(m){if(!d){if(d=!0,u=m,m=n(m),i!==void 0&&o.hasValue){var x=o.value;if(i(x,m))return f=x}return f=m}if(x=f,V9(u,m))return x;var g=n(m);return i!==void 0&&i(x,g)?(u=m,x):(u=m,f=g)}var d=!1,u,f,p=r===void 0?null:r;return[function(){return c(t())},p===null?void 0:function(){return c(p())}]},[t,r,n,i]);var l=Y9(e,s[0],s[1]);return Z9(function(){o.hasValue=!0,o.value=l},[l]),J9(l),l};function Q9(e){e()}function eB(){let e=null,t=null;return{clear(){e=null,t=null},notify(){Q9(()=>{let r=e;for(;r;)r.callback(),r=r.next})},get(){const r=[];let n=e;for(;n;)r.push(n),n=n.next;return r},subscribe(r){let n=!0;const i=t={callback:r,next:null,prev:t};return i.prev?i.prev.next=i:e=i,function(){!n||e===null||(n=!1,i.next?i.next.prev=i.prev:t=i.prev,i.prev?i.prev.next=i.next:e=i.next)}}}}var gv={notify(){},get:()=>[]};function tB(e,t){let r,n=gv,i=0,s=!1;function o(g){u();const b=n.subscribe(g);let v=!1;return()=>{v||(v=!0,b(),f())}}function l(){n.notify()}function c(){x.onStateChange&&x.onStateChange()}function d(){return s}function u(){i++,r||(r=e.subscribe(c),n=eB())}function f(){i--,r&&i===0&&(r(),r=void 0,n.clear(),n=gv)}function p(){s||(s=!0,u())}function m(){s&&(s=!1,f())}const x={addNestedSub:o,notifyNestedSubs:l,handleChangeWrapper:c,isSubscribed:d,trySubscribe:p,tryUnsubscribe:m,getListeners:()=>n};return x}var rB=()=>typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",nB=rB(),iB=()=>typeof navigator<"u"&&navigator.product==="ReactNative",aB=iB(),sB=()=>nB||aB?h.useLayoutEffect:h.useEffect,oB=sB(),Cd=Symbol.for("react-redux-context"),Ad=typeof globalThis<"u"?globalThis:{};function lB(){if(!h.createContext)return{};const e=Ad[Cd]??(Ad[Cd]=new Map);let t=e.get(h.createContext);return t||(t=h.createContext(null),e.set(h.createContext,t)),t}var cB=lB();function uB(e){const{children:t,context:r,serverState:n,store:i}=e,s=h.useMemo(()=>{const c=tB(i);return{store:i,subscription:c,getServerState:n?()=>n:void 0}},[i,n]),o=h.useMemo(()=>i.getState(),[i]);oB(()=>{const{subscription:c}=s;return c.onStateChange=c.notifyNestedSubs,c.trySubscribe(),o!==i.getState()&&c.notifyNestedSubs(),()=>{c.tryUnsubscribe(),c.onStateChange=void 0}},[s,o]);const l=r||cB;return h.createElement(l.Provider,{value:s},t)}var dB=uB,fB=(e,t)=>t,og=I([fB,ue,n2,We,tN,pn,C$,rt],I$),lg=e=>{var t=e.currentTarget.getBoundingClientRect(),r=t.width/e.currentTarget.offsetWidth,n=t.height/e.currentTarget.offsetHeight;return{chartX:Math.round((e.clientX-t.left)/r),chartY:Math.round((e.clientY-t.top)/n)}},hk=ar("mouseClick"),mk=Qs();mk.startListening({actionCreator:hk,effect:(e,t)=>{var r=e.payload,n=og(t.getState(),lg(r));(n==null?void 0:n.activeIndex)!=null&&t.dispatch(OI({activeIndex:n.activeIndex,activeDataKey:void 0,activeCoordinate:n.activeCoordinate}))}});var Np=ar("mouseMove"),gk=Qs();gk.startListening({actionCreator:Np,effect:(e,t)=>{var r=e.payload,n=t.getState(),i=Um(n,n.tooltip.settings.shared),s=og(n,lg(r));i==="axis"&&((s==null?void 0:s.activeIndex)!=null?t.dispatch(K2({activeIndex:s.activeIndex,activeDataKey:void 0,activeCoordinate:s.activeCoordinate})):t.dispatch(H2()))}});var xv={accessibilityLayer:!0,barCategoryGap:"10%",barGap:4,barSize:void 0,className:void 0,maxBarSize:void 0,stackOffset:"none",syncId:void 0,syncMethod:"index",baseValue:void 0},xk=At({name:"rootProps",initialState:xv,reducers:{updateOptions:(e,t)=>{var r;e.accessibilityLayer=t.payload.accessibilityLayer,e.barCategoryGap=t.payload.barCategoryGap,e.barGap=(r=t.payload.barGap)!==null&&r!==void 0?r:xv.barGap,e.barSize=t.payload.barSize,e.maxBarSize=t.payload.maxBarSize,e.stackOffset=t.payload.stackOffset,e.syncId=t.payload.syncId,e.syncMethod=t.payload.syncMethod,e.className=t.payload.className,e.baseValue=t.payload.baseValue}}}),pB=xk.reducer,{updateOptions:hB}=xk.actions,yk=At({name:"polarOptions",initialState:null,reducers:{updatePolarOptions:(e,t)=>t.payload}}),{updatePolarOptions:Z7}=yk.actions,mB=yk.reducer,vk=ar("keyDown"),bk=ar("focus"),cg=Qs();cg.startListening({actionCreator:vk,effect:(e,t)=>{var r=t.getState(),n=r.rootProps.accessibilityLayer!==!1;if(n){var{keyboardInteraction:i}=r.tooltip,s=e.payload;if(!(s!=="ArrowRight"&&s!=="ArrowLeft"&&s!=="Enter")){var o=Number(qm(i,Ta(r))),l=pn(r);if(s==="Enter"){var c=pc(r,"axis","hover",String(i.index));t.dispatch(dp({active:!i.active,activeIndex:i.index,activeDataKey:i.dataKey,activeCoordinate:c}));return}var d=wI(r),u=d==="left-to-right"?1:-1,f=s==="ArrowRight"?1:-1,p=o+f*u;if(!(l==null||p>=l.length||p<0)){var m=pc(r,"axis","hover",String(p));t.dispatch(dp({active:!0,activeIndex:p.toString(),activeDataKey:void 0,activeCoordinate:m}))}}}}});cg.startListening({actionCreator:bk,effect:(e,t)=>{var r=t.getState(),n=r.rootProps.accessibilityLayer!==!1;if(n){var{keyboardInteraction:i}=r.tooltip;if(!i.active&&i.index==null){var s="0",o=pc(r,"axis","hover",String(s));t.dispatch(dp({activeDataKey:void 0,active:!0,activeIndex:s,activeCoordinate:o}))}}}});var Vt=ar("externalEvent"),jk=Qs();jk.startListening({actionCreator:Vt,effect:(e,t)=>{if(e.payload.handler!=null){var r=t.getState(),n={activeCoordinate:p$(r),activeDataKey:d$(r),activeIndex:Us(r),activeLabel:iN(r),activeTooltipIndex:Us(r),isTooltipActive:h$(r)};e.payload.handler(n,e.payload.reactEvent)}}});var gB=I([Da],e=>e.tooltipItemPayloads),xB=I([gB,uo,(e,t,r)=>t,(e,t,r)=>r],(e,t,r,n)=>{var i=e.find(l=>l.settings.dataKey===n);if(i!=null){var{positions:s}=i;if(s!=null){var o=t(s,r);return o}}}),wk=ar("touchMove"),Sk=Qs();Sk.startListening({actionCreator:wk,effect:(e,t)=>{var r=e.payload;if(!(r.touches==null||r.touches.length===0)){var n=t.getState(),i=Um(n,n.tooltip.settings.shared);if(i==="axis"){var s=og(n,lg({clientX:r.touches[0].clientX,clientY:r.touches[0].clientY,currentTarget:r.currentTarget}));(s==null?void 0:s.activeIndex)!=null&&t.dispatch(K2({activeIndex:s.activeIndex,activeDataKey:void 0,activeCoordinate:s.activeCoordinate}))}else if(i==="item"){var o,l=r.touches[0];if(document.elementFromPoint==null)return;var c=document.elementFromPoint(l.clientX,l.clientY);if(!c||!c.getAttribute)return;var d=c.getAttribute(IE),u=(o=c.getAttribute($E))!==null&&o!==void 0?o:void 0,f=xB(t.getState(),d,u);t.dispatch(AI({activeDataKey:u,activeIndex:d,activeCoordinate:f}))}}}});var yB=nw({brush:Dz,cartesianAxis:mz,chartData:dL,errorBars:Nz,graphicalItems:X8,layout:yE,legend:d3,options:sL,polarAxis:P8,polarOptions:mB,referenceElements:Rz,rootProps:pB,tooltip:EI,zIndex:G$}),vB=function(t){return U4({reducer:yB,preloadedState:t,middleware:r=>r({serializableCheck:!1}).concat([mk.middleware,gk.middleware,cg.middleware,jk.middleware,Sk.middleware]),enhancers:r=>{var n=r;return typeof r=="function"&&(n=r()),n.concat(mw({type:"raf"}))},devTools:Ci.devToolsEnabled})};function bB(e){var{preloadedState:t,children:r,reduxStoreName:n}=e,i=pt(),s=h.useRef(null);if(i)return r;s.current==null&&(s.current=vB(t));var o=Hh;return h.createElement(dB,{context:o,store:s.current},r)}function jB(e){var{layout:t,margin:r}=e,n=Ve(),i=pt();return h.useEffect(()=>{i||(n(mE(t)),n(hE(r)))},[n,i,t,r]),null}function wB(e){var t=Ve();return h.useEffect(()=>{t(hB(e))},[t,e]),null}function yv(e){var{zIndex:t,isPanorama:r}=e,n=r?"recharts-zindex-panorama-":"recharts-zindex-",i=BN("".concat(n).concat(t)),s=Ve();return h.useLayoutEffect(()=>(s(V$({zIndex:t,elementId:i,isPanorama:r})),()=>{s(Y$({zIndex:t,isPanorama:r}))}),[s,t,i,r]),h.createElement("g",{id:i})}function vv(e){var{children:t,isPanorama:r}=e,n=G(L$);if(!n||n.length===0)return t;var i=n.filter(o=>o<0),s=n.filter(o=>o>0);return h.createElement(h.Fragment,null,i.map(o=>h.createElement(yv,{key:o,zIndex:o,isPanorama:r})),t,s.map(o=>h.createElement(yv,{key:o,zIndex:o,isPanorama:r})))}var SB=["children"];function NB(e,t){if(e==null)return{};var r,n,i=kB(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var r=Fw(),n=Ww(),i=qw();if(!Er(r)||!Er(n))return null;var{children:s,otherAttributes:o,title:l,desc:c}=e,d,u;return o!=null&&(typeof o.tabIndex=="number"?d=o.tabIndex:d=i?0:void 0,typeof o.role=="string"?u=o.role:u=i?"application":void 0),h.createElement(lj,Sc({},o,{title:l,desc:c,role:u,tabIndex:d,width:r,height:n,style:PB,ref:t}),s)}),CB=e=>{var{children:t}=e,r=G(cu);if(!r)return null;var{width:n,height:i,y:s,x:o}=r;return h.createElement(lj,{width:n,height:i,x:o,y:s},t)},bv=h.forwardRef((e,t)=>{var{children:r}=e,n=NB(e,SB),i=pt();return i?h.createElement(CB,null,h.createElement(vv,{isPanorama:!0},r)):h.createElement(_B,Sc({ref:t},n),h.createElement(vv,{isPanorama:!1},r))});function AB(){var e=Ve(),[t,r]=h.useState(null),n=G(ME);return h.useEffect(()=>{if(t!=null){var i=t.getBoundingClientRect(),s=i.width/t.offsetWidth;_e(s)&&s!==n&&e(xE(s))}},[t,e,n]),r}function jv(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function OB(e){for(var t=1;t(bL(),null);function Nc(e){if(typeof e=="number")return e;if(typeof e=="string"){var t=parseFloat(e);if(!Number.isNaN(t))return t}return 0}var IB=h.forwardRef((e,t)=>{var r,n,i=h.useRef(null),[s,o]=h.useState({containerWidth:Nc((r=e.style)===null||r===void 0?void 0:r.width),containerHeight:Nc((n=e.style)===null||n===void 0?void 0:n.height)}),l=h.useCallback((d,u)=>{o(f=>{var p=Math.round(d),m=Math.round(u);return f.containerWidth===p&&f.containerHeight===m?f:{containerWidth:p,containerHeight:m}})},[]),c=h.useCallback(d=>{if(typeof t=="function"&&t(d),d!=null&&typeof ResizeObserver<"u"){var{width:u,height:f}=d.getBoundingClientRect();l(u,f);var p=x=>{var{width:g,height:b}=x[0].contentRect;l(g,b)},m=new ResizeObserver(p);m.observe(d),i.current=m}},[t,l]);return h.useEffect(()=>()=>{var d=i.current;d!=null&&d.disconnect()},[l]),h.createElement(h.Fragment,null,h.createElement(du,{width:s.containerWidth,height:s.containerHeight}),h.createElement("div",Ni({ref:c},e)))}),$B=h.forwardRef((e,t)=>{var{width:r,height:n}=e,[i,s]=h.useState({containerWidth:Nc(r),containerHeight:Nc(n)}),o=h.useCallback((c,d)=>{s(u=>{var f=Math.round(c),p=Math.round(d);return u.containerWidth===f&&u.containerHeight===p?u:{containerWidth:f,containerHeight:p}})},[]),l=h.useCallback(c=>{if(typeof t=="function"&&t(c),c!=null){var{width:d,height:u}=c.getBoundingClientRect();o(d,u)}},[t,o]);return h.createElement(h.Fragment,null,h.createElement(du,{width:i.containerWidth,height:i.containerHeight}),h.createElement("div",Ni({ref:l},e)))}),LB=h.forwardRef((e,t)=>{var{width:r,height:n}=e;return h.createElement(h.Fragment,null,h.createElement(du,{width:r,height:n}),h.createElement("div",Ni({ref:t},e)))}),zB=h.forwardRef((e,t)=>{var{width:r,height:n}=e;return Qr(r)||Qr(n)?h.createElement($B,Ni({},e,{ref:t})):h.createElement(LB,Ni({},e,{ref:t}))});function RB(e){return e===!0?IB:zB}var BB=h.forwardRef((e,t)=>{var{children:r,className:n,height:i,onClick:s,onContextMenu:o,onDoubleClick:l,onMouseDown:c,onMouseEnter:d,onMouseLeave:u,onMouseMove:f,onMouseUp:p,onTouchEnd:m,onTouchMove:x,onTouchStart:g,style:b,width:v,responsive:j,dispatchTouchEvents:y=!0}=e,w=h.useRef(null),S=Ve(),[N,P]=h.useState(null),[_,T]=h.useState(null),$=AB(),M=Qh(),C=(M==null?void 0:M.width)>0?M.width:v,R=(M==null?void 0:M.height)>0?M.height:i,q=h.useCallback(z=>{$(z),typeof t=="function"&&t(z),P(z),T(z),z!=null&&(w.current=z)},[$,t,P,T]),Z=h.useCallback(z=>{S(hk(z)),S(Vt({handler:s,reactEvent:z}))},[S,s]),E=h.useCallback(z=>{S(Np(z)),S(Vt({handler:d,reactEvent:z}))},[S,d]),D=h.useCallback(z=>{S(H2()),S(Vt({handler:u,reactEvent:z}))},[S,u]),O=h.useCallback(z=>{S(Np(z)),S(Vt({handler:f,reactEvent:z}))},[S,f]),k=h.useCallback(()=>{S(bk())},[S]),L=h.useCallback(z=>{S(vk(z.key))},[S]),U=h.useCallback(z=>{S(Vt({handler:o,reactEvent:z}))},[S,o]),H=h.useCallback(z=>{S(Vt({handler:l,reactEvent:z}))},[S,l]),te=h.useCallback(z=>{S(Vt({handler:c,reactEvent:z}))},[S,c]),re=h.useCallback(z=>{S(Vt({handler:p,reactEvent:z}))},[S,p]),we=h.useCallback(z=>{S(Vt({handler:g,reactEvent:z}))},[S,g]),A=h.useCallback(z=>{y&&S(wk(z)),S(Vt({handler:x,reactEvent:z}))},[S,y,x]),J=h.useCallback(z=>{S(Vt({handler:m,reactEvent:z}))},[S,m]),Ot=RB(j);return h.createElement(pN.Provider,{value:N},h.createElement(a6.Provider,{value:_},h.createElement(Ot,{width:C??(b==null?void 0:b.width),height:R??(b==null?void 0:b.height),className:ce("recharts-wrapper",n),style:OB({position:"relative",cursor:"default",width:C,height:R},b),onClick:Z,onContextMenu:U,onDoubleClick:H,onFocus:k,onKeyDown:L,onMouseDown:te,onMouseEnter:E,onMouseLeave:D,onMouseMove:O,onMouseUp:re,onTouchEnd:J,onTouchMove:A,onTouchStart:we,ref:q},h.createElement(MB,null),r)))}),FB=["width","height","responsive","children","className","style","compact","title","desc"];function WB(e,t){if(e==null)return{};var r,n,i=UB(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n{var{width:r,height:n,responsive:i,children:s,className:o,style:l,compact:c,title:d,desc:u}=e,f=WB(e,FB),p=nr(f);return c?h.createElement(h.Fragment,null,h.createElement(du,{width:r,height:n}),h.createElement(bv,{otherAttributes:p,title:d,desc:u},s)):h.createElement(BB,{className:o,style:l,width:r,height:n,responsive:i??!1,onClick:e.onClick,onMouseLeave:e.onMouseLeave,onMouseEnter:e.onMouseEnter,onMouseMove:e.onMouseMove,onMouseDown:e.onMouseDown,onMouseUp:e.onMouseUp,onContextMenu:e.onContextMenu,onDoubleClick:e.onDoubleClick,onTouchStart:e.onTouchStart,onTouchMove:e.onTouchMove,onTouchEnd:e.onTouchEnd},h.createElement(bv,{otherAttributes:p,title:d,desc:u,ref:t},h.createElement(Fz,null,s)))});function kp(){return kp=Object.assign?Object.assign.bind():function(e){for(var t=1;th.createElement(Nk,{chartName:"LineChart",defaultTooltipEventType:"axis",validateTooltipEventTypes:VB,tooltipPayloadSearcher:mN,categoricalChartProps:e,ref:t})),GB=["axis"],ZB=h.forwardRef((e,t)=>h.createElement(Nk,{chartName:"AreaChart",defaultTooltipEventType:"axis",validateTooltipEventTypes:GB,tooltipPayloadSearcher:mN,categoricalChartProps:e,ref:t}));function XB(){var b,v,j,y,w,S,N,P,_,T,$,M,C,R,q,Z,E,D,O,k,L;const[e,t]=h.useState(null),[r,n]=h.useState(null),[i,s]=h.useState(!0),[o,l]=h.useState(0),[c,d]=h.useState(!1);h.useEffect(()=>{f(),u()},[]);const u=async()=>{try{const H=(await B.getChangeStats()).pending_count;l(H);const te=localStorage.getItem("dismissedPendingChangesCount"),re=te&&parseInt(te)>=H;d(H>0&&!re)}catch(U){console.error("Failed to load change stats:",U),d(!1)}},f=async()=>{try{const[U,H]=await Promise.all([B.getDashboardStats(),B.getDashboardActivity()]);t(U),n(H)}catch(U){console.error("Failed to load dashboard:",U)}finally{s(!1)}},p=()=>{localStorage.setItem("dismissedPendingChangesCount",o.toString()),d(!1)};if(i)return a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx(Xt,{className:"w-8 h-8 animate-spin text-gray-400"})})});const m=Math.round(((b=e==null?void 0:e.products)==null?void 0:b.with_images)/((v=e==null?void 0:e.products)==null?void 0:v.total)*100)||0;Math.round(((j=e==null?void 0:e.stores)==null?void 0:j.active)/((y=e==null?void 0:e.stores)==null?void 0:y.total)*100);const x=[{date:"Mon",products:120},{date:"Tue",products:145},{date:"Wed",products:132},{date:"Thu",products:178},{date:"Fri",products:195},{date:"Sat",products:210},{date:"Sun",products:((w=e==null?void 0:e.products)==null?void 0:w.total)||225}],g=[{time:"00:00",scrapes:5},{time:"04:00",scrapes:12},{time:"08:00",scrapes:18},{time:"12:00",scrapes:25},{time:"16:00",scrapes:30},{time:"20:00",scrapes:22},{time:"24:00",scrapes:((S=r==null?void 0:r.recent_scrapes)==null?void 0:S.length)||15}];return a.jsxs(X,{children:[c&&a.jsx("div",{className:"mb-6 bg-amber-50 border-l-4 border-amber-500 rounded-lg p-4",children:a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("div",{className:"flex items-center gap-3 flex-1",children:[a.jsx(nj,{className:"w-5 h-5 text-amber-600 flex-shrink-0"}),a.jsxs("div",{className:"flex-1",children:[a.jsxs("h3",{className:"text-sm font-semibold text-amber-900",children:[o," pending change",o!==1?"s":""," require review"]}),a.jsx("p",{className:"text-sm text-amber-700 mt-0.5",children:"Proposed changes to dispensary data are waiting for approval"})]})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("button",{className:"btn btn-sm bg-amber-600 hover:bg-amber-700 text-white border-none",children:"Review Changes"}),a.jsx("button",{onClick:p,className:"btn btn-sm btn-ghost text-amber-900 hover:bg-amber-100","aria-label":"Dismiss notification",children:a.jsx(ij,{className:"w-4 h-4"})})]})]})}),a.jsxs("div",{className:"space-y-8",children:[a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:"Dashboard"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Monitor your dispensary data aggregation"})]}),a.jsxs("button",{onClick:f,className:"inline-flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium text-gray-700",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-blue-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-green-600",children:[a.jsx(zn,{className:"w-3 h-3"}),a.jsx("span",{children:"12.5%"})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((P=(N=e==null?void 0:e.products)==null?void 0:N.total)==null?void 0:P.toLocaleString())||0}),a.jsxs("p",{className:"text-xs text-gray-500",children:[((_=e==null?void 0:e.products)==null?void 0:_.in_stock)||0," in stock"]})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-emerald-50 rounded-lg",children:a.jsx(Il,{className:"w-5 h-5 text-emerald-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-green-600",children:[a.jsx(zn,{className:"w-3 h-3"}),a.jsx("span",{children:"8.2%"})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Total Dispensaries"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((T=e==null?void 0:e.stores)==null?void 0:T.total)||0}),a.jsxs("p",{className:"text-xs text-gray-500",children:[(($=e==null?void 0:e.stores)==null?void 0:$.active)||0," active (crawlable)"]})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(rj,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-red-600",children:[a.jsx(FO,{className:"w-3 h-3"}),a.jsx("span",{children:"3.1%"})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Active Campaigns"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((M=e==null?void 0:e.campaigns)==null?void 0:M.active)||0}),a.jsxs("p",{className:"text-xs text-gray-500",children:[((C=e==null?void 0:e.campaigns)==null?void 0:C.total)||0," total campaigns"]})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-amber-50 rounded-lg",children:a.jsx(lO,{className:"w-5 h-5 text-amber-600"})}),a.jsxs("span",{className:"text-xs font-medium text-gray-600",children:[m,"%"]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Images Downloaded"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((q=(R=e==null?void 0:e.products)==null?void 0:R.with_images)==null?void 0:q.toLocaleString())||0}),a.jsx("div",{className:"mt-3",children:a.jsx("div",{className:"w-full bg-gray-100 rounded-full h-1.5",children:a.jsx("div",{className:"bg-amber-500 h-1.5 rounded-full transition-all",style:{width:`${m}%`}})})})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-cyan-50 rounded-lg",children:a.jsx(vO,{className:"w-5 h-5 text-cyan-600"})}),a.jsx(xr,{className:"w-4 h-4 text-gray-400"})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Clicks (24h)"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((E=(Z=e==null?void 0:e.clicks)==null?void 0:Z.clicks_24h)==null?void 0:E.toLocaleString())||0}),a.jsx("p",{className:"text-xs text-gray-500",children:"Last 24 hours"})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-indigo-50 rounded-lg",children:a.jsx(Cr,{className:"w-5 h-5 text-indigo-600"})}),a.jsx(Ds,{className:"w-4 h-4 text-gray-400"})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:((D=e==null?void 0:e.brands)==null?void 0:D.total)||((O=e==null?void 0:e.products)==null?void 0:O.unique_brands)||0}),a.jsx("p",{className:"text-xs text-gray-500",children:"Unique brands tracked"})]})]})]}),a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"mb-6",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Product Growth"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Weekly product count trend"})]}),a.jsx(p0,{width:"100%",height:200,children:a.jsxs(ZB,{data:x,children:[a.jsx("defs",{children:a.jsxs("linearGradient",{id:"productGradient",x1:"0",y1:"0",x2:"0",y2:"1",children:[a.jsx("stop",{offset:"5%",stopColor:"#3b82f6",stopOpacity:.1}),a.jsx("stop",{offset:"95%",stopColor:"#3b82f6",stopOpacity:0})]})}),a.jsx(vp,{strokeDasharray:"3 3",stroke:"#f1f5f9"}),a.jsx(jp,{dataKey:"date",tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(Sp,{tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(By,{contentStyle:{backgroundColor:"#ffffff",border:"1px solid #e2e8f0",borderRadius:"8px",fontSize:"12px"}}),a.jsx(pk,{type:"monotone",dataKey:"products",stroke:"#3b82f6",strokeWidth:2,fill:"url(#productGradient)"})]})})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"mb-6",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Scrape Activity"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Scrapes over the last 24 hours"})]}),a.jsx(p0,{width:"100%",height:200,children:a.jsxs(YB,{data:g,children:[a.jsx(vp,{strokeDasharray:"3 3",stroke:"#f1f5f9"}),a.jsx(jp,{dataKey:"time",tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(Sp,{tick:{fill:"#94a3b8",fontSize:12},axisLine:{stroke:"#e2e8f0"}}),a.jsx(By,{contentStyle:{backgroundColor:"#ffffff",border:"1px solid #e2e8f0",borderRadius:"8px",fontSize:"12px"}}),a.jsx(ak,{type:"monotone",dataKey:"scrapes",stroke:"#10b981",strokeWidth:2,dot:{fill:"#10b981",r:4}})]})})]})]}),a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200",children:[a.jsxs("div",{className:"px-6 py-4 border-b border-gray-200",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Recent Scrapes"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Latest data collection activities"})]}),a.jsx("div",{className:"divide-y divide-gray-100",children:((k=r==null?void 0:r.recent_scrapes)==null?void 0:k.length)>0?r.recent_scrapes.slice(0,5).map((U,H)=>a.jsx("div",{className:"px-6 py-4 hover:bg-gray-50 transition-colors",children:a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsx("p",{className:"text-sm font-medium text-gray-900 truncate",children:U.name}),a.jsx("p",{className:"text-xs text-gray-500 mt-1",children:new Date(U.last_scraped_at).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})})]}),a.jsx("div",{className:"ml-4 flex-shrink-0",children:a.jsxs("span",{className:"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700",children:[U.product_count," products"]})})]})},H)):a.jsxs("div",{className:"px-6 py-12 text-center",children:[a.jsx(Ds,{className:"w-8 h-8 text-gray-300 mx-auto mb-2"}),a.jsx("p",{className:"text-sm text-gray-500",children:"No recent scrapes"})]})})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200",children:[a.jsxs("div",{className:"px-6 py-4 border-b border-gray-200",children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:"Recent Products"}),a.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Newly added to inventory"})]}),a.jsx("div",{className:"divide-y divide-gray-100",children:((L=r==null?void 0:r.recent_products)==null?void 0:L.length)>0?r.recent_products.slice(0,5).map((U,H)=>a.jsx("div",{className:"px-6 py-4 hover:bg-gray-50 transition-colors",children:a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsx("p",{className:"text-sm font-medium text-gray-900 truncate",children:U.name}),a.jsx("p",{className:"text-xs text-gray-500 mt-1",children:U.store_name})]}),U.price&&a.jsx("div",{className:"ml-4 flex-shrink-0",children:a.jsxs("span",{className:"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700",children:["$",U.price]})})]})},H)):a.jsxs("div",{className:"px-6 py-12 text-center",children:[a.jsx(Ct,{className:"w-8 h-8 text-gray-300 mx-auto mb-2"}),a.jsx("p",{className:"text-sm text-gray-500",children:"No recent products"})]})})]})]})]})]})}function JB(){const[e,t]=nA(),r=dt(),[n,i]=h.useState([]),[s,o]=h.useState([]),[l,c]=h.useState([]),[d,u]=h.useState(!1),[f,p]=h.useState(""),[m,x]=h.useState(""),[g,b]=h.useState(""),[v,j]=h.useState(""),[y,w]=h.useState(0),[S,N]=h.useState(0),P=50;h.useEffect(()=>{const k=e.get("store");k&&x(k),T()},[]),h.useEffect(()=>{m&&($(),_())},[f,m,g,v,S]);const _=async()=>{try{const k=await B.getCategoryTree(parseInt(m));c(k.categories||[])}catch(k){console.error("Failed to load categories:",k)}},T=async()=>{try{const k=await B.getStores();o(k.stores)}catch(k){console.error("Failed to load stores:",k)}},$=async()=>{u(!0);try{const k={limit:P,offset:S,store_id:m};f&&(k.search=f),g&&(k.category_id=g),v&&(k.in_stock=v);const L=await B.getProducts(k);i(L.products),w(L.total)}catch(k){console.error("Failed to load products:",k)}finally{u(!1)}},M=k=>{p(k),N(0)},C=k=>{x(k),b(""),N(0),p(""),t(k?{store:k}:{})},R=k=>{b(k),N(0)},q=(k,L=0)=>k.map(U=>a.jsxs("div",{style:{marginLeft:`${L*20}px`},children:[a.jsxs("button",{onClick:()=>R(U.id.toString()),style:{width:"100%",textAlign:"left",padding:"10px 15px",background:g===U.id.toString()?"#667eea":"transparent",color:g===U.id.toString()?"white":"#333",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:L===0?"600":"400",fontSize:L===0?"15px":"14px",marginBottom:"4px",transition:"all 0.2s"},onMouseEnter:H=>{g!==U.id.toString()&&(H.currentTarget.style.background="#f5f5f5")},onMouseLeave:H=>{g!==U.id.toString()&&(H.currentTarget.style.background="transparent")},children:[U.name," (",U.product_count||0,")"]}),U.children&&U.children.length>0&&q(U.children,L+1)]},U.id)),Z=l.find(k=>k.id.toString()===g),E=()=>{S+P{S>0&&N(Math.max(0,S-P))},O=s.find(k=>k.id.toString()===m);return a.jsx(X,{children:a.jsxs("div",{children:[a.jsx("h1",{style:{fontSize:"32px",marginBottom:"30px"},children:"Products"}),a.jsxs("div",{style:{background:"white",padding:"30px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"15px",fontSize:"18px",fontWeight:"600",color:"#333"},children:"Select a Store to View Products:"}),a.jsxs("select",{value:m,onChange:k=>C(k.target.value),style:{width:"100%",padding:"15px",border:"2px solid #667eea",borderRadius:"8px",fontSize:"16px",fontWeight:"500",cursor:"pointer",background:"white"},children:[a.jsx("option",{value:"",children:"-- Select a Store --"}),s.map(k=>a.jsx("option",{value:k.id,children:k.name},k.id))]})]}),m?a.jsxs(a.Fragment,{children:[a.jsxs("div",{style:{background:"#667eea",color:"white",padding:"20px",borderRadius:"8px",marginBottom:"20px",display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{children:[a.jsx("h2",{style:{margin:0,fontSize:"24px"},children:O==null?void 0:O.name}),Z&&a.jsx("div",{style:{marginTop:"8px",fontSize:"14px",opacity:.9},children:Z.name})]}),a.jsxs("div",{style:{fontSize:"18px",fontWeight:"bold"},children:[y," Products"]})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"280px 1fr",gap:"20px"},children:[a.jsx("div",{children:a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",position:"sticky",top:"20px"},children:[a.jsx("h3",{style:{margin:"0 0 16px 0",fontSize:"18px",fontWeight:"600",color:"#333"},children:"Categories"}),a.jsxs("button",{onClick:()=>R(""),style:{width:"100%",textAlign:"left",padding:"10px 15px",background:g===""?"#667eea":"transparent",color:g===""?"white":"#333",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600",fontSize:"15px",marginBottom:"12px"},children:["All Products (",y,")"]}),a.jsx("div",{style:{borderTop:"1px solid #eee",marginBottom:"12px"}}),q(l)]})}),a.jsxs("div",{children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"20px",display:"flex",gap:"15px",flexWrap:"wrap"},children:[a.jsx("input",{type:"text",placeholder:"Search products...",value:f,onChange:k=>M(k.target.value),style:{flex:"1",minWidth:"200px",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}}),a.jsxs("select",{value:v,onChange:k=>{j(k.target.value),N(0)},style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"",children:"All Products"}),a.jsx("option",{value:"true",children:"In Stock"}),a.jsx("option",{value:"false",children:"Out of Stock"})]})]}),d?a.jsx("div",{style:{textAlign:"center",padding:"40px"},children:"Loading..."}):a.jsxs(a.Fragment,{children:[a.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(250px, 1fr))",gap:"20px",marginBottom:"20px"},children:n.map(k=>a.jsx(QB,{product:k,onViewDetails:()=>r(`/products/${k.id}`)},k.id))}),n.length===0&&a.jsx("div",{style:{background:"white",padding:"40px",borderRadius:"8px",textAlign:"center",color:"#999"},children:"No products found"}),y>P&&a.jsxs("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",gap:"15px",marginTop:"30px"},children:[a.jsx("button",{onClick:D,disabled:S===0,style:{padding:"10px 20px",background:S===0?"#ddd":"#667eea",color:S===0?"#999":"white",border:"none",borderRadius:"6px",cursor:S===0?"not-allowed":"pointer"},children:"Previous"}),a.jsxs("span",{children:["Showing ",S+1," - ",Math.min(S+P,y)," of ",y]}),a.jsx("button",{onClick:E,disabled:S+P>=y,style:{padding:"10px 20px",background:S+P>=y?"#ddd":"#667eea",color:S+P>=y?"#999":"white",border:"none",borderRadius:"6px",cursor:S+P>=y?"not-allowed":"pointer"},children:"Next"})]})]})]})]})]}):a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"🏪"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"Please select a store from the dropdown above to view products"})]})]})})}function QB({product:e,onViewDetails:t}){const r=n=>{if(!n)return"Never";const i=new Date(n),o=new Date().getTime()-i.getTime(),l=Math.floor(o/(1e3*60*60*24));return l===0?"Today":l===1?"Yesterday":l<7?`${l} days ago`:i.toLocaleDateString()};return a.jsxs("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden",transition:"transform 0.2s"},onMouseEnter:n=>n.currentTarget.style.transform="translateY(-4px)",onMouseLeave:n=>n.currentTarget.style.transform="translateY(0)",children:[e.image_url_full?a.jsx("img",{src:e.image_url_full,alt:e.name,style:{width:"100%",height:"200px",objectFit:"cover",background:"#f5f5f5"},onError:n=>{n.currentTarget.src='data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="200" height="200"%3E%3Crect fill="%23ddd" width="200" height="200"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23999"%3ENo Image%3C/text%3E%3C/svg%3E'}}):a.jsx("div",{style:{width:"100%",height:"200px",background:"#f5f5f5",display:"flex",alignItems:"center",justifyContent:"center",color:"#999"},children:"No Image"}),a.jsxs("div",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"500",marginBottom:"8px",fontSize:"14px",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.name}),a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"8px"},children:[a.jsx("div",{style:{fontWeight:"bold",color:"#667eea"},children:e.price?`$${e.price}`:"N/A"}),a.jsx("div",{style:{padding:"4px 8px",borderRadius:"4px",fontSize:"12px",background:e.in_stock?"#d4edda":"#f8d7da",color:e.in_stock?"#155724":"#721c24"},children:e.in_stock?"In Stock":"Out of Stock"})]}),a.jsxs("div",{style:{fontSize:"11px",color:"#888",marginBottom:"12px",borderTop:"1px solid #eee",paddingTop:"8px"},children:["Last Updated: ",r(e.last_seen_at)]}),a.jsxs("div",{style:{display:"flex",gap:"8px"},children:[e.dutchie_url&&a.jsx("a",{href:e.dutchie_url,target:"_blank",rel:"noopener noreferrer",style:{flex:1,padding:"8px 12px",background:"#f0f0f0",color:"#333",textDecoration:"none",borderRadius:"6px",fontSize:"12px",fontWeight:"500",textAlign:"center",border:"1px solid #ddd"},onClick:n=>n.stopPropagation(),children:"Dutchie"}),a.jsx("button",{onClick:n=>{n.stopPropagation(),t()},style:{flex:1,padding:"8px 12px",background:"#667eea",color:"white",border:"none",borderRadius:"6px",fontSize:"12px",fontWeight:"500",cursor:"pointer"},children:"Details"})]})]})]})}function e7(){const{id:e}=Na(),t=dt(),[r,n]=h.useState(null),[i,s]=h.useState(!0),[o,l]=h.useState(null);h.useEffect(()=>{c()},[e]);const c=async()=>{if(e){s(!0),l(null);try{const p=await B.getProduct(parseInt(e));n(p.product)}catch(p){l(p.message||"Failed to load product")}finally{s(!1)}}};if(i)return a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})});if(o||!r)return a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx(Ct,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Product not found"}),a.jsx("p",{className:"text-gray-500 mb-4",children:o}),a.jsx("button",{onClick:()=>t(-1),className:"text-blue-600 hover:text-blue-700",children:"← Go back"})]})});const d=r.metadata||{},f=r.image_url_full?r.image_url_full:r.medium_path?`http://localhost:9020/dutchie/${r.medium_path}`:r.thumbnail_path?`http://localhost:9020/dutchie/${r.thumbnail_path}`:null;return a.jsx(X,{children:a.jsxs("div",{className:"max-w-6xl mx-auto",children:[a.jsxs("button",{onClick:()=>t(-1),className:"flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6",children:[a.jsx(_h,{className:"w-4 h-4"}),"Back"]}),a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 overflow-hidden",children:a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-8 p-6",children:[a.jsx("div",{className:"aspect-square bg-gray-50 rounded-lg overflow-hidden",children:f?a.jsx("img",{src:f,alt:r.name,className:"w-full h-full object-contain"}):a.jsx("div",{className:"w-full h-full flex items-center justify-center text-gray-400",children:a.jsx(Ct,{className:"w-24 h-24"})})}),a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[r.in_stock?a.jsx("span",{className:"px-2 py-1 bg-green-100 text-green-700 text-xs font-medium rounded",children:"In Stock"}):a.jsx("span",{className:"px-2 py-1 bg-red-100 text-red-700 text-xs font-medium rounded",children:"Out of Stock"}),r.strain_type&&a.jsx("span",{className:"px-2 py-1 bg-purple-100 text-purple-700 text-xs font-medium rounded capitalize",children:r.strain_type})]}),a.jsx("h1",{className:"text-2xl font-bold text-gray-900 mb-2",children:r.name}),r.brand&&a.jsx("p",{className:"text-lg text-gray-600 font-medium",children:r.brand}),a.jsxs("div",{className:"flex items-center gap-4 mt-2 text-sm text-gray-500",children:[r.store_name&&a.jsx("span",{children:r.store_name}),r.category_name&&a.jsxs(a.Fragment,{children:[a.jsx("span",{children:"•"}),a.jsx("span",{children:r.category_name})]})]})]}),r.price!==null&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsxs("div",{className:"text-3xl font-bold text-blue-600",children:["$",parseFloat(r.price).toFixed(2)]}),r.weight&&a.jsx("div",{className:"text-sm text-gray-500 mt-1",children:r.weight})]}),(r.thc_percentage||r.cbd_percentage)&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-3",children:"Cannabinoid Content"}),a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[r.thc_percentage!==null&&a.jsxs("div",{className:"bg-green-50 rounded-lg p-3",children:[a.jsx("div",{className:"text-xs text-gray-500 uppercase",children:"THC"}),a.jsxs("div",{className:"text-xl font-bold text-green-600",children:[r.thc_percentage,"%"]})]}),r.cbd_percentage!==null&&a.jsxs("div",{className:"bg-blue-50 rounded-lg p-3",children:[a.jsx("div",{className:"text-xs text-gray-500 uppercase",children:"CBD"}),a.jsxs("div",{className:"text-xl font-bold text-blue-600",children:[r.cbd_percentage,"%"]})]})]})]}),r.description&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Description"}),a.jsx("p",{className:"text-gray-600 text-sm leading-relaxed",children:r.description})]}),d.terpenes&&d.terpenes.length>0&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Terpenes"}),a.jsx("div",{className:"flex flex-wrap gap-2",children:d.terpenes.map(p=>a.jsx("span",{className:"px-2 py-1 bg-amber-100 text-amber-700 text-xs font-medium rounded",children:p},p))})]}),d.effects&&d.effects.length>0&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Effects"}),a.jsx("div",{className:"flex flex-wrap gap-2",children:d.effects.map(p=>a.jsx("span",{className:"px-2 py-1 bg-indigo-100 text-indigo-700 text-xs font-medium rounded",children:p},p))})]}),d.flavors&&d.flavors.length>0&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Flavors"}),a.jsx("div",{className:"flex flex-wrap gap-2",children:d.flavors.map(p=>a.jsx("span",{className:"px-2 py-1 bg-pink-100 text-pink-700 text-xs font-medium rounded",children:p},p))})]}),d.lineage&&a.jsxs("div",{className:"border-t border-gray-100 pt-4",children:[a.jsx("h3",{className:"text-sm font-semibold text-gray-700 mb-2",children:"Lineage"}),a.jsx("p",{className:"text-gray-600 text-sm",children:d.lineage})]}),r.dutchie_url&&a.jsx("div",{className:"border-t border-gray-100 pt-4",children:a.jsxs("a",{href:r.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700",children:["View on Dutchie",a.jsx(Jr,{className:"w-4 h-4"})]})}),r.last_seen_at&&a.jsxs("div",{className:"text-xs text-gray-400 pt-4 border-t border-gray-100",children:["Last updated: ",new Date(r.last_seen_at).toLocaleString()]})]})]})})]})})}function t7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(new Set),[o,l]=h.useState(new Set),c=dt();h.useEffect(()=>{d()},[]);const d=async()=>{n(!0);try{const v=await B.getStores();t(v.stores)}catch(v){console.error("Failed to load stores:",v)}finally{n(!1)}},u=v=>{const j=v.match(/(?:^|-)(al|ak|az|ar|ca|co|ct|de|fl|ga|hi|id|il|in|ia|ks|ky|la|me|md|ma|mi|mn|ms|mo|mt|ne|nv|nh|nj|nm|ny|nc|nd|oh|ok|or|pa|ri|sc|sd|tn|tx|ut|vt|va|wa|wv|wi|wy)-/i),y={peoria:"AZ"};let w=j?j[1].toUpperCase():null;if(!w){for(const[S,N]of Object.entries(y))if(v.toLowerCase().includes(S)){w=N;break}}return w||"UNKNOWN"},f=v=>{const j=u(v.slug).toLowerCase(),y=v.name.match(/^([^-]+)/),w=y?y[1].trim().toLowerCase().replace(/\s+/g,"-"):"other";return`/stores/${j}/${w}/${v.slug}`},p=e.reduce((v,j)=>{const y=j.name.match(/^([^-]+)/),w=y?y[1].trim():"Other",S=u(j.slug),P={AZ:"Arizona",FL:"Florida",PA:"Pennsylvania",NJ:"New Jersey",MA:"Massachusetts",IL:"Illinois",NY:"New York",MD:"Maryland",MI:"Michigan",OH:"Ohio",CT:"Connecticut",ME:"Maine",MO:"Missouri",NV:"Nevada",OR:"Oregon",UT:"Utah"}[S]||S;return v[w]||(v[w]={}),v[w][P]||(v[w][P]=[]),v[w][P].push(j),v},{}),m=async(v,j,y)=>{y.stopPropagation();try{await B.updateStore(v,{scrape_enabled:!j}),t(e.map(w=>w.id===v?{...w,scrape_enabled:!j}:w))}catch(w){console.error("Failed to update scraping status:",w)}},x=v=>v?new Date(v).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"2-digit",minute:"2-digit"}):"Never",g=v=>{const j=new Set(i);j.has(v)?j.delete(v):j.add(v),s(j)},b=(v,j)=>{const y=`${v}-${j}`,w=new Set(o);w.has(y)?w.delete(y):w.add(y),l(w)};return r?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsx("div",{className:"flex items-center justify-between",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Il,{className:"w-6 h-6 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:"Stores"}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:[e.length," total stores"]})]})]})}),a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 overflow-hidden",children:a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50 border-b border-gray-200",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Store Name"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Type"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"View Online"}),a.jsx("th",{className:"px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Categories"}),a.jsx("th",{className:"px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Products"}),a.jsx("th",{className:"px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Scraping"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Last Scraped"})]})}),a.jsx("tbody",{children:Object.entries(p).map(([v,j])=>{const y=Object.values(j).flat().length,w=y===1,S=i.has(v);if(w){const _=Object.values(j).flat()[0];return a.jsxs("tr",{className:"border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors",onClick:()=>c(f(_)),title:"Click to view store",children:[a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[_.logo_url?a.jsx("img",{src:_.logo_url,alt:`${_.name} logo`,className:"w-8 h-8 object-contain flex-shrink-0",onError:T=>{T.target.style.display="none"}}):null,a.jsxs("div",{children:[a.jsx("div",{className:"font-semibold text-gray-900",children:_.name}),a.jsx("div",{className:"text-xs text-gray-500",children:_.slug})]})]})}),a.jsx("td",{className:"px-6 py-4",children:a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-blue-50 text-blue-700 rounded",children:"Dutchie"})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("a",{href:_.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700",onClick:T=>T.stopPropagation(),children:[a.jsx("span",{children:"View Online"}),a.jsx(Jr,{className:"w-3 h-3"})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Cr,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:_.category_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Ct,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:_.product_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",onClick:T=>T.stopPropagation(),children:a.jsx("button",{onClick:T=>m(_.id,_.scrape_enabled,T),className:"inline-flex items-center gap-1 text-sm font-medium transition-colors",children:_.scrape_enabled?a.jsxs(a.Fragment,{children:[a.jsx(Ix,{className:"w-5 h-5 text-green-600"}),a.jsx("span",{className:"text-green-600",children:"On"})]}):a.jsxs(a.Fragment,{children:[a.jsx(Mx,{className:"w-5 h-5 text-gray-400"}),a.jsx("span",{className:"text-gray-500",children:"Off"})]})})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(xr,{className:"w-4 h-4 text-gray-400"}),x(_.last_scraped_at)]})})]},_.id)}const N=Object.values(j).flat()[0],P=N==null?void 0:N.logo_url;return a.jsxs(fs.Fragment,{children:[a.jsx("tr",{className:"bg-gray-100 border-b border-gray-200 cursor-pointer hover:bg-gray-150 transition-colors",onClick:()=>g(v),children:a.jsx("td",{colSpan:7,className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx(Cf,{className:`w-5 h-5 text-gray-600 transition-transform ${S?"rotate-90":""}`}),P&&a.jsx("img",{src:P,alt:`${v} logo`,className:"w-8 h-8 object-contain flex-shrink-0",onError:_=>{_.target.style.display="none"}}),a.jsx("span",{className:"text-base font-semibold text-gray-900",children:v}),a.jsxs("span",{className:"text-sm text-gray-500",children:["(",y," stores)"]})]})})}),S&&Object.entries(j).map(([_,T])=>{const $=`${v}-${_}`,M=o.has($);return a.jsxs(fs.Fragment,{children:[a.jsx("tr",{className:"bg-gray-50 border-b border-gray-100 cursor-pointer hover:bg-gray-100 transition-colors",onClick:()=>b(v,_),children:a.jsx("td",{colSpan:7,className:"px-6 py-3 pl-12",children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Cf,{className:`w-4 h-4 text-gray-500 transition-transform ${M?"rotate-90":""}`}),a.jsx("span",{className:"text-sm font-medium text-gray-700",children:_}),a.jsxs("span",{className:"text-xs text-gray-500",children:["(",T.length," locations)"]})]})})}),M&&T.map(C=>a.jsxs("tr",{className:"border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors",onClick:()=>c(f(C)),title:"Click to view store",children:[a.jsx("td",{className:"px-6 py-4 pl-16",children:a.jsxs("div",{children:[a.jsx("div",{className:"font-semibold text-gray-900",children:C.name}),a.jsx("div",{className:"text-xs text-gray-500",children:C.slug})]})}),a.jsx("td",{className:"px-6 py-4",children:a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-blue-50 text-blue-700 rounded",children:"Dutchie"})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("a",{href:C.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700",onClick:R=>R.stopPropagation(),children:[a.jsx("span",{children:"View Online"}),a.jsx(Jr,{className:"w-3 h-3"})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Cr,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:C.category_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",children:a.jsxs("div",{className:"flex items-center justify-center gap-1",children:[a.jsx(Ct,{className:"w-4 h-4 text-gray-400"}),a.jsx("span",{className:"text-sm font-medium text-gray-900",children:C.product_count||0})]})}),a.jsx("td",{className:"px-6 py-4 text-center",onClick:R=>R.stopPropagation(),children:a.jsx("button",{onClick:R=>m(C.id,C.scrape_enabled,R),className:"inline-flex items-center gap-1 text-sm font-medium transition-colors",children:C.scrape_enabled?a.jsxs(a.Fragment,{children:[a.jsx(Ix,{className:"w-5 h-5 text-green-600"}),a.jsx("span",{className:"text-green-600",children:"On"})]}):a.jsxs(a.Fragment,{children:[a.jsx(Mx,{className:"w-5 h-5 text-gray-400"}),a.jsx("span",{className:"text-gray-500",children:"Off"})]})})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(xr,{className:"w-4 h-4 text-gray-400"}),x(C.last_scraped_at)]})})]},C.id))]},`state-${$}`)})]},`chain-${v}`)})})]})})}),e.length===0&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-12 text-center",children:[a.jsx(Il,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-2",children:"No stores found"}),a.jsx("p",{className:"text-gray-500",children:"Start by adding stores to your database"})]})]})})}function r7(){const e=dt(),[t,r]=h.useState([]),[n,i]=h.useState(!0),[s,o]=h.useState(""),[l,c]=h.useState(""),[d,u]=h.useState(null),[f,p]=h.useState({});h.useEffect(()=>{m()},[]);const m=async()=>{i(!0);try{const y=await B.getDispensaries();r(y.dispensaries)}catch(y){console.error("Failed to load dispensaries:",y)}finally{i(!1)}},x=y=>{u(y),p({dba_name:y.dba_name||"",website:y.website||"",phone:y.phone||"",google_rating:y.google_rating||"",google_review_count:y.google_review_count||""})},g=async()=>{if(d)try{await B.updateDispensary(d.id,f),await m(),u(null),p({})}catch(y){console.error("Failed to update dispensary:",y),alert("Failed to update dispensary")}},b=()=>{u(null),p({})},v=t.filter(y=>{const w=s.toLowerCase(),S=!s||y.name.toLowerCase().includes(w)||y.company_name&&y.company_name.toLowerCase().includes(w)||y.dba_name&&y.dba_name.toLowerCase().includes(w),N=!l||y.city===l;return S&&N}),j=Array.from(new Set(t.map(y=>y.city).filter(Boolean))).sort();return a.jsxs(X,{children:[a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Dispensaries"}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:["AZDHS official dispensary directory (",t.length," total)"]})]}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Search"}),a.jsxs("div",{className:"relative",children:[a.jsx(AO,{className:"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"}),a.jsx("input",{type:"text",value:s,onChange:y=>o(y.target.value),placeholder:"Search by name or company...",className:"w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Filter by City"}),a.jsxs("select",{value:l,onChange:y=>c(y.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"",children:"All Cities"}),j.map(y=>a.jsx("option",{value:y,children:y},y))]})]})]})}),n?a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading dispensaries..."})]}):a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 overflow-hidden",children:[a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50 border-b border-gray-200",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Name"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Company"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Address"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"City"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Phone"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Email"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Website"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider",children:"Actions"})]})}),a.jsx("tbody",{className:"divide-y divide-gray-200",children:v.length===0?a.jsx("tr",{children:a.jsx("td",{colSpan:8,className:"px-4 py-8 text-center text-sm text-gray-500",children:"No dispensaries found"})}):v.map(y=>a.jsxs("tr",{className:"hover:bg-gray-50",children:[a.jsx("td",{className:"px-4 py-3",children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Ln,{className:"w-4 h-4 text-gray-400 flex-shrink-0"}),a.jsxs("div",{className:"flex flex-col",children:[a.jsx("span",{className:"text-sm font-medium text-gray-900",children:y.dba_name||y.name}),y.dba_name&&y.google_rating&&a.jsxs("span",{className:"text-xs text-gray-500",children:["⭐ ",y.google_rating," (",y.google_review_count," reviews)"]})]})]})}),a.jsx("td",{className:"px-4 py-3",children:a.jsx("span",{className:"text-sm text-gray-600",children:y.company_name||"-"})}),a.jsx("td",{className:"px-4 py-3",children:a.jsxs("div",{className:"flex items-start gap-1",children:[a.jsx(yi,{className:"w-3 h-3 text-gray-400 flex-shrink-0 mt-0.5"}),a.jsx("span",{className:"text-sm text-gray-600",children:y.address||"-"})]})}),a.jsxs("td",{className:"px-4 py-3",children:[a.jsx("span",{className:"text-sm text-gray-600",children:y.city||"-"}),y.zip&&a.jsxs("span",{className:"text-xs text-gray-400 ml-1",children:["(",y.zip,")"]})]}),a.jsx("td",{className:"px-4 py-3",children:y.phone?a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx(Ah,{className:"w-3 h-3 text-gray-400"}),a.jsx("span",{className:"text-sm text-gray-600",children:y.phone.replace(/(\d{3})(\d{3})(\d{4})/,"($1) $2-$3")})]}):a.jsx("span",{className:"text-sm text-gray-400",children:"-"})}),a.jsx("td",{className:"px-4 py-3",children:y.email?a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx(tj,{className:"w-3 h-3 text-gray-400"}),a.jsx("a",{href:`mailto:${y.email}`,className:"text-sm text-blue-600 hover:text-blue-800",children:y.email})]}):a.jsx("span",{className:"text-sm text-gray-400",children:"-"})}),a.jsx("td",{className:"px-4 py-3",children:y.website?a.jsxs("a",{href:y.website,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(Jr,{className:"w-3 h-3"}),a.jsx("span",{children:"Visit Site"})]}):a.jsx("span",{className:"text-sm text-gray-400",children:"-"})}),a.jsx("td",{className:"px-4 py-3",children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("button",{onClick:()=>x(y),className:"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-lg transition-colors",title:"Edit",children:a.jsx(wO,{className:"w-4 h-4"})}),a.jsx("button",{onClick:()=>{const w=y.city.toLowerCase().replace(/\s+/g,"-");e(`/dispensaries/${y.state}/${w}/${y.slug}`)},className:"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors",title:"View",children:a.jsx(rO,{className:"w-4 h-4"})})]})})]},y.id))})]})}),a.jsx("div",{className:"bg-gray-50 px-4 py-3 border-t border-gray-200",children:a.jsxs("div",{className:"text-sm text-gray-600",children:["Showing ",v.length," of ",t.length," dispensaries"]})})]})]}),d&&a.jsx("div",{className:"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50",children:a.jsxs("div",{className:"bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto",children:[a.jsxs("div",{className:"flex items-center justify-between p-6 border-b border-gray-200",children:[a.jsxs("h2",{className:"text-xl font-bold text-gray-900",children:["Edit Dispensary: ",d.name]}),a.jsx("button",{onClick:b,className:"text-gray-400 hover:text-gray-600",children:a.jsx(ij,{className:"w-6 h-6"})})]}),a.jsxs("div",{className:"p-6 space-y-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"DBA Name (Display Name)"}),a.jsx("input",{type:"text",value:f.dba_name,onChange:y=>p({...f,dba_name:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"e.g., Green Med Wellness"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Website"}),a.jsx("input",{type:"url",value:f.website,onChange:y=>p({...f,website:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"https://example.com"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Phone Number"}),a.jsx("input",{type:"tel",value:f.phone,onChange:y=>p({...f,phone:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"5551234567"})]}),a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Google Rating"}),a.jsx("input",{type:"number",step:"0.1",min:"0",max:"5",value:f.google_rating,onChange:y=>p({...f,google_rating:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"4.5"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Review Count"}),a.jsx("input",{type:"number",min:"0",value:f.google_review_count,onChange:y=>p({...f,google_review_count:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",placeholder:"123"})]})]}),a.jsxs("div",{className:"bg-gray-50 p-4 rounded-lg space-y-2",children:[a.jsxs("div",{className:"text-sm",children:[a.jsx("span",{className:"font-medium text-gray-700",children:"AZDHS Name:"})," ",a.jsx("span",{className:"text-gray-600",children:d.name})]}),a.jsxs("div",{className:"text-sm",children:[a.jsx("span",{className:"font-medium text-gray-700",children:"Address:"})," ",a.jsxs("span",{className:"text-gray-600",children:[d.address,", ",d.city,", ",d.state," ",d.zip]})]})]})]}),a.jsxs("div",{className:"flex items-center justify-end gap-3 p-6 border-t border-gray-200",children:[a.jsx("button",{onClick:b,className:"px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",children:"Cancel"}),a.jsxs("button",{onClick:g,className:"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors",children:[a.jsx(_O,{className:"w-4 h-4"}),"Save Changes"]})]})]})})]})}function n7(){const{state:e,city:t,slug:r}=Na(),n=dt(),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState([]),[u,f]=h.useState([]),[p,m]=h.useState(!0),[x,g]=h.useState("products"),[b,v]=h.useState(!1),[j,y]=h.useState(!1),[w,S]=h.useState(""),[N,P]=h.useState(1),[_]=h.useState(25),T=D=>{if(!D)return"Never";const O=new Date(D),L=new Date().getTime()-O.getTime(),U=Math.floor(L/(1e3*60*60*24));return U===0?"Today":U===1?"Yesterday":U<7?`${U} days ago`:O.toLocaleDateString()};h.useEffect(()=>{$()},[r]);const $=async()=>{m(!0);try{const[D,O,k,L]=await Promise.all([B.getDispensary(r),B.getDispensaryProducts(r).catch(()=>({products:[]})),B.getDispensaryBrands(r).catch(()=>({brands:[]})),B.getDispensarySpecials(r).catch(()=>({specials:[]}))]);s(D),l(O.products),d(k.brands),f(L.specials)}catch(D){console.error("Failed to load dispensary:",D)}finally{m(!1)}},M=async D=>{v(!1),y(!0);try{const O=await fetch(`https://dispos.crawlsy.com/api/dispensaries/${r}/scrape`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${localStorage.getItem("token")}`},body:JSON.stringify({type:D})});if(!O.ok)throw new Error("Failed to trigger scraping");const k=await O.json();alert(`${D.charAt(0).toUpperCase()+D.slice(1)} update started! ${k.message||""}`)}catch(O){console.error("Failed to trigger scraping:",O),alert("Failed to start update. Please try again.")}finally{y(!1)}},C=o.filter(D=>{var k,L,U,H,te;if(!w)return!0;const O=w.toLowerCase();return((k=D.name)==null?void 0:k.toLowerCase().includes(O))||((L=D.brand)==null?void 0:L.toLowerCase().includes(O))||((U=D.variant)==null?void 0:U.toLowerCase().includes(O))||((H=D.description)==null?void 0:H.toLowerCase().includes(O))||((te=D.strain_type)==null?void 0:te.toLowerCase().includes(O))}),R=Math.ceil(C.length/_),q=(N-1)*_,Z=q+_,E=C.slice(q,Z);return h.useEffect(()=>{P(1)},[w]),p?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading dispensary..."})]})}):i?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("button",{onClick:()=>n("/dispensaries"),className:"flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900",children:[a.jsx(_h,{className:"w-4 h-4"}),"Back to Dispensaries"]}),a.jsxs("div",{className:"relative",children:[a.jsxs("button",{onClick:()=>v(!b),disabled:j,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed",children:[a.jsx(Xt,{className:`w-4 h-4 ${j?"animate-spin":""}`}),j?"Updating...":"Update",!j&&a.jsx(ej,{className:"w-4 h-4"})]}),b&&!j&&a.jsxs("div",{className:"absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-10",children:[a.jsx("button",{onClick:()=>M("products"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-t-lg",children:"Products"}),a.jsx("button",{onClick:()=>M("brands"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100",children:"Brands"}),a.jsx("button",{onClick:()=>M("specials"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100",children:"Specials"}),a.jsx("button",{onClick:()=>M("all"),className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-b-lg border-t border-gray-200",children:"All"})]})]})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-start justify-between gap-4 mb-4",children:[a.jsxs("div",{className:"flex items-start gap-4",children:[a.jsx("div",{className:"p-3 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-8 h-8 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:i.dba_name||i.name}),i.company_name&&a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:i.company_name})]})]}),a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600 bg-gray-50 px-4 py-2 rounded-lg",children:[a.jsx(Ch,{className:"w-4 h-4"}),a.jsxs("div",{children:[a.jsx("span",{className:"font-medium",children:"Last Crawl Date:"}),a.jsx("span",{className:"ml-2",children:i.last_menu_scrape?new Date(i.last_menu_scrape).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):"Never"})]})]})]}),a.jsxs("div",{className:"flex flex-wrap gap-4",children:[i.address&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(yi,{className:"w-4 h-4"}),a.jsxs("span",{children:[i.address,", ",i.city,", ",i.state," ",i.zip]})]}),i.phone&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(Ah,{className:"w-4 h-4"}),a.jsx("span",{children:i.phone.replace(/(\d{3})(\d{3})(\d{4})/,"($1) $2-$3")})]}),a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(Jr,{className:"w-4 h-4"}),i.website?a.jsx("a",{href:i.website,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:text-blue-800",children:"Website"}):a.jsx("span",{children:"Website N/A"})]}),i.email&&a.jsxs("div",{className:"flex items-center gap-2 text-sm",children:[a.jsx(tj,{className:"w-4 h-4 text-gray-400"}),a.jsx("a",{href:`mailto:${i.email}`,className:"text-blue-600 hover:text-blue-800",children:i.email})]}),i.azdhs_url&&a.jsxs("a",{href:i.azdhs_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(Jr,{className:"w-4 h-4"}),a.jsx("span",{children:"AZDHS Profile"})]}),a.jsxs(K1,{to:"/schedule",className:"flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(xr,{className:"w-4 h-4"}),a.jsx("span",{children:"View Schedule"})]})]})]}),a.jsxs("div",{className:"grid grid-cols-4 gap-6",children:[a.jsx("button",{onClick:()=>{g("products"),S("")},className:"bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:o.length})]})]})}),a.jsx("button",{onClick:()=>g("brands"),className:"bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(Cr,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:c.length})]})]})}),a.jsx("button",{onClick:()=>g("specials"),className:"bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(zn,{className:"w-5 h-5 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Active Specials"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:u.length})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(QA,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Avg Price"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:o.length>0?`$${(o.reduce((D,O)=>D+(O.sale_price||O.regular_price||0),0)/o.length).toFixed(2)}`:"-"})]})]})})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"border-b border-gray-200",children:a.jsxs("div",{className:"flex gap-4 px-6",children:[a.jsxs("button",{onClick:()=>g("products"),className:`py-4 px-2 text-sm font-medium border-b-2 ${x==="products"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Products (",o.length,")"]}),a.jsxs("button",{onClick:()=>g("brands"),className:`py-4 px-2 text-sm font-medium border-b-2 ${x==="brands"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Brands (",c.length,")"]}),a.jsxs("button",{onClick:()=>g("specials"),className:`py-4 px-2 text-sm font-medium border-b-2 ${x==="specials"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Specials (",u.length,")"]})]})}),a.jsxs("div",{className:"p-6",children:[x==="products"&&a.jsx("div",{className:"space-y-4",children:o.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No products available"}):a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:"flex items-center gap-4 mb-4",children:[a.jsx("input",{type:"text",placeholder:"Search products by name, brand, variant, description, or strain type...",value:w,onChange:D=>S(D.target.value),className:"input input-bordered input-sm flex-1"}),w&&a.jsx("button",{onClick:()=>S(""),className:"btn btn-sm btn-ghost",children:"Clear"}),a.jsxs("div",{className:"text-sm text-gray-600",children:["Showing ",q+1,"-",Math.min(Z,C.length)," of ",C.length," products"]})]}),a.jsx("div",{className:"overflow-x-auto -mx-6 px-6",children:a.jsxs("table",{className:"table table-xs table-zebra table-pin-rows w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Image"}),a.jsx("th",{children:"Product Name"}),a.jsx("th",{children:"Brand"}),a.jsx("th",{children:"Variant"}),a.jsx("th",{children:"Description"}),a.jsx("th",{className:"text-right",children:"Price"}),a.jsx("th",{className:"text-center",children:"THC %"}),a.jsx("th",{className:"text-center",children:"CBD %"}),a.jsx("th",{className:"text-center",children:"Strain Type"}),a.jsx("th",{className:"text-center",children:"In Stock"}),a.jsx("th",{children:"Last Updated"}),a.jsx("th",{children:"Actions"})]})}),a.jsx("tbody",{children:E.map(D=>a.jsxs("tr",{children:[a.jsx("td",{className:"whitespace-nowrap",children:D.image_url?a.jsx("img",{src:D.image_url,alt:D.name,className:"w-12 h-12 object-cover rounded",onError:O=>O.currentTarget.style.display="none"}):"-"}),a.jsx("td",{className:"font-medium max-w-[150px]",children:a.jsx("div",{className:"line-clamp-2",title:D.name,children:D.name})}),a.jsx("td",{className:"max-w-[120px]",children:a.jsx("div",{className:"line-clamp-2",title:D.brand||"-",children:D.brand||"-"})}),a.jsx("td",{className:"max-w-[100px]",children:a.jsx("div",{className:"line-clamp-2",title:D.variant||"-",children:D.variant||"-"})}),a.jsx("td",{className:"w-[120px]",children:a.jsx("span",{title:D.description,children:D.description?D.description.length>15?D.description.substring(0,15)+"...":D.description:"-"})}),a.jsx("td",{className:"text-right font-semibold whitespace-nowrap",children:D.sale_price?a.jsxs("div",{className:"flex flex-col items-end",children:[a.jsxs("span",{className:"text-error",children:["$",D.sale_price]}),a.jsxs("span",{className:"text-gray-400 line-through text-xs",children:["$",D.regular_price]})]}):D.regular_price?`$${D.regular_price}`:"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.thc_percentage?a.jsxs("span",{className:"badge badge-success badge-sm",children:[D.thc_percentage,"%"]}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.cbd_percentage?a.jsxs("span",{className:"badge badge-info badge-sm",children:[D.cbd_percentage,"%"]}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.strain_type?a.jsx("span",{className:"badge badge-ghost badge-sm",children:D.strain_type}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:D.in_stock?a.jsx("span",{className:"badge badge-success badge-sm",children:"Yes"}):D.in_stock===!1?a.jsx("span",{className:"badge badge-error badge-sm",children:"No"}):"-"}),a.jsx("td",{className:"whitespace-nowrap text-xs text-gray-500",children:D.updated_at?T(D.updated_at):"-"}),a.jsx("td",{children:a.jsxs("div",{className:"flex gap-1",children:[D.dutchie_url&&a.jsx("a",{href:D.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"btn btn-xs btn-outline",children:"Dutchie"}),a.jsx("button",{onClick:()=>n(`/products/${D.id}`),className:"btn btn-xs btn-primary",children:"Details"})]})})]},D.id))})]})}),R>1&&a.jsxs("div",{className:"flex justify-center items-center gap-2 mt-4",children:[a.jsx("button",{onClick:()=>P(D=>Math.max(1,D-1)),disabled:N===1,className:"btn btn-sm btn-outline",children:"Previous"}),a.jsx("div",{className:"flex gap-1",children:Array.from({length:R},(D,O)=>O+1).map(D=>{const O=D===1||D===R||D>=N-1&&D<=N+1;return D===2&&N>3||D===R-1&&NP(D),className:`btn btn-sm ${N===D?"btn-primary":"btn-outline"}`,children:D},D):null})}),a.jsx("button",{onClick:()=>P(D=>Math.min(R,D+1)),disabled:N===R,className:"btn btn-sm btn-outline",children:"Next"})]})]})}),x==="brands"&&a.jsx("div",{className:"space-y-4",children:c.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No brands available"}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:c.map(D=>a.jsxs("button",{onClick:()=>{g("products"),S(D.brand)},className:"border border-gray-200 rounded-lg p-4 text-center hover:border-blue-300 hover:shadow-md transition-all cursor-pointer",children:[a.jsx("p",{className:"font-medium text-gray-900 line-clamp-2",children:D.brand}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:[D.product_count," product",D.product_count!==1?"s":""]})]},D.brand))})}),x==="specials"&&a.jsx("div",{className:"space-y-4",children:u.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No active specials"}):a.jsx("div",{className:"space-y-3",children:u.map(D=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4",children:[a.jsx("h4",{className:"font-medium text-gray-900",children:D.name}),D.description&&a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:D.description}),a.jsxs("div",{className:"flex items-center gap-4 mt-2 text-sm text-gray-500",children:[a.jsxs("span",{children:[new Date(D.start_date).toLocaleDateString()," -"," ",D.end_date?new Date(D.end_date).toLocaleDateString():"Ongoing"]}),a.jsxs("span",{children:[D.product_count," products"]})]})]},D.id))})})]})]})]})}):a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-600",children:"Dispensary not found"})})})}function i7(){var $,M;const{slug:e}=Na(),t=dt(),[r,n]=h.useState(null),[i,s]=h.useState([]),[o,l]=h.useState([]),[c,d]=h.useState([]),[u,f]=h.useState(!0),[p,m]=h.useState(null),[x,g]=h.useState(""),[b,v]=h.useState("products"),[j,y]=h.useState("name");h.useEffect(()=>{w()},[e]),h.useEffect(()=>{r&&S()},[p,x,j,r]);const w=async()=>{f(!0);try{const R=(await B.getStores()).stores.find(D=>D.slug===e);if(!R)throw new Error("Store not found");const[q,Z,E]=await Promise.all([B.getStore(R.id),B.getCategories(R.id),B.getStoreBrands(R.id)]);n(q),l(Z.categories||[]),d(E.brands||[])}catch(C){console.error("Failed to load store data:",C)}finally{f(!1)}},S=async()=>{if(r)try{const C={store_id:r.id,limit:1e3};p&&(C.category_id=p),x&&(C.brand=x);let q=(await B.getProducts(C)).products||[];q.sort((Z,E)=>{switch(j){case"name":return(Z.name||"").localeCompare(E.name||"");case"price_asc":return(Z.price||0)-(E.price||0);case"price_desc":return(E.price||0)-(Z.price||0);case"thc":return(E.thc_percentage||0)-(Z.thc_percentage||0);default:return 0}}),s(q)}catch(C){console.error("Failed to load products:",C)}},N=C=>C.image_url_full?C.image_url_full:C.medium_path?`http://localhost:9020/dutchie/${C.medium_path}`:C.thumbnail_path?`http://localhost:9020/dutchie/${C.thumbnail_path}`:"https://via.placeholder.com/300x300?text=No+Image",P=C=>C?new Date(C).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"2-digit",minute:"2-digit"}):"Never",_=C=>{switch(C==null?void 0:C.toLowerCase()){case"dutchie":return"bg-green-100 text-green-700";case"jane":return"bg-purple-100 text-purple-700";case"treez":return"bg-blue-100 text-blue-700";case"weedmaps":return"bg-orange-100 text-orange-700";case"leafly":return"bg-emerald-100 text-emerald-700";default:return"bg-gray-100 text-gray-700"}},T=C=>{switch(C){case"completed":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full flex items-center gap-1",children:[a.jsx(_r,{className:"w-3 h-3"})," Completed"]});case"running":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 rounded-full flex items-center gap-1",children:[a.jsx(Xt,{className:"w-3 h-3 animate-spin"})," Running"]});case"failed":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded-full flex items-center gap-1",children:[a.jsx(Hr,{className:"w-3 h-3"})," Failed"]});case"pending":return a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-700 rounded-full flex items-center gap-1",children:[a.jsx(xr,{className:"w-3 h-3"})," Pending"]});default:return a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-gray-100 text-gray-700 rounded-full",children:C})}};return u?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})}):r?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-start justify-between mb-6",children:[a.jsxs("div",{className:"flex items-start gap-4",children:[a.jsx("button",{onClick:()=>t("/stores"),className:"text-gray-600 hover:text-gray-900 mt-1",children:"← Back"}),a.jsxs("div",{children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:r.name}),a.jsx("span",{className:`px-2 py-1 text-xs font-medium rounded ${_(r.provider)}`,children:r.provider||"Unknown"})]}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:["Store ID: ",r.id]})]})]}),a.jsxs("a",{href:r.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700",children:["View Menu ",a.jsx(Jr,{className:"w-4 h-4"})]})]}),a.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-6",children:[a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[a.jsx(Ct,{className:"w-4 h-4"}),"Products"]}),a.jsx("p",{className:"text-xl font-semibold text-gray-900",children:r.product_count||0})]}),a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[a.jsx(Cr,{className:"w-4 h-4"}),"Categories"]}),a.jsx("p",{className:"text-xl font-semibold text-gray-900",children:r.category_count||0})]}),a.jsxs("div",{className:"p-4 bg-green-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-green-600 text-xs mb-1",children:[a.jsx(_r,{className:"w-4 h-4"}),"In Stock"]}),a.jsx("p",{className:"text-xl font-semibold text-green-700",children:r.in_stock_count||0})]}),a.jsxs("div",{className:"p-4 bg-red-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-red-600 text-xs mb-1",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Out of Stock"]}),a.jsx("p",{className:"text-xl font-semibold text-red-700",children:r.out_of_stock_count||0})]}),a.jsxs("div",{className:`p-4 rounded-lg ${r.is_stale?"bg-yellow-50":"bg-blue-50"}`,children:[a.jsxs("div",{className:`flex items-center gap-2 text-xs mb-1 ${r.is_stale?"text-yellow-600":"text-blue-600"}`,children:[a.jsx(xr,{className:"w-4 h-4"}),"Freshness"]}),a.jsx("p",{className:`text-sm font-semibold ${r.is_stale?"text-yellow-700":"text-blue-700"}`,children:r.freshness||"Never scraped"})]}),a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-500 text-xs mb-1",children:[a.jsx(Ch,{className:"w-4 h-4"}),"Next Crawl"]}),a.jsx("p",{className:"text-sm font-semibold text-gray-700",children:($=r.schedule)!=null&&$.next_run_at?P(r.schedule.next_run_at):"Not scheduled"})]})]}),r.linked_dispensary&&a.jsxs("div",{className:"p-4 bg-indigo-50 rounded-lg mb-6",children:[a.jsxs("div",{className:"flex items-center gap-2 text-indigo-600 text-xs mb-2",children:[a.jsx(WA,{className:"w-4 h-4"}),"Linked Dispensary"]}),a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("p",{className:"font-semibold text-indigo-900",children:r.linked_dispensary.name}),a.jsxs("p",{className:"text-sm text-indigo-700 flex items-center gap-1",children:[a.jsx(yi,{className:"w-3 h-3"}),r.linked_dispensary.city,", ",r.linked_dispensary.state,r.linked_dispensary.address&&` - ${r.linked_dispensary.address}`]})]}),a.jsx("button",{onClick:()=>t(`/dispensaries/${r.linked_dispensary.slug}`),className:"text-sm text-indigo-600 hover:text-indigo-700 font-medium",children:"View Dispensary →"})]})]}),a.jsxs("div",{className:"flex gap-2 border-b border-gray-200",children:[a.jsx("button",{onClick:()=>v("products"),className:`px-4 py-2 border-b-2 transition-colors ${b==="products"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Ct,{className:"w-4 h-4"}),"Products (",i.length,")"]})}),a.jsx("button",{onClick:()=>v("brands"),className:`px-4 py-2 border-b-2 transition-colors ${b==="brands"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Cr,{className:"w-4 h-4"}),"Brands (",c.length,")"]})}),a.jsx("button",{onClick:()=>v("specials"),className:`px-4 py-2 border-b-2 transition-colors ${b==="specials"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx($x,{className:"w-4 h-4"}),"Specials"]})}),a.jsx("button",{onClick:()=>v("crawl-history"),className:`px-4 py-2 border-b-2 transition-colors ${b==="crawl-history"?"border-blue-600 text-blue-600 font-medium":"border-transparent text-gray-600 hover:text-gray-900"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(Ds,{className:"w-4 h-4"}),"Crawl History (",((M=r.recent_jobs)==null?void 0:M.length)||0,")"]})})]})]}),b==="crawl-history"&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 overflow-hidden",children:[a.jsxs("div",{className:"p-4 border-b border-gray-200",children:[a.jsx("h2",{className:"text-lg font-semibold text-gray-900",children:"Recent Crawl Jobs"}),a.jsx("p",{className:"text-sm text-gray-500",children:"Last 10 crawl jobs for this store"})]}),r.recent_jobs&&r.recent_jobs.length>0?a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Status"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Type"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Started"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Completed"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"Found"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"New"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"Updated"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"In Stock"}),a.jsx("th",{className:"px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase",children:"Out of Stock"}),a.jsx("th",{className:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase",children:"Error"})]})}),a.jsx("tbody",{className:"divide-y divide-gray-100",children:r.recent_jobs.map(C=>a.jsxs("tr",{className:"hover:bg-gray-50",children:[a.jsx("td",{className:"px-4 py-3",children:T(C.status)}),a.jsx("td",{className:"px-4 py-3 text-sm text-gray-700",children:C.job_type||"-"}),a.jsx("td",{className:"px-4 py-3 text-sm text-gray-700",children:P(C.started_at)}),a.jsx("td",{className:"px-4 py-3 text-sm text-gray-700",children:P(C.completed_at)}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-gray-900",children:C.products_found??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-green-600",children:C.products_new??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-blue-600",children:C.products_updated??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-green-600",children:C.in_stock_count??"-"}),a.jsx("td",{className:"px-4 py-3 text-center text-sm font-medium text-red-600",children:C.out_of_stock_count??"-"}),a.jsx("td",{className:"px-4 py-3 text-sm text-red-600 max-w-xs truncate",title:C.error_message||"",children:C.error_message||"-"})]},C.id))})]})}):a.jsxs("div",{className:"text-center py-12",children:[a.jsx(Ds,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("p",{className:"text-gray-500",children:"No crawl history available"})]})]}),b==="products"&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 p-4",children:a.jsxs("div",{className:"flex flex-wrap gap-4",children:[a.jsxs("div",{className:"flex-1 min-w-[200px]",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Category"}),a.jsxs("select",{value:p||"",onChange:C=>m(C.target.value?parseInt(C.target.value):null),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"",children:"All Categories"}),o.map(C=>a.jsxs("option",{value:C.id,children:[C.name," (",i.filter(R=>R.category_id===C.id).length,")"]},C.id))]})]}),a.jsxs("div",{className:"flex-1 min-w-[200px]",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Brand"}),a.jsxs("select",{value:x,onChange:C=>g(C.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"",children:"All Brands"}),c.map(C=>a.jsx("option",{value:C,children:C},C))]})]}),a.jsxs("div",{className:"flex-1 min-w-[200px]",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Sort By"}),a.jsxs("select",{value:j,onChange:C=>y(C.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"name",children:"Name (A-Z)"}),a.jsx("option",{value:"price_asc",children:"Price (Low to High)"}),a.jsx("option",{value:"price_desc",children:"Price (High to Low)"}),a.jsx("option",{value:"thc",children:"THC % (High to Low)"})]})]})]})}),i.length>0?a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",children:i.map(C=>a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow",children:[a.jsxs("div",{className:"aspect-square bg-gray-50 relative",children:[a.jsx("img",{src:N(C),alt:C.name,className:"w-full h-full object-cover"}),C.in_stock?a.jsx("span",{className:"absolute top-2 right-2 px-2 py-1 bg-green-500 text-white text-xs font-medium rounded",children:"In Stock"}):a.jsx("span",{className:"absolute top-2 right-2 px-2 py-1 bg-red-500 text-white text-xs font-medium rounded",children:"Out of Stock"})]}),a.jsxs("div",{className:"p-3 space-y-2",children:[a.jsx("h3",{className:"font-semibold text-sm text-gray-900 line-clamp-2",children:C.name}),C.brand&&a.jsx("p",{className:"text-xs text-gray-600 font-medium",children:C.brand}),C.category_name&&a.jsx("p",{className:"text-xs text-gray-500",children:C.category_name}),a.jsxs("div",{className:"grid grid-cols-2 gap-2 pt-2 border-t border-gray-100",children:[C.price!==null&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Price:"}),a.jsxs("span",{className:"ml-1 font-semibold text-blue-600",children:["$",parseFloat(C.price).toFixed(2)]})]}),C.weight&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Weight:"}),a.jsx("span",{className:"ml-1 font-medium",children:C.weight})]}),C.thc_percentage!==null&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"THC:"}),a.jsxs("span",{className:"ml-1 font-medium text-green-600",children:[C.thc_percentage,"%"]})]}),C.cbd_percentage!==null&&a.jsxs("div",{className:"text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"CBD:"}),a.jsxs("span",{className:"ml-1 font-medium text-blue-600",children:[C.cbd_percentage,"%"]})]}),C.strain_type&&a.jsxs("div",{className:"text-xs col-span-2",children:[a.jsx("span",{className:"text-gray-500",children:"Type:"}),a.jsx("span",{className:"ml-1 font-medium capitalize",children:C.strain_type})]})]}),C.description&&a.jsx("p",{className:"text-xs text-gray-600 line-clamp-2 pt-2 border-t border-gray-100",children:C.description}),C.last_seen_at&&a.jsxs("p",{className:"text-xs text-gray-400 pt-2 border-t border-gray-100",children:["Updated: ",new Date(C.last_seen_at).toLocaleDateString()]}),a.jsxs("div",{className:"flex gap-2 mt-3 pt-3 border-t border-gray-100",children:[C.dutchie_url&&a.jsx("a",{href:C.dutchie_url,target:"_blank",rel:"noopener noreferrer",className:"flex-1 px-3 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors text-center border border-gray-200",children:"Dutchie"}),a.jsx("button",{onClick:()=>t(`/products/${C.id}`),className:"flex-1 px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors",children:"Details"})]})]})]},C.id))}):a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-12 text-center",children:[a.jsx(Ct,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-2",children:"No products found"}),a.jsx("p",{className:"text-gray-500",children:"Try adjusting your filters"})]})]}),b==="brands"&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("h2",{className:"text-lg font-semibold text-gray-900 mb-4",children:["All Brands (",c.length,")"]}),c.length>0?a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3",children:c.map(C=>{const R=i.filter(q=>q.brand===C);return a.jsxs("div",{className:"p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all cursor-pointer",onClick:()=>{v("products"),g(C)},children:[a.jsx("p",{className:"font-medium text-gray-900 text-sm",children:C}),a.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:[R.length," products"]})]},C)})}):a.jsxs("div",{className:"text-center py-12",children:[a.jsx(Cr,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("p",{className:"text-gray-500",children:"No brands found"})]})]}),b==="specials"&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsx("h2",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Daily Specials"}),a.jsxs("div",{className:"text-center py-12",children:[a.jsx($x,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("p",{className:"text-gray-500",children:"No specials available"})]})]})]})}):a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Store not found"}),a.jsx("button",{onClick:()=>t("/stores"),className:"text-blue-600 hover:text-blue-700",children:"← Back to stores"})]})})}function a7(){const{state:e,storeName:t,slug:r}=Na(),n=dt(),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState(!0);h.useEffect(()=>{u()},[r]);const u=async()=>{d(!0);try{const p=(await B.getStores()).stores.find(x=>x.slug===r);if(!p)throw new Error("Store not found");s(p);const m=await B.getStoreBrands(p.id);l(m.brands)}catch(f){console.error("Failed to load brands:",f)}finally{d(!1)}};return c?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("span",{className:"loading loading-spinner loading-lg"})})}):i?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsx("button",{onClick:()=>n(`/stores/${e}/${t}/${r}`),className:"btn btn-ghost btn-sm",children:"← Back to Store"}),a.jsxs("div",{className:"flex-1",children:[a.jsxs("h1",{className:"text-3xl font-bold",children:[i.name," - Brands"]}),a.jsxs("div",{className:"flex gap-3 mt-2",children:[a.jsx("a",{href:`${i.dutchie_url}/brands`,target:"_blank",rel:"noopener noreferrer",className:"link link-primary text-sm",children:"View Live Brands Page"}),a.jsx("span",{className:"text-sm text-gray-500",children:"•"}),a.jsxs("span",{className:"text-sm text-gray-500",children:[o.length," ",o.length===1?"Brand":"Brands"]})]})]})]}),o.length>0?a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",children:o.map((f,p)=>a.jsx("div",{className:"card bg-base-100 shadow-md hover:shadow-lg transition-shadow",children:a.jsx("div",{className:"card-body p-6",children:a.jsx("h3",{className:"text-lg font-semibold text-center",children:f})})},p))}):a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body text-center py-12",children:[a.jsx("p",{className:"text-gray-500",children:"No brands found for this store"}),a.jsx("p",{className:"text-sm text-gray-400 mt-2",children:"Brands are automatically extracted from products"})]})})]})}):a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-500",children:"Store not found"})})})}function s7(){const{state:e,storeName:t,slug:r}=Na(),n=dt(),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState(new Date().toISOString().split("T")[0]),[u,f]=h.useState(!0);h.useEffect(()=>{p()},[r,c]);const p=async()=>{f(!0);try{const x=(await B.getStores()).stores.find(b=>b.slug===r);if(!x)throw new Error("Store not found");s(x);const g=await B.getStoreSpecials(x.id,c);l(g.specials)}catch(m){console.error("Failed to load specials:",m)}finally{f(!1)}};return u?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("span",{className:"loading loading-spinner loading-lg"})})}):i?a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsx("button",{onClick:()=>n(`/stores/${e}/${t}/${r}`),className:"btn btn-ghost btn-sm",children:"← Back to Store"}),a.jsxs("div",{className:"flex-1",children:[a.jsxs("h1",{className:"text-3xl font-bold",children:[i.name," - Specials"]}),a.jsxs("div",{className:"flex gap-3 mt-2",children:[a.jsx("a",{href:`${i.dutchie_url}/specials`,target:"_blank",rel:"noopener noreferrer",className:"link link-primary text-sm",children:"View Live Specials Page"}),a.jsx("span",{className:"text-sm text-gray-500",children:"•"}),a.jsxs("span",{className:"text-sm text-gray-500",children:[o.length," ",o.length===1?"Special":"Specials"]})]})]})]}),a.jsx("div",{className:"card bg-base-100 shadow-md",children:a.jsx("div",{className:"card-body p-4",children:a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsx("label",{className:"font-semibold",children:"Select Date:"}),a.jsx("input",{type:"date",value:c,onChange:m=>d(m.target.value),className:"input input-bordered input-sm"})]})})}),o.length>0?a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:o.map((m,x)=>a.jsx("div",{className:"card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h3",{className:"card-title text-lg",children:m.name}),m.description&&a.jsx("p",{className:"text-sm text-gray-600",children:m.description}),a.jsxs("div",{className:"space-y-2 mt-2",children:[m.original_price&&a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsx("span",{className:"text-sm opacity-60",children:"Original Price:"}),a.jsxs("span",{className:"line-through text-gray-500",children:["$",parseFloat(m.original_price).toFixed(2)]})]}),m.special_price&&a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsx("span",{className:"text-sm opacity-60",children:"Special Price:"}),a.jsxs("span",{className:"text-lg font-bold text-success",children:["$",parseFloat(m.special_price).toFixed(2)]})]}),m.discount_percentage&&a.jsxs("div",{className:"badge badge-success badge-lg",children:[m.discount_percentage,"% OFF"]}),m.discount_amount&&a.jsxs("div",{className:"badge badge-info badge-lg",children:["Save $",parseFloat(m.discount_amount).toFixed(2)]})]})]})},x))}):a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body text-center py-12",children:[a.jsxs("p",{className:"text-gray-500",children:["No specials found for ",c]}),a.jsx("p",{className:"text-sm text-gray-400 mt-2",children:"Try selecting a different date"})]})})]})}):a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-500",children:"Store not found"})})})}function o7(){const[e,t]=h.useState([]),[r,n]=h.useState(null),[i,s]=h.useState([]),[o,l]=h.useState(!0);h.useEffect(()=>{c()},[]),h.useEffect(()=>{r&&d()},[r]);const c=async()=>{try{const f=await B.getStores();t(f.stores),f.stores.length>0&&n(f.stores[0].id)}catch(f){console.error("Failed to load stores:",f)}finally{l(!1)}},d=async()=>{if(r)try{const f=await B.getCategoryTree(r);s(f.tree)}catch(f){console.error("Failed to load categories:",f)}};if(o)return a.jsx(X,{children:a.jsx("div",{children:"Loading..."})});const u=e.find(f=>f.id===r);return a.jsx(X,{children:a.jsxs("div",{children:[a.jsx("h1",{style:{fontSize:"32px",marginBottom:"30px"},children:"Categories"}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"10px",fontWeight:"600"},children:"Select Store:"}),a.jsx("select",{value:r||"",onChange:f=>n(parseInt(f.target.value)),style:{width:"100%",padding:"12px",border:"2px solid #667eea",borderRadius:"6px",fontSize:"16px",cursor:"pointer"},children:e.map(f=>a.jsxs("option",{value:f.id,children:[f.name," (",f.category_count||0," categories)"]},f.id))})]}),u&&a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsxs("h2",{style:{marginBottom:"20px",fontSize:"24px"},children:[u.name," - Categories"]}),i.length===0?a.jsx("div",{style:{color:"#999",textAlign:"center",padding:"40px"},children:'No categories found. Run "Discover Categories" from the Stores page.'}):a.jsx("div",{children:i.map(f=>a.jsx(kk,{category:f},f.id))})]})]})})}function kk({category:e,level:t=0}){return a.jsxs("div",{style:{marginLeft:t*30+"px",marginBottom:"10px"},children:[a.jsxs("div",{style:{padding:"12px 15px",background:t===0?"#f0f0ff":"#f8f9fa",borderRadius:"6px",borderLeft:`4px solid ${t===0?"#667eea":"#999"}`,display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{children:[a.jsxs("strong",{style:{fontSize:t===0?"16px":"14px"},children:[t>0&&"└── ",e.name]}),a.jsxs("span",{style:{marginLeft:"10px",color:"#666",fontSize:"12px"},children:["(",e.product_count||0," products)"]})]}),a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:a.jsx("code",{children:e.path})})]}),e.children&&e.children.length>0&&a.jsx("div",{style:{marginTop:"5px"},children:e.children.map(r=>a.jsx(kk,{category:r,level:t+1},r.id))})]})}function Vn({message:e,type:t,onClose:r,duration:n=4e3}){h.useEffect(()=>{const s=setTimeout(r,n);return()=>clearTimeout(s)},[n,r]);const i={success:"#10b981",error:"#ef4444",info:"#3b82f6"};return a.jsxs("div",{style:{position:"fixed",top:"20px",right:"20px",background:i[t],color:"white",padding:"16px 24px",borderRadius:"8px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:9999,maxWidth:"400px",animation:"slideIn 0.3s ease-out",fontSize:"14px",fontWeight:"500"},children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"12px"},children:[a.jsx("div",{style:{flex:1,whiteSpace:"pre-wrap"},children:e}),a.jsx("button",{onClick:r,style:{background:"transparent",border:"none",color:"white",cursor:"pointer",fontSize:"18px",padding:"0 4px",opacity:.8},children:"×"})]}),a.jsx("style",{children:` + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + `})]})}function l7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(!1),[o,l]=h.useState(null);h.useEffect(()=>{c()},[]);const c=async()=>{n(!0);try{const u=await B.getCampaigns();t(u.campaigns)}catch(u){console.error("Failed to load campaigns:",u)}finally{n(!1)}},d=async(u,f)=>{if(confirm(`Are you sure you want to delete campaign "${f}"?`))try{await B.deleteCampaign(u),c()}catch(p){l({message:"Failed to delete campaign: "+p.message,type:"error"})}};return r?a.jsx(X,{children:a.jsx("div",{children:"Loading..."})}):a.jsxs(X,{children:[o&&a.jsx(Vn,{message:o.message,type:o.type,onClose:()=>l(null)}),a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Campaigns"}),a.jsx("button",{onClick:()=>s(!0),style:{padding:"12px 24px",background:"#667eea",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontSize:"14px",fontWeight:"500"},children:"+ Create Campaign"})]}),i&&a.jsx(c7,{onClose:()=>s(!1),onSuccess:()=>{s(!1),c()}}),a.jsx("div",{style:{display:"grid",gap:"20px"},children:e.map(u=>a.jsx("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsxs("h3",{style:{marginBottom:"10px",fontSize:"20px"},children:[u.name,a.jsx("span",{style:{marginLeft:"10px",padding:"4px 8px",borderRadius:"4px",fontSize:"12px",background:u.active?"#d4edda":"#f8d7da",color:u.active?"#155724":"#721c24"},children:u.active?"Active":"Inactive"})]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"5px"},children:[a.jsx("strong",{children:"Slug:"})," ",u.slug]}),u.description&&a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"5px"},children:[a.jsx("strong",{children:"Description:"})," ",u.description]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"5px"},children:[a.jsx("strong",{children:"Display Style:"})," ",u.display_style]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666"},children:[a.jsx("strong",{children:"Products:"})," ",u.product_count||0]})]}),a.jsx("div",{style:{display:"flex",gap:"10px"},children:a.jsx("button",{onClick:()=>d(u.id,u.name),style:{padding:"8px 16px",background:"#e74c3c",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontSize:"14px"},children:"Delete"})})]})},u.id))}),e.length===0&&!i&&a.jsx("div",{style:{background:"white",padding:"40px",borderRadius:"8px",textAlign:"center",color:"#999"},children:"No campaigns found"})]})]})}function c7({onClose:e,onSuccess:t}){const[r,n]=h.useState(""),[i,s]=h.useState(""),[o,l]=h.useState(""),[c,d]=h.useState("grid"),[u,f]=h.useState(!0),[p,m]=h.useState(!1),[x,g]=h.useState(null),b=async v=>{v.preventDefault(),m(!0);try{await B.createCampaign({name:r,slug:i,description:o,display_style:c,active:u}),t()}catch(j){g({message:"Failed to create campaign: "+j.message,type:"error"})}finally{m(!1)}};return a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"20px"},children:[x&&a.jsx(Vn,{message:x.message,type:x.type,onClose:()=>g(null)}),a.jsx("h3",{style:{marginBottom:"20px"},children:"Create New Campaign"}),a.jsxs("form",{onSubmit:b,children:[a.jsxs("div",{style:{display:"grid",gap:"15px"},children:[a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Name *"}),a.jsx("input",{type:"text",value:r,onChange:v=>n(v.target.value),required:!0,style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Slug *"}),a.jsx("input",{type:"text",value:i,onChange:v=>s(v.target.value),required:!0,placeholder:"e.g., summer-sale",style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Description"}),a.jsx("textarea",{value:o,onChange:v=>l(v.target.value),rows:3,style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"}})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"5px",fontWeight:"500"},children:"Display Style"}),a.jsxs("select",{value:c,onChange:v=>d(v.target.value),style:{width:"100%",padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"grid",children:"Grid"}),a.jsx("option",{value:"list",children:"List"}),a.jsx("option",{value:"carousel",children:"Carousel"})]})]}),a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px"},children:[a.jsx("input",{type:"checkbox",id:"active",checked:u,onChange:v=>f(v.target.checked)}),a.jsx("label",{htmlFor:"active",children:"Active"})]})]}),a.jsxs("div",{style:{display:"flex",gap:"10px",marginTop:"20px"},children:[a.jsx("button",{type:"submit",disabled:p,style:{padding:"10px 20px",background:p?"#999":"#667eea",color:"white",border:"none",borderRadius:"6px",cursor:p?"not-allowed":"pointer",fontSize:"14px"},children:p?"Creating...":"Create Campaign"}),a.jsx("button",{type:"button",onClick:e,style:{padding:"10px 20px",background:"#ddd",color:"#333",border:"none",borderRadius:"6px",cursor:"pointer",fontSize:"14px"},children:"Cancel"})]})]})]})}function u7(){var l,c,d,u;const[e,t]=h.useState(null),[r,n]=h.useState(!0),[i,s]=h.useState(30);h.useEffect(()=>{o()},[i]);const o=async()=>{n(!0);try{const f=await B.getAnalyticsOverview(i);t(f)}catch(f){console.error("Failed to load analytics:",f)}finally{n(!1)}};return r?a.jsx(X,{children:a.jsx("div",{children:"Loading..."})}):a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Analytics"}),a.jsxs("select",{value:i,onChange:f=>s(parseInt(f.target.value)),style:{padding:"10px 15px",border:"1px solid #ddd",borderRadius:"6px",fontSize:"14px"},children:[a.jsx("option",{value:7,children:"Last 7 days"}),a.jsx("option",{value:30,children:"Last 30 days"}),a.jsx("option",{value:90,children:"Last 90 days"}),a.jsx("option",{value:365,children:"Last year"})]})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(250px, 1fr))",gap:"20px",marginBottom:"30px"},children:[a.jsx(wv,{title:"Total Clicks",value:((l=e==null?void 0:e.overview)==null?void 0:l.total_clicks)||0,icon:"👆",color:"#3498db"}),a.jsx(wv,{title:"Unique Products Clicked",value:((c=e==null?void 0:e.overview)==null?void 0:c.unique_products)||0,icon:"📦",color:"#2ecc71"})]}),a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px"},children:[a.jsx("h3",{style:{marginBottom:"20px"},children:"Clicks Over Time"}),((d=e==null?void 0:e.clicks_by_day)==null?void 0:d.length)>0?a.jsx("div",{style:{overflowX:"auto"},children:a.jsx("div",{style:{display:"flex",alignItems:"flex-end",gap:"8px",minWidth:"600px",height:"200px"},children:e.clicks_by_day.slice().reverse().map((f,p)=>{const m=Math.max(...e.clicks_by_day.map(g=>g.clicks)),x=f.clicks/m*180;return a.jsxs("div",{style:{flex:1,display:"flex",flexDirection:"column",alignItems:"center"},children:[a.jsx("div",{style:{width:"100%",height:`${x}px`,background:"#667eea",borderRadius:"4px 4px 0 0",position:"relative",minHeight:"2px"},title:`${f.clicks} clicks`,children:a.jsx("div",{style:{position:"absolute",top:"-25px",left:"50%",transform:"translateX(-50%)",fontSize:"12px",fontWeight:"bold"},children:f.clicks})}),a.jsx("div",{style:{fontSize:"10px",marginTop:"5px",color:"#666",transform:"rotate(-45deg)",transformOrigin:"left",whiteSpace:"nowrap"},children:new Date(f.date).toLocaleDateString("en-US",{month:"short",day:"numeric"})})]},p)})})}):a.jsx("div",{style:{color:"#999",textAlign:"center",padding:"40px"},children:"No click data for this period"})]}),a.jsxs("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("h3",{style:{marginBottom:"20px"},children:"Top Products"}),((u=e==null?void 0:e.top_products)==null?void 0:u.length)>0?a.jsx("div",{children:e.top_products.map((f,p)=>a.jsxs("div",{style:{padding:"15px 0",borderBottom:p{u()},[]);const u=async()=>{n(!0);try{const x=await B.getSettings();t(x.settings)}catch(x){console.error("Failed to load settings:",x)}finally{n(!1)}},f=(x,g)=>{l(b=>({...b,[x]:g}))},p=async()=>{s(!0);try{const x=Object.entries(o).map(([g,b])=>({key:g,value:b}));await B.updateSettings(x),l({}),u(),d({message:"Settings saved successfully!",type:"success"})}catch(x){d({message:"Failed to save settings: "+x.message,type:"error"})}finally{s(!1)}},m=Object.keys(o).length>0;return r?a.jsx(X,{children:a.jsx("div",{children:"Loading..."})}):a.jsxs(X,{children:[c&&a.jsx(Vn,{message:c.message,type:c.type,onClose:()=>d(null)}),a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Settings"}),m&&a.jsx("button",{onClick:p,disabled:i,style:{padding:"12px 24px",background:i?"#999":"#2ecc71",color:"white",border:"none",borderRadius:"6px",cursor:i?"not-allowed":"pointer",fontSize:"14px",fontWeight:"500"},children:i?"Saving...":"Save Changes"})]}),a.jsx("div",{style:{background:"white",padding:"25px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:a.jsx("div",{style:{display:"grid",gap:"25px"},children:e.map(x=>{const g=o[x.key]!==void 0?o[x.key]:x.value;return a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"500",fontSize:"16px"},children:f7(x.key)}),x.description&&a.jsx("div",{style:{fontSize:"13px",color:"#666",marginBottom:"8px"},children:x.description}),a.jsx("input",{type:"text",value:g,onChange:b=>f(x.key,b.target.value),style:{width:"100%",maxWidth:"500px",padding:"10px",border:o[x.key]!==void 0?"2px solid #667eea":"1px solid #ddd",borderRadius:"6px",fontSize:"14px"}}),a.jsxs("div",{style:{fontSize:"12px",color:"#999",marginTop:"5px"},children:["Last updated: ",new Date(x.updated_at).toLocaleString()]})]},x.key)})})}),m&&a.jsx("div",{style:{marginTop:"20px",padding:"15px",background:"#fff3cd",border:"1px solid #ffc107",borderRadius:"6px",color:"#856404"},children:'⚠️ You have unsaved changes. Click "Save Changes" to apply them.'})]})]})}function f7(e){return e.split("_").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function p7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(!1),[o,l]=h.useState({}),[c,d]=h.useState(null),[u,f]=h.useState(null);h.useEffect(()=>{p(),m()},[]),h.useEffect(()=>{if(!(c!=null&&c.id))return;if(c.status==="completed"||c.status==="cancelled"||c.status==="failed"){p();return}const P=setInterval(async()=>{try{const _=await B.getProxyTestJob(c.id);d(_.job),(_.job.status==="completed"||_.job.status==="cancelled"||_.job.status==="failed")&&(clearInterval(P),p())}catch(_){console.error("Failed to poll job status:",_)}},2e3);return()=>clearInterval(P)},[c==null?void 0:c.id]);const p=async()=>{n(!0);try{const N=await B.getProxies();t(N.proxies)}catch(N){console.error("Failed to load proxies:",N)}finally{n(!1)}},m=async()=>{try{const N=await B.getActiveProxyTestJob();N.job&&d(N.job)}catch{console.log("No active job found")}},x=async N=>{l(P=>({...P,[N]:!0}));try{await B.testProxy(N),p()}catch(P){f({message:"Test failed: "+P.message,type:"error"})}finally{l(P=>({...P,[N]:!1}))}},g=async N=>{l(P=>({...P,[N]:!0})),B.testProxy(N).then(()=>{p(),l(P=>({...P,[N]:!1}))}).catch(()=>{l(P=>({...P,[N]:!1}))})},b=async()=>{try{const N=await B.testAllProxies();f({message:"Proxy testing job started",type:"success"}),d({id:N.jobId,status:"pending",tested_proxies:0,total_proxies:e.length,passed_proxies:0,failed_proxies:0})}catch(N){f({message:"Failed to start testing: "+N.message,type:"error"})}},v=async()=>{if(c!=null&&c.id)try{await B.cancelProxyTestJob(c.id),f({message:"Job cancelled",type:"info"})}catch(N){f({message:"Failed to cancel job: "+N.message,type:"error"})}},j=async()=>{try{await B.updateProxyLocations(),f({message:"Location update job started",type:"success"})}catch(N){f({message:"Failed to start location update: "+N.message,type:"error"})}},y=async N=>{if(confirm("Delete this proxy?"))try{await B.deleteProxy(N),p()}catch(P){f({message:"Failed to delete proxy: "+P.message,type:"error"})}};if(r)return a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("div",{className:"w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"})})});const w={total:e.length,passed:e.filter(N=>N.active).length,failed:e.filter(N=>!N.active).length},S=w.total>0?Math.round(w.passed/w.total*100):0;return a.jsxs(X,{children:[u&&a.jsx(Vn,{message:u.message,type:u.type,onClose:()=>f(null)}),a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(sl,{className:"w-6 h-6 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-semibold text-gray-900",children:"Proxies"}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:[w.total," total • ",w.passed," active • ",w.failed," inactive"]})]})]}),a.jsxs("div",{className:"flex gap-2",children:[a.jsxs("button",{onClick:b,disabled:!!c&&c.status!=="completed"&&c.status!=="cancelled"&&c.status!=="failed",className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Test All"]}),a.jsxs("button",{onClick:j,className:"inline-flex items-center gap-2 px-4 py-2 bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors text-sm font-medium",children:[a.jsx(yi,{className:"w-4 h-4"}),"Update Locations"]}),a.jsxs("button",{onClick:()=>s(!0),className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium",children:[a.jsx(Af,{className:"w-4 h-4"}),"Add Proxy"]})]})]}),a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-4 gap-4",children:[a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(sl,{className:"w-5 h-5 text-blue-600"})}),a.jsx("div",{className:"flex items-center gap-1 text-xs text-gray-500",children:a.jsxs("span",{children:[S,"% pass rate"]})})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Total Proxies"}),a.jsx("p",{className:"text-3xl font-semibold text-gray-900",children:w.total})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{className:"flex items-center gap-1 text-xs text-green-600",children:[a.jsx(zn,{className:"w-3 h-3"}),a.jsxs("span",{children:[Math.round(w.passed/w.total*100)||0,"%"]})]})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Active"}),a.jsx("p",{className:"text-3xl font-semibold text-green-600",children:w.passed}),a.jsx("p",{className:"text-xs text-gray-500",children:"Passing health checks"})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-center justify-between mb-4",children:[a.jsx("div",{className:"p-2 bg-red-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-red-600"})}),a.jsx("div",{className:"flex items-center gap-1 text-xs text-red-600",children:a.jsxs("span",{children:[Math.round(w.failed/w.total*100)||0,"%"]})})]}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Inactive"}),a.jsx("p",{className:"text-3xl font-semibold text-red-600",children:w.failed}),a.jsx("p",{className:"text-xs text-gray-500",children:"Failed health checks"})]})]}),a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsx("div",{className:"flex items-center justify-between mb-4",children:a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(zn,{className:"w-5 h-5 text-purple-600"})})}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("p",{className:"text-sm font-medium text-gray-600",children:"Success Rate"}),a.jsxs("p",{className:"text-3xl font-semibold text-gray-900",children:[S,"%"]}),a.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2 mt-3",children:a.jsx("div",{className:"bg-green-600 h-2 rounded-full transition-all",style:{width:`${S}%`}})})]})]})]}),c&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex justify-between items-center mb-4",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Xt,{className:`w-5 h-5 text-blue-600 ${c.status==="running"?"animate-spin":""}`})}),a.jsxs("div",{children:[a.jsx("h3",{className:"font-semibold text-gray-900",children:"Proxy Testing Job"}),a.jsx("p",{className:"text-sm text-gray-500",children:c.status.charAt(0).toUpperCase()+c.status.slice(1)})]})]}),a.jsxs("div",{className:"flex gap-2",children:[c.status==="running"&&a.jsx("button",{onClick:v,className:"px-3 py-1.5 bg-red-50 text-red-700 rounded-lg hover:bg-red-100 transition-colors text-sm font-medium",children:"Cancel"}),(c.status==="completed"||c.status==="cancelled"||c.status==="failed")&&a.jsx("button",{onClick:()=>d(null),className:"px-3 py-1.5 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors text-sm font-medium",children:"Dismiss"})]})]}),a.jsxs("div",{className:"mb-4",children:[a.jsxs("div",{className:"text-sm text-gray-600 mb-2",children:["Progress: ",c.tested_proxies||0," / ",c.total_proxies||0," proxies tested"]}),a.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2",children:a.jsx("div",{className:`h-2 rounded-full transition-all ${c.status==="completed"?"bg-green-600":c.status==="cancelled"||c.status==="failed"?"bg-red-600":"bg-blue-600"}`,style:{width:`${(c.tested_proxies||0)/(c.total_proxies||100)*100}%`}})})]}),a.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{className:"text-sm text-gray-600",children:"Passed:"}),a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-green-50 text-green-700 rounded",children:c.passed_proxies||0})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{className:"text-sm text-gray-600",children:"Failed:"}),a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-red-50 text-red-700 rounded",children:c.failed_proxies||0})]})]}),c.error&&a.jsxs("div",{className:"mt-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2",children:[a.jsx(Uc,{className:"w-5 h-5 text-red-600 flex-shrink-0 mt-0.5"}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm font-medium text-red-900",children:"Error"}),a.jsx("p",{className:"text-sm text-red-700",children:c.error})]})]})]}),i&&a.jsx(h7,{onClose:()=>s(!1),onSuccess:()=>{s(!1),p()}}),a.jsx("div",{className:"space-y-3",children:e.map(N=>a.jsx("div",{className:"bg-white rounded-xl border border-gray-200 p-4 hover:shadow-lg transition-shadow",children:a.jsxs("div",{className:"flex justify-between items-center",children:[a.jsxs("div",{className:"flex-1",children:[a.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[a.jsxs("h3",{className:"font-semibold text-gray-900",children:[N.protocol,"://",N.host,":",N.port]}),a.jsx("span",{className:`px-2 py-1 text-xs font-medium rounded ${N.active?"bg-green-50 text-green-700":"bg-red-50 text-red-700"}`,children:N.active?"Active":"Inactive"}),N.is_anonymous&&a.jsx("span",{className:"px-2 py-1 text-xs font-medium bg-blue-50 text-blue-700 rounded",children:"Anonymous"}),(N.city||N.state||N.country)&&a.jsxs("span",{className:"px-2 py-1 text-xs font-medium bg-purple-50 text-purple-700 rounded flex items-center gap-1",children:[a.jsx(yi,{className:"w-3 h-3"}),N.city&&`${N.city}/`,N.state&&`${N.state.substring(0,2).toUpperCase()} `,N.country]})]}),a.jsx("div",{className:"text-sm text-gray-600",children:N.last_tested_at?a.jsxs("div",{className:"flex items-center gap-4",children:[a.jsxs("div",{className:"flex items-center gap-1",children:[a.jsx(xr,{className:"w-4 h-4 text-gray-400"}),a.jsxs("span",{children:["Last tested: ",new Date(N.last_tested_at).toLocaleString()]})]}),N.test_result==="success"?a.jsxs("span",{className:"flex items-center gap-1 text-green-600",children:[a.jsx(_r,{className:"w-4 h-4"}),"Success (",N.response_time_ms,"ms)"]}):a.jsxs("span",{className:"flex items-center gap-1 text-red-600",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Failed"]})]}):"Not tested yet"})]}),a.jsxs("div",{className:"flex gap-2",children:[N.active?a.jsx("button",{onClick:()=>x(N.id),disabled:o[N.id],className:"inline-flex items-center gap-1 px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors text-sm font-medium disabled:opacity-50",children:o[N.id]?a.jsx("div",{className:"w-4 h-4 border-2 border-blue-700 border-t-transparent rounded-full animate-spin"}):a.jsxs(a.Fragment,{children:[a.jsx(Xt,{className:"w-4 h-4"}),"Test"]})}):a.jsx("button",{onClick:()=>g(N.id),disabled:o[N.id],className:"inline-flex items-center gap-1 px-3 py-1.5 bg-yellow-50 text-yellow-700 rounded-lg hover:bg-yellow-100 transition-colors text-sm font-medium disabled:opacity-50",children:o[N.id]?a.jsx("div",{className:"w-4 h-4 border-2 border-yellow-700 border-t-transparent rounded-full animate-spin"}):a.jsxs(a.Fragment,{children:[a.jsx(Xt,{className:"w-4 h-4"}),"Retest"]})}),a.jsxs("button",{onClick:()=>y(N.id),className:"inline-flex items-center gap-1 px-3 py-1.5 bg-red-50 text-red-700 rounded-lg hover:bg-red-100 transition-colors text-sm font-medium",children:[a.jsx(RO,{className:"w-4 h-4"}),"Delete"]})]})]})},N.id))}),e.length===0&&!i&&a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200 p-12 text-center",children:[a.jsx(sl,{className:"w-16 h-16 text-gray-300 mx-auto mb-4"}),a.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-2",children:"No proxies configured"}),a.jsx("p",{className:"text-gray-500 mb-6",children:"Add your first proxy to get started with scraping"}),a.jsxs("button",{onClick:()=>s(!0),className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium",children:[a.jsx(Af,{className:"w-4 h-4"}),"Add Proxy"]})]})]})]})}function h7({onClose:e,onSuccess:t}){const[r,n]=h.useState("single"),[i,s]=h.useState(""),[o,l]=h.useState(""),[c,d]=h.useState("http"),[u,f]=h.useState(""),[p,m]=h.useState(""),[x,g]=h.useState(""),[b,v]=h.useState(!1),[j,y]=h.useState(null),w=_=>{if(_=_.trim(),!_||_.startsWith("#"))return null;let T;return T=_.match(/^(https?|socks5):\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/),T?{protocol:T[1],username:T[2],password:T[3],host:T[4],port:parseInt(T[5])}:(T=_.match(/^(https?|socks5):\/\/([^:]+):(\d+)$/),T?{protocol:T[1],host:T[2],port:parseInt(T[3])}:(T=_.match(/^([^:]+):(\d+):([^:]+):(.+)$/),T?{protocol:"http",host:T[1],port:parseInt(T[2]),username:T[3],password:T[4]}:(T=_.match(/^([^:]+):(\d+)$/),T?{protocol:"http",host:T[1],port:parseInt(T[2])}:null)))},S=async()=>{const T=x.split(` +`).map($=>w($)).filter($=>$!==null);if(T.length===0){y({message:"No valid proxies found. Please check the format.",type:"error"});return}v(!0);try{const $=await B.addProxiesBulk(T),M=`Import complete! + +Added: ${$.added} +Duplicates: ${$.duplicates||0} +Failed: ${$.failed} + +Proxies are inactive by default. Use "Test All Proxies" to verify and activate them.`;y({message:M,type:"success"}),t()}catch($){y({message:"Failed to import proxies: "+$.message,type:"error"})}finally{v(!1)}},N=async _=>{var M;const T=(M=_.target.files)==null?void 0:M[0];if(!T)return;const $=await T.text();g($)},P=async _=>{if(_.preventDefault(),r==="bulk"){await S();return}v(!0);try{await B.addProxy({host:i,port:parseInt(o),protocol:c,username:u||void 0,password:p||void 0}),t()}catch(T){y({message:"Failed to add proxy: "+T.message,type:"error"})}finally{v(!1)}};return a.jsxs("div",{className:"bg-white rounded-xl border border-gray-200",children:[j&&a.jsx(Vn,{message:j.message,type:j.type,onClose:()=>y(null)}),a.jsxs("div",{className:"p-6",children:[a.jsxs("div",{className:"flex justify-between items-center mb-6",children:[a.jsx("h2",{className:"text-xl font-semibold text-gray-900",children:"Add Proxies"}),a.jsxs("div",{className:"flex bg-gray-100 rounded-lg p-1",children:[a.jsx("button",{type:"button",onClick:()=>n("single"),className:`px-4 py-2 rounded-md text-sm font-medium transition-colors ${r==="single"?"bg-white text-gray-900 shadow-sm":"text-gray-600 hover:text-gray-900"}`,children:"Single"}),a.jsx("button",{type:"button",onClick:()=>n("bulk"),className:`px-4 py-2 rounded-md text-sm font-medium transition-colors ${r==="bulk"?"bg-white text-gray-900 shadow-sm":"text-gray-600 hover:text-gray-900"}`,children:"Bulk Import"})]})]}),a.jsxs("form",{onSubmit:P,children:[r==="single"?a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Host *"}),a.jsx("input",{type:"text",value:i,onChange:_=>s(_.target.value),required:!0,placeholder:"proxy.example.com",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Port *"}),a.jsx("input",{type:"number",value:o,onChange:_=>l(_.target.value),required:!0,placeholder:"8080",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Protocol *"}),a.jsxs("select",{value:c,onChange:_=>d(_.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[a.jsx("option",{value:"http",children:"HTTP"}),a.jsx("option",{value:"https",children:"HTTPS"}),a.jsx("option",{value:"socks5",children:"SOCKS5"})]})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Username"}),a.jsx("input",{type:"text",value:u,onChange:_=>f(_.target.value),placeholder:"Optional",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]}),a.jsxs("div",{className:"md:col-span-2",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Password"}),a.jsx("input",{type:"password",value:p,onChange:_=>m(_.target.value),placeholder:"Optional",className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"})]})]}):a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Upload File"}),a.jsxs("label",{className:"flex items-center justify-center w-full px-4 py-6 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition-colors",children:[a.jsxs("div",{className:"flex flex-col items-center",children:[a.jsx(HO,{className:"w-8 h-8 text-gray-400 mb-2"}),a.jsx("span",{className:"text-sm text-gray-600",children:"Click to upload or drag and drop"}),a.jsx("span",{className:"text-xs text-gray-500 mt-1",children:".txt or .list files"})]}),a.jsx("input",{type:"file",accept:".txt,.list",onChange:N,className:"hidden"})]})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Or Paste Proxies (one per line)"}),a.jsx("textarea",{value:x,onChange:_=>g(_.target.value),placeholder:`Supported formats: +host:port +protocol://host:port +host:port:username:password +protocol://username:password@host:port + +Example: +192.168.1.1:8080 +http://proxy.example.com:3128 +10.0.0.1:8080:user:pass +socks5://user:pass@proxy.example.com:1080`,rows:12,className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"})]}),a.jsxs("div",{className:"p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-2",children:[a.jsx(Uc,{className:"w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5"}),a.jsx("span",{className:"text-sm text-blue-900",children:"Lines starting with # are treated as comments and ignored."})]})]}),a.jsxs("div",{className:"flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200",children:[a.jsx("button",{type:"button",onClick:e,className:"px-4 py-2 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors font-medium",children:"Cancel"}),a.jsx("button",{type:"submit",disabled:b,className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed",children:b?a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"}),a.jsx("span",{children:"Processing..."})]}):a.jsxs(a.Fragment,{children:[a.jsx(Af,{className:"w-4 h-4"}),r==="bulk"?"Import Proxies":"Add Proxy"]})})]})]})]})]})}function m7(){const[e,t]=h.useState([]),[r,n]=h.useState(!0),[i,s]=h.useState(!0),[o,l]=h.useState(""),[c,d]=h.useState(""),[u,f]=h.useState(200),[p,m]=h.useState(null),x=h.useRef(null);h.useEffect(()=>{g()},[o,c,u]),h.useEffect(()=>{if(!i)return;const S=setInterval(()=>{g()},2e3);return()=>clearInterval(S)},[i,o,c,u]);const g=async()=>{try{const S=await B.getLogs(u,o,c);t(S.logs)}catch(S){console.error("Failed to load logs:",S)}finally{n(!1)}},b=async()=>{if(confirm("Are you sure you want to clear all logs?"))try{await B.clearLogs(),t([]),m({message:"Logs cleared successfully",type:"success"})}catch(S){m({message:"Failed to clear logs: "+S.message,type:"error"})}},v=()=>{var S;(S=x.current)==null||S.scrollIntoView({behavior:"smooth"})},j=S=>{switch(S){case"error":return"#dc3545";case"warn":return"#ffc107";case"info":return"#17a2b8";case"debug":return"#6c757d";default:return"#333"}},y=S=>{switch(S){case"error":return"#f8d7da";case"warn":return"#fff3cd";case"info":return"#d1ecf1";case"debug":return"#e2e3e5";default:return"#f8f9fa"}},w=S=>{switch(S){case"scraper":return"🔍";case"images":return"📸";case"categories":return"📂";case"system":return"⚙️";case"api":return"🌐";default:return"📝"}};return r?a.jsx(X,{children:a.jsx("div",{children:"Loading logs..."})}):a.jsxs(X,{children:[p&&a.jsx(Vn,{message:p.message,type:p.type,onClose:()=>m(null)}),a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"20px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"System Logs"}),a.jsxs("div",{style:{display:"flex",gap:"10px",alignItems:"center"},children:[a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"5px"},children:[a.jsx("input",{type:"checkbox",checked:i,onChange:S=>s(S.target.checked)}),"Auto-refresh (2s)"]}),a.jsx("button",{onClick:v,style:{padding:"8px 16px",background:"#6c757d",color:"white",border:"none",borderRadius:"6px",cursor:"pointer"},children:"⬇️ Scroll to Bottom"}),a.jsx("button",{onClick:b,style:{padding:"8px 16px",background:"#dc3545",color:"white",border:"none",borderRadius:"6px",cursor:"pointer"},children:"🗑️ Clear Logs"})]})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"20px",display:"flex",gap:"15px",flexWrap:"wrap"},children:[a.jsxs("select",{value:o,onChange:S=>l(S.target.value),style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"",children:"All Levels"}),a.jsx("option",{value:"info",children:"Info"}),a.jsx("option",{value:"warn",children:"Warning"}),a.jsx("option",{value:"error",children:"Error"}),a.jsx("option",{value:"debug",children:"Debug"})]}),a.jsxs("select",{value:c,onChange:S=>d(S.target.value),style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"",children:"All Categories"}),a.jsx("option",{value:"scraper",children:"🔍 Scraper"}),a.jsx("option",{value:"images",children:"📸 Images"}),a.jsx("option",{value:"categories",children:"📂 Categories"}),a.jsx("option",{value:"system",children:"⚙️ System"}),a.jsx("option",{value:"api",children:"🌐 API"})]}),a.jsxs("select",{value:u,onChange:S=>f(parseInt(S.target.value)),style:{padding:"10px",border:"1px solid #ddd",borderRadius:"6px"},children:[a.jsx("option",{value:"50",children:"Last 50"}),a.jsx("option",{value:"100",children:"Last 100"}),a.jsx("option",{value:"200",children:"Last 200"}),a.jsx("option",{value:"500",children:"Last 500"}),a.jsx("option",{value:"1000",children:"Last 1000"})]}),a.jsx("div",{style:{marginLeft:"auto",display:"flex",alignItems:"center",gap:"10px"},children:a.jsxs("span",{style:{fontSize:"14px",color:"#666"},children:["Showing ",e.length," logs"]})})]}),a.jsxs("div",{style:{background:"#1e1e1e",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",maxHeight:"70vh",overflowY:"auto",fontFamily:"monospace",fontSize:"13px"},children:[e.length===0?a.jsx("div",{style:{color:"#999",textAlign:"center",padding:"40px"},children:"No logs to display"}):e.map((S,N)=>a.jsxs("div",{style:{padding:"8px 12px",marginBottom:"4px",borderRadius:"4px",background:y(S.level),borderLeft:`4px solid ${j(S.level)}`,display:"flex",gap:"10px",alignItems:"flex-start"},children:[a.jsx("span",{style:{color:"#666",fontSize:"11px",whiteSpace:"nowrap"},children:new Date(S.timestamp).toLocaleTimeString()}),a.jsx("span",{style:{fontSize:"14px"},children:w(S.category)}),a.jsx("span",{style:{padding:"2px 6px",borderRadius:"3px",fontSize:"10px",fontWeight:"bold",background:j(S.level),color:"white",textTransform:"uppercase"},children:S.level}),a.jsx("span",{style:{padding:"2px 6px",borderRadius:"3px",fontSize:"10px",background:"#e2e3e5",color:"#333"},children:S.category}),a.jsx("span",{style:{flex:1,color:"#333",wordBreak:"break-word"},children:S.message})]},N)),a.jsx("div",{ref:x})]})]})]})}function g7(){const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState(null),[o,l]=h.useState([]),[c,d]=h.useState([]),[u,f]=h.useState([]),[p,m]=h.useState(!0),[x,g]=h.useState(!0),[b,v]=h.useState("az-live"),[j,y]=h.useState(null),[w,S]=h.useState(""),[N,P]=h.useState(null),[_,T]=h.useState({scheduledJobs:[],crawlJobs:[],inMemoryScrapers:[],totalActive:0}),[$,M]=h.useState({jobLogs:[],crawlJobs:[]}),[C,R]=h.useState([]);h.useEffect(()=>{if(q(),x){const E=setInterval(q,3e3);return()=>clearInterval(E)}},[x]);const q=async()=>{try{const[E,D,O,k,L,U]=await Promise.all([B.getActiveScrapers(),B.getScraperHistory(),B.getJobStats(),B.getActiveJobs(),B.getWorkerStats(),B.getRecentJobs({limit:50})]);t(E.scrapers||[]),n(D.history||[]),s(O),l(k.jobs||[]),d(L.workers||[]),f(U.jobs||[]);const[H,te,re,we]=await Promise.all([B.getAZMonitorSummary().catch(()=>null),B.getAZMonitorActiveJobs().catch(()=>({scheduledJobs:[],crawlJobs:[],inMemoryScrapers:[],totalActive:0})),B.getAZMonitorRecentJobs(30).catch(()=>({jobLogs:[],crawlJobs:[]})),B.getAZMonitorErrors({limit:10,hours:24}).catch(()=>({errors:[]}))]);P(H),T(te),M(re),R((we==null?void 0:we.errors)||[])}catch(E){console.error("Failed to load scraper data:",E)}finally{m(!1)}},Z=E=>{const D=Math.floor(E/1e3),O=Math.floor(D/60),k=Math.floor(O/60);return k>0?`${k}h ${O%60}m ${D%60}s`:O>0?`${O}m ${D%60}s`:`${D}s`};return a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Scraper Monitor"}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:x,onChange:E=>g(E.target.checked),style:{width:"18px",height:"18px",cursor:"pointer"}}),a.jsx("span",{children:"Auto-refresh (3s)"})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"10px",borderBottom:"2px solid #eee"},children:[a.jsxs("button",{onClick:()=>v("az-live"),style:{padding:"12px 24px",background:b==="az-live"?"white":"transparent",border:"none",borderBottom:b==="az-live"?"3px solid #10b981":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:b==="az-live"?"600":"400",color:b==="az-live"?"#10b981":"#666",marginBottom:"-2px"},children:["AZ Live ",_.totalActive>0&&a.jsx("span",{style:{marginLeft:"8px",padding:"2px 8px",background:"#10b981",color:"white",borderRadius:"10px",fontSize:"12px"},children:_.totalActive})]}),a.jsx("button",{onClick:()=>v("jobs"),style:{padding:"12px 24px",background:b==="jobs"?"white":"transparent",border:"none",borderBottom:b==="jobs"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:b==="jobs"?"600":"400",color:b==="jobs"?"#2563eb":"#666",marginBottom:"-2px"},children:"Dispensary Jobs"}),a.jsx("button",{onClick:()=>v("scrapers"),style:{padding:"12px 24px",background:b==="scrapers"?"white":"transparent",border:"none",borderBottom:b==="scrapers"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:b==="scrapers"?"600":"400",color:b==="scrapers"?"#2563eb":"#666",marginBottom:"-2px"},children:"Crawl History"})]}),b==="az-live"&&a.jsxs(a.Fragment,{children:[N&&a.jsx("div",{style:{marginBottom:"30px"},children:a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(180px, 1fr))",gap:"15px"},children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Running Jobs"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:_.totalActive>0?"#10b981":"#666"},children:_.totalActive})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Successful (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#10b981"},children:(N.successful_jobs_24h||0)+(N.successful_crawls_24h||0)})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Failed (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:(N.failed_jobs_24h||0)+(N.failed_crawls_24h||0)>0?"#ef4444":"#666"},children:(N.failed_jobs_24h||0)+(N.failed_crawls_24h||0)})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Products (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#8b5cf6"},children:N.products_found_24h||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Snapshots (24h)"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#06b6d4"},children:N.snapshots_created_24h||0})]})]})}),a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px",display:"flex",alignItems:"center",gap:"10px"},children:["Active Jobs",_.totalActive>0&&a.jsxs("span",{style:{padding:"4px 12px",background:"#d1fae5",color:"#065f46",borderRadius:"12px",fontSize:"14px",fontWeight:"600"},children:[_.totalActive," running"]})]}),_.totalActive===0?a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"😴"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"No jobs currently running"})]}):a.jsxs("div",{style:{display:"grid",gap:"15px"},children:[_.scheduledJobs.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #10b981"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsx("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:E.job_name}),a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:E.job_description||"Scheduled job"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(120px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Processed"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600"},children:E.items_processed||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Succeeded"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#10b981"},children:E.items_succeeded||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Failed"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:E.items_failed>0?"#ef4444":"#666"},children:E.items_failed||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[Math.floor((E.duration_seconds||0)/60),"m ",Math.floor((E.duration_seconds||0)%60),"s"]})]})]})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#d1fae5",color:"#065f46"},children:"RUNNING"})]})},`sched-${E.id}`)),_.crawlJobs.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #3b82f6"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsx("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:E.dispensary_name||"Unknown Store"}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:[E.city," | ",E.job_type||"crawl"]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(120px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#8b5cf6"},children:E.products_found||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Snapshots"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#06b6d4"},children:E.snapshots_created||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[Math.floor((E.duration_seconds||0)/60),"m ",Math.floor((E.duration_seconds||0)%60),"s"]})]})]})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#dbeafe",color:"#1e40af"},children:"CRAWLING"})]})},`crawl-${E.id}`))]})]}),(N==null?void 0:N.nextRuns)&&N.nextRuns.length>0&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:"Next Scheduled Runs"}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Job"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Next Run"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Last Status"})]})}),a.jsx("tbody",{children:N.nextRuns.map(E=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:E.job_name}),a.jsx("div",{style:{fontSize:"13px",color:"#666"},children:E.description})]}),a.jsx("td",{style:{padding:"15px"},children:a.jsx("div",{style:{fontWeight:"600",color:"#2563eb"},children:E.next_run_at?new Date(E.next_run_at).toLocaleString():"-"})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.last_status==="success"?"#d1fae5":E.last_status==="error"?"#fee2e2":"#fef3c7",color:E.last_status==="success"?"#065f46":E.last_status==="error"?"#991b1b":"#92400e"},children:E.last_status||"never"})})]},E.id))})]})})]}),C.length>0&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h2",{style:{fontSize:"24px",marginBottom:"20px",color:"#ef4444"},children:"Recent Errors (24h)"}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:C.map((E,D)=>a.jsxs("div",{style:{padding:"15px",borderBottom:Da.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:E.job_name}),a.jsxs("div",{style:{fontSize:"12px",color:"#999"},children:["Log #",E.id]})]}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.status==="success"?"#d1fae5":E.status==="running"?"#dbeafe":E.status==="error"?"#fee2e2":"#fef3c7",color:E.status==="success"?"#065f46":E.status==="running"?"#1e40af":E.status==="error"?"#991b1b":"#92400e"},children:E.status})}),a.jsxs("td",{style:{padding:"15px",textAlign:"right"},children:[a.jsx("span",{style:{color:"#10b981"},children:E.items_succeeded||0})," / ",a.jsx("span",{children:E.items_processed||0})]}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:E.duration_ms?`${Math.floor(E.duration_ms/6e4)}m ${Math.floor(E.duration_ms%6e4/1e3)}s`:"-"}),a.jsx("td",{style:{padding:"15px",color:"#666"},children:E.completed_at?new Date(E.completed_at).toLocaleString():"-"})]},`log-${E.id}`))})]})})]})]}),b==="jobs"&&a.jsxs(a.Fragment,{children:[i&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:"Job Statistics"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(200px, 1fr))",gap:"15px"},children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Pending"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#f59e0b"},children:i.pending||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"In Progress"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#3b82f6"},children:i.in_progress||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Completed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#10b981"},children:i.completed||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Failed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#ef4444"},children:i.failed||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#8b5cf6"},children:i.total_products_found||0})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Products Saved"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#06b6d4"},children:i.total_products_saved||0})]})]})]}),c.length>0&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Active Workers (",c.length,")"]}),a.jsx("div",{style:{display:"grid",gap:"15px"},children:c.map(E=>a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #10b981"},children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"12px"},children:["Worker: ",E.worker_id]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#d1fae5",color:"#065f46"},children:"ACTIVE"})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Active Jobs"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600"},children:E.active_jobs})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#8b5cf6"},children:E.total_products_found||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Saved"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#10b981"},children:E.total_products_saved||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Running Since"}),a.jsx("div",{style:{fontSize:"14px"},children:new Date(E.earliest_start).toLocaleTimeString()})]})]})]},E.worker_id))})]}),a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Active Jobs (",o.length,")"]}),o.length===0?a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"😴"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"No jobs currently running"})]}):a.jsx("div",{style:{display:"grid",gap:"15px"},children:o.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:"4px solid #3b82f6"},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsx("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:E.dispensary_name||E.brand_name}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:[E.job_type||"crawl"," | Job #",E.id]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Found"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#8b5cf6"},children:E.products_found||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Products Saved"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#10b981"},children:E.products_saved||0})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[Math.floor(E.duration_seconds/60),"m ",Math.floor(E.duration_seconds%60),"s"]})]})]})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:"#dbeafe",color:"#1e40af"},children:"IN PROGRESS"})]})},E.id))})]}),a.jsxs("div",{children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Recent Jobs (",u.length,")"]}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Type"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Found"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Saved"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Duration"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Completed"})]})}),a.jsx("tbody",{children:u.map(E=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsx("td",{style:{padding:"15px"},children:E.dispensary_name||E.brand_name}),a.jsx("td",{style:{padding:"15px",fontSize:"14px",color:"#666"},children:E.job_type||"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.status==="completed"?"#d1fae5":E.status==="in_progress"?"#dbeafe":E.status==="failed"?"#fee2e2":"#fef3c7",color:E.status==="completed"?"#065f46":E.status==="in_progress"?"#1e40af":E.status==="failed"?"#991b1b":"#92400e"},children:E.status})}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E.products_found||0}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600",color:"#10b981"},children:E.products_saved||0}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:E.duration_seconds?`${Math.floor(E.duration_seconds/60)}m ${Math.floor(E.duration_seconds%60)}s`:"-"}),a.jsx("td",{style:{padding:"15px",color:"#666"},children:E.completed_at?new Date(E.completed_at).toLocaleString():"-"})]},E.id))})]})})]})]}),b==="scrapers"&&a.jsxs(a.Fragment,{children:[a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Active Scrapers (",e.length,")"]}),e.length===0?a.jsxs("div",{style:{background:"white",padding:"60px 40px",borderRadius:"8px",textAlign:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"48px",marginBottom:"20px"},children:"🛌"}),a.jsx("div",{style:{fontSize:"18px",color:"#666"},children:"No scrapers currently running"})]}):a.jsx("div",{style:{display:"grid",gap:"15px"},children:e.map(E=>a.jsx("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",borderLeft:`4px solid ${E.status==="running"?E.isStale?"#ff9800":"#2ecc71":E.status==="error"?"#e74c3c":"#95a5a6"}`},children:a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start"},children:[a.jsxs("div",{style:{flex:1},children:[a.jsxs("div",{style:{fontSize:"18px",fontWeight:"600",marginBottom:"8px"},children:[E.storeName," - ",E.categoryName]}),a.jsxs("div",{style:{fontSize:"14px",color:"#666",marginBottom:"12px"},children:["ID: ",E.id]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"12px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Requests"}),a.jsxs("div",{style:{fontSize:"16px",fontWeight:"600"},children:[E.stats.requestsSuccess," / ",E.stats.requestsTotal]})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Items Saved"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#2ecc71"},children:E.stats.itemsSaved})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Items Dropped"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:"#e74c3c"},children:E.stats.itemsDropped})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Errors"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600",color:E.stats.errorsCount>0?"#ff9800":"#999"},children:E.stats.errorsCount})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"12px",color:"#999",marginBottom:"4px"},children:"Duration"}),a.jsx("div",{style:{fontSize:"16px",fontWeight:"600"},children:Z(E.duration)})]})]}),E.currentActivity&&a.jsxs("div",{style:{marginTop:"12px",padding:"8px 12px",background:"#f8f8f8",borderRadius:"4px",fontSize:"14px",color:"#666"},children:["📍 ",E.currentActivity]}),E.isStale&&a.jsx("div",{style:{marginTop:"12px",padding:"8px 12px",background:"#fff3cd",borderRadius:"4px",fontSize:"14px",color:"#856404"},children:"⚠️ No update in over 1 minute - scraper may be stuck"})]}),a.jsx("div",{style:{padding:"6px 12px",borderRadius:"4px",fontSize:"13px",fontWeight:"600",background:E.status==="running"?"#d4edda":E.status==="error"?"#f8d7da":"#e7e7e7",color:E.status==="running"?"#155724":E.status==="error"?"#721c24":"#666"},children:E.status.toUpperCase()})]})},E.id))})]}),a.jsxs("div",{children:[a.jsxs("h2",{style:{fontSize:"24px",marginBottom:"20px"},children:["Recent Scrapes (",r.length,")"]}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Found"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Products"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Last Crawled"})]})}),a.jsx("tbody",{children:r.map((E,D)=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsx("td",{style:{padding:"15px"},children:E.dispensary_name||E.store_name}),a.jsx("td",{style:{padding:"15px"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",background:E.status==="completed"?"#d1fae5":E.status==="failed"?"#fee2e2":"#fef3c7",color:E.status==="completed"?"#065f46":E.status==="failed"?"#991b1b":"#92400e"},children:E.status||"-"})}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E.products_found||"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E.product_count}),a.jsx("td",{style:{padding:"15px",color:"#666"},children:E.last_scraped_at?new Date(E.last_scraped_at).toLocaleString():"-"})]},D))})]})})]})]})]})})}function x7(){var we;const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState([]),[o,l]=h.useState(!0),[c,d]=h.useState(!0),[u,f]=h.useState("dispensaries"),[p,m]=h.useState(null),[x,g]=h.useState(null),[b,v]=h.useState(null),[j,y]=h.useState(null),[w,S]=h.useState(!1),[N,P]=h.useState("all"),[_,T]=h.useState(""),[$,M]=h.useState("");h.useEffect(()=>{const A=setTimeout(()=>{T($)},300);return()=>clearTimeout(A)},[$]),h.useEffect(()=>{if(C(),c){const A=setInterval(C,5e3);return()=>clearInterval(A)}},[c,N,_]);const C=async()=>{try{const A={};N==="AZ"&&(A.state="AZ"),_.trim()&&(A.search=_.trim());const[J,Ot,z]=await Promise.all([B.getGlobalSchedule(),B.getDispensarySchedules(Object.keys(A).length>0?A:void 0),B.getDispensaryCrawlJobs(100)]);t(J.schedules||[]),n(Ot.dispensaries||[]),s(z.jobs||[])}catch(A){console.error("Failed to load schedule data:",A)}finally{l(!1)}},R=async A=>{m(A);try{await B.triggerDispensaryCrawl(A),await C()}catch(J){console.error("Failed to trigger crawl:",J)}finally{m(null)}},q=async()=>{if(confirm("This will create crawl jobs for ALL active stores. Continue?"))try{const A=await B.triggerAllCrawls();alert(`Created ${A.jobs_created} crawl jobs`),await C()}catch(A){console.error("Failed to trigger all crawls:",A)}},Z=async A=>{try{await B.cancelCrawlJob(A),await C()}catch(J){console.error("Failed to cancel job:",J)}},E=async A=>{g(A);try{const J=await B.resolvePlatformId(A);J.success?alert(J.message):alert(`Failed: ${J.error||J.message}`),await C()}catch(J){console.error("Failed to resolve platform ID:",J),alert(`Error: ${J.message}`)}finally{g(null)}},D=async A=>{v(A);try{const J=await B.refreshDetection(A);alert(`Detected: ${J.menu_type}${J.platform_dispensary_id?`, Platform ID: ${J.platform_dispensary_id}`:""}`),await C()}catch(J){console.error("Failed to refresh detection:",J),alert(`Error: ${J.message}`)}finally{v(null)}},O=async(A,J)=>{y(A);try{await B.toggleDispensarySchedule(A,!J),await C()}catch(Ot){console.error("Failed to toggle schedule:",Ot),alert(`Error: ${Ot.message}`)}finally{y(null)}},k=async(A,J)=>{try{await B.updateGlobalSchedule(A,J),await C()}catch(Ot){console.error("Failed to update global schedule:",Ot)}},L=A=>{if(!A)return"Never";const J=new Date(A),z=new Date().getTime()-J.getTime(),ee=Math.floor(z/6e4),ne=Math.floor(ee/60),W=Math.floor(ne/24);return ee<1?"Just now":ee<60?`${ee}m ago`:ne<24?`${ne}h ago`:`${W}d ago`},U=A=>{const J=new Date(A),Ot=new Date,z=J.getTime()-Ot.getTime();if(z<0)return"Overdue";const ee=Math.floor(z/6e4),ne=Math.floor(ee/60);return ee<60?`${ee}m`:`${ne}h ${ee%60}m`},H=A=>{switch(A){case"completed":case"success":return{bg:"#d1fae5",color:"#065f46"};case"running":return{bg:"#dbeafe",color:"#1e40af"};case"failed":case"error":return{bg:"#fee2e2",color:"#991b1b"};case"cancelled":return{bg:"#f3f4f6",color:"#374151"};case"pending":return{bg:"#fef3c7",color:"#92400e"};case"sandbox_only":return{bg:"#e0e7ff",color:"#3730a3"};case"detection_only":return{bg:"#fce7f3",color:"#9d174d"};default:return{bg:"#f3f4f6",color:"#374151"}}},te=e.find(A=>A.schedule_type==="global_interval"),re=e.find(A=>A.schedule_type==="daily_special");return a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Crawler Schedule"}),a.jsxs("div",{style:{display:"flex",gap:"15px",alignItems:"center"},children:[a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:c,onChange:A=>d(A.target.checked),style:{width:"18px",height:"18px",cursor:"pointer"}}),a.jsx("span",{children:"Auto-refresh (5s)"})]}),a.jsx("button",{onClick:q,style:{padding:"10px 20px",background:"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:"Crawl All Stores"})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"10px",borderBottom:"2px solid #eee"},children:[a.jsxs("button",{onClick:()=>f("dispensaries"),style:{padding:"12px 24px",background:u==="dispensaries"?"white":"transparent",border:"none",borderBottom:u==="dispensaries"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:u==="dispensaries"?"600":"400",color:u==="dispensaries"?"#2563eb":"#666",marginBottom:"-2px"},children:["Dispensary Schedules (",r.length,")"]}),a.jsxs("button",{onClick:()=>f("jobs"),style:{padding:"12px 24px",background:u==="jobs"?"white":"transparent",border:"none",borderBottom:u==="jobs"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:u==="jobs"?"600":"400",color:u==="jobs"?"#2563eb":"#666",marginBottom:"-2px"},children:["Job Queue (",i.filter(A=>A.status==="pending"||A.status==="running").length,")"]}),a.jsx("button",{onClick:()=>f("global"),style:{padding:"12px 24px",background:u==="global"?"white":"transparent",border:"none",borderBottom:u==="global"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:u==="global"?"600":"400",color:u==="global"?"#2563eb":"#666",marginBottom:"-2px"},children:"Global Settings"})]}),u==="global"&&a.jsxs("div",{style:{display:"grid",gap:"20px"},children:[a.jsxs("div",{style:{background:"white",padding:"24px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start",marginBottom:"20px"},children:[a.jsxs("div",{children:[a.jsx("h2",{style:{fontSize:"20px",margin:0,marginBottom:"8px"},children:"Interval Crawl Schedule"}),a.jsx("p",{style:{color:"#666",margin:0},children:"Crawl all stores periodically"})]}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("span",{style:{color:"#666"},children:"Enabled"}),a.jsx("input",{type:"checkbox",checked:(te==null?void 0:te.enabled)??!0,onChange:A=>k("global_interval",{enabled:A.target.checked}),style:{width:"20px",height:"20px",cursor:"pointer"}})]})]}),a.jsx("div",{style:{display:"flex",alignItems:"center",gap:"15px"},children:a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px"},children:[a.jsx("span",{children:"Crawl every"}),a.jsxs("select",{value:(te==null?void 0:te.interval_hours)??4,onChange:A=>k("global_interval",{interval_hours:parseInt(A.target.value)}),style:{padding:"8px 12px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"16px"},children:[a.jsx("option",{value:1,children:"1 hour"}),a.jsx("option",{value:2,children:"2 hours"}),a.jsx("option",{value:4,children:"4 hours"}),a.jsx("option",{value:6,children:"6 hours"}),a.jsx("option",{value:8,children:"8 hours"}),a.jsx("option",{value:12,children:"12 hours"}),a.jsx("option",{value:24,children:"24 hours"})]})]})})]}),a.jsxs("div",{style:{background:"white",padding:"24px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"start",marginBottom:"20px"},children:[a.jsxs("div",{children:[a.jsx("h2",{style:{fontSize:"20px",margin:0,marginBottom:"8px"},children:"Daily Special Crawl"}),a.jsx("p",{style:{color:"#666",margin:0},children:"Crawl stores at local midnight to capture daily specials"})]}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("span",{style:{color:"#666"},children:"Enabled"}),a.jsx("input",{type:"checkbox",checked:(re==null?void 0:re.enabled)??!0,onChange:A=>k("daily_special",{enabled:A.target.checked}),style:{width:"20px",height:"20px",cursor:"pointer"}})]})]}),a.jsx("div",{style:{display:"flex",alignItems:"center",gap:"15px"},children:a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px"},children:[a.jsx("span",{children:"Run at"}),a.jsx("input",{type:"time",value:((we=re==null?void 0:re.run_time)==null?void 0:we.slice(0,5))??"00:01",onChange:A=>k("daily_special",{run_time:A.target.value}),style:{padding:"8px 12px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"16px"}}),a.jsx("span",{style:{color:"#666"},children:"(store local time)"})]})})]})]}),u==="dispensaries"&&a.jsxs("div",{children:[a.jsxs("div",{style:{marginBottom:"15px",display:"flex",gap:"20px",alignItems:"center",flexWrap:"wrap"},children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[a.jsx("span",{style:{fontWeight:"500",color:"#374151"},children:"State:"}),a.jsxs("div",{style:{display:"flex",borderRadius:"6px",overflow:"hidden",border:"1px solid #d1d5db"},children:[a.jsx("button",{onClick:()=>P("all"),style:{padding:"6px 14px",background:N==="all"?"#2563eb":"white",color:N==="all"?"white":"#374151",border:"none",cursor:"pointer",fontSize:"14px",fontWeight:"500"},children:"All"}),a.jsx("button",{onClick:()=>P("AZ"),style:{padding:"6px 14px",background:N==="AZ"?"#2563eb":"white",color:N==="AZ"?"white":"#374151",border:"none",borderLeft:"1px solid #d1d5db",cursor:"pointer",fontSize:"14px",fontWeight:"500"},children:"AZ Only"})]})]}),a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[a.jsx("span",{style:{fontWeight:"500",color:"#374151"},children:"Search:"}),a.jsx("input",{type:"text",placeholder:"Store name or slug...",value:$,onChange:A=>M(A.target.value),style:{padding:"6px 12px",borderRadius:"6px",border:"1px solid #d1d5db",fontSize:"14px",width:"200px"}}),$&&a.jsx("button",{onClick:()=>{M(""),T("")},style:{padding:"4px 8px",background:"#f3f4f6",border:"1px solid #d1d5db",borderRadius:"4px",cursor:"pointer",fontSize:"12px"},children:"Clear"})]}),a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"8px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:w,onChange:A=>S(A.target.checked),style:{width:"16px",height:"16px",cursor:"pointer"}}),a.jsx("span",{children:"Dutchie only"})]}),a.jsxs("span",{style:{color:"#666",fontSize:"14px",marginLeft:"auto"},children:["Showing ",(w?r.filter(A=>A.menu_type==="dutchie"):r).length," dispensaries"]})]}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"auto"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",minWidth:"1200px"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600"},children:"Menu Type"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600"},children:"Platform ID"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Last Run"}),a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Next Run"}),a.jsx("th",{style:{padding:"12px",textAlign:"left",fontWeight:"600"},children:"Last Result"}),a.jsx("th",{style:{padding:"12px",textAlign:"center",fontWeight:"600",minWidth:"220px"},children:"Actions"})]})}),a.jsx("tbody",{children:(w?r.filter(A=>A.menu_type==="dutchie"):r).map(A=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"12px"},children:[a.jsx("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:A.state&&A.city&&(A.dispensary_slug||A.slug)?a.jsx(K1,{to:`/dispensaries/${A.state}/${A.city.toLowerCase().replace(/\s+/g,"-")}/${A.dispensary_slug||A.slug}`,style:{fontWeight:"600",color:"#2563eb",textDecoration:"none"},children:A.dispensary_name}):a.jsx("span",{style:{fontWeight:"600"},children:A.dispensary_name})}),a.jsx("div",{style:{fontSize:"12px",color:"#666"},children:A.city?`${A.city}, ${A.state}`:A.state})]}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:A.menu_type?a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"11px",fontWeight:"600",background:A.menu_type==="dutchie"?"#d1fae5":"#e0e7ff",color:A.menu_type==="dutchie"?"#065f46":"#3730a3"},children:A.menu_type}):a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"11px",fontWeight:"600",background:"#f3f4f6",color:"#666"},children:"unknown"})}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:A.platform_dispensary_id?a.jsx("span",{style:{padding:"4px 8px",borderRadius:"4px",fontSize:"10px",fontFamily:"monospace",background:"#d1fae5",color:"#065f46"},title:A.platform_dispensary_id,children:A.platform_dispensary_id.length>12?`${A.platform_dispensary_id.slice(0,6)}...${A.platform_dispensary_id.slice(-4)}`:A.platform_dispensary_id}):a.jsx("span",{style:{padding:"4px 8px",borderRadius:"4px",fontSize:"10px",background:"#fee2e2",color:"#991b1b"},children:"missing"})}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:a.jsxs("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",gap:"4px"},children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"11px",fontWeight:"600",background:A.can_crawl?"#d1fae5":A.is_active!==!1?"#fef3c7":"#fee2e2",color:A.can_crawl?"#065f46":A.is_active!==!1?"#92400e":"#991b1b"},children:A.can_crawl?"Ready":A.is_active!==!1?"Not Ready":"Disabled"}),A.schedule_status_reason&&A.schedule_status_reason!=="ready"&&a.jsx("span",{style:{fontSize:"10px",color:"#666",maxWidth:"100px",textAlign:"center"},children:A.schedule_status_reason}),A.interval_minutes&&a.jsxs("span",{style:{fontSize:"10px",color:"#999"},children:["Every ",Math.round(A.interval_minutes/60),"h"]})]})}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{children:L(A.last_run_at)}),A.last_run_at&&a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:new Date(A.last_run_at).toLocaleString()})]}),a.jsx("td",{style:{padding:"15px"},children:a.jsx("div",{style:{fontWeight:"600",color:"#2563eb"},children:A.next_run_at?U(A.next_run_at):"Not scheduled"})}),a.jsx("td",{style:{padding:"15px"},children:A.last_status||A.latest_job_status?a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px",marginBottom:"4px"},children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...H(A.last_status||A.latest_job_status||"pending")},children:A.last_status||A.latest_job_status}),A.last_error&&a.jsx("button",{onClick:()=>alert(A.last_error),style:{padding:"2px 6px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"10px"},children:"Error"})]}),A.last_summary?a.jsx("div",{style:{fontSize:"12px",color:"#666",maxWidth:"250px"},children:A.last_summary}):A.latest_products_found!==null?a.jsxs("div",{style:{fontSize:"12px",color:"#666"},children:[A.latest_products_found," products"]}):null]}):a.jsx("span",{style:{color:"#999",fontSize:"13px"},children:"No runs yet"})}),a.jsx("td",{style:{padding:"12px",textAlign:"center"},children:a.jsxs("div",{style:{display:"flex",gap:"6px",justifyContent:"center",flexWrap:"wrap"},children:[a.jsx("button",{onClick:()=>D(A.dispensary_id),disabled:b===A.dispensary_id,style:{padding:"4px 8px",background:b===A.dispensary_id?"#94a3b8":"#f3f4f6",color:"#374151",border:"1px solid #d1d5db",borderRadius:"4px",cursor:b===A.dispensary_id?"wait":"pointer",fontSize:"11px"},title:"Re-detect menu type and resolve platform ID",children:b===A.dispensary_id?"...":"Refresh"}),A.menu_type==="dutchie"&&!A.platform_dispensary_id&&a.jsx("button",{onClick:()=>E(A.dispensary_id),disabled:x===A.dispensary_id,style:{padding:"4px 8px",background:x===A.dispensary_id?"#94a3b8":"#fef3c7",color:"#92400e",border:"1px solid #fcd34d",borderRadius:"4px",cursor:x===A.dispensary_id?"wait":"pointer",fontSize:"11px"},title:"Resolve platform dispensary ID via GraphQL",children:x===A.dispensary_id?"...":"Resolve ID"}),a.jsx("button",{onClick:()=>R(A.dispensary_id),disabled:p===A.dispensary_id||!A.can_crawl,style:{padding:"4px 8px",background:p===A.dispensary_id?"#94a3b8":A.can_crawl?"#2563eb":"#e5e7eb",color:A.can_crawl?"white":"#9ca3af",border:"none",borderRadius:"4px",cursor:p===A.dispensary_id||!A.can_crawl?"not-allowed":"pointer",fontSize:"11px"},title:A.can_crawl?"Trigger immediate crawl":`Cannot crawl: ${A.schedule_status_reason}`,children:p===A.dispensary_id?"...":"Run"}),a.jsx("button",{onClick:()=>O(A.dispensary_id,A.is_active),disabled:j===A.dispensary_id,style:{padding:"4px 8px",background:j===A.dispensary_id?"#94a3b8":A.is_active?"#fee2e2":"#d1fae5",color:A.is_active?"#991b1b":"#065f46",border:"none",borderRadius:"4px",cursor:j===A.dispensary_id?"wait":"pointer",fontSize:"11px"},title:A.is_active?"Disable scheduled crawling":"Enable scheduled crawling",children:j===A.dispensary_id?"...":A.is_active?"Disable":"Enable"})]})})]},A.dispensary_id))})]})})]}),u==="jobs"&&a.jsxs(a.Fragment,{children:[a.jsx("div",{style:{marginBottom:"30px"},children:a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"15px"},children:[a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Pending"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#f59e0b"},children:i.filter(A=>A.status==="pending").length})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Running"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#3b82f6"},children:i.filter(A=>A.status==="running").length})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Completed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#10b981"},children:i.filter(A=>A.status==="completed").length})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#999",marginBottom:"8px"},children:"Failed"}),a.jsx("div",{style:{fontSize:"32px",fontWeight:"600",color:"#ef4444"},children:i.filter(A=>A.status==="failed").length})]})]})}),a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Dispensary"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Type"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Trigger"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Products"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Started"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Completed"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Actions"})]})}),a.jsx("tbody",{children:i.length===0?a.jsx("tr",{children:a.jsx("td",{colSpan:8,style:{padding:"40px",textAlign:"center",color:"#666"},children:"No crawl jobs found"})}):i.map(A=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:A.dispensary_name}),a.jsxs("div",{style:{fontSize:"12px",color:"#999"},children:["Job #",A.id]})]}),a.jsx("td",{style:{padding:"15px",textAlign:"center",fontSize:"13px"},children:A.job_type}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"3px 8px",borderRadius:"4px",fontSize:"12px",background:A.trigger_type==="manual"?"#e0e7ff":A.trigger_type==="daily_special"?"#fce7f3":"#f3f4f6",color:A.trigger_type==="manual"?"#3730a3":A.trigger_type==="daily_special"?"#9d174d":"#374151"},children:A.trigger_type})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...H(A.status)},children:A.status})}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:A.products_found!==null?a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600"},children:A.products_found}),A.products_new!==null&&A.products_updated!==null&&a.jsxs("div",{style:{fontSize:"12px",color:"#666"},children:["+",A.products_new," / ~",A.products_updated]})]}):"-"}),a.jsx("td",{style:{padding:"15px",fontSize:"13px"},children:A.started_at?new Date(A.started_at).toLocaleString():"-"}),a.jsx("td",{style:{padding:"15px",fontSize:"13px"},children:A.completed_at?new Date(A.completed_at).toLocaleString():"-"}),a.jsxs("td",{style:{padding:"15px",textAlign:"center"},children:[A.status==="pending"&&a.jsx("button",{onClick:()=>Z(A.id),style:{padding:"4px 10px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"12px"},children:"Cancel"}),A.error_message&&a.jsx("button",{onClick:()=>alert(A.error_message),style:{padding:"4px 10px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"12px"},children:"View Error"})]})]},A.id))})]})})]})]})})}function y7(){const[e,t]=h.useState([]),[r,n]=h.useState(null),[i,s]=h.useState(3),[o,l]=h.useState("rotate-desktop"),[c,d]=h.useState(!1),[u,f]=h.useState(!1),[p,m]=h.useState(null),[x,g]=h.useState(!0);h.useEffect(()=>{b()},[]);const b=async()=>{g(!0);try{const S=(await B.getDispensaries()).dispensaries.filter(N=>N.menu_url&&N.scrape_enabled);t(S),S.length>0&&n(S[0].id)}catch(w){console.error("Failed to load dispensaries:",w)}finally{g(!1)}},v=async()=>{if(!(!r||c)){d(!0);try{await B.triggerDispensaryCrawl(r),m({message:"Crawl started for dispensary! Check the Scraper Monitor for progress.",type:"success"})}catch(w){m({message:"Failed to start crawl: "+w.message,type:"error"})}finally{d(!1)}}},j=async()=>{if(!(!r||u)){f(!0);try{m({message:"Image download feature coming soon!",type:"info"})}catch(w){m({message:"Failed to start image download: "+w.message,type:"error"})}finally{f(!1)}}},y=e.find(w=>w.id===r);return x?a.jsx(X,{children:a.jsx("div",{className:"flex items-center justify-center h-64",children:a.jsx("span",{className:"loading loading-spinner loading-lg"})})}):a.jsxs(X,{children:[p&&a.jsx(Vn,{message:p.message,type:p.type,onClose:()=>m(null)}),a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-3xl font-bold",children:"Scraper Tools"}),a.jsx("p",{className:"text-gray-500 mt-2",children:"Manage crawling operations for dispensaries"})]}),a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h2",{className:"card-title",children:"Select Dispensary"}),a.jsx("select",{className:"select select-bordered w-full max-w-md",value:r||"",onChange:w=>n(parseInt(w.target.value)),children:e.map(w=>a.jsxs("option",{value:w.id,children:[w.dba_name||w.name," - ",w.city,", ",w.state]},w.id))}),y&&a.jsx("div",{className:"mt-4 p-4 bg-base-200 rounded-lg",children:a.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 gap-4 text-sm",children:[a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Status"}),a.jsx("div",{className:"font-semibold",children:y.scrape_enabled?a.jsx("span",{className:"badge badge-success",children:"Enabled"}):a.jsx("span",{className:"badge badge-error",children:"Disabled"})})]}),a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Provider"}),a.jsx("div",{className:"font-semibold",children:y.provider_type||"Unknown"})]}),a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Products"}),a.jsx("div",{className:"font-semibold",children:y.product_count||0})]}),a.jsxs("div",{children:[a.jsx("div",{className:"text-gray-500",children:"Last Crawled"}),a.jsx("div",{className:"font-semibold",children:y.last_crawl_at?new Date(y.last_crawl_at).toLocaleDateString():"Never"})]})]})})]})}),a.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6",children:[a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h2",{className:"card-title",children:"Crawl Dispensary"}),a.jsx("p",{className:"text-sm text-gray-500",children:"Start crawling products from the selected dispensary menu"}),a.jsx("div",{className:"card-actions justify-end mt-4",children:a.jsx("button",{onClick:v,disabled:!r||c,className:`btn btn-primary ${c?"loading":""}`,children:c?"Starting...":"Start Crawl"})})]})}),a.jsx("div",{className:"card bg-base-100 shadow-xl",children:a.jsxs("div",{className:"card-body",children:[a.jsx("h2",{className:"card-title",children:"Download Images"}),a.jsx("p",{className:"text-sm text-gray-500",children:"Download missing product images for the selected dispensary"}),a.jsx("div",{className:"card-actions justify-end mt-auto",children:a.jsx("button",{onClick:j,disabled:!r||u,className:`btn btn-secondary ${u?"loading":""}`,children:u?"Downloading...":"Download Missing Images"})})]})})]}),a.jsxs("div",{className:"alert alert-info",children:[a.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",className:"stroke-current shrink-0 w-6 h-6",children:a.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"})}),a.jsxs("span",{children:["After starting scraper operations, check the"," ",a.jsx("a",{href:"/scraper-monitor",className:"link",children:"Scraper Monitor"})," for real-time progress and ",a.jsx("a",{href:"/logs",className:"link",children:"Logs"})," for detailed output."]})]})]})]})}function v7(){const[e,t]=h.useState([]),[r,n]=h.useState(null),[i,s]=h.useState(!0),[o,l]=h.useState("pending"),[c,d]=h.useState(null);h.useEffect(()=>{u()},[o]);const u=async()=>{s(!0);try{const[g,b]=await Promise.all([B.getChanges(o==="all"?void 0:o),B.getChangeStats()]);t(g.changes),n(b)}catch(g){console.error("Failed to load changes:",g)}finally{s(!1)}},f=async g=>{d(g);try{(await B.approveChange(g)).requires_recrawl&&alert("Change approved! This dispensary requires a menu recrawl."),await u()}catch(b){console.error("Failed to approve change:",b),alert("Failed to approve change. Please try again.")}finally{d(null)}},p=async g=>{const b=prompt("Enter rejection reason (optional):");d(g);try{await B.rejectChange(g,b||void 0),await u()}catch(v){console.error("Failed to reject change:",v),alert("Failed to reject change. Please try again.")}finally{d(null)}},m=g=>({dba_name:"DBA Name",website:"Website",phone:"Phone",email:"Email",google_rating:"Google Rating",google_review_count:"Google Review Count",menu_url:"Menu URL"})[g]||g,x=g=>({high:"bg-green-100 text-green-800",medium:"bg-yellow-100 text-yellow-800",low:"bg-red-100 text-red-800"})[g]||"bg-gray-100 text-gray-800";return i?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading changes..."})]})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Change Approval"}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"Review and approve proposed changes to dispensary data"})]}),a.jsxs("button",{onClick:u,className:"flex items-center gap-2 px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),r&&a.jsxs("div",{className:"grid grid-cols-4 gap-6",children:[a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-yellow-50 rounded-lg",children:a.jsx(xr,{className:"w-5 h-5 text-yellow-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Pending"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.pending_count})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(nj,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Needs Recrawl"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.pending_recrawl_count})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Approved"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.approved_count})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-red-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-red-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Rejected"}),a.jsx("p",{className:"text-2xl font-bold text-gray-900",children:r.rejected_count})]})]})})]}),a.jsx("div",{className:"flex gap-2",children:["all","pending","approved","rejected"].map(g=>a.jsx("button",{onClick:()=>l(g),className:`px-4 py-2 text-sm font-medium rounded-lg ${o===g?"bg-blue-600 text-white":"bg-white text-gray-700 border border-gray-300 hover:bg-gray-50"}`,children:g.charAt(0).toUpperCase()+g.slice(1)},g))}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200",children:e.length===0?a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-600",children:"No changes found"})}):a.jsx("div",{className:"divide-y divide-gray-200",children:e.map(g=>a.jsx("div",{className:"p-6",children:a.jsxs("div",{className:"flex items-start justify-between",children:[a.jsxs("div",{className:"flex-1",children:[a.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900",children:g.dispensary_name}),a.jsx("span",{className:`px-2 py-1 text-xs font-medium rounded ${x(g.confidence_score)}`,children:g.confidence_score||"N/A"}),g.requires_recrawl&&a.jsx("span",{className:"px-2 py-1 text-xs font-medium rounded bg-orange-100 text-orange-800",children:"Requires Recrawl"})]}),a.jsxs("p",{className:"text-sm text-gray-600 mb-3",children:[g.city,", ",g.state," • Source: ",g.source]}),a.jsxs("div",{className:"grid grid-cols-2 gap-4 mb-3",children:[a.jsxs("div",{children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"Field"}),a.jsx("p",{className:"text-sm text-gray-900",children:m(g.field_name)})]}),a.jsxs("div",{children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"Old Value"}),a.jsx("p",{className:"text-sm text-gray-900",children:g.old_value||a.jsx("em",{className:"text-gray-400",children:"None"})})]}),a.jsxs("div",{className:"col-span-2",children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"New Value"}),a.jsx("p",{className:"text-sm font-medium text-blue-600",children:g.new_value})]}),g.change_notes&&a.jsxs("div",{className:"col-span-2",children:[a.jsx("label",{className:"text-xs font-medium text-gray-500",children:"Notes"}),a.jsx("p",{className:"text-sm text-gray-700",children:g.change_notes})]})]}),a.jsxs("p",{className:"text-xs text-gray-500",children:["Created ",new Date(g.created_at).toLocaleString()]}),g.status==="rejected"&&g.rejection_reason&&a.jsxs("div",{className:"mt-2 p-3 bg-red-50 rounded border border-red-200",children:[a.jsx("p",{className:"text-xs font-medium text-red-800",children:"Rejection Reason:"}),a.jsx("p",{className:"text-sm text-red-700",children:g.rejection_reason})]})]}),a.jsxs("div",{className:"flex items-center gap-2 ml-4",children:[g.status==="pending"&&a.jsxs(a.Fragment,{children:[a.jsxs("button",{onClick:()=>f(g.id),disabled:c===g.id,className:"flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 disabled:opacity-50",children:[a.jsx(_r,{className:"w-4 h-4"}),"Approve"]}),a.jsxs("button",{onClick:()=>p(g.id),disabled:c===g.id,className:"flex items-center gap-2 px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 disabled:opacity-50",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Reject"]})]}),g.status==="approved"&&a.jsxs("span",{className:"flex items-center gap-2 px-4 py-2 bg-green-100 text-green-800 text-sm font-medium rounded-lg",children:[a.jsx(_r,{className:"w-4 h-4"}),"Approved"]}),g.status==="rejected"&&a.jsxs("span",{className:"flex items-center gap-2 px-4 py-2 bg-red-100 text-red-800 text-sm font-medium rounded-lg",children:[a.jsx(Hr,{className:"w-4 h-4"}),"Rejected"]}),a.jsx("a",{href:`/dispensaries/${g.dispensary_slug}`,target:"_blank",rel:"noopener noreferrer",className:"p-2 text-gray-400 hover:text-gray-600",children:a.jsx(Jr,{className:"w-4 h-4"})})]})]})},g.id))})})]})})}function b7(){const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState(!0),[o,l]=h.useState(!1),[c,d]=h.useState({user_name:"",store_id:"",allowed_ips:"",allowed_domains:""}),[u,f]=h.useState(null);h.useEffect(()=>{m(),p()},[]);const p=async()=>{try{const y=await B.getApiPermissionDispensaries();n(y.dispensaries)}catch(y){console.error("Failed to load dispensaries:",y)}},m=async()=>{s(!0);try{const y=await B.getApiPermissions();t(y.permissions)}catch(y){f({message:"Failed to load API permissions: "+y.message,type:"error"})}finally{s(!1)}},x=async y=>{if(y.preventDefault(),!c.user_name.trim()){f({message:"User name is required",type:"error"});return}if(!c.store_id){f({message:"Store is required",type:"error"});return}try{const w=await B.createApiPermission({...c,store_id:parseInt(c.store_id)});f({message:w.message,type:"success"}),d({user_name:"",store_id:"",allowed_ips:"",allowed_domains:""}),l(!1),m()}catch(w){f({message:"Failed to create permission: "+w.message,type:"error"})}},g=async y=>{try{await B.toggleApiPermission(y),f({message:"Permission status updated",type:"success"}),m()}catch(w){f({message:"Failed to toggle permission: "+w.message,type:"error"})}},b=async y=>{if(confirm("Are you sure you want to delete this API permission?"))try{await B.deleteApiPermission(y),f({message:"Permission deleted successfully",type:"success"}),m()}catch(w){f({message:"Failed to delete permission: "+w.message,type:"error"})}},v=y=>{navigator.clipboard.writeText(y),f({message:"API key copied to clipboard!",type:"success"})},j=y=>{if(!y)return"Never";const w=new Date(y);return w.toLocaleDateString()+" "+w.toLocaleTimeString()};return i?a.jsx(X,{children:a.jsx("div",{className:"p-6",children:a.jsx("div",{className:"text-center text-gray-600",children:"Loading API permissions..."})})}):a.jsx(X,{children:a.jsxs("div",{className:"p-6",children:[u&&a.jsx(Vn,{message:u.message,type:u.type,onClose:()=>f(null)}),a.jsxs("div",{className:"flex justify-between items-center mb-6",children:[a.jsx("h1",{className:"text-2xl font-bold",children:"API Permissions"}),a.jsx("button",{onClick:()=>l(!o),className:"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700",children:o?"Cancel":"Add New Permission"})]}),a.jsxs("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6",children:[a.jsx("h3",{className:"font-semibold text-blue-900 mb-2",children:"How it works:"}),a.jsx("p",{className:"text-blue-800 text-sm",children:"Users with valid permissions can access your API without entering tokens. Access is automatically validated based on their IP address and/or domain name."})]}),o&&a.jsxs("div",{className:"bg-white rounded-lg shadow-md p-6 mb-6",children:[a.jsx("h2",{className:"text-xl font-semibold mb-4",children:"Add New API User"}),a.jsxs("form",{onSubmit:x,children:[a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"User/Client Name *"}),a.jsx("input",{type:"text",value:c.user_name,onChange:y=>d({...c,user_name:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",placeholder:"e.g., My Website",required:!0}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"A friendly name to identify this API user"})]}),a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Store *"}),a.jsxs("select",{value:c.store_id,onChange:y=>d({...c,store_id:y.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",required:!0,children:[a.jsx("option",{value:"",children:"Select a store..."}),r.map(y=>a.jsx("option",{value:y.id,children:y.name},y.id))]}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"The store this API token can access"})]}),a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Allowed IP Addresses"}),a.jsx("textarea",{value:c.allowed_ips,onChange:y=>d({...c,allowed_ips:y.target.value}),rows:3,className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm",placeholder:`192.168.1.1 +10.0.0.0/8 +2001:db8::/32`}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:["One IP address or CIDR range per line. Leave empty to allow any IP.",a.jsx("br",{}),"Supports IPv4, IPv6, and CIDR notation (e.g., 192.168.0.0/24)"]})]}),a.jsxs("div",{className:"mb-4",children:[a.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Allowed Domains"}),a.jsx("textarea",{value:c.allowed_domains,onChange:y=>d({...c,allowed_domains:y.target.value}),rows:3,className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm",placeholder:`example.com +*.example.com +subdomain.example.com`}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"One domain per line. Wildcards supported (e.g., *.example.com). Leave empty to allow any domain."})]}),a.jsx("button",{type:"submit",className:"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700",children:"Create API Permission"})]})]}),a.jsxs("div",{className:"bg-white rounded-lg shadow-md overflow-hidden",children:[a.jsx("div",{className:"px-6 py-4 border-b border-gray-200",children:a.jsx("h2",{className:"text-xl font-semibold",children:"Active API Users"})}),e.length===0?a.jsx("div",{className:"p-6 text-center text-gray-600",children:"No API permissions configured yet. Add your first user above."}):a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"w-full",children:[a.jsx("thead",{className:"bg-gray-50",children:a.jsxs("tr",{children:[a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"User Name"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Store"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"API Key"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Allowed IPs"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Allowed Domains"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Status"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Last Used"}),a.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Actions"})]})}),a.jsx("tbody",{className:"bg-white divide-y divide-gray-200",children:e.map(y=>a.jsxs("tr",{children:[a.jsx("td",{className:"px-6 py-4 whitespace-nowrap",children:a.jsx("div",{className:"font-medium text-gray-900",children:y.user_name})}),a.jsx("td",{className:"px-6 py-4 whitespace-nowrap",children:a.jsx("div",{className:"text-sm text-gray-900",children:y.store_name||a.jsx("span",{className:"text-gray-400 italic",children:"No store"})})}),a.jsx("td",{className:"px-6 py-4",children:a.jsxs("div",{className:"flex items-center space-x-2",children:[a.jsxs("code",{className:"text-xs bg-gray-100 px-2 py-1 rounded",children:[y.api_key.substring(0,16),"..."]}),a.jsx("button",{onClick:()=>v(y.api_key),className:"text-blue-600 hover:text-blue-800 text-sm",children:"Copy"})]})}),a.jsx("td",{className:"px-6 py-4",children:y.allowed_ips?a.jsxs("div",{className:"text-sm text-gray-600",children:[y.allowed_ips.split(` +`).slice(0,2).join(", "),y.allowed_ips.split(` +`).length>2&&a.jsxs("span",{className:"text-gray-400",children:[" +",y.allowed_ips.split(` +`).length-2," more"]})]}):a.jsx("span",{className:"text-gray-400 italic",children:"Any IP"})}),a.jsx("td",{className:"px-6 py-4",children:y.allowed_domains?a.jsxs("div",{className:"text-sm text-gray-600",children:[y.allowed_domains.split(` +`).slice(0,2).join(", "),y.allowed_domains.split(` +`).length>2&&a.jsxs("span",{className:"text-gray-400",children:[" +",y.allowed_domains.split(` +`).length-2," more"]})]}):a.jsx("span",{className:"text-gray-400 italic",children:"Any domain"})}),a.jsx("td",{className:"px-6 py-4 whitespace-nowrap",children:y.is_active?a.jsx("span",{className:"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800",children:"Active"}):a.jsx("span",{className:"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800",children:"Disabled"})}),a.jsx("td",{className:"px-6 py-4 whitespace-nowrap text-sm text-gray-600",children:j(y.last_used_at)}),a.jsxs("td",{className:"px-6 py-4 whitespace-nowrap text-sm space-x-2",children:[a.jsx("button",{onClick:()=>g(y.id),className:"text-blue-600 hover:text-blue-800",children:y.is_active?"Disable":"Enable"}),a.jsx("button",{onClick:()=>b(y.id),className:"text-red-600 hover:text-red-800",children:"Delete"})]})]},y.id))})]})})]})]})})}function j7(){const[e,t]=h.useState([]),[r,n]=h.useState([]),[i,s]=h.useState(null),[o,l]=h.useState(null),[c,d]=h.useState(!0),[u,f]=h.useState(!0),[p,m]=h.useState("schedules"),[x,g]=h.useState(null),[b,v]=h.useState(!1),[j,y]=h.useState(!1),[w,S]=h.useState(null);h.useEffect(()=>{if(N(),u){const k=setInterval(N,1e4);return()=>clearInterval(k)}},[u]);const N=async()=>{try{const[k,L,U,H]=await Promise.all([B.getDutchieAZSchedules(),B.getDutchieAZRunLogs({limit:50}),B.getDutchieAZSchedulerStatus(),B.getDetectionStats().catch(()=>null)]);t(k.schedules||[]),n(L.logs||[]),s(U),l(H)}catch(k){console.error("Failed to load schedule data:",k)}finally{d(!1)}},P=async()=>{try{i!=null&&i.running?await B.stopDutchieAZScheduler():await B.startDutchieAZScheduler(),await N()}catch(k){console.error("Failed to toggle scheduler:",k)}},_=async()=>{try{await B.initDutchieAZSchedules(),await N()}catch(k){console.error("Failed to initialize schedules:",k)}},T=async k=>{try{await B.triggerDutchieAZSchedule(k),await N()}catch(L){console.error("Failed to trigger schedule:",L)}},$=async k=>{try{await B.updateDutchieAZSchedule(k.id,{enabled:!k.enabled}),await N()}catch(L){console.error("Failed to toggle schedule:",L)}},M=async(k,L)=>{try{const U={description:L.description??void 0,enabled:L.enabled,baseIntervalMinutes:L.baseIntervalMinutes,jitterMinutes:L.jitterMinutes,jobConfig:L.jobConfig??void 0};await B.updateDutchieAZSchedule(k,U),g(null),await N()}catch(U){console.error("Failed to update schedule:",U)}},C=async()=>{if(confirm("Run menu detection on all dispensaries with unknown/missing menu_type?")){y(!0),S(null);try{const k=await B.detectAllDispensaries({state:"AZ",onlyUnknown:!0});S(k),await N()}catch(k){console.error("Failed to run bulk detection:",k)}finally{y(!1)}}},R=async()=>{if(confirm("Resolve platform IDs for all Dutchie dispensaries missing them?")){y(!0),S(null);try{const k=await B.detectAllDispensaries({state:"AZ",onlyMissingPlatformId:!0,onlyUnknown:!1});S(k),await N()}catch(k){console.error("Failed to resolve platform IDs:",k)}finally{y(!1)}}},q=k=>{if(!k)return"Never";const L=new Date(k),H=new Date().getTime()-L.getTime(),te=Math.floor(H/6e4),re=Math.floor(te/60),we=Math.floor(re/24);return te<1?"Just now":te<60?`${te}m ago`:re<24?`${re}h ago`:`${we}d ago`},Z=k=>{if(!k)return"Not scheduled";const L=new Date(k),U=new Date,H=L.getTime()-U.getTime();if(H<0)return"Overdue";const te=Math.floor(H/6e4),re=Math.floor(te/60);return te<60?`${te}m`:`${re}h ${te%60}m`},E=k=>{if(!k)return"-";if(k<1e3)return`${k}ms`;const L=Math.floor(k/1e3),U=Math.floor(L/60);return U<1?`${L}s`:`${U}m ${L%60}s`},D=(k,L)=>{const U=Math.floor(k/60),H=k%60,te=Math.floor(L/60),re=L%60;let we=U>0?`${U}h`:"";H>0&&(we+=`${H}m`);let A=te>0?`${te}h`:"";return re>0&&(A+=`${re}m`),`${we} +/- ${A}`},O=k=>{switch(k){case"success":return{bg:"#d1fae5",color:"#065f46"};case"running":return{bg:"#dbeafe",color:"#1e40af"};case"error":return{bg:"#fee2e2",color:"#991b1b"};case"partial":return{bg:"#fef3c7",color:"#92400e"};default:return{bg:"#f3f4f6",color:"#374151"}}};return a.jsx(X,{children:a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"30px"},children:[a.jsxs("div",{children:[a.jsx("h1",{style:{fontSize:"32px",margin:0},children:"Dutchie AZ Schedule"}),a.jsx("p",{style:{color:"#666",margin:"8px 0 0 0"},children:"Jittered scheduling for Arizona Dutchie product crawls"})]}),a.jsx("div",{style:{display:"flex",gap:"15px",alignItems:"center"},children:a.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"10px",cursor:"pointer"},children:[a.jsx("input",{type:"checkbox",checked:u,onChange:k=>f(k.target.checked),style:{width:"18px",height:"18px",cursor:"pointer"}}),a.jsx("span",{children:"Auto-refresh (10s)"})]})})]}),a.jsxs("div",{style:{background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",marginBottom:"30px",display:"flex",justifyContent:"space-between",alignItems:"center"},children:[a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"20px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"4px"},children:"Scheduler Status"}),a.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[a.jsx("span",{style:{width:"12px",height:"12px",borderRadius:"50%",background:i!=null&&i.running?"#10b981":"#ef4444",display:"inline-block"}}),a.jsx("span",{style:{fontWeight:"600",fontSize:"18px"},children:i!=null&&i.running?"Running":"Stopped"})]})]}),a.jsxs("div",{style:{borderLeft:"1px solid #eee",paddingLeft:"20px"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"4px"},children:"Poll Interval"}),a.jsx("div",{style:{fontWeight:"600"},children:i?`${i.pollIntervalMs/1e3}s`:"-"})]}),a.jsxs("div",{style:{borderLeft:"1px solid #eee",paddingLeft:"20px"},children:[a.jsx("div",{style:{fontSize:"14px",color:"#666",marginBottom:"4px"},children:"Active Schedules"}),a.jsxs("div",{style:{fontWeight:"600"},children:[e.filter(k=>k.enabled).length," / ",e.length]})]})]}),a.jsxs("div",{style:{display:"flex",gap:"10px"},children:[a.jsx("button",{onClick:P,style:{padding:"10px 20px",background:i!=null&&i.running?"#ef4444":"#10b981",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:i!=null&&i.running?"Stop Scheduler":"Start Scheduler"}),e.length===0&&a.jsx("button",{onClick:_,style:{padding:"10px 20px",background:"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:"Initialize Default Schedules"})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"10px",borderBottom:"2px solid #eee"},children:[a.jsxs("button",{onClick:()=>m("schedules"),style:{padding:"12px 24px",background:p==="schedules"?"white":"transparent",border:"none",borderBottom:p==="schedules"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:p==="schedules"?"600":"400",color:p==="schedules"?"#2563eb":"#666",marginBottom:"-2px"},children:["Schedule Configs (",e.length,")"]}),a.jsxs("button",{onClick:()=>m("logs"),style:{padding:"12px 24px",background:p==="logs"?"white":"transparent",border:"none",borderBottom:p==="logs"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:p==="logs"?"600":"400",color:p==="logs"?"#2563eb":"#666",marginBottom:"-2px"},children:["Run Logs (",r.length,")"]}),a.jsxs("button",{onClick:()=>m("detection"),style:{padding:"12px 24px",background:p==="detection"?"white":"transparent",border:"none",borderBottom:p==="detection"?"3px solid #2563eb":"3px solid transparent",cursor:"pointer",fontSize:"16px",fontWeight:p==="detection"?"600":"400",color:p==="detection"?"#2563eb":"#666",marginBottom:"-2px"},children:["Menu Detection ",o!=null&&o.needsDetection?`(${o.needsDetection} pending)`:""]})]}),p==="schedules"&&a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:e.length===0?a.jsx("div",{style:{padding:"40px",textAlign:"center",color:"#666"},children:'No schedules configured. Click "Initialize Default Schedules" to create the default crawl schedule.'}):a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Job Name"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Enabled"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Interval (Jitter)"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Last Run"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Next Run"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Last Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Actions"})]})}),a.jsx("tbody",{children:e.map(k=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:k.jobName}),k.description&&a.jsx("div",{style:{fontSize:"13px",color:"#666",marginTop:"4px"},children:k.description}),k.jobConfig&&a.jsxs("div",{style:{fontSize:"11px",color:"#999",marginTop:"4px"},children:["Config: ",JSON.stringify(k.jobConfig)]})]}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("button",{onClick:()=>$(k),style:{padding:"4px 12px",borderRadius:"12px",border:"none",cursor:"pointer",fontWeight:"600",fontSize:"12px",background:k.enabled?"#d1fae5":"#fee2e2",color:k.enabled?"#065f46":"#991b1b"},children:k.enabled?"ON":"OFF"})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsx("div",{style:{fontWeight:"600"},children:D(k.baseIntervalMinutes,k.jitterMinutes)})}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{children:q(k.lastRunAt)}),k.lastDurationMs&&a.jsxs("div",{style:{fontSize:"12px",color:"#666"},children:["Duration: ",E(k.lastDurationMs)]})]}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600",color:"#2563eb"},children:Z(k.nextRunAt)}),k.nextRunAt&&a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:new Date(k.nextRunAt).toLocaleString()})]}),a.jsx("td",{style:{padding:"15px"},children:k.lastStatus?a.jsxs("div",{children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...O(k.lastStatus)},children:k.lastStatus}),k.lastErrorMessage&&a.jsx("button",{onClick:()=>alert(k.lastErrorMessage),style:{marginLeft:"8px",padding:"2px 6px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"10px"},children:"Error"})]}):a.jsx("span",{style:{color:"#999"},children:"Never run"})}),a.jsx("td",{style:{padding:"15px",textAlign:"center"},children:a.jsxs("div",{style:{display:"flex",gap:"8px",justifyContent:"center"},children:[a.jsx("button",{onClick:()=>T(k.id),disabled:k.lastStatus==="running",style:{padding:"6px 12px",background:k.lastStatus==="running"?"#94a3b8":"#2563eb",color:"white",border:"none",borderRadius:"4px",cursor:k.lastStatus==="running"?"not-allowed":"pointer",fontSize:"13px"},children:"Run Now"}),a.jsx("button",{onClick:()=>g(k),style:{padding:"6px 12px",background:"#f3f4f6",color:"#374151",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"13px"},children:"Edit"})]})})]},k.id))})]})}),p==="logs"&&a.jsx("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",overflow:"hidden"},children:r.length===0?a.jsx("div",{style:{padding:"40px",textAlign:"center",color:"#666"},children:"No run logs yet. Logs will appear here after jobs execute."}):a.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[a.jsx("thead",{children:a.jsxs("tr",{style:{background:"#f8f8f8",borderBottom:"2px solid #eee"},children:[a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Job"}),a.jsx("th",{style:{padding:"15px",textAlign:"center",fontWeight:"600"},children:"Status"}),a.jsx("th",{style:{padding:"15px",textAlign:"left",fontWeight:"600"},children:"Started"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Duration"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Processed"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Succeeded"}),a.jsx("th",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:"Failed"})]})}),a.jsx("tbody",{children:r.map(k=>a.jsxs("tr",{style:{borderBottom:"1px solid #eee"},children:[a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{style:{fontWeight:"600"},children:k.job_name}),a.jsxs("div",{style:{fontSize:"12px",color:"#999"},children:["Run #",k.id]})]}),a.jsxs("td",{style:{padding:"15px",textAlign:"center"},children:[a.jsx("span",{style:{padding:"4px 10px",borderRadius:"12px",fontSize:"12px",fontWeight:"600",...O(k.status)},children:k.status}),k.error_message&&a.jsx("button",{onClick:()=>alert(k.error_message),style:{marginLeft:"8px",padding:"2px 6px",background:"#fee2e2",color:"#991b1b",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"10px"},children:"Error"})]}),a.jsxs("td",{style:{padding:"15px"},children:[a.jsx("div",{children:k.started_at?new Date(k.started_at).toLocaleString():"-"}),a.jsx("div",{style:{fontSize:"12px",color:"#999"},children:q(k.started_at)})]}),a.jsx("td",{style:{padding:"15px",textAlign:"right",fontWeight:"600"},children:E(k.duration_ms)}),a.jsx("td",{style:{padding:"15px",textAlign:"right"},children:k.items_processed??"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"right",color:"#10b981"},children:k.items_succeeded??"-"}),a.jsx("td",{style:{padding:"15px",textAlign:"right",color:k.items_failed?"#ef4444":"inherit"},children:k.items_failed??"-"})]},k.id))})]})}),p==="detection"&&a.jsxs("div",{style:{background:"white",borderRadius:"8px",boxShadow:"0 2px 8px rgba(0,0,0,0.1)",padding:"30px"},children:[o&&a.jsxs("div",{style:{marginBottom:"30px"},children:[a.jsx("h3",{style:{margin:"0 0 20px 0"},children:"Detection Statistics"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:"20px"},children:[a.jsxs("div",{style:{padding:"20px",background:"#f8f8f8",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#2563eb"},children:o.totalDispensaries}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"Total Dispensaries"})]}),a.jsxs("div",{style:{padding:"20px",background:"#f8f8f8",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#10b981"},children:o.withMenuType}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"With Menu Type"})]}),a.jsxs("div",{style:{padding:"20px",background:"#f8f8f8",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#10b981"},children:o.withPlatformId}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"With Platform ID"})]}),a.jsxs("div",{style:{padding:"20px",background:"#fef3c7",borderRadius:"8px",textAlign:"center"},children:[a.jsx("div",{style:{fontSize:"32px",fontWeight:"700",color:"#92400e"},children:o.needsDetection}),a.jsx("div",{style:{color:"#666",marginTop:"4px"},children:"Needs Detection"})]})]}),Object.keys(o.byProvider).length>0&&a.jsxs("div",{style:{marginTop:"20px"},children:[a.jsx("h4",{style:{margin:"0 0 10px 0"},children:"By Provider"}),a.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"10px"},children:Object.entries(o.byProvider).map(([k,L])=>a.jsxs("span",{style:{padding:"6px 14px",background:k==="dutchie"?"#dbeafe":"#f3f4f6",borderRadius:"16px",fontSize:"14px",fontWeight:"600"},children:[k,": ",L]},k))})]})]}),a.jsxs("div",{style:{marginBottom:"30px",display:"flex",gap:"15px",flexWrap:"wrap"},children:[a.jsx("button",{onClick:C,disabled:j||!(o!=null&&o.needsDetection),style:{padding:"12px 24px",background:j?"#94a3b8":"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:j?"not-allowed":"pointer",fontWeight:"600",fontSize:"14px"},children:j?"Detecting...":"Detect All Unknown"}),a.jsx("button",{onClick:R,disabled:j,style:{padding:"12px 24px",background:j?"#94a3b8":"#10b981",color:"white",border:"none",borderRadius:"6px",cursor:j?"not-allowed":"pointer",fontWeight:"600",fontSize:"14px"},children:j?"Resolving...":"Resolve Missing Platform IDs"})]}),w&&a.jsxs("div",{style:{marginBottom:"30px",padding:"20px",background:"#f8f8f8",borderRadius:"8px"},children:[a.jsx("h4",{style:{margin:"0 0 15px 0"},children:"Detection Results"}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(4, 1fr)",gap:"15px",marginBottom:"15px"},children:[a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px"},children:w.totalProcessed}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Processed"})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px",color:"#10b981"},children:w.totalSucceeded}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Succeeded"})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px",color:"#ef4444"},children:w.totalFailed}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Failed"})]}),a.jsxs("div",{children:[a.jsx("div",{style:{fontWeight:"600",fontSize:"24px",color:"#666"},children:w.totalSkipped}),a.jsx("div",{style:{color:"#666",fontSize:"13px"},children:"Skipped"})]})]}),w.errors&&w.errors.length>0&&a.jsxs("div",{style:{marginTop:"15px"},children:[a.jsx("div",{style:{fontWeight:"600",marginBottom:"8px",color:"#991b1b"},children:"Errors:"}),a.jsxs("div",{style:{maxHeight:"150px",overflow:"auto",background:"#fee2e2",padding:"10px",borderRadius:"4px",fontSize:"12px"},children:[w.errors.slice(0,10).map((k,L)=>a.jsx("div",{style:{marginBottom:"4px"},children:k},L)),w.errors.length>10&&a.jsxs("div",{style:{fontStyle:"italic",marginTop:"8px"},children:["...and ",w.errors.length-10," more"]})]})]})]}),a.jsxs("div",{style:{padding:"20px",background:"#f0f9ff",borderRadius:"8px",fontSize:"14px"},children:[a.jsx("h4",{style:{margin:"0 0 10px 0",color:"#1e40af"},children:"About Menu Detection"}),a.jsxs("ul",{style:{margin:0,paddingLeft:"20px",color:"#1e40af"},children:[a.jsxs("li",{style:{marginBottom:"8px"},children:[a.jsx("strong",{children:"Detect All Unknown:"})," Scans dispensaries with no menu_type set and detects the provider (dutchie, treez, jane, etc.) from their menu_url."]}),a.jsxs("li",{style:{marginBottom:"8px"},children:[a.jsx("strong",{children:"Resolve Missing Platform IDs:"}),' For dispensaries already detected as "dutchie", extracts the cName from menu_url and resolves the platform_dispensary_id via GraphQL.']}),a.jsxs("li",{children:[a.jsx("strong",{children:"Automatic scheduling:"}),' A "Menu Detection" job runs daily (24h +/- 1h jitter) to detect new dispensaries.']})]})]})]}),x&&a.jsx("div",{style:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.5)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e3},children:a.jsxs("div",{style:{background:"white",padding:"30px",borderRadius:"12px",width:"500px",maxWidth:"90vw"},children:[a.jsxs("h2",{style:{margin:"0 0 20px 0"},children:["Edit Schedule: ",x.jobName]}),a.jsxs("div",{style:{marginBottom:"20px"},children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"600"},children:"Description"}),a.jsx("input",{type:"text",value:x.description||"",onChange:k=>g({...x,description:k.target.value}),style:{width:"100%",padding:"10px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"14px"}})]}),a.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"20px",marginBottom:"20px"},children:[a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"600"},children:"Base Interval (minutes)"}),a.jsx("input",{type:"number",value:x.baseIntervalMinutes,onChange:k=>g({...x,baseIntervalMinutes:parseInt(k.target.value)||240}),style:{width:"100%",padding:"10px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"14px"}}),a.jsxs("div",{style:{fontSize:"12px",color:"#666",marginTop:"4px"},children:["= ",Math.floor(x.baseIntervalMinutes/60),"h ",x.baseIntervalMinutes%60,"m"]})]}),a.jsxs("div",{children:[a.jsx("label",{style:{display:"block",marginBottom:"8px",fontWeight:"600"},children:"Jitter (minutes)"}),a.jsx("input",{type:"number",value:x.jitterMinutes,onChange:k=>g({...x,jitterMinutes:parseInt(k.target.value)||30}),style:{width:"100%",padding:"10px",borderRadius:"6px",border:"1px solid #ddd",fontSize:"14px"}}),a.jsxs("div",{style:{fontSize:"12px",color:"#666",marginTop:"4px"},children:["+/- ",x.jitterMinutes,"m random offset"]})]})]}),a.jsxs("div",{style:{fontSize:"13px",color:"#666",marginBottom:"20px",padding:"15px",background:"#f8f8f8",borderRadius:"6px"},children:[a.jsx("strong",{children:"Effective range:"})," ",Math.floor((x.baseIntervalMinutes-x.jitterMinutes)/60),"h ",(x.baseIntervalMinutes-x.jitterMinutes)%60,"m"," to ",Math.floor((x.baseIntervalMinutes+x.jitterMinutes)/60),"h ",(x.baseIntervalMinutes+x.jitterMinutes)%60,"m"]}),a.jsxs("div",{style:{display:"flex",gap:"10px",justifyContent:"flex-end"},children:[a.jsx("button",{onClick:()=>g(null),style:{padding:"10px 20px",background:"#f3f4f6",color:"#374151",border:"none",borderRadius:"6px",cursor:"pointer"},children:"Cancel"}),a.jsx("button",{onClick:()=>M(x.id,{description:x.description,baseIntervalMinutes:x.baseIntervalMinutes,jitterMinutes:x.jitterMinutes}),style:{padding:"10px 20px",background:"#2563eb",color:"white",border:"none",borderRadius:"6px",cursor:"pointer",fontWeight:"600"},children:"Save Changes"})]})]})})]})})}function w7(){const e=dt(),[t,r]=h.useState([]),[n,i]=h.useState(0),[s,o]=h.useState(!0),[l,c]=h.useState(null);h.useEffect(()=>{d()},[]);const d=async()=>{o(!0);try{const[u,f]=await Promise.all([B.getDutchieAZStores({limit:200}),B.getDutchieAZDashboard()]);r(u.stores),i(u.total),c(f)}catch(u){console.error("Failed to load data:",u)}finally{o(!1)}};return s?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading stores..."})]})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Dutchie AZ Stores"}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"Arizona dispensaries using the Dutchie platform - data from the new pipeline"})]}),a.jsxs("button",{onClick:d,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),l&&a.jsxs("div",{className:"grid grid-cols-4 gap-4",children:[a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-5 h-5 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Dispensaries"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.dispensaryCount})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.productCount.toLocaleString()})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.brandCount})]})]})}),a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Failed Jobs (24h)"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:l.failedJobCount})]})]})})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"p-4 border-b border-gray-200",children:a.jsxs("h2",{className:"text-lg font-semibold text-gray-900",children:["All Stores (",n,")"]})}),a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"table table-zebra w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Name"}),a.jsx("th",{children:"City"}),a.jsx("th",{children:"Platform ID"}),a.jsx("th",{children:"Status"}),a.jsx("th",{children:"Actions"})]})}),a.jsx("tbody",{children:t.map(u=>a.jsxs("tr",{children:[a.jsx("td",{children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-4 h-4 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"font-medium text-gray-900",children:u.dba_name||u.name}),u.company_name&&u.company_name!==u.name&&a.jsx("p",{className:"text-xs text-gray-500",children:u.company_name})]})]})}),a.jsx("td",{children:a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(yi,{className:"w-4 h-4"}),u.city,", ",u.state]})}),a.jsx("td",{children:u.platform_dispensary_id?a.jsx("span",{className:"text-xs font-mono text-gray-600",children:u.platform_dispensary_id}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Not Resolved"})}),a.jsx("td",{children:u.platform_dispensary_id?a.jsx("span",{className:"badge badge-success badge-sm",children:"Ready"}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Pending"})}),a.jsx("td",{children:a.jsx("button",{onClick:()=>e(`/az/stores/${u.id}`),className:"btn btn-sm btn-primary",disabled:!u.platform_dispensary_id,children:"View Products"})})]},u.id))})]})})]})]})})}function S7(){const{id:e}=Na(),t=dt(),[r,n]=h.useState(null),[i,s]=h.useState([]),[o,l]=h.useState(!0),[c,d]=h.useState(!1),[u,f]=h.useState("products"),[p,m]=h.useState(!1),[x,g]=h.useState(!1),[b,v]=h.useState(""),[j,y]=h.useState(1),[w,S]=h.useState(0),[N]=h.useState(25),[P,_]=h.useState(""),T=O=>{if(!O)return"Never";const k=new Date(O),U=new Date().getTime()-k.getTime(),H=Math.floor(U/(1e3*60*60*24));return H===0?"Today":H===1?"Yesterday":H<7?`${H} days ago`:k.toLocaleDateString()};h.useEffect(()=>{e&&$()},[e]),h.useEffect(()=>{e&&u==="products"&&M()},[e,j,b,P,u]),h.useEffect(()=>{y(1)},[b,P]);const $=async()=>{l(!0);try{const O=await B.getDutchieAZStoreSummary(parseInt(e,10));n(O)}catch(O){console.error("Failed to load store summary:",O)}finally{l(!1)}},M=async()=>{if(e){d(!0);try{const O=await B.getDutchieAZStoreProducts(parseInt(e,10),{search:b||void 0,stockStatus:P||void 0,limit:N,offset:(j-1)*N});s(O.products),S(O.total)}catch(O){console.error("Failed to load products:",O)}finally{d(!1)}}},C=async()=>{m(!1),g(!0);try{await B.triggerDutchieAZCrawl(parseInt(e,10)),alert("Crawl started! Refresh the page in a few minutes to see updated data.")}catch(O){console.error("Failed to trigger crawl:",O),alert("Failed to start crawl. Please try again.")}finally{g(!1)}},R=Math.ceil(w/N);if(o)return a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading store..."})]})});if(!r)return a.jsx(X,{children:a.jsx("div",{className:"text-center py-12",children:a.jsx("p",{className:"text-gray-600",children:"Store not found"})})});const{dispensary:q,brands:Z,categories:E,lastCrawl:D}=r;return a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("button",{onClick:()=>t("/az"),className:"flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900",children:[a.jsx(_h,{className:"w-4 h-4"}),"Back to AZ Stores"]}),a.jsxs("div",{className:"relative",children:[a.jsxs("button",{onClick:()=>m(!p),disabled:x,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed",children:[a.jsx(Xt,{className:`w-4 h-4 ${x?"animate-spin":""}`}),x?"Crawling...":"Crawl Now",!x&&a.jsx(ej,{className:"w-4 h-4"})]}),p&&!x&&a.jsx("div",{className:"absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-10",children:a.jsx("button",{onClick:C,className:"w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-lg",children:"Start Full Crawl"})})]})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 p-6",children:[a.jsxs("div",{className:"flex items-start justify-between gap-4 mb-4",children:[a.jsxs("div",{className:"flex items-start gap-4",children:[a.jsx("div",{className:"p-3 bg-blue-50 rounded-lg",children:a.jsx(Ln,{className:"w-8 h-8 text-blue-600"})}),a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:q.dba_name||q.name}),q.company_name&&a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:q.company_name}),a.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:["Platform ID: ",q.platform_dispensary_id||"Not resolved"]})]})]}),a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600 bg-gray-50 px-4 py-2 rounded-lg",children:[a.jsx(xr,{className:"w-4 h-4"}),a.jsxs("div",{children:[a.jsx("span",{className:"font-medium",children:"Last Crawl:"}),a.jsx("span",{className:"ml-2",children:D!=null&&D.completed_at?new Date(D.completed_at).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):"Never"}),(D==null?void 0:D.status)&&a.jsx("span",{className:`ml-2 px-2 py-0.5 rounded text-xs ${D.status==="completed"?"bg-green-100 text-green-800":D.status==="failed"?"bg-red-100 text-red-800":"bg-yellow-100 text-yellow-800"}`,children:D.status})]})]})]}),a.jsxs("div",{className:"flex flex-wrap gap-4",children:[q.address&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(yi,{className:"w-4 h-4"}),a.jsxs("span",{children:[q.address,", ",q.city,", ",q.state," ",q.zip]})]}),q.phone&&a.jsxs("div",{className:"flex items-center gap-2 text-sm text-gray-600",children:[a.jsx(Ah,{className:"w-4 h-4"}),a.jsx("span",{children:q.phone})]}),q.website&&a.jsxs("a",{href:q.website,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800",children:[a.jsx(Jr,{className:"w-4 h-4"}),"Website"]})]})]}),a.jsxs("div",{className:"grid grid-cols-5 gap-4",children:[a.jsx("button",{onClick:()=>{f("products"),_(""),v("")},className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${u==="products"&&!P?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-green-50 rounded-lg",children:a.jsx(Ct,{className:"w-5 h-5 text-green-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Total Products"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.totalProducts})]})]})}),a.jsx("button",{onClick:()=>{f("products"),_("in_stock"),v("")},className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${P==="in_stock"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-emerald-50 rounded-lg",children:a.jsx(_r,{className:"w-5 h-5 text-emerald-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"In Stock"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.inStockCount})]})]})}),a.jsx("button",{onClick:()=>{f("products"),_("out_of_stock"),v("")},className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${P==="out_of_stock"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-red-50 rounded-lg",children:a.jsx(Hr,{className:"w-5 h-5 text-red-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Out of Stock"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.outOfStockCount})]})]})}),a.jsx("button",{onClick:()=>f("brands"),className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${u==="brands"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-purple-50 rounded-lg",children:a.jsx(Cr,{className:"w-5 h-5 text-purple-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Brands"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.brandCount})]})]})}),a.jsx("button",{onClick:()=>f("categories"),className:`bg-white rounded-lg border p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer text-left ${u==="categories"?"border-blue-500":"border-gray-200"}`,children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:"p-2 bg-orange-50 rounded-lg",children:a.jsx(Uc,{className:"w-5 h-5 text-orange-600"})}),a.jsxs("div",{children:[a.jsx("p",{className:"text-sm text-gray-600",children:"Categories"}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:r.categoryCount})]})]})})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"border-b border-gray-200",children:a.jsxs("div",{className:"flex gap-4 px-6",children:[a.jsxs("button",{onClick:()=>{f("products"),_("")},className:`py-4 px-2 text-sm font-medium border-b-2 ${u==="products"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Products (",r.totalProducts,")"]}),a.jsxs("button",{onClick:()=>f("brands"),className:`py-4 px-2 text-sm font-medium border-b-2 ${u==="brands"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Brands (",r.brandCount,")"]}),a.jsxs("button",{onClick:()=>f("categories"),className:`py-4 px-2 text-sm font-medium border-b-2 ${u==="categories"?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:["Categories (",r.categoryCount,")"]})]})}),a.jsxs("div",{className:"p-6",children:[u==="products"&&a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{className:"flex items-center gap-4 mb-4",children:[a.jsx("input",{type:"text",placeholder:"Search products by name or brand...",value:b,onChange:O=>v(O.target.value),className:"input input-bordered input-sm flex-1"}),a.jsxs("select",{value:P,onChange:O=>_(O.target.value),className:"select select-bordered select-sm",children:[a.jsx("option",{value:"",children:"All Stock"}),a.jsx("option",{value:"in_stock",children:"In Stock"}),a.jsx("option",{value:"out_of_stock",children:"Out of Stock"}),a.jsx("option",{value:"unknown",children:"Unknown"})]}),(b||P)&&a.jsx("button",{onClick:()=>{v(""),_("")},className:"btn btn-sm btn-ghost",children:"Clear"}),a.jsxs("div",{className:"text-sm text-gray-600",children:[w," products"]})]}),c?a.jsxs("div",{className:"text-center py-8",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-6 w-6 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading products..."})]}):i.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No products found"}):a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"overflow-x-auto -mx-6 px-6",children:a.jsxs("table",{className:"table table-xs table-zebra table-pin-rows w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Image"}),a.jsx("th",{children:"Product Name"}),a.jsx("th",{children:"Brand"}),a.jsx("th",{children:"Type"}),a.jsx("th",{className:"text-right",children:"Price"}),a.jsx("th",{className:"text-center",children:"THC %"}),a.jsx("th",{className:"text-center",children:"Stock"}),a.jsx("th",{className:"text-center",children:"Qty"}),a.jsx("th",{children:"Last Updated"})]})}),a.jsx("tbody",{children:i.map(O=>a.jsxs("tr",{children:[a.jsx("td",{className:"whitespace-nowrap",children:O.image_url?a.jsx("img",{src:O.image_url,alt:O.name,className:"w-12 h-12 object-cover rounded",onError:k=>k.currentTarget.style.display="none"}):"-"}),a.jsx("td",{className:"font-medium max-w-[200px]",children:a.jsx("div",{className:"line-clamp-2",title:O.name,children:O.name})}),a.jsx("td",{className:"max-w-[120px]",children:a.jsx("div",{className:"line-clamp-2",title:O.brand||"-",children:O.brand||"-"})}),a.jsxs("td",{className:"whitespace-nowrap",children:[a.jsx("span",{className:"badge badge-ghost badge-sm",children:O.type||"-"}),O.subcategory&&a.jsx("span",{className:"badge badge-ghost badge-sm ml-1",children:O.subcategory})]}),a.jsx("td",{className:"text-right font-semibold whitespace-nowrap",children:O.sale_price?a.jsxs("div",{className:"flex flex-col items-end",children:[a.jsxs("span",{className:"text-error",children:["$",O.sale_price]}),a.jsxs("span",{className:"text-gray-400 line-through text-xs",children:["$",O.regular_price]})]}):O.regular_price?`$${O.regular_price}`:"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:O.thc_percentage?a.jsxs("span",{className:"badge badge-success badge-sm",children:[O.thc_percentage,"%"]}):"-"}),a.jsx("td",{className:"text-center whitespace-nowrap",children:O.stock_status==="in_stock"?a.jsx("span",{className:"badge badge-success badge-sm",children:"In Stock"}):O.stock_status==="out_of_stock"?a.jsx("span",{className:"badge badge-error badge-sm",children:"Out"}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Unknown"})}),a.jsx("td",{className:"text-center whitespace-nowrap",children:O.total_quantity!=null?O.total_quantity:"-"}),a.jsx("td",{className:"whitespace-nowrap text-xs text-gray-500",children:O.updated_at?T(O.updated_at):"-"})]},O.id))})]})}),R>1&&a.jsxs("div",{className:"flex justify-center items-center gap-2 mt-4",children:[a.jsx("button",{onClick:()=>y(O=>Math.max(1,O-1)),disabled:j===1,className:"btn btn-sm btn-outline",children:"Previous"}),a.jsx("div",{className:"flex gap-1",children:Array.from({length:Math.min(5,R)},(O,k)=>{let L;return R<=5||j<=3?L=k+1:j>=R-2?L=R-4+k:L=j-2+k,a.jsx("button",{onClick:()=>y(L),className:`btn btn-sm ${j===L?"btn-primary":"btn-outline"}`,children:L},L)})}),a.jsx("button",{onClick:()=>y(O=>Math.min(R,O+1)),disabled:j===R,className:"btn btn-sm btn-outline",children:"Next"})]})]})]}),u==="brands"&&a.jsx("div",{className:"space-y-4",children:Z.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No brands found"}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:Z.map(O=>a.jsxs("button",{onClick:()=>{f("products"),v(O.brand_name),_("")},className:"border border-gray-200 rounded-lg p-4 text-center hover:border-blue-300 hover:shadow-md transition-all cursor-pointer",children:[a.jsx("p",{className:"font-medium text-gray-900 line-clamp-2",children:O.brand_name}),a.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:[O.product_count," product",O.product_count!==1?"s":""]})]},O.brand_name))})}),u==="categories"&&a.jsx("div",{className:"space-y-4",children:E.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No categories found"}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:E.map((O,k)=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4 text-center",children:[a.jsx("p",{className:"font-medium text-gray-900",children:O.type}),O.subcategory&&a.jsx("p",{className:"text-sm text-gray-600",children:O.subcategory}),a.jsxs("p",{className:"text-sm text-gray-500 mt-1",children:[O.product_count," product",O.product_count!==1?"s":""]})]},k))})})]})]})]})})}function N7(){const e=dt(),[t,r]=h.useState(null),[n,i]=h.useState([]),[s,o]=h.useState([]),[l,c]=h.useState([]),[d,u]=h.useState(!0),[f,p]=h.useState("overview");h.useEffect(()=>{m()},[]);const m=async()=>{u(!0);try{const[g,b,v,j]=await Promise.all([B.getDutchieAZDashboard(),B.getDutchieAZStores({limit:200}),B.getDutchieAZBrands?B.getDutchieAZBrands({limit:100}):Promise.resolve({brands:[]}),B.getDutchieAZCategories?B.getDutchieAZCategories():Promise.resolve({categories:[]})]);r(g),i(b.stores||[]),o(v.brands||[]),c(j.categories||[])}catch(g){console.error("Failed to load analytics data:",g)}finally{u(!1)}},x=g=>{if(!g)return"Never";const b=new Date(g),j=new Date().getTime()-b.getTime(),y=Math.floor(j/(1e3*60*60)),w=Math.floor(j/(1e3*60*60*24));return y<1?"Just now":y<24?`${y}h ago`:w===1?"Yesterday":w<7?`${w} days ago`:b.toLocaleDateString()};return d?a.jsx(X,{children:a.jsxs("div",{className:"text-center py-12",children:[a.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"}),a.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading analytics..."})]})}):a.jsx(X,{children:a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsxs("div",{children:[a.jsx("h1",{className:"text-2xl font-bold text-gray-900",children:"Wholesale & Inventory Analytics"}),a.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"Arizona Dutchie dispensaries data overview"})]}),a.jsxs("button",{onClick:m,className:"flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50",children:[a.jsx(Xt,{className:"w-4 h-4"}),"Refresh"]})]}),a.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4",children:[a.jsx(Ii,{title:"Dispensaries",value:(t==null?void 0:t.dispensaryCount)||0,icon:a.jsx(Ln,{className:"w-5 h-5 text-blue-600"}),color:"blue"}),a.jsx(Ii,{title:"Total Products",value:(t==null?void 0:t.productCount)||0,icon:a.jsx(Ct,{className:"w-5 h-5 text-green-600"}),color:"green"}),a.jsx(Ii,{title:"Brands",value:(t==null?void 0:t.brandCount)||0,icon:a.jsx(Cr,{className:"w-5 h-5 text-purple-600"}),color:"purple"}),a.jsx(Ii,{title:"Categories",value:(t==null?void 0:t.categoryCount)||0,icon:a.jsx(Tx,{className:"w-5 h-5 text-orange-600"}),color:"orange"}),a.jsx(Ii,{title:"Snapshots (24h)",value:(t==null?void 0:t.snapshotCount24h)||0,icon:a.jsx(zn,{className:"w-5 h-5 text-cyan-600"}),color:"cyan"}),a.jsx(Ii,{title:"Failed Jobs (24h)",value:(t==null?void 0:t.failedJobCount)||0,icon:a.jsx(Uc,{className:"w-5 h-5 text-red-600"}),color:"red"}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:[a.jsxs("div",{className:"flex items-center gap-2 text-gray-600 mb-1",children:[a.jsx(xr,{className:"w-4 h-4"}),a.jsx("span",{className:"text-xs",children:"Last Crawl"})]}),a.jsx("p",{className:"text-sm font-semibold text-gray-900",children:x((t==null?void 0:t.lastCrawlTime)||null)})]})]}),a.jsxs("div",{className:"bg-white rounded-lg border border-gray-200",children:[a.jsx("div",{className:"border-b border-gray-200",children:a.jsxs("div",{className:"flex gap-4 px-6",children:[a.jsx(Vo,{active:f==="overview",onClick:()=>p("overview"),icon:a.jsx(HA,{className:"w-4 h-4"}),label:"Overview"}),a.jsx(Vo,{active:f==="stores",onClick:()=>p("stores"),icon:a.jsx(Ln,{className:"w-4 h-4"}),label:`Stores (${n.length})`}),a.jsx(Vo,{active:f==="brands",onClick:()=>p("brands"),icon:a.jsx(Cr,{className:"w-4 h-4"}),label:`Brands (${s.length})`}),a.jsx(Vo,{active:f==="categories",onClick:()=>p("categories"),icon:a.jsx(Tx,{className:"w-4 h-4"}),label:`Categories (${l.length})`})]})}),a.jsxs("div",{className:"p-6",children:[f==="overview"&&a.jsxs("div",{className:"space-y-6",children:[a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Top Stores by Products"}),a.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",children:n.slice(0,6).map(g=>a.jsxs("button",{onClick:()=>e(`/az/stores/${g.id}`),className:"flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors text-left",children:[a.jsxs("div",{children:[a.jsx("p",{className:"font-medium text-gray-900",children:g.dba_name||g.name}),a.jsxs("p",{className:"text-sm text-gray-600",children:[g.city,", ",g.state]}),a.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:["Last crawl: ",x(g.last_crawl_at||null)]})]}),a.jsx(Cf,{className:"w-5 h-5 text-gray-400"})]},g.id))})]}),a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Top Brands"}),a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4",children:s.slice(0,12).map(g=>a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg text-center",children:[a.jsx("p",{className:"font-medium text-gray-900 text-sm line-clamp-2",children:g.brand_name}),a.jsx("p",{className:"text-lg font-bold text-purple-600 mt-1",children:g.product_count}),a.jsx("p",{className:"text-xs text-gray-500",children:"products"})]},g.brand_name))})]}),a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-4",children:"Product Categories"}),a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4",children:l.slice(0,12).map((g,b)=>a.jsxs("div",{className:"p-4 bg-gray-50 rounded-lg text-center",children:[a.jsx("p",{className:"font-medium text-gray-900",children:g.type}),g.subcategory&&a.jsx("p",{className:"text-xs text-gray-600",children:g.subcategory}),a.jsx("p",{className:"text-lg font-bold text-orange-600 mt-1",children:g.product_count}),a.jsx("p",{className:"text-xs text-gray-500",children:"products"})]},b))})]})]}),f==="stores"&&a.jsx("div",{className:"space-y-4",children:a.jsx("div",{className:"overflow-x-auto",children:a.jsxs("table",{className:"table table-sm w-full",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Store Name"}),a.jsx("th",{children:"City"}),a.jsx("th",{className:"text-center",children:"Platform ID"}),a.jsx("th",{className:"text-center",children:"Last Crawl"}),a.jsx("th",{})]})}),a.jsx("tbody",{children:n.map(g=>a.jsxs("tr",{className:"hover",children:[a.jsx("td",{className:"font-medium",children:g.dba_name||g.name}),a.jsxs("td",{children:[g.city,", ",g.state]}),a.jsx("td",{className:"text-center",children:g.platform_dispensary_id?a.jsx("span",{className:"badge badge-success badge-sm",children:"Resolved"}):a.jsx("span",{className:"badge badge-warning badge-sm",children:"Pending"})}),a.jsx("td",{className:"text-center text-sm text-gray-600",children:x(g.last_crawl_at||null)}),a.jsx("td",{children:a.jsx("button",{onClick:()=>e(`/az/stores/${g.id}`),className:"btn btn-xs btn-ghost",children:"View"})})]},g.id))})]})})}),f==="brands"&&a.jsx("div",{className:"space-y-4",children:s.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No brands found. Run a crawl to populate brand data."}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4",children:s.map(g=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4 text-center hover:border-purple-300 hover:shadow-md transition-all",children:[a.jsx("p",{className:"font-medium text-gray-900 text-sm line-clamp-2 h-10",children:g.brand_name}),a.jsxs("div",{className:"mt-2 space-y-1",children:[a.jsxs("div",{className:"flex justify-between text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Products:"}),a.jsx("span",{className:"font-semibold",children:g.product_count})]}),a.jsxs("div",{className:"flex justify-between text-xs",children:[a.jsx("span",{className:"text-gray-500",children:"Stores:"}),a.jsx("span",{className:"font-semibold",children:g.dispensary_count})]})]})]},g.brand_name))})}),f==="categories"&&a.jsx("div",{className:"space-y-4",children:l.length===0?a.jsx("p",{className:"text-center py-8 text-gray-500",children:"No categories found. Run a crawl to populate category data."}):a.jsx("div",{className:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",children:l.map((g,b)=>a.jsxs("div",{className:"border border-gray-200 rounded-lg p-4 hover:border-orange-300 hover:shadow-md transition-all",children:[a.jsx("p",{className:"font-medium text-gray-900",children:g.type}),g.subcategory&&a.jsx("p",{className:"text-sm text-gray-600",children:g.subcategory}),a.jsxs("div",{className:"mt-3 grid grid-cols-2 gap-2 text-xs",children:[a.jsxs("div",{className:"bg-gray-50 rounded p-2 text-center",children:[a.jsx("p",{className:"font-bold text-lg text-orange-600",children:g.product_count}),a.jsx("p",{className:"text-gray-500",children:"products"})]}),a.jsxs("div",{className:"bg-gray-50 rounded p-2 text-center",children:[a.jsx("p",{className:"font-bold text-lg text-blue-600",children:g.brand_count}),a.jsx("p",{className:"text-gray-500",children:"brands"})]})]}),g.avg_thc!=null&&a.jsxs("p",{className:"text-xs text-gray-500 mt-2 text-center",children:["Avg THC: ",g.avg_thc.toFixed(1),"%"]})]},b))})})]})]})]})})}function Ii({title:e,value:t,icon:r,color:n}){const i={blue:"bg-blue-50",green:"bg-green-50",purple:"bg-purple-50",orange:"bg-orange-50",cyan:"bg-cyan-50",red:"bg-red-50"};return a.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4",children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:`p-2 ${i[n]||"bg-gray-50"} rounded-lg`,children:r}),a.jsxs("div",{children:[a.jsx("p",{className:"text-xs text-gray-600",children:e}),a.jsx("p",{className:"text-xl font-bold text-gray-900",children:t.toLocaleString()})]})]})})}function Vo({active:e,onClick:t,icon:r,label:n}){return a.jsxs("button",{onClick:t,className:`flex items-center gap-2 py-4 px-2 text-sm font-medium border-b-2 transition-colors ${e?"border-blue-600 text-blue-600":"border-transparent text-gray-600 hover:text-gray-900"}`,children:[r,n]})}function ve({children:e}){const{isAuthenticated:t,checkAuth:r}=Ph();return h.useEffect(()=>{r()},[]),t?a.jsx(a.Fragment,{children:e}):a.jsx(H1,{to:"/login",replace:!0})}function k7(){return a.jsx(QC,{children:a.jsxs(HC,{children:[a.jsx(de,{path:"/login",element:a.jsx(DA,{})}),a.jsx(de,{path:"/",element:a.jsx(ve,{children:a.jsx(XB,{})})}),a.jsx(de,{path:"/products",element:a.jsx(ve,{children:a.jsx(JB,{})})}),a.jsx(de,{path:"/products/:id",element:a.jsx(ve,{children:a.jsx(e7,{})})}),a.jsx(de,{path:"/stores",element:a.jsx(ve,{children:a.jsx(t7,{})})}),a.jsx(de,{path:"/dispensaries",element:a.jsx(ve,{children:a.jsx(r7,{})})}),a.jsx(de,{path:"/dispensaries/:state/:city/:slug",element:a.jsx(ve,{children:a.jsx(n7,{})})}),a.jsx(de,{path:"/stores/:state/:storeName/:slug/brands",element:a.jsx(ve,{children:a.jsx(a7,{})})}),a.jsx(de,{path:"/stores/:state/:storeName/:slug/specials",element:a.jsx(ve,{children:a.jsx(s7,{})})}),a.jsx(de,{path:"/stores/:state/:storeName/:slug",element:a.jsx(ve,{children:a.jsx(i7,{})})}),a.jsx(de,{path:"/categories",element:a.jsx(ve,{children:a.jsx(o7,{})})}),a.jsx(de,{path:"/campaigns",element:a.jsx(ve,{children:a.jsx(l7,{})})}),a.jsx(de,{path:"/analytics",element:a.jsx(ve,{children:a.jsx(u7,{})})}),a.jsx(de,{path:"/settings",element:a.jsx(ve,{children:a.jsx(d7,{})})}),a.jsx(de,{path:"/changes",element:a.jsx(ve,{children:a.jsx(v7,{})})}),a.jsx(de,{path:"/proxies",element:a.jsx(ve,{children:a.jsx(p7,{})})}),a.jsx(de,{path:"/logs",element:a.jsx(ve,{children:a.jsx(m7,{})})}),a.jsx(de,{path:"/scraper-tools",element:a.jsx(ve,{children:a.jsx(y7,{})})}),a.jsx(de,{path:"/scraper-monitor",element:a.jsx(ve,{children:a.jsx(g7,{})})}),a.jsx(de,{path:"/scraper-schedule",element:a.jsx(ve,{children:a.jsx(x7,{})})}),a.jsx(de,{path:"/az-schedule",element:a.jsx(ve,{children:a.jsx(j7,{})})}),a.jsx(de,{path:"/az",element:a.jsx(ve,{children:a.jsx(w7,{})})}),a.jsx(de,{path:"/az/stores/:id",element:a.jsx(ve,{children:a.jsx(S7,{})})}),a.jsx(de,{path:"/api-permissions",element:a.jsx(ve,{children:a.jsx(b7,{})})}),a.jsx(de,{path:"/wholesale-analytics",element:a.jsx(ve,{children:a.jsx(N7,{})})}),a.jsx(de,{path:"*",element:a.jsx(H1,{to:"/",replace:!0})})]})})}Od.createRoot(document.getElementById("root")).render(a.jsx(fs.StrictMode,{children:a.jsx(k7,{})})); diff --git a/frontend/dist/index.html b/frontend/dist/index.html new file mode 100644 index 00000000..37352ff8 --- /dev/null +++ b/frontend/dist/index.html @@ -0,0 +1,14 @@ + + + + + + + Dutchie Menus Admin + + + + +
+ + diff --git a/frontend/dist/wordpress/menus-v1.2.0.zip b/frontend/dist/wordpress/menus-v1.2.0.zip new file mode 100644 index 00000000..47cecdc8 Binary files /dev/null and b/frontend/dist/wordpress/menus-v1.2.0.zip differ diff --git a/frontend/dist/wordpress/menus-v1.3.0.zip b/frontend/dist/wordpress/menus-v1.3.0.zip new file mode 100644 index 00000000..668e3d17 Binary files /dev/null and b/frontend/dist/wordpress/menus-v1.3.0.zip differ diff --git a/frontend/dist/wordpress/menus-v1.4.0.zip b/frontend/dist/wordpress/menus-v1.4.0.zip new file mode 100644 index 00000000..f4afdbf0 Binary files /dev/null and b/frontend/dist/wordpress/menus-v1.4.0.zip differ diff --git a/frontend/node_modules/.vite/deps/_metadata.json b/frontend/node_modules/.vite/deps/_metadata.json new file mode 100644 index 00000000..e41c7c71 --- /dev/null +++ b/frontend/node_modules/.vite/deps/_metadata.json @@ -0,0 +1,73 @@ +{ + "hash": "8bfc08c5", + "configHash": "1b04c5b0", + "lockfileHash": "04a90b39", + "browserHash": "ef95e9f7", + "optimized": { + "react": { + "src": "../../react/index.js", + "file": "react.js", + "fileHash": "6b13114c", + "needsInterop": true + }, + "react-dom": { + "src": "../../react-dom/index.js", + "file": "react-dom.js", + "fileHash": "2e7833d6", + "needsInterop": true + }, + "react/jsx-dev-runtime": { + "src": "../../react/jsx-dev-runtime.js", + "file": "react_jsx-dev-runtime.js", + "fileHash": "23d3b5f4", + "needsInterop": true + }, + "react/jsx-runtime": { + "src": "../../react/jsx-runtime.js", + "file": "react_jsx-runtime.js", + "fileHash": "c8341440", + "needsInterop": true + }, + "lucide-react": { + "src": "../../lucide-react/dist/esm/lucide-react.js", + "file": "lucide-react.js", + "fileHash": "7bbb8f0f", + "needsInterop": false + }, + "react-dom/client": { + "src": "../../react-dom/client.js", + "file": "react-dom_client.js", + "fileHash": "e475747d", + "needsInterop": true + }, + "react-router-dom": { + "src": "../../react-router-dom/dist/index.js", + "file": "react-router-dom.js", + "fileHash": "666b7910", + "needsInterop": false + }, + "recharts": { + "src": "../../recharts/es6/index.js", + "file": "recharts.js", + "fileHash": "875015f3", + "needsInterop": false + }, + "zustand": { + "src": "../../zustand/esm/index.mjs", + "file": "zustand.js", + "fileHash": "8be8cf46", + "needsInterop": false + } + }, + "chunks": { + "chunk-OOIH53S6": { + "file": "chunk-OOIH53S6.js" + }, + "chunk-QGEDPT23": { + "file": "chunk-QGEDPT23.js" + }, + "chunk-WXTH2UMW": { + "file": "chunk-WXTH2UMW.js" + } + } +} \ No newline at end of file diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-OOIH53S6.js b/frontend/node_modules/.vite/deps/chunk-OOIH53S6.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-OOIH53S6.js rename to frontend/node_modules/.vite/deps/chunk-OOIH53S6.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-OOIH53S6.js.map b/frontend/node_modules/.vite/deps/chunk-OOIH53S6.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-OOIH53S6.js.map rename to frontend/node_modules/.vite/deps/chunk-OOIH53S6.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-QGEDPT23.js b/frontend/node_modules/.vite/deps/chunk-QGEDPT23.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-QGEDPT23.js rename to frontend/node_modules/.vite/deps/chunk-QGEDPT23.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-QGEDPT23.js.map b/frontend/node_modules/.vite/deps/chunk-QGEDPT23.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-QGEDPT23.js.map rename to frontend/node_modules/.vite/deps/chunk-QGEDPT23.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-WXTH2UMW.js b/frontend/node_modules/.vite/deps/chunk-WXTH2UMW.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-WXTH2UMW.js rename to frontend/node_modules/.vite/deps/chunk-WXTH2UMW.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-WXTH2UMW.js.map b/frontend/node_modules/.vite/deps/chunk-WXTH2UMW.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/chunk-WXTH2UMW.js.map rename to frontend/node_modules/.vite/deps/chunk-WXTH2UMW.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/lucide-react.js b/frontend/node_modules/.vite/deps/lucide-react.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/lucide-react.js rename to frontend/node_modules/.vite/deps/lucide-react.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/lucide-react.js.map b/frontend/node_modules/.vite/deps/lucide-react.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/lucide-react.js.map rename to frontend/node_modules/.vite/deps/lucide-react.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/package.json b/frontend/node_modules/.vite/deps/package.json similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/package.json rename to frontend/node_modules/.vite/deps/package.json diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom.js b/frontend/node_modules/.vite/deps/react-dom.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom.js rename to frontend/node_modules/.vite/deps/react-dom.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom.js.map b/frontend/node_modules/.vite/deps/react-dom.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom.js.map rename to frontend/node_modules/.vite/deps/react-dom.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom_client.js b/frontend/node_modules/.vite/deps/react-dom_client.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom_client.js rename to frontend/node_modules/.vite/deps/react-dom_client.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom_client.js.map b/frontend/node_modules/.vite/deps/react-dom_client.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react-dom_client.js.map rename to frontend/node_modules/.vite/deps/react-dom_client.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react-router-dom.js b/frontend/node_modules/.vite/deps/react-router-dom.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react-router-dom.js rename to frontend/node_modules/.vite/deps/react-router-dom.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react-router-dom.js.map b/frontend/node_modules/.vite/deps/react-router-dom.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react-router-dom.js.map rename to frontend/node_modules/.vite/deps/react-router-dom.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react.js b/frontend/node_modules/.vite/deps/react.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react.js rename to frontend/node_modules/.vite/deps/react.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react.js.map b/frontend/node_modules/.vite/deps/react.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react.js.map rename to frontend/node_modules/.vite/deps/react.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-dev-runtime.js b/frontend/node_modules/.vite/deps/react_jsx-dev-runtime.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-dev-runtime.js rename to frontend/node_modules/.vite/deps/react_jsx-dev-runtime.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-dev-runtime.js.map b/frontend/node_modules/.vite/deps/react_jsx-dev-runtime.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-dev-runtime.js.map rename to frontend/node_modules/.vite/deps/react_jsx-dev-runtime.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-runtime.js b/frontend/node_modules/.vite/deps/react_jsx-runtime.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-runtime.js rename to frontend/node_modules/.vite/deps/react_jsx-runtime.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-runtime.js.map b/frontend/node_modules/.vite/deps/react_jsx-runtime.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/react_jsx-runtime.js.map rename to frontend/node_modules/.vite/deps/react_jsx-runtime.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/recharts.js b/frontend/node_modules/.vite/deps/recharts.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/recharts.js rename to frontend/node_modules/.vite/deps/recharts.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/recharts.js.map b/frontend/node_modules/.vite/deps/recharts.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/recharts.js.map rename to frontend/node_modules/.vite/deps/recharts.js.map diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/zustand.js b/frontend/node_modules/.vite/deps/zustand.js similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/zustand.js rename to frontend/node_modules/.vite/deps/zustand.js diff --git a/frontend/node_modules/.vite/deps_temp_d0ac3138/zustand.js.map b/frontend/node_modules/.vite/deps/zustand.js.map similarity index 100% rename from frontend/node_modules/.vite/deps_temp_d0ac3138/zustand.js.map rename to frontend/node_modules/.vite/deps/zustand.js.map diff --git a/frontend/public/wordpress/menus-v1.4.0.zip b/frontend/public/wordpress/menus-v1.4.0.zip new file mode 100644 index 00000000..f4afdbf0 Binary files /dev/null and b/frontend/public/wordpress/menus-v1.4.0.zip differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dae4888a..79685970 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,7 @@ import { ApiPermissions } from './pages/ApiPermissions'; import { DutchieAZSchedule } from './pages/DutchieAZSchedule'; import { DutchieAZStores } from './pages/DutchieAZStores'; import { DutchieAZStoreDetail } from './pages/DutchieAZStoreDetail'; +import { WholesaleAnalytics } from './pages/WholesaleAnalytics'; import { PrivateRoute } from './components/PrivateRoute'; export default function App() { @@ -53,6 +54,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 6c4b9b9e..d01542ce 100755 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -164,6 +164,12 @@ export function Layout({ children }: LayoutProps) { + } + label="Wholesale Analytics" + isActive={isActive('/wholesale-analytics')} + /> } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 701e6527..4972a8dd 100755 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -344,6 +344,56 @@ class ApiClient { return this.request(`/api/scraper-monitor/jobs/workers${params}`); } + // AZ Monitor (Dutchie AZ live crawler status) + async getAZMonitorActiveJobs() { + return this.request<{ + scheduledJobs: any[]; + crawlJobs: any[]; + inMemoryScrapers: any[]; + totalActive: number; + }>('/api/az/monitor/active-jobs'); + } + + async getAZMonitorRecentJobs(limit?: number) { + const params = limit ? `?limit=${limit}` : ''; + return this.request<{ + jobLogs: any[]; + crawlJobs: any[]; + }>(`/api/az/monitor/recent-jobs${params}`); + } + + async getAZMonitorErrors(params?: { limit?: number; hours?: number }) { + const searchParams = new URLSearchParams(); + if (params?.limit) searchParams.append('limit', params.limit.toString()); + if (params?.hours) searchParams.append('hours', params.hours.toString()); + const queryString = searchParams.toString() ? `?${searchParams.toString()}` : ''; + return this.request<{ errors: any[] }>(`/api/az/monitor/errors${queryString}`); + } + + async getAZMonitorSummary() { + return this.request<{ + running_scheduled_jobs: number; + running_crawl_jobs: number; + successful_jobs_24h: number; + failed_jobs_24h: number; + successful_crawls_24h: number; + failed_crawls_24h: number; + products_found_24h: number; + snapshots_created_24h: number; + last_job_started: string | null; + last_job_completed: string | null; + nextRuns: Array<{ + id: number; + job_name: string; + description: string; + enabled: boolean; + next_run_at: string; + last_status: string; + last_run_at: string; + }>; + }>('/api/az/monitor/summary'); + } + // Change Approval async getChanges(status?: 'pending' | 'approved' | 'rejected') { const params = status ? `?status=${status}` : ''; @@ -402,7 +452,7 @@ class ApiClient { return this.request<{ dispensaries: Array<{ id: number; name: string }> }>('/api/api-permissions/dispensaries'); } - async createApiPermission(data: { user_name: string; dispensary_id: number; allowed_ips?: string; allowed_domains?: string }) { + async createApiPermission(data: { user_name: string; store_id: number; allowed_ips?: string; allowed_domains?: string }) { return this.request<{ permission: any; message: string }>('/api/api-permissions', { method: 'POST', body: JSON.stringify(data), @@ -828,6 +878,39 @@ class ApiClient { }>(`/api/az/stores/${id}/categories`); } + // Dutchie AZ Global Brands/Categories (from v_brands/v_categories views) + async getDutchieAZBrands(params?: { limit?: number; offset?: number }) { + const searchParams = new URLSearchParams(); + if (params?.limit) searchParams.append('limit', params.limit.toString()); + if (params?.offset) searchParams.append('offset', params.offset.toString()); + const queryString = searchParams.toString() ? `?${searchParams.toString()}` : ''; + return this.request<{ + brands: Array<{ + brand_name: string; + brand_id: string | null; + brand_logo_url: string | null; + product_count: number; + dispensary_count: number; + product_types: string[]; + }>; + }>(`/api/az/brands${queryString}`); + } + + async getDutchieAZCategories() { + return this.request<{ + categories: Array<{ + type: string; + subcategory: string | null; + product_count: number; + dispensary_count: number; + brand_count: number; + avg_thc: number | null; + min_thc: number | null; + max_thc: number | null; + }>; + }>('/api/az/categories'); + } + // Dutchie AZ Debug async getDutchieAZDebugSummary() { return this.request<{ @@ -893,6 +976,65 @@ class ApiClient { body: JSON.stringify(options || {}), }); } + + // Dutchie AZ Menu Detection + async getDetectionStats() { + return this.request<{ + totalDispensaries: number; + withMenuType: number; + withPlatformId: number; + needsDetection: number; + byProvider: Record; + }>('/api/az/admin/detection/stats'); + } + + async getDispensariesNeedingDetection(params?: { state?: string; limit?: number }) { + const searchParams = new URLSearchParams(); + if (params?.state) searchParams.append('state', params.state); + if (params?.limit) searchParams.append('limit', params.limit.toString()); + const queryString = searchParams.toString() ? `?${searchParams.toString()}` : ''; + return this.request<{ dispensaries: any[]; total: number }>(`/api/az/admin/detection/pending${queryString}`); + } + + async detectDispensary(id: number) { + return this.request<{ + dispensaryId: number; + dispensaryName: string; + previousMenuType: string | null; + detectedProvider: string; + cName: string | null; + platformDispensaryId: string | null; + success: boolean; + error?: string; + }>(`/api/az/admin/detection/detect/${id}`, { + method: 'POST', + }); + } + + async detectAllDispensaries(options?: { + state?: string; + onlyUnknown?: boolean; + onlyMissingPlatformId?: boolean; + limit?: number; + }) { + return this.request<{ + totalProcessed: number; + totalSucceeded: number; + totalFailed: number; + totalSkipped: number; + results: any[]; + errors: string[]; + }>('/api/az/admin/detection/detect-all', { + method: 'POST', + body: JSON.stringify(options || {}), + }); + } + + async triggerMenuDetectionJob() { + return this.request<{ success: boolean; message: string }>('/api/az/admin/detection/trigger', { + method: 'POST', + }); + } } export const api = new ApiClient(API_URL); diff --git a/frontend/src/pages/ApiPermissions.tsx b/frontend/src/pages/ApiPermissions.tsx index 7a43de10..b4c794db 100644 --- a/frontend/src/pages/ApiPermissions.tsx +++ b/frontend/src/pages/ApiPermissions.tsx @@ -12,8 +12,8 @@ interface ApiPermission { is_active: number; created_at: string; last_used_at: string | null; - dispensary_id: number | null; - dispensary_name: string | null; + store_id: number | null; + store_name: string | null; } interface Dispensary { @@ -28,7 +28,7 @@ export function ApiPermissions() { const [showAddForm, setShowAddForm] = useState(false); const [newPermission, setNewPermission] = useState({ user_name: '', - dispensary_id: '', + store_id: '', allowed_ips: '', allowed_domains: '', }); @@ -68,18 +68,18 @@ export function ApiPermissions() { return; } - if (!newPermission.dispensary_id) { - setNotification({ message: 'Dispensary is required', type: 'error' }); + if (!newPermission.store_id) { + setNotification({ message: 'Store is required', type: 'error' }); return; } try { const result = await api.createApiPermission({ ...newPermission, - dispensary_id: parseInt(newPermission.dispensary_id), + store_id: parseInt(newPermission.store_id), }); setNotification({ message: result.message, type: 'success' }); - setNewPermission({ user_name: '', dispensary_id: '', allowed_ips: '', allowed_domains: '' }); + setNewPermission({ user_name: '', store_id: '', allowed_ips: '', allowed_domains: '' }); setShowAddForm(false); loadPermissions(); } catch (error: any) { @@ -182,22 +182,22 @@ export function ApiPermissions() {
-

The dispensary this API token can access

+

The store this API token can access

@@ -261,7 +261,7 @@ export function ApiPermissions() { User Name - Dispensary + Store API Key @@ -290,7 +290,7 @@ export function ApiPermissions() {
{perm.user_name}
-
{perm.dispensary_name || No dispensary}
+
{perm.store_name || No store}
diff --git a/frontend/src/pages/DutchieAZSchedule.tsx b/frontend/src/pages/DutchieAZSchedule.tsx index fcdf7a8a..e1288b79 100644 --- a/frontend/src/pages/DutchieAZSchedule.tsx +++ b/frontend/src/pages/DutchieAZSchedule.tsx @@ -35,15 +35,26 @@ interface RunLog { created_at: string; } +interface DetectionStats { + totalDispensaries: number; + withMenuType: number; + withPlatformId: number; + needsDetection: number; + byProvider: Record; +} + export function DutchieAZSchedule() { const [schedules, setSchedules] = useState([]); const [runLogs, setRunLogs] = useState([]); const [schedulerStatus, setSchedulerStatus] = useState<{ running: boolean; pollIntervalMs: number } | null>(null); + const [detectionStats, setDetectionStats] = useState(null); const [loading, setLoading] = useState(true); const [autoRefresh, setAutoRefresh] = useState(true); - const [activeTab, setActiveTab] = useState<'schedules' | 'logs'>('schedules'); + const [activeTab, setActiveTab] = useState<'schedules' | 'logs' | 'detection'>('schedules'); const [editingSchedule, setEditingSchedule] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); + const [detectingAll, setDetectingAll] = useState(false); + const [detectionResults, setDetectionResults] = useState(null); useEffect(() => { loadData(); @@ -56,15 +67,17 @@ export function DutchieAZSchedule() { const loadData = async () => { try { - const [schedulesData, logsData, statusData] = await Promise.all([ + const [schedulesData, logsData, statusData, detectionData] = await Promise.all([ api.getDutchieAZSchedules(), api.getDutchieAZRunLogs({ limit: 50 }), api.getDutchieAZSchedulerStatus(), + api.getDetectionStats().catch(() => null), ]); setSchedules(schedulesData.schedules || []); setRunLogs(logsData.logs || []); setSchedulerStatus(statusData); + setDetectionStats(detectionData); } catch (error) { console.error('Failed to load schedule data:', error); } finally { @@ -139,6 +152,36 @@ export function DutchieAZSchedule() { } }; + const handleDetectAll = async () => { + if (!confirm('Run menu detection on all dispensaries with unknown/missing menu_type?')) return; + setDetectingAll(true); + setDetectionResults(null); + try { + const result = await api.detectAllDispensaries({ state: 'AZ', onlyUnknown: true }); + setDetectionResults(result); + await loadData(); + } catch (error) { + console.error('Failed to run bulk detection:', error); + } finally { + setDetectingAll(false); + } + }; + + const handleDetectMissingIds = async () => { + if (!confirm('Resolve platform IDs for all Dutchie dispensaries missing them?')) return; + setDetectingAll(true); + setDetectionResults(null); + try { + const result = await api.detectAllDispensaries({ state: 'AZ', onlyMissingPlatformId: true, onlyUnknown: false }); + setDetectionResults(result); + await loadData(); + } catch (error) { + console.error('Failed to resolve platform IDs:', error); + } finally { + setDetectingAll(false); + } + }; + const formatTimeAgo = (dateString: string | null) => { if (!dateString) return 'Never'; const date = new Date(dateString); @@ -334,6 +377,22 @@ export function DutchieAZSchedule() { > Run Logs ({runLogs.length}) +
{activeTab === 'schedules' && ( @@ -574,6 +633,150 @@ export function DutchieAZSchedule() {
)} + {activeTab === 'detection' && ( +
+ {/* Detection Stats */} + {detectionStats && ( +
+

Detection Statistics

+
+
+
{detectionStats.totalDispensaries}
+
Total Dispensaries
+
+
+
{detectionStats.withMenuType}
+
With Menu Type
+
+
+
{detectionStats.withPlatformId}
+
With Platform ID
+
+
+
{detectionStats.needsDetection}
+
Needs Detection
+
+
+ + {/* Provider Breakdown */} + {Object.keys(detectionStats.byProvider).length > 0 && ( +
+

By Provider

+
+ {Object.entries(detectionStats.byProvider).map(([provider, count]) => ( + + {provider}: {count} + + ))} +
+
+ )} +
+ )} + + {/* Actions */} +
+ + +
+ + {/* Detection Results */} + {detectionResults && ( +
+

Detection Results

+
+
+
{detectionResults.totalProcessed}
+
Processed
+
+
+
{detectionResults.totalSucceeded}
+
Succeeded
+
+
+
{detectionResults.totalFailed}
+
Failed
+
+
+
{detectionResults.totalSkipped}
+
Skipped
+
+
+ {detectionResults.errors && detectionResults.errors.length > 0 && ( +
+
Errors:
+
+ {detectionResults.errors.slice(0, 10).map((error: string, i: number) => ( +
{error}
+ ))} + {detectionResults.errors.length > 10 && ( +
...and {detectionResults.errors.length - 10} more
+ )} +
+
+ )} +
+ )} + + {/* Info */} +
+

About Menu Detection

+
    +
  • + Detect All Unknown: Scans dispensaries with no menu_type set and detects the provider (dutchie, treez, jane, etc.) from their menu_url. +
  • +
  • + Resolve Missing Platform IDs: For dispensaries already detected as "dutchie", extracts the cName from menu_url and resolves the platform_dispensary_id via GraphQL. +
  • +
  • + Automatic scheduling: A "Menu Detection" job runs daily (24h +/- 1h jitter) to detect new dispensaries. +
  • +
+
+
+ )} + {/* Edit Modal */} {editingSchedule && (
([]); + const [totalStores, setTotalStores] = useState(0); const [loading, setLoading] = useState(true); const [dashboard, setDashboard] = useState(null); @@ -25,10 +26,11 @@ export function DutchieAZStores() { setLoading(true); try { const [storesData, dashboardData] = await Promise.all([ - api.getDutchieAZStores({ limit: 100 }), + api.getDutchieAZStores({ limit: 200 }), api.getDutchieAZDashboard(), ]); setStores(storesData.stores); + setTotalStores(storesData.total); setDashboard(dashboardData); } catch (error) { console.error('Failed to load data:', error); @@ -124,7 +126,7 @@ export function DutchieAZStores() { {/* Stores List */}
-

All Stores ({stores.length})

+

All Stores ({totalStores})

diff --git a/frontend/src/pages/ScraperMonitor.tsx b/frontend/src/pages/ScraperMonitor.tsx index 78081332..df685cb3 100644 --- a/frontend/src/pages/ScraperMonitor.tsx +++ b/frontend/src/pages/ScraperMonitor.tsx @@ -11,10 +11,16 @@ export function ScraperMonitor() { const [recentJobs, setRecentJobs] = useState([]); const [loading, setLoading] = useState(true); const [autoRefresh, setAutoRefresh] = useState(true); - const [activeTab, setActiveTab] = useState<'scrapers' | 'jobs'>('jobs'); + const [activeTab, setActiveTab] = useState<'az-live' | 'jobs' | 'scrapers'>('az-live'); const [selectedWorker, setSelectedWorker] = useState(null); const [workerLogs, setWorkerLogs] = useState(''); + // AZ Crawler state + const [azSummary, setAzSummary] = useState(null); + const [azActiveJobs, setAzActiveJobs] = useState({ scheduledJobs: [], crawlJobs: [], inMemoryScrapers: [], totalActive: 0 }); + const [azRecentJobs, setAzRecentJobs] = useState({ jobLogs: [], crawlJobs: [] }); + const [azErrors, setAzErrors] = useState([]); + useEffect(() => { loadData(); @@ -41,6 +47,19 @@ export function ScraperMonitor() { setActiveJobs(jobsData.jobs || []); setWorkers(workersData.workers || []); setRecentJobs(recentJobsData.jobs || []); + + // Load AZ monitor data + const [azSummaryData, azActiveData, azRecentData, azErrorsData] = await Promise.all([ + api.getAZMonitorSummary().catch(() => null), + api.getAZMonitorActiveJobs().catch(() => ({ scheduledJobs: [], crawlJobs: [], inMemoryScrapers: [], totalActive: 0 })), + api.getAZMonitorRecentJobs(30).catch(() => ({ jobLogs: [], crawlJobs: [] })), + api.getAZMonitorErrors({ limit: 10, hours: 24 }).catch(() => ({ errors: [] })), + ]); + + setAzSummary(azSummaryData); + setAzActiveJobs(azActiveData); + setAzRecentJobs(azRecentData); + setAzErrors(azErrorsData?.errors || []); } catch (error) { console.error('Failed to load scraper data:', error); } finally { @@ -79,6 +98,22 @@ export function ScraperMonitor() { {/* Tabs */}
+
- {activeTab === 'jobs' ? ( + {activeTab === 'az-live' && ( + <> + {/* AZ Summary Stats */} + {azSummary && ( +
+
+
+
Running Jobs
+
0 ? '#10b981' : '#666' }}> + {azActiveJobs.totalActive} +
+
+
+
Successful (24h)
+
+ {(azSummary.successful_jobs_24h || 0) + (azSummary.successful_crawls_24h || 0)} +
+
+
+
Failed (24h)
+
0 ? '#ef4444' : '#666' }}> + {(azSummary.failed_jobs_24h || 0) + (azSummary.failed_crawls_24h || 0)} +
+
+
+
Products (24h)
+
+ {azSummary.products_found_24h || 0} +
+
+
+
Snapshots (24h)
+
+ {azSummary.snapshots_created_24h || 0} +
+
+
+
+ )} + + {/* Active Jobs Section */} +
+

+ Active Jobs + {azActiveJobs.totalActive > 0 && ( + + {azActiveJobs.totalActive} running + + )} +

+ + {azActiveJobs.totalActive === 0 ? ( +
+
😴
+
No jobs currently running
+
+ ) : ( +
+ {/* Scheduled Jobs */} + {azActiveJobs.scheduledJobs.map((job: any) => ( +
+
+
+
+ {job.job_name} +
+
+ {job.job_description || 'Scheduled job'} +
+
+
+
Processed
+
{job.items_processed || 0}
+
+
+
Succeeded
+
{job.items_succeeded || 0}
+
+
+
Failed
+
0 ? '#ef4444' : '#666' }}>{job.items_failed || 0}
+
+
+
Duration
+
+ {Math.floor((job.duration_seconds || 0) / 60)}m {Math.floor((job.duration_seconds || 0) % 60)}s +
+
+
+
+
+ RUNNING +
+
+
+ ))} + + {/* Individual Crawl Jobs */} + {azActiveJobs.crawlJobs.map((job: any) => ( +
+
+
+
+ {job.dispensary_name || 'Unknown Store'} +
+
+ {job.city} | {job.job_type || 'crawl'} +
+
+
+
Products Found
+
{job.products_found || 0}
+
+
+
Snapshots
+
{job.snapshots_created || 0}
+
+
+
Duration
+
+ {Math.floor((job.duration_seconds || 0) / 60)}m {Math.floor((job.duration_seconds || 0) % 60)}s +
+
+
+
+
+ CRAWLING +
+
+
+ ))} +
+ )} +
+ + {/* Next Scheduled Runs */} + {azSummary?.nextRuns && azSummary.nextRuns.length > 0 && ( +
+

Next Scheduled Runs

+
+
+ + + + + + + + + {azSummary.nextRuns.map((run: any) => ( + + + + + + ))} + +
JobNext RunLast Status
+
{run.job_name}
+
{run.description}
+
+
+ {run.next_run_at ? new Date(run.next_run_at).toLocaleString() : '-'} +
+
+ + {run.last_status || 'never'} + +
+
+
+ )} + + {/* Recent Errors */} + {azErrors.length > 0 && ( +
+

Recent Errors (24h)

+
+ {azErrors.map((error: any, i: number) => ( +
+
+
{error.job_name || error.dispensary_name}
+ + {error.status} + +
+ {error.error_message && ( +
+ {error.error_message} +
+ )} +
+ {error.started_at ? new Date(error.started_at).toLocaleString() : '-'} +
+
+ ))} +
+
+ )} + + {/* Recent Jobs */} +
+

Recent Job Runs

+
+ + + + + + + + + + + + {azRecentJobs.jobLogs.slice(0, 20).map((job: any) => ( + + + + + + + + ))} + +
JobStatusProcessedDurationCompleted
+
{job.job_name}
+
Log #{job.id}
+
+ + {job.status} + + + {job.items_succeeded || 0} + {' / '} + {job.items_processed || 0} + + {job.duration_ms ? `${Math.floor(job.duration_ms / 60000)}m ${Math.floor((job.duration_ms % 60000) / 1000)}s` : '-'} + + {job.completed_at ? new Date(job.completed_at).toLocaleString() : '-'} +
+
+
+ + )} + + {activeTab === 'jobs' && ( <> {/* Job Stats */} {jobStats && ( @@ -343,7 +700,9 @@ export function ScraperMonitor() {
- ) : ( + )} + + {activeTab === 'scrapers' && ( <> {/* Active Scrapers */}
diff --git a/frontend/src/pages/WholesaleAnalytics.tsx b/frontend/src/pages/WholesaleAnalytics.tsx new file mode 100644 index 00000000..f7f32b71 --- /dev/null +++ b/frontend/src/pages/WholesaleAnalytics.tsx @@ -0,0 +1,470 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Layout } from '../components/Layout'; +import { api } from '../lib/api'; +import { + Building2, + Package, + Tag, + TrendingUp, + AlertCircle, + CheckCircle, + XCircle, + Clock, + RefreshCw, + ChevronRight, + BarChart3, + Layers, +} from 'lucide-react'; + +interface DashboardStats { + dispensaryCount: number; + productCount: number; + snapshotCount24h: number; + lastCrawlTime: string | null; + failedJobCount: number; + brandCount: number; + categoryCount: number; +} + +interface Store { + id: number; + name: string; + dba_name?: string; + city: string; + state: string; + platform_dispensary_id?: string; + last_crawl_at?: string; + product_count?: number; +} + +interface Brand { + brand_name: string; + product_count: number; + dispensary_count: number; + product_types?: string[]; +} + +interface Category { + type: string; + subcategory: string | null; + product_count: number; + dispensary_count: number; + brand_count: number; + avg_thc?: number | null; +} + +export function WholesaleAnalytics() { + const navigate = useNavigate(); + const [dashboard, setDashboard] = useState(null); + const [stores, setStores] = useState([]); + const [brands, setBrands] = useState([]); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState<'overview' | 'stores' | 'brands' | 'categories'>('overview'); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const [dashboardData, storesData, brandsData, categoriesData] = await Promise.all([ + api.getDutchieAZDashboard(), + api.getDutchieAZStores({ limit: 200 }), + api.getDutchieAZBrands ? api.getDutchieAZBrands({ limit: 100 }) : Promise.resolve({ brands: [] }), + api.getDutchieAZCategories ? api.getDutchieAZCategories() : Promise.resolve({ categories: [] }), + ]); + setDashboard(dashboardData); + setStores(storesData.stores || []); + setBrands(brandsData.brands || []); + setCategories(categoriesData.categories || []); + } catch (error) { + console.error('Failed to load analytics data:', error); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateStr: string | null) => { + if (!dateStr) return 'Never'; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffHours < 1) return 'Just now'; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays === 1) return 'Yesterday'; + if (diffDays < 7) return `${diffDays} days ago`; + return date.toLocaleDateString(); + }; + + if (loading) { + return ( + +
+
+

Loading analytics...

+
+
+ ); + } + + return ( + +
+ {/* Header */} +
+
+

Wholesale & Inventory Analytics

+

+ Arizona Dutchie dispensaries data overview +

+
+ +
+ + {/* Top Stats Grid */} +
+ } + color="blue" + /> + } + color="green" + /> + } + color="purple" + /> + } + color="orange" + /> + } + color="cyan" + /> + } + color="red" + /> +
+
+ + Last Crawl +
+

+ {formatDate(dashboard?.lastCrawlTime || null)} +

+
+
+ + {/* Navigation Tabs */} +
+
+
+ setActiveTab('overview')} + icon={} + label="Overview" + /> + setActiveTab('stores')} + icon={} + label={`Stores (${stores.length})`} + /> + setActiveTab('brands')} + icon={} + label={`Brands (${brands.length})`} + /> + setActiveTab('categories')} + icon={} + label={`Categories (${categories.length})`} + /> +
+
+ +
+ {/* Overview Tab */} + {activeTab === 'overview' && ( +
+ {/* Top Stores */} +
+

Top Stores by Products

+
+ {stores.slice(0, 6).map((store) => ( + + ))} +
+
+ + {/* Top Brands */} +
+

Top Brands

+
+ {brands.slice(0, 12).map((brand) => ( +
+

+ {brand.brand_name} +

+

+ {brand.product_count} +

+

products

+
+ ))} +
+
+ + {/* Top Categories */} +
+

Product Categories

+
+ {categories.slice(0, 12).map((cat, idx) => ( +
+

{cat.type}

+ {cat.subcategory && ( +

{cat.subcategory}

+ )} +

+ {cat.product_count} +

+

products

+
+ ))} +
+
+
+ )} + + {/* Stores Tab */} + {activeTab === 'stores' && ( +
+
+ + + + + + + + + + + + {stores.map((store) => ( + + + + + + + + ))} + +
Store NameCityPlatform IDLast Crawl
+ {store.dba_name || store.name} + {store.city}, {store.state} + {store.platform_dispensary_id ? ( + Resolved + ) : ( + Pending + )} + + {formatDate(store.last_crawl_at || null)} + + +
+
+
+ )} + + {/* Brands Tab */} + {activeTab === 'brands' && ( +
+ {brands.length === 0 ? ( +

+ No brands found. Run a crawl to populate brand data. +

+ ) : ( +
+ {brands.map((brand) => ( +
+

+ {brand.brand_name} +

+
+
+ Products: + {brand.product_count} +
+
+ Stores: + {brand.dispensary_count} +
+
+
+ ))} +
+ )} +
+ )} + + {/* Categories Tab */} + {activeTab === 'categories' && ( +
+ {categories.length === 0 ? ( +

+ No categories found. Run a crawl to populate category data. +

+ ) : ( +
+ {categories.map((cat, idx) => ( +
+

{cat.type}

+ {cat.subcategory && ( +

{cat.subcategory}

+ )} +
+
+

{cat.product_count}

+

products

+
+
+

{cat.brand_count}

+

brands

+
+
+ {cat.avg_thc != null && ( +

+ Avg THC: {cat.avg_thc.toFixed(1)}% +

+ )} +
+ ))} +
+ )} +
+ )} +
+
+
+
+ ); +} + +interface StatCardProps { + title: string; + value: number; + icon: React.ReactNode; + color: string; +} + +function StatCard({ title, value, icon, color }: StatCardProps) { + const colorClasses: Record = { + blue: 'bg-blue-50', + green: 'bg-green-50', + purple: 'bg-purple-50', + orange: 'bg-orange-50', + cyan: 'bg-cyan-50', + red: 'bg-red-50', + }; + + return ( +
+
+
+ {icon} +
+
+

{title}

+

{value.toLocaleString()}

+
+
+
+ ); +} + +interface TabButtonProps { + active: boolean; + onClick: () => void; + icon: React.ReactNode; + label: string; +} + +function TabButton({ active, onClick, icon, label }: TabButtonProps) { + return ( + + ); +} diff --git a/k8s/scraper-worker.yaml b/k8s/scraper-worker.yaml new file mode 100644 index 00000000..6435afa7 --- /dev/null +++ b/k8s/scraper-worker.yaml @@ -0,0 +1,65 @@ +# Dutchie AZ Worker Deployment +# These workers poll the job queue and process crawl jobs. +# Scale this deployment to increase crawl throughput. +# +# Architecture: +# - The main 'scraper' deployment runs the API server + scheduler (1 replica) +# - This 'scraper-worker' deployment runs workers that poll and claim jobs (5 replicas) +# - Workers use DB-level locking (FOR UPDATE SKIP LOCKED) to prevent double-crawls +# - Each worker sends heartbeats; stale jobs are recovered automatically +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scraper-worker + namespace: dispensary-scraper +spec: + replicas: 5 + selector: + matchLabels: + app: scraper-worker + template: + metadata: + labels: + app: scraper-worker + spec: + imagePullSecrets: + - name: regcred + containers: + - name: worker + image: code.cannabrands.app/creationshop/dispensary-scraper:latest + # Run the worker process instead of the main server + command: ["node"] + args: ["dist/dutchie-az/services/worker.js"] + envFrom: + - configMapRef: + name: scraper-config + - secretRef: + name: scraper-secrets + env: + # Worker-specific environment variables + - name: WORKER_MODE + value: "true" + # Pod name becomes part of worker ID for debugging + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + # Health check - workers don't expose ports, but we can use a file check + livenessProbe: + exec: + command: + - /bin/sh + - -c + - "pgrep -f 'worker.js' > /dev/null" + initialDelaySeconds: 10 + periodSeconds: 30 + failureThreshold: 3 + # Graceful shutdown - give workers time to complete current job + terminationGracePeriodSeconds: 60 diff --git a/llm-scraper b/llm-scraper new file mode 160000 index 00000000..7b994402 --- /dev/null +++ b/llm-scraper @@ -0,0 +1 @@ +Subproject commit 7b994402c5a33b4de9a5e5e5bc68dc7410a79980 diff --git a/wordpress-plugin/crawlsy-menus.php b/wordpress-plugin/crawlsy-menus.php index 09d71d85..d513f63f 100644 --- a/wordpress-plugin/crawlsy-menus.php +++ b/wordpress-plugin/crawlsy-menus.php @@ -3,7 +3,7 @@ * Plugin Name: Crawlsy Menus * Plugin URI: https://creationshop.io * Description: Display cannabis product menus from Crawlsy with Elementor integration - * Version: 1.4.0 + * Version: 1.5.0 * Author: Creationshop * Author URI: https://creationshop.io * License: GPL v2 or later @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; // Exit if accessed directly } -define('CRAWLSY_MENUS_VERSION', '1.4.0'); +define('CRAWLSY_MENUS_VERSION', '1.5.0'); define('CRAWLSY_MENUS_API_URL', 'https://dispos.crawlsy.com/api/v1'); define('CRAWLSY_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CRAWLSY_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__)); diff --git a/wordpress-plugin/dutchie-analytics.zip b/wordpress-plugin/dutchie-analytics.zip deleted file mode 100644 index 28cfa0cb..00000000 Binary files a/wordpress-plugin/dutchie-analytics.zip and /dev/null differ diff --git a/wordpress-plugin/dutchie-analytics/README.md b/wordpress-plugin/dutchie-analytics/README.md deleted file mode 100755 index cc3e7f2b..00000000 --- a/wordpress-plugin/dutchie-analytics/README.md +++ /dev/null @@ -1,196 +0,0 @@ -# Dispensary Analytics WordPress Plugin - -Beautiful WordPress plugin for displaying Arizona dispensary product data with Elementor widgets and shortcodes. - -**By CreationShop** - [https://creationshop.io](https://creationshop.io) - -## Features - -- **Elementor Widgets**: Drag-and-drop widgets for products, stores, brands, and specials -- **Beautiful Carousels**: Auto-playing product sliders with Swiper.js -- **Flexible Shortcodes**: Easy-to-use shortcodes for any page builder -- **Field Selection**: Choose which data fields to display -- **Multiple Styles**: Modern, Minimal, and Bold card designs -- **Responsive Design**: Perfect on all devices -- **High-Performance**: Lazy loading and optimized API calls -- **SEO Friendly**: Semantic HTML and proper image alt tags - -## Installation - -1. Upload the `dutchie-analytics` folder to `/wp-content/plugins/` -2. Activate the plugin through the 'Plugins' menu in WordPress -3. Go to Settings → Dutchie Analytics -4. Enter your API URL and authentication token -5. Test the connection - -## Configuration - -### API Settings - -Navigate to **Settings → Dutchie Analytics**: - -- **API URL**: Your Dutchie Analytics API endpoint (e.g., `https://api.dutchieanalytics.com/api`) -- **API Token**: Your JWT authentication token - -## Shortcodes - -### Products Grid - -Display a grid of products: - -``` -[dutchie_products store_id="1" limit="12" columns="4"] -``` - -**Parameters:** -- `store_id` (required): Store ID -- `limit` (optional): Number of products (default: 12) -- `columns` (optional): Grid columns (default: 4) -- `category_id` (optional): Filter by category -- `in_stock` (optional): Show only in-stock products (default: true) -- `search` (optional): Search term -- `fields` (optional): Comma-separated fields to display -- `style` (optional): Card style - modern, minimal, bold (default: modern) - -### Products Carousel - -Display a sliding carousel: - -``` -[dutchie_carousel store_id="1" limit="20" slides_per_view="4"] -``` - -**Parameters:** -- `store_id` (required): Store ID -- `limit` (optional): Number of products (default: 20) -- `slides_per_view` (optional): Slides visible at once (default: 4) -- `category_id` (optional): Filter by category -- `autoplay` (optional): Enable autoplay (default: true) -- `style` (optional): Card style (default: modern) - -### Stores List - -Display all stores: - -``` -[dutchie_stores] -``` - -### Brands List - -Display brands for a store: - -``` -[dutchie_brands store_id="1"] -``` - -### Daily Specials - -Display store specials: - -``` -[dutchie_specials store_id="1" date="2025-01-15"] -``` - -**Parameters:** -- `store_id` (required): Store ID -- `date` (optional): Date for specials (default: today) - -## Field Selection - -Control which fields to display using the `fields` parameter: - -``` -[dutchie_products store_id="1" fields="id,name,price,brand,thc_percentage,in_stock"] -``` - -**Available Fields:** -- `id` - Product ID -- `name` - Product name -- `brand` - Brand name -- `price` - Price -- `description` - Description -- `thc_percentage` - THC % -- `cbd_percentage` - CBD % -- `weight` - Weight -- `strain_type` - Strain type -- `in_stock` - Stock status -- `image_url_full` - High-resolution image (2000x2000) - -## Elementor Widgets - -Find all widgets in the **Dutchie Analytics** category in Elementor: - -1. **Products Grid** - Customizable product grid -2. **Products Carousel** - Auto-playing slider -3. **Stores List** - Store directory -4. **Brands List** - Brand badges -5. **Specials** - Daily deals - -### Widget Controls - -Each widget provides: -- **Content Tab**: Data source, filters, display options -- **Style Tab**: Colors, spacing, card styles -- **Advanced Tab**: CSS classes, animations - -## Card Styles - -### Modern (Default) -- Clean white cards -- Subtle shadows -- Smooth hover effects -- Blue accent colors - -### Minimal -- Simple borders -- No shadows -- Hover border highlights -- Ultra-clean design - -### Bold -- Gradient backgrounds -- Vibrant colors -- Large shadows -- Eye-catching design - -## Customization - -### Custom CSS - -Add custom styles in your theme's CSS: - -```css -.dutchie-product-card-modern { - border-radius: 20px; - /* Your custom styles */ -} -``` - -### Hooks and Filters - -Coming soon: WordPress hooks for advanced customization. - -## Requirements - -- WordPress 5.0 or higher -- PHP 7.4 or higher -- Elementor (optional, for widgets) - -## Support - -For support, visit: [https://dutchieanalytics.com/support](https://dutchieanalytics.com/support) - -## Changelog - -### 1.0.0 -- Initial release -- Elementor widgets -- Shortcodes -- Field selection -- Multiple card styles -- Carousel support - -## License - -GPL v2 or later diff --git a/wordpress-plugin/dutchie-analytics/USER_GUIDE.md b/wordpress-plugin/dutchie-analytics/USER_GUIDE.md deleted file mode 100644 index 719b85ef..00000000 --- a/wordpress-plugin/dutchie-analytics/USER_GUIDE.md +++ /dev/null @@ -1,326 +0,0 @@ -# Dispensary Analytics - User Guide - -## Table of Contents -1. [Installation & Setup](#installation--setup) -2. [Plugin Configuration](#plugin-configuration) -3. [Using Elementor Widgets](#using-elementor-widgets) -4. [Using Shortcodes](#using-shortcodes) -5. [Widget Reference](#widget-reference) -6. [Shortcode Reference](#shortcode-reference) -7. [Troubleshooting](#troubleshooting) - ---- - -## Installation & Setup - -### Requirements -- WordPress 5.0 or higher -- PHP 7.4 or higher -- Elementor plugin (for widget functionality) - -### Installation Steps -1. Download the `dutchie-analytics.zip` file -2. Go to WordPress Admin → Plugins → Add New -3. Click "Upload Plugin" and select the zip file -4. Click "Install Now" and then "Activate" - ---- - -## Plugin Configuration - -### Setting Up API Connection - -1. Go to **WordPress Admin → Settings → Dispensary Analytics** -2. Configure the following settings: - - **API URL**: Your backend API URL (e.g., `http://localhost:3010/api` or `https://yourdomain.com/api`) - - **API Token**: Your authentication token (if required) -3. Click "Save Changes" - ---- - -## Using Elementor Widgets - -### Finding the Widgets - -1. Edit any page with **Elementor** -2. Look for the **"Dutchie Analytics"** category in the widgets panel -3. You'll find 5 widgets available: - - Products Grid - - Products Carousel - - Stores List - - Brands List - - Specials - -### Adding a Widget to Your Page - -1. Open a page in **Elementor Editor** -2. Drag the desired widget from the sidebar to your page -3. Configure the widget settings in the left panel -4. Click "Update" to save your changes - ---- - -## Using Shortcodes - -Shortcodes allow you to display dispensary data in any post, page, or widget area without using Elementor. - -### How to Use Shortcodes - -1. Edit any post or page -2. Add a shortcode block (or paste directly in the classic editor) -3. Copy and paste one of the shortcodes below -4. Customize the parameters as needed -5. Publish or update your page - ---- - -## Widget Reference - -### 1. Products Grid Widget - -**Purpose**: Display products in a responsive grid layout - -**Widget Settings**: -- **Store ID** (required): The dispensary store ID -- **Category ID** (optional): Filter by specific category -- **Limit**: Number of products to show (default: 12) -- **Columns**: Number of columns (default: 4) -- **In Stock Only**: Show only available products (default: Yes) -- **Search**: Filter by keyword -- **Style**: Choose visual style (modern, classic, minimal) -- **Fields**: Customize which product fields to display - -**Elementor Location**: Dutchie Analytics → Products Grid - ---- - -### 2. Products Carousel Widget - -**Purpose**: Display products in a sliding carousel/slider - -**Widget Settings**: -- **Store ID** (required): The dispensary store ID -- **Category ID** (optional): Filter by specific category -- **Limit**: Number of products to show (default: 12) -- **Slides Per View**: How many products visible at once (default: 4) -- **Autoplay**: Enable automatic sliding (default: Yes) -- **Loop**: Enable infinite loop (default: Yes) -- **Navigation**: Show prev/next arrows (default: Yes) -- **Pagination**: Show dot indicators (default: Yes) - -**Elementor Location**: Dutchie Analytics → Products Carousel - ---- - -### 3. Stores List Widget - -**Purpose**: Display a list of dispensary locations - -**Widget Settings**: -- **Limit**: Number of stores to show (default: 10) -- **State**: Filter by state (e.g., "Arizona") -- **City**: Filter by city -- **Style**: List style (cards, list, table) -- **Show Address**: Display store address (default: Yes) -- **Show Phone**: Display phone number (default: Yes) -- **Show Hours**: Display business hours (default: Yes) - -**Elementor Location**: Dutchie Analytics → Stores List - ---- - -### 4. Brands List Widget - -**Purpose**: Display available brands - -**Widget Settings**: -- **Store ID** (optional): Filter brands by store -- **Limit**: Number of brands to show (default: 20) -- **Style**: Display style (grid, list, carousel) -- **Show Logo**: Display brand logos (default: Yes) -- **Show Product Count**: Show number of products per brand (default: Yes) - -**Elementor Location**: Dutchie Analytics → Brands List - ---- - -### 5. Specials Widget - -**Purpose**: Display current deals and promotions - -**Widget Settings**: -- **Store ID** (required): The dispensary store ID -- **Limit**: Number of specials to show (default: 6) -- **Active Only**: Show only active promotions (default: Yes) -- **Style**: Display style (banner, card, minimal) -- **Show Expiration**: Display when deal expires (default: Yes) - -**Elementor Location**: Dutchie Analytics → Specials - ---- - -## Shortcode Reference - -### 1. Products Grid Shortcode - -Display products in a grid layout. - -**Basic Usage**: -``` -[dutchie_products store_id="1"] -``` - -**Full Example**: -``` -[dutchie_products store_id="1" limit="12" category_id="5" columns="4" in_stock="true" style="modern" search="indica"] -``` - -**Parameters**: -- `store_id` (required) - Store ID number -- `limit` - Number of products (default: 12) -- `category_id` - Filter by category ID -- `columns` - Grid columns: 2, 3, 4, or 6 (default: 4) -- `in_stock` - Show only in-stock: "true" or "false" (default: true) -- `search` - Filter by keyword -- `style` - Visual style: "modern", "classic", "minimal" -- `fields` - Comma-separated fields: "name,price,brand,thc,cbd" - ---- - -### 2. Products Carousel Shortcode - -Display products in a carousel/slider. - -**Basic Usage**: -``` -[dutchie_carousel store_id="1"] -``` - -**Full Example**: -``` -[dutchie_carousel store_id="1" limit="16" slides_per_view="4" autoplay="true" loop="true"] -``` - -**Parameters**: -- `store_id` (required) - Store ID number -- `limit` - Number of products (default: 12) -- `category_id` - Filter by category ID -- `slides_per_view` - Products visible at once (default: 4) -- `autoplay` - Auto-slide: "true" or "false" (default: true) -- `loop` - Infinite loop: "true" or "false" (default: true) -- `navigation` - Show arrows: "true" or "false" (default: true) -- `pagination` - Show dots: "true" or "false" (default: true) - ---- - -### 3. Stores List Shortcode - -Display dispensary locations. - -**Basic Usage**: -``` -[dutchie_stores] -``` - -**Full Example**: -``` -[dutchie_stores limit="10" state="Arizona" city="Phoenix" style="cards"] -``` - -**Parameters**: -- `limit` - Number of stores (default: 10) -- `state` - Filter by state -- `city` - Filter by city -- `style` - Display style: "cards", "list", "table" - ---- - -### 4. Brands List Shortcode - -Display available brands. - -**Basic Usage**: -``` -[dutchie_brands] -``` - -**Full Example**: -``` -[dutchie_brands store_id="1" limit="20" style="grid"] -``` - -**Parameters**: -- `store_id` - Filter by store ID -- `limit` - Number of brands (default: 20) -- `style` - Display style: "grid", "list", "carousel" - ---- - -### 5. Specials Shortcode - -Display current deals and promotions. - -**Basic Usage**: -``` -[dutchie_specials store_id="1"] -``` - -**Full Example**: -``` -[dutchie_specials store_id="1" limit="6" active_only="true" style="banner"] -``` - -**Parameters**: -- `store_id` (required) - Store ID number -- `limit` - Number of specials (default: 6) -- `active_only` - Show only active: "true" or "false" (default: true) -- `style` - Display style: "banner", "card", "minimal" - ---- - -## Troubleshooting - -### Widget Not Appearing -- Make sure Elementor is installed and activated -- Clear Elementor cache: Elementor → Tools → Regenerate CSS & Data -- Check if plugin is activated - -### No Data Displaying -- Verify API URL in Settings → Dispensary Analytics -- Check that store_id is correct -- Test API connection in browser -- Check browser console for errors (F12) - -### Shortcode Shows as Text -- Make sure you're using the correct shortcode format: `[shortcode_name]` -- Verify the plugin is activated -- Check that the post/page content is not in HTML mode - -### Styling Issues -- Clear WordPress cache -- Clear browser cache (Ctrl+F5) -- Check for theme CSS conflicts -- Try switching to a default WordPress theme temporarily - -### API Connection Issues -- Verify API URL is correct and accessible -- Check if API token is required and configured -- Ensure backend server is running -- Check server firewall settings - ---- - -## Support - -For additional support: -- Check plugin settings: WordPress Admin → Settings → Dispensary Analytics -- Review backend API logs -- Contact: creationshop.io - ---- - -## Version Information - -**Current Version**: 1.0.2 -**Author**: Creationshop LLC -**License**: GPL v2 or later diff --git a/wordpress-plugin/dutchie-analytics/assets/css/dutchie-analytics.css b/wordpress-plugin/dutchie-analytics/assets/css/dutchie-analytics.css deleted file mode 100755 index b8b7fd03..00000000 --- a/wordpress-plugin/dutchie-analytics/assets/css/dutchie-analytics.css +++ /dev/null @@ -1,456 +0,0 @@ -/** - * Dutchie Analytics - Beautiful Styles - * Modern, clean, and professional design - */ - -/* Products Grid */ -.dutchie-products-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 24px; - margin: 24px 0; -} - -.dutchie-products-grid[data-columns="3"] { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); -} - -.dutchie-products-grid[data-columns="4"] { - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); -} - -.dutchie-products-grid[data-columns="5"] { - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); -} - -/* Product Cards - Modern Style */ -.dutchie-product-card-modern { - background: #ffffff; - border-radius: 16px; - overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transition: all 0.3s ease; - border: 1px solid #e5e7eb; -} - -.dutchie-product-card-modern:hover { - transform: translateY(-4px); - box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); -} - -/* Product Cards - Minimal Style */ -.dutchie-product-card-minimal { - background: #ffffff; - border-radius: 8px; - overflow: hidden; - box-shadow: none; - transition: all 0.2s ease; - border: 1px solid #e5e7eb; -} - -.dutchie-product-card-minimal:hover { - border-color: #3b82f6; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1); -} - -/* Product Cards - Bold Style */ -.dutchie-product-card-bold { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 20px; - overflow: hidden; - box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3); - transition: all 0.3s ease; - color: #ffffff; -} - -.dutchie-product-card-bold:hover { - transform: scale(1.05); - box-shadow: 0 12px 28px rgba(102, 126, 234, 0.4); -} - -.dutchie-product-card-bold .dutchie-product-name, -.dutchie-product-card-bold .dutchie-product-brand, -.dutchie-product-card-bold .dutchie-product-description { - color: #ffffff; -} - -/* Product Image */ -.dutchie-product-image { - position: relative; - width: 100%; - aspect-ratio: 1; - background: #f9fafb; - overflow: hidden; -} - -.dutchie-product-image img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; -} - -.dutchie-product-card:hover .dutchie-product-image img { - transform: scale(1.05); -} - -/* Stock Badge */ -.dutchie-stock-badge { - position: absolute; - top: 12px; - right: 12px; - padding: 6px 12px; - border-radius: 20px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - backdrop-filter: blur(10px); -} - -.dutchie-stock-badge.in-stock { - background: rgba(16, 185, 129, 0.9); - color: #ffffff; -} - -.dutchie-stock-badge.out-of-stock { - background: rgba(239, 68, 68, 0.9); - color: #ffffff; -} - -/* Product Content */ -.dutchie-product-content { - padding: 20px; -} - -.dutchie-product-name { - font-size: 16px; - font-weight: 600; - color: #111827; - margin: 0 0 8px 0; - line-height: 1.4; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.dutchie-product-brand { - font-size: 13px; - color: #6b7280; - margin: 0 0 12px 0; - font-weight: 500; -} - -.dutchie-product-description { - font-size: 13px; - color: #6b7280; - line-height: 1.5; - margin: 0 0 16px 0; -} - -/* Product Meta */ -.dutchie-product-meta { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - flex-wrap: wrap; -} - -.dutchie-product-price { - font-size: 20px; - font-weight: 700; - color: #3b82f6; -} - -.dutchie-thc-badge, -.dutchie-cbd-badge { - display: inline-flex; - align-items: center; - padding: 4px 10px; - border-radius: 12px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.dutchie-thc-badge { - background: #ecfdf5; - color: #059669; -} - -.dutchie-cbd-badge { - background: #eff6ff; - color: #2563eb; -} - -.dutchie-product-weight { - font-size: 12px; - color: #9ca3af; - margin: 8px 0 0 0; -} - -/* Carousel Wrapper */ -.dutchie-products-carousel-wrapper { - margin: 24px 0; - position: relative; -} - -.dutchie-products-carousel-wrapper .swiper { - padding: 0 50px 50px 50px; -} - -.dutchie-products-carousel-wrapper .swiper-button-prev, -.dutchie-products-carousel-wrapper .swiper-button-next { - width: 44px; - height: 44px; - background: #ffffff; - border-radius: 50%; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; -} - -.dutchie-products-carousel-wrapper .swiper-button-prev:hover, -.dutchie-products-carousel-wrapper .swiper-button-next:hover { - background: #3b82f6; - color: #ffffff; - transform: scale(1.1); -} - -.dutchie-products-carousel-wrapper .swiper-button-prev::after, -.dutchie-products-carousel-wrapper .swiper-button-next::after { - font-size: 18px; -} - -.dutchie-products-carousel-wrapper .swiper-pagination-bullet { - width: 10px; - height: 10px; - background: #d1d5db; - opacity: 1; - transition: all 0.2s ease; -} - -.dutchie-products-carousel-wrapper .swiper-pagination-bullet-active { - background: #3b82f6; - width: 28px; - border-radius: 5px; -} - -/* Stores Grid */ -.dutchie-stores-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 24px; - margin: 24px 0; -} - -.dutchie-store-card { - background: #ffffff; - border-radius: 16px; - padding: 24px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transition: all 0.3s ease; - text-align: center; - border: 1px solid #e5e7eb; -} - -.dutchie-store-card:hover { - transform: translateY(-4px); - box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); -} - -.dutchie-store-logo { - width: 80px; - height: 80px; - object-fit: contain; - margin: 0 auto 16px; - display: block; -} - -.dutchie-store-card h3 { - font-size: 18px; - font-weight: 600; - color: #111827; - margin: 0 0 12px 0; -} - -.dutchie-store-stats { - font-size: 14px; - color: #6b7280; - margin: 0 0 16px 0; -} - -.dutchie-store-link { - display: inline-block; - padding: 10px 20px; - background: #3b82f6; - color: #ffffff; - text-decoration: none; - border-radius: 8px; - font-weight: 500; - font-size: 14px; - transition: all 0.2s ease; -} - -.dutchie-store-link:hover { - background: #2563eb; - transform: scale(1.05); - color: #ffffff; -} - -/* Brands Grid */ -.dutchie-brands-grid { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin: 24px 0; -} - -.dutchie-brand-badge { - display: inline-flex; - align-items: center; - padding: 10px 18px; - background: #f3f4f6; - border: 1px solid #e5e7eb; - border-radius: 24px; - font-size: 14px; - font-weight: 500; - color: #374151; - transition: all 0.2s ease; -} - -.dutchie-brand-badge:hover { - background: #3b82f6; - color: #ffffff; - border-color: #3b82f6; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2); -} - -/* Specials Grid */ -.dutchie-specials-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 24px; - margin: 24px 0; -} - -.dutchie-special-card { - background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); - border-radius: 16px; - padding: 24px; - box-shadow: 0 8px 16px rgba(251, 191, 36, 0.3); - color: #ffffff; - position: relative; - overflow: hidden; -} - -.dutchie-special-card::before { - content: ''; - position: absolute; - top: -50%; - right: -50%; - width: 200%; - height: 200%; - background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); - pointer-events: none; -} - -.dutchie-special-card img { - width: 100%; - height: 200px; - object-fit: cover; - border-radius: 12px; - margin-bottom: 16px; -} - -.dutchie-special-card h3 { - font-size: 20px; - font-weight: 700; - color: #ffffff; - margin: 0 0 12px 0; -} - -.dutchie-special-card p { - font-size: 14px; - color: rgba(255, 255, 255, 0.9); - margin: 0 0 16px 0; -} - -.dutchie-discount-badge { - display: inline-block; - padding: 8px 16px; - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); - border: 2px solid rgba(255, 255, 255, 0.4); - border-radius: 20px; - font-size: 16px; - font-weight: 700; - color: #ffffff; - text-transform: uppercase; - letter-spacing: 1px; -} - -/* Responsive Design */ -@media (max-width: 768px) { - .dutchie-products-grid, - .dutchie-stores-grid, - .dutchie-specials-grid { - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 16px; - } - - .dutchie-products-carousel-wrapper .swiper { - padding: 0 0 40px 0; - } - - .dutchie-product-name { - font-size: 15px; - } - - .dutchie-product-price { - font-size: 18px; - } -} - -@media (max-width: 480px) { - .dutchie-products-grid, - .dutchie-stores-grid, - .dutchie-specials-grid { - grid-template-columns: 1fr; - } -} - -/* Loading State */ -.dutchie-loading { - text-align: center; - padding: 40px; - color: #6b7280; -} - -.dutchie-loading::after { - content: ''; - display: inline-block; - width: 32px; - height: 32px; - border: 3px solid #e5e7eb; - border-top-color: #3b82f6; - border-radius: 50%; - animation: dutchie-spin 0.8s linear infinite; -} - -@keyframes dutchie-spin { - to { transform: rotate(360deg); } -} - -/* Error State */ -.dutchie-error { - background: #fee2e2; - border: 1px solid #fecaca; - border-radius: 8px; - padding: 16px; - color: #991b1b; - margin: 16px 0; -} diff --git a/wordpress-plugin/dutchie-analytics/assets/js/dutchie-analytics.js b/wordpress-plugin/dutchie-analytics/assets/js/dutchie-analytics.js deleted file mode 100755 index f5dc0b07..00000000 --- a/wordpress-plugin/dutchie-analytics/assets/js/dutchie-analytics.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Dutchie Analytics - Frontend JavaScript - */ - -(function($) { - 'use strict'; - - // Lazy load images - if ('IntersectionObserver' in window) { - const imageObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const img = entry.target; - if (img.dataset.src) { - img.src = img.dataset.src; - img.removeAttribute('data-src'); - observer.unobserve(img); - } - } - }); - }); - - $('.dutchie-product-image img[data-src]').each(function() { - imageObserver.observe(this); - }); - } - - // Add hover effects - $('.dutchie-product-card').on('mouseenter', function() { - $(this).addClass('dutchie-hover'); - }).on('mouseleave', function() { - $(this).removeClass('dutchie-hover'); - }); - - // Optional: Track product views for analytics - $('.dutchie-product-card').on('click', function() { - const productId = $(this).data('product-id'); - if (productId && typeof gtag !== 'undefined') { - gtag('event', 'view_item', { - 'item_id': productId - }); - } - }); - -})(jQuery); diff --git a/wordpress-plugin/dutchie-analytics/bump-version.sh b/wordpress-plugin/dutchie-analytics/bump-version.sh deleted file mode 100755 index 0c3d56ed..00000000 --- a/wordpress-plugin/dutchie-analytics/bump-version.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -# WordPress Plugin Version Bump Script -# Usage: ./bump-version.sh [major|minor|patch] - -PLUGIN_FILE="dutchie-analytics.php" -BUMP_TYPE=${1:-patch} # Default to patch if not specified - -# Get current version -CURRENT_VERSION=$(grep "^ \* Version:" "$PLUGIN_FILE" | sed 's/.*Version: \(.*\)/\1/') - -if [ -z "$CURRENT_VERSION" ]; then - echo "Error: Could not find current version" - exit 1 -fi - -echo "Current version: $CURRENT_VERSION" - -# Split version into parts -IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" -MAJOR="${VERSION_PARTS[0]}" -MINOR="${VERSION_PARTS[1]}" -PATCH="${VERSION_PARTS[2]}" - -# Bump version based on type -case $BUMP_TYPE in - major) - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - ;; - minor) - MINOR=$((MINOR + 1)) - PATCH=0 - ;; - patch) - PATCH=$((PATCH + 1)) - ;; - *) - echo "Error: Invalid bump type. Use: major, minor, or patch" - exit 1 - ;; -esac - -NEW_VERSION="$MAJOR.$MINOR.$PATCH" -echo "New version: $NEW_VERSION" - -# Update version in plugin file -sed -i "s/^ \* Version: .*/ * Version: $NEW_VERSION/" "$PLUGIN_FILE" -sed -i "s/define('DUTCHIE_ANALYTICS_VERSION', '.*');/define('DUTCHIE_ANALYTICS_VERSION', '$NEW_VERSION');/" "$PLUGIN_FILE" - -echo "✓ Version bumped from $CURRENT_VERSION to $NEW_VERSION" -echo "" -echo "Changes made to: $PLUGIN_FILE" -echo " - Plugin header: Version: $NEW_VERSION" -echo " - Constant: DUTCHIE_ANALYTICS_VERSION = '$NEW_VERSION'" diff --git a/wordpress-plugin/dutchie-analytics/dutchie-analytics.php b/wordpress-plugin/dutchie-analytics/dutchie-analytics.php deleted file mode 100644 index 1c23e27e..00000000 --- a/wordpress-plugin/dutchie-analytics/dutchie-analytics.php +++ /dev/null @@ -1,130 +0,0 @@ -register(new \DutchieAnalytics\Elementor\Products_Grid_Widget()); - $widgets_manager->register(new \DutchieAnalytics\Elementor\Products_Carousel_Widget()); - $widgets_manager->register(new \DutchieAnalytics\Elementor\Stores_List_Widget()); - $widgets_manager->register(new \DutchieAnalytics\Elementor\Brands_List_Widget()); - $widgets_manager->register(new \DutchieAnalytics\Elementor\Specials_Widget()); - }); - - // Register Elementor Widget Category - add_action('elementor/elements/categories_registered', function($elements_manager) { - $elements_manager->add_category( - 'dutchie-analytics', - [ - 'title' => __('Dutchie Analytics', 'dutchie-analytics'), - 'icon' => 'fa fa-plug', - ] - ); - }); -} -add_action('plugins_loaded', 'dutchie_analytics_init'); - -// Enqueue styles and scripts -function dutchie_analytics_enqueue_assets() { - wp_enqueue_style( - 'dutchie-analytics-styles', - DUTCHIE_ANALYTICS_URL . 'assets/css/dutchie-analytics.css', - [], - DUTCHIE_ANALYTICS_VERSION - ); - - wp_enqueue_script( - 'dutchie-analytics-scripts', - DUTCHIE_ANALYTICS_URL . 'assets/js/dutchie-analytics.js', - ['jquery'], - DUTCHIE_ANALYTICS_VERSION, - true - ); - - // Enqueue Swiper for carousels - wp_enqueue_style( - 'swiper-css', - 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css', - [], - '11.0.0' - ); - - wp_enqueue_script( - 'swiper-js', - 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js', - [], - '11.0.0', - true - ); -} -add_action('wp_enqueue_scripts', 'dutchie_analytics_enqueue_assets'); - -// Activation hook -register_activation_hook(__FILE__, function() { - // Set default options - add_option('dutchie_analytics_api_url', 'http://localhost:3010/api'); - add_option('dutchie_analytics_api_token', ''); -}); - -// Deactivation hook -register_deactivation_hook(__FILE__, function() { - // Cleanup if needed -}); diff --git a/wordpress-plugin/dutchie-analytics/includes/API_Client.php b/wordpress-plugin/dutchie-analytics/includes/API_Client.php deleted file mode 100644 index 16d1b635..00000000 --- a/wordpress-plugin/dutchie-analytics/includes/API_Client.php +++ /dev/null @@ -1,121 +0,0 @@ -api_url = get_option('dutchie_analytics_api_url', 'http://localhost:3010/api'); - - // Try to get API key from permissions first, fallback to manual token - $this->api_token = get_option('dutchie_analytics_api_token', ''); - - if (empty($this->api_token) && class_exists('DutchieAnalytics\API_Permissions')) { - $this->api_token = API_Permissions::get_site_api_key(); - } - } - - /** - * Make API request - */ - private function request($endpoint, $params = []) { - $url = $this->api_url . $endpoint; - - if (!empty($params)) { - $url .= '?' . http_build_query($params); - } - - $args = [ - 'headers' => [ - 'X-API-Key' => $this->api_token, - 'Content-Type' => 'application/json', - ], - 'timeout' => 30, - ]; - - $response = wp_remote_get($url, $args); - - if (is_wp_error($response)) { - error_log('Dutchie Analytics API Error: ' . $response->get_error_message()); - return null; - } - - $body = wp_remote_retrieve_body($response); - return json_decode($body, true); - } - - /** - * Get all stores - */ - public function get_stores() { - return $this->request('/stores'); - } - - /** - * Get single store - */ - public function get_store($id) { - return $this->request('/stores/' . $id); - } - - /** - * Get store brands - */ - public function get_store_brands($store_id) { - return $this->request('/stores/' . $store_id . '/brands'); - } - - /** - * Get store specials - */ - public function get_store_specials($store_id, $date = null) { - $params = $date ? ['date' => $date] : []; - return $this->request('/stores/' . $store_id . '/specials', $params); - } - - /** - * Get products - * - * @param array $args { - * @type int $store_id Filter by store ID - * @type int $category_id Filter by category ID - * @type bool $in_stock Filter by stock availability - * @type string $search Search query - * @type int $limit Results per page (default: 50, max: 1000) - * @type int $offset Pagination offset - * @type string $fields Comma-separated list of fields to return - * } - */ - public function get_products($args = []) { - $defaults = [ - 'limit' => 50, - 'offset' => 0, - ]; - - $params = wp_parse_args($args, $defaults); - return $this->request('/products', $params); - } - - /** - * Get single product - */ - public function get_product($id) { - return $this->request('/products/' . $id); - } - - /** - * Get categories - */ - public function get_categories($store_id = null) { - $params = $store_id ? ['store_id' => $store_id] : []; - return $this->request('/categories', $params); - } - - /** - * Get category tree - */ - public function get_category_tree($store_id) { - return $this->request('/categories/tree', ['store_id' => $store_id]); - } -} diff --git a/wordpress-plugin/dutchie-analytics/includes/Admin_Settings.php b/wordpress-plugin/dutchie-analytics/includes/Admin_Settings.php deleted file mode 100644 index cdcf07fd..00000000 --- a/wordpress-plugin/dutchie-analytics/includes/Admin_Settings.php +++ /dev/null @@ -1,113 +0,0 @@ - -
-

-
- -
- -
- -

- get_stores(); - - if ($stores && isset($stores['stores'])) { - echo '

✓ Connection successful!

'; - echo '

' . sprintf(__('Found %d stores', 'dutchie-analytics'), count($stores['stores'])) . '

'; - } else { - echo '

✗ Connection failed

'; - echo '

' . __('Please check your API URL and Token', 'dutchie-analytics') . '

'; - } - ?> - -
- -

-

-
    -
  • [dutchie_products store_id="1" limit="12"] - Display products grid
  • -
  • [dutchie_carousel store_id="1" limit="20"] - Display products carousel
  • -
  • [dutchie_stores] - Display all stores
  • -
  • [dutchie_brands store_id="1"] - Display store brands
  • -
  • [dutchie_specials store_id="1"] - Display today's specials
  • -
- -

-

You can select which fields to display using the fields parameter:

- [dutchie_products store_id="1" fields="id,name,price,brand,in_stock"] - -

-

Find all widgets in the Dutchie Analytics category in Elementor.

-
- ' . __('Configure your Dutchie Analytics API connection', 'dutchie-analytics') . '

'; - } - - public function render_api_url_field() { - $value = get_option('dutchie_analytics_api_url', 'http://localhost:3010/api'); - echo ''; - echo '

' . __('Example: https://api.dutchieanalytics.com/api', 'dutchie-analytics') . '

'; - } - - public function render_api_token_field() { - $value = get_option('dutchie_analytics_api_token', ''); - echo ''; - echo '

' . __('Your JWT authentication token', 'dutchie-analytics') . '

'; - } -} diff --git a/wordpress-plugin/dutchie-analytics/includes/Shortcodes.php b/wordpress-plugin/dutchie-analytics/includes/Shortcodes.php deleted file mode 100644 index e4757763..00000000 --- a/wordpress-plugin/dutchie-analytics/includes/Shortcodes.php +++ /dev/null @@ -1,349 +0,0 @@ - '', - 'category_id' => '', - 'limit' => 12, - 'in_stock' => 'true', - 'search' => '', - 'columns' => 4, - 'fields' => '', - 'style' => 'modern', - ], $atts); - - if (empty($atts['store_id'])) { - return '

' . __('Please specify a store_id', 'dutchie-analytics') . '

'; - } - - $api = new API_Client(); - - $args = [ - 'store_id' => $atts['store_id'], - 'limit' => (int) $atts['limit'], - ]; - - if (!empty($atts['category_id'])) { - $args['category_id'] = $atts['category_id']; - } - - if (!empty($atts['search'])) { - $args['search'] = $atts['search']; - } - - if ($atts['in_stock'] === 'true') { - $args['in_stock'] = true; - } - - if (!empty($atts['fields'])) { - $args['fields'] = $atts['fields']; - } - - $data = $api->get_products($args); - $products = $data['products'] ?? []; - - if (empty($products)) { - return '

' . __('No products found', 'dutchie-analytics') . '

'; - } - - ob_start(); - ?> -
- -
- -
- <?php echo esc_attr($product['name']); ?> - - - - - -
- - -
-

- - -

- - - -

- - -
- - $ - - - - % THC - - - - % CBD - -
- - -

- -
-
- -
- '', - 'category_id' => '', - 'limit' => 20, - 'in_stock' => 'true', - 'slides_per_view' => 4, - 'autoplay' => 'true', - 'style' => 'modern', - ], $atts); - - if (empty($atts['store_id'])) { - return '

' . __('Please specify a store_id', 'dutchie-analytics') . '

'; - } - - $api = new API_Client(); - - $args = [ - 'store_id' => $atts['store_id'], - 'limit' => (int) $atts['limit'], - ]; - - if (!empty($atts['category_id'])) { - $args['category_id'] = $atts['category_id']; - } - - if ($atts['in_stock'] === 'true') { - $args['in_stock'] = true; - } - - $data = $api->get_products($args); - $products = $data['products'] ?? []; - - if (empty($products)) { - return '

' . __('No products found', 'dutchie-analytics') . '

'; - } - - $carousel_id = 'dutchie-carousel-' . uniqid(); - - ob_start(); - ?> - - - - get_stores(); - $stores = $data['stores'] ?? []; - - if (empty($stores)) { - return '

' . __('No stores found', 'dutchie-analytics') . '

'; - } - - ob_start(); - ?> -
- -
- - - -

-

- - • - -

- - - - - -
- -
- ''], $atts); - - if (empty($atts['store_id'])) { - return '

' . __('Please specify a store_id', 'dutchie-analytics') . '

'; - } - - $api = new API_Client(); - $data = $api->get_store_brands($atts['store_id']); - $brands = $data['brands'] ?? []; - - if (empty($brands)) { - return '

' . __('No brands found', 'dutchie-analytics') . '

'; - } - - ob_start(); - ?> -
- -
- -
- '', - 'date' => '', - ], $atts); - - if (empty($atts['store_id'])) { - return '

' . __('Please specify a store_id', 'dutchie-analytics') . '

'; - } - - $api = new API_Client(); - $data = $api->get_store_specials($atts['store_id'], $atts['date'] ?: null); - $specials = $data['specials'] ?? []; - - if (empty($specials)) { - return '

' . __('No specials for this date', 'dutchie-analytics') . '

'; - } - - ob_start(); - ?> -
- -
- - <?php echo esc_attr($special['name']); ?> - -

- -

- - - % OFF - -
- -
- start_controls_section( - 'content_section', - [ - 'label' => __('Content', 'dutchie-analytics'), - 'tab' => Controls_Manager::TAB_CONTENT, - ] - ); - - $api = new API_Client(); - $stores_data = $api->get_stores(); - $stores = []; - if ($stores_data && isset($stores_data['stores'])) { - foreach ($stores_data['stores'] as $store) { - $stores[$store['id']] = $store['name']; - } - } - - $this->add_control( - 'store_id', - [ - 'label' => __('Store', 'dutchie-analytics'), - 'type' => Controls_Manager::SELECT, - 'options' => $stores, - 'default' => array_key_first($stores) ?: '', - ] - ); - - $this->end_controls_section(); - } - - protected function render() { - $settings = $this->get_settings_for_display(); - - if (empty($settings['store_id'])) { - echo '

' . __('Please select a store', 'dutchie-analytics') . '

'; - return; - } - - $api = new API_Client(); - $data = $api->get_store_brands($settings['store_id']); - $brands = $data['brands'] ?? []; - - if (empty($brands)) { - echo '

' . __('No brands found', 'dutchie-analytics') . '

'; - return; - } - - ?> -
- -
- -
- start_controls_section( - 'content_section', - [ - 'label' => __('Content', 'dutchie-analytics'), - 'tab' => Controls_Manager::TAB_CONTENT, - ] - ); - - // Store selection - $api = new API_Client(); - $stores_data = $api->get_stores(); - $stores = []; - if ($stores_data && isset($stores_data['stores'])) { - foreach ($stores_data['stores'] as $store) { - $stores[$store['id']] = $store['name']; - } - } - - $this->add_control( - 'store_id', - [ - 'label' => __('Store', 'dutchie-analytics'), - 'type' => Controls_Manager::SELECT, - 'options' => $stores, - 'default' => array_key_first($stores) ?: '', - ] - ); - - $this->add_control( - 'category_id', - [ - 'label' => __('Category', 'dutchie-analytics'), - 'type' => Controls_Manager::TEXT, - 'placeholder' => __('Leave empty for all categories', 'dutchie-analytics'), - ] - ); - - $this->add_control( - 'limit', - [ - 'label' => __('Number of Products', 'dutchie-analytics'), - 'type' => Controls_Manager::NUMBER, - 'default' => 20, - 'min' => 1, - 'max' => 100, - ] - ); - - $this->add_control( - 'in_stock_only', - [ - 'label' => __('In Stock Only', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'yes', - ] - ); - - $this->add_control( - 'slides_per_view', - [ - 'label' => __('Slides Per View', 'dutchie-analytics'), - 'type' => Controls_Manager::NUMBER, - 'default' => 4, - 'min' => 1, - 'max' => 6, - ] - ); - - $this->add_control( - 'space_between', - [ - 'label' => __('Space Between Slides', 'dutchie-analytics'), - 'type' => Controls_Manager::NUMBER, - 'default' => 24, - 'min' => 0, - 'max' => 100, - ] - ); - - $this->add_control( - 'autoplay', - [ - 'label' => __('Autoplay', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'yes', - ] - ); - - $this->add_control( - 'loop', - [ - 'label' => __('Loop', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'yes', - ] - ); - - // Field selection - $this->add_control( - 'show_brand', - [ - 'label' => __('Show Brand', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'yes', - ] - ); - - $this->add_control( - 'show_description', - [ - 'label' => __('Show Description', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'no', - ] - ); - - $this->add_control( - 'show_thc', - [ - 'label' => __('Show THC %', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'yes', - ] - ); - - $this->add_control( - 'show_price', - [ - 'label' => __('Show Price', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'yes', - ] - ); - - $this->end_controls_section(); - - // Style Section - $this->start_controls_section( - 'style_section', - [ - 'label' => __('Style', 'dutchie-analytics'), - 'tab' => Controls_Manager::TAB_STYLE, - ] - ); - - $this->add_control( - 'card_style', - [ - 'label' => __('Card Style', 'dutchie-analytics'), - 'type' => Controls_Manager::SELECT, - 'options' => [ - 'modern' => __('Modern', 'dutchie-analytics'), - 'minimal' => __('Minimal', 'dutchie-analytics'), - 'bold' => __('Bold', 'dutchie-analytics'), - ], - 'default' => 'modern', - ] - ); - - $this->end_controls_section(); - } - - protected function render() { - $settings = $this->get_settings_for_display(); - - if (empty($settings['store_id'])) { - echo '

' . __('Please select a store', 'dutchie-analytics') . '

'; - return; - } - - $api = new API_Client(); - - $args = [ - 'store_id' => $settings['store_id'], - 'limit' => $settings['limit'], - ]; - - if (!empty($settings['category_id'])) { - $args['category_id'] = $settings['category_id']; - } - - if ($settings['in_stock_only'] === 'yes') { - $args['in_stock'] = true; - } - - $data = $api->get_products($args); - $products = $data['products'] ?? []; - - if (empty($products)) { - echo '

' . __('No products found', 'dutchie-analytics') . '

'; - return; - } - - $carousel_id = 'dutchie-carousel-' . uniqid(); - $card_class = 'dutchie-product-card-' . $settings['card_style']; - - ?> - - - - start_controls_section( - 'content_section', - [ - 'label' => __('Content', 'dutchie-analytics'), - 'tab' => Controls_Manager::TAB_CONTENT, - ] - ); - - $api = new API_Client(); - $stores_data = $api->get_stores(); - $stores = []; - if ($stores_data && isset($stores_data['stores'])) { - foreach ($stores_data['stores'] as $store) { - $stores[$store['id']] = $store['name']; - } - } - - $this->add_control( - 'store_id', - [ - 'label' => __('Store', 'dutchie-analytics'), - 'type' => Controls_Manager::SELECT, - 'options' => $stores, - 'default' => array_key_first($stores) ?: '', - ] - ); - - $this->add_control( - 'category_id', - [ - 'label' => __('Category', 'dutchie-analytics'), - 'type' => Controls_Manager::TEXT, - 'placeholder' => __('Leave empty for all categories', 'dutchie-analytics'), - ] - ); - - $this->add_control( - 'limit', - [ - 'label' => __('Number of Products', 'dutchie-analytics'), - 'type' => Controls_Manager::NUMBER, - 'default' => 12, - 'min' => 1, - 'max' => 100, - ] - ); - - $this->add_control( - 'columns', - [ - 'label' => __('Columns', 'dutchie-analytics'), - 'type' => Controls_Manager::NUMBER, - 'default' => 4, - 'min' => 1, - 'max' => 6, - ] - ); - - $this->add_control( - 'in_stock_only', - [ - 'label' => __('In Stock Only', 'dutchie-analytics'), - 'type' => Controls_Manager::SWITCHER, - 'label_on' => __('Yes', 'dutchie-analytics'), - 'label_off' => __('No', 'dutchie-analytics'), - 'return_value' => 'yes', - 'default' => 'yes', - ] - ); - - $this->add_control( - 'card_style', - [ - 'label' => __('Card Style', 'dutchie-analytics'), - 'type' => Controls_Manager::SELECT, - 'options' => [ - 'modern' => __('Modern', 'dutchie-analytics'), - 'minimal' => __('Minimal', 'dutchie-analytics'), - 'bold' => __('Bold', 'dutchie-analytics'), - ], - 'default' => 'modern', - ] - ); - - $this->end_controls_section(); - } - - protected function render() { - $settings = $this->get_settings_for_display(); - - if (empty($settings['store_id'])) { - echo '

' . __('Please select a store', 'dutchie-analytics') . '

'; - return; - } - - $api = new API_Client(); - - $args = [ - 'store_id' => $settings['store_id'], - 'limit' => $settings['limit'], - ]; - - if (!empty($settings['category_id'])) { - $args['category_id'] = $settings['category_id']; - } - - if ($settings['in_stock_only'] === 'yes') { - $args['in_stock'] = true; - } - - $data = $api->get_products($args); - $products = $data['products'] ?? []; - - if (empty($products)) { - echo '

' . __('No products found', 'dutchie-analytics') . '

'; - return; - } - - ?> -
- -
- -
- <?php echo esc_attr($product['name']); ?> - - - - - -
- - -
-

- - -

- - - -

- - -
- - $ - - - - % THC - - - - % CBD - -
- - -

- -
-
- -
- start_controls_section( - 'content_section', - [ - 'label' => __('Content', 'dutchie-analytics'), - 'tab' => Controls_Manager::TAB_CONTENT, - ] - ); - - $api = new API_Client(); - $stores_data = $api->get_stores(); - $stores = []; - if ($stores_data && isset($stores_data['stores'])) { - foreach ($stores_data['stores'] as $store) { - $stores[$store['id']] = $store['name']; - } - } - - $this->add_control( - 'store_id', - [ - 'label' => __('Store', 'dutchie-analytics'), - 'type' => Controls_Manager::SELECT, - 'options' => $stores, - 'default' => array_key_first($stores) ?: '', - ] - ); - - $this->add_control( - 'date', - [ - 'label' => __('Date', 'dutchie-analytics'), - 'type' => Controls_Manager::DATE_TIME, - 'picker_options' => [ - 'enableTime' => false, - ], - ] - ); - - $this->end_controls_section(); - } - - protected function render() { - $settings = $this->get_settings_for_display(); - - if (empty($settings['store_id'])) { - echo '

' . __('Please select a store', 'dutchie-analytics') . '

'; - return; - } - - $api = new API_Client(); - $date = !empty($settings['date']) ? date('Y-m-d', strtotime($settings['date'])) : null; - $data = $api->get_store_specials($settings['store_id'], $date); - $specials = $data['specials'] ?? []; - - if (empty($specials)) { - echo '

' . __('No specials for this date', 'dutchie-analytics') . '

'; - return; - } - - ?> -
- -
- - <?php echo esc_attr($special['name']); ?> - -

- -

- - - % OFF - -
- -
- start_controls_section( - 'content_section', - [ - 'label' => __('Content', 'dutchie-analytics'), - 'tab' => Controls_Manager::TAB_CONTENT, - ] - ); - - $this->add_control( - 'columns', - [ - 'label' => __('Columns', 'dutchie-analytics'), - 'type' => Controls_Manager::NUMBER, - 'default' => 3, - 'min' => 1, - 'max' => 4, - ] - ); - - $this->end_controls_section(); - } - - protected function render() { - $api = new API_Client(); - $data = $api->get_stores(); - $stores = $data['stores'] ?? []; - - if (empty($stores)) { - echo '

' . __('No stores found', 'dutchie-analytics') . '

'; - return; - } - - ?> -
- -
- - - -

-

- - • - -

- - - - - -
- -
-
- -
- + +
onclick="window.open('', '_blank')" + style="cursor: ;"> +
- <?php echo esc_attr($product['name']); ?>
diff --git a/wordpress-plugin/widgets/product-grid.php b/wordpress-plugin/widgets/product-grid.php index 822c0179..84499c9d 100644 --- a/wordpress-plugin/widgets/product-grid.php +++ b/wordpress-plugin/widgets/product-grid.php @@ -262,11 +262,16 @@ class Crawlsy_Menus_Product_Grid_Widget extends \Elementor\Widget_Base { $columns = $settings['columns']; ?>
- -
- + +
onclick="window.open('', '_blank')" + style="cursor: ;"> +
- <?php echo esc_attr($product['name']); ?>
diff --git a/wordpress-plugin/widgets/single-product.php b/wordpress-plugin/widgets/single-product.php index 76394a4d..9d7bcaeb 100644 --- a/wordpress-plugin/widgets/single-product.php +++ b/wordpress-plugin/widgets/single-product.php @@ -25,6 +25,33 @@ class Crawlsy_Menus_Single_Product_Widget extends \Elementor\Widget_Base { return ['general']; } + /** + * Get products for the SELECT2 dropdown + * + * @return array Associative array of product_id => product_name + */ + protected function get_products_for_select() { + $options = ['' => __('-- Select a Product --', 'crawlsy-menus')]; + + $plugin = Crawlsy_Menus_Plugin::instance(); + $products = $plugin->fetch_products(['limit' => 500]); + + if ($products && is_array($products)) { + foreach ($products as $product) { + $label = $product['name']; + if (!empty($product['brand'])) { + $label = $product['brand'] . ' - ' . $label; + } + if (!empty($product['category'])) { + $label .= ' (' . $product['category'] . ')'; + } + $options[$product['id']] = $label; + } + } + + return $options; + } + protected function register_controls() { // Content Section @@ -36,13 +63,31 @@ class Crawlsy_Menus_Single_Product_Widget extends \Elementor\Widget_Base { ] ); + // Get products for the select dropdown + $products_options = $this->get_products_for_select(); + $this->add_control( 'product_id', [ - 'label' => __('Product ID', 'crawlsy-menus'), + 'label' => __('Select Product', 'crawlsy-menus'), + 'type' => \Elementor\Controls_Manager::SELECT2, + 'options' => $products_options, + 'default' => '', + 'label_block' => true, + 'description' => __('Search and select a product to display', 'crawlsy-menus'), + ] + ); + + $this->add_control( + 'product_id_manual', + [ + 'label' => __('Or Enter Product ID', 'crawlsy-menus'), 'type' => \Elementor\Controls_Manager::NUMBER, 'default' => '', - 'description' => __('Enter the product ID to display', 'crawlsy-menus'), + 'description' => __('Manually enter a product ID if not found in dropdown', 'crawlsy-menus'), + 'condition' => [ + 'product_id' => '', + ], ] ); @@ -228,13 +273,16 @@ class Crawlsy_Menus_Single_Product_Widget extends \Elementor\Widget_Base { protected function render() { $settings = $this->get_settings_for_display(); - if (empty($settings['product_id'])) { - echo '

' . __('Please enter a product ID.', 'crawlsy-menus') . '

'; + // Use dropdown selection, or fall back to manual ID + $product_id = !empty($settings['product_id']) ? $settings['product_id'] : $settings['product_id_manual']; + + if (empty($product_id)) { + echo '

' . __('Please select or enter a product ID.', 'crawlsy-menus') . '

'; return; } $plugin = Crawlsy_Menus_Plugin::instance(); - $product = $plugin->fetch_product($settings['product_id']); + $product = $plugin->fetch_product($product_id); if (!$product) { echo '

' . __('Product not found.', 'crawlsy-menus') . '

';