"""Dev-only: seed a LOCAL dashboard with clickable batch + booking leads (overlay image, outline geometry, pricing, prior edit) so the address popup / drag-to-edit outline / measurement history can be browser-tested WITHOUT running the real (model-heavy) pipeline. ./.venv/Scripts/python.exe scripts/dev_seed.py # then open http://127.0.0.1:8000/dashboard?token=devtoken (Leads screen → click an address) Standalone — nothing here ships to prod. It runs the app in-process (so the in-memory viz cache is seeded too) with a dev-token login (no Clerk) and an in-memory SQLite DB. """ from __future__ import annotations import json import os import uuid # Dev env must be set BEFORE importing the app (config is read at import). os.environ.setdefault("WARM_MODEL_ON_STARTUP", "0") # skip the 3-model load os.environ.setdefault("DASHBOARD_DEV_TOKEN", "devtoken") os.environ.setdefault("DASHBOARD_DEV_TENANT", "thelawnstandard") os.environ.setdefault("ALLOWED_API_KEYS", "dev:devkey") os.environ.setdefault("WIDGET_API_KEYS", "thelawnstandard:pub-dev") # tenant:publishable → /widget?key=pub-dev os.environ.setdefault("ADMIN_DEV_TOKEN", "admintoken") # /admin?token=admintoken os.environ.setdefault("PLATFORM_ADMIN_EMAILS", "admin@localhost") os.environ.setdefault("LAWN_DB_PATH", ":memory:") for _k in ("DATABASE_URL", "CLERK_SECRET_KEY", "CLERK_PUBLISHABLE_KEY"): os.environ.pop(_k, None) # force the dev-token gate, local SQLite import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from lawn_estimator import api TENANT = os.environ["DASHBOARD_DEV_TENANT"] TOKEN = os.environ["DASHBOARD_DEV_TOKEN"] def _overlay_png(path, poly): """A faux 'lawn overlay' (aerial-ish background + the measured lawn) so the popup image + drag editor render. The frontend draws its own editable SVG polygon on top of this.""" fig = plt.figure(figsize=(6.4, 6.4), dpi=200) # 1280x1280 ax = fig.add_axes([0, 0, 1, 1]) ax.set_xlim(0, 1280) ax.set_ylim(1280, 0) ax.axis("off") ax.add_patch(plt.Rectangle((0, 0), 1280, 1280, color="#5c6b4f")) xs = [p[0] for p in poly] + [poly[0][0]] ys = [p[1] for p in poly] + [poly[0][1]] ax.fill(xs, ys, color="#38c860", alpha=0.35) ax.plot(xs, ys, color="#2e7d32", lw=3) fig.savefig(path) plt.close(fig) def _seed_batch(address, zip_code, sqft, poly): rid = uuid.uuid4().hex[:12] pricing = {"currency": "USD", "line_items": [ {"service": "mow", "label": "Mowing", "price": round(sqft / 1000 * 10, 2)}, {"service": "fert", "label": "Fertilizer", "price": round(sqft / 1000 * 7, 2)}]} price_total = round(sum(li["price"] for li in pricing["line_items"]), 2) api.METER.record(TENANT, address, api.BATCH, lawn_sqft=sqft, result_id=rid, zip=zip_code, method="lidar+rgb", confidence="high") detail = json.dumps({"lawn_polygon_px": poly, "pixel_area_sqft": 0.35, "image_size_px": [1280, 1280], "pricing": pricing, "original_sqft": sqft, "original_pricing": pricing}) api.LEADS.record(TENANT, "batch", address, email="ops@thelawnstandard.com", zip=zip_code, lawn_sqft=sqft, price_total=price_total, confidence="high", result_id=rid, detail=detail) api.OUTPUT_DIR.mkdir(parents=True, exist_ok=True) overlay = api.OUTPUT_DIR / f"dev_{rid}.png" _overlay_png(overlay, poly) api._remember_viz(rid, overlay, overlay, TENANT) # viz + overlay = the placeholder; owner=tenant return rid def seed(): if not api.USERS.by_clerk_id(api.DASHBOARD_DEV_CLERK_ID): api.USERS.create(TENANT, api.DASHBOARD_DEV_CLERK_ID, "dev@localhost", "owner", status="active") r1 = _seed_batch("1423 N 75th St, Omaha, NE 68134", "68134", 5200.0, [[300, 320], [980, 300], [1010, 980], [280, 1000]]) # a batch lead that already carries a prior tenant edit, so its popup shows Original + Edit + Revert r2 = _seed_batch("7863 N 144th Ave, Bennington, NE 68007", "68007", 8100.0, [[260, 260], [1020, 300], [980, 1010], [300, 980]]) api.MEDITS.record(TENANT, r2, 8600.0, kind="tenant", original_sqft=8100.0, address="7863 N 144th Ave, Bennington, NE 68007", user_id=1) api._persist_measurement_edit(TENANT, r2, 8600.0, {"currency": "USD", "line_items": [{"service": "mow", "label": "Mowing", "price": 86.0}]}) # A team member (Team screen) + a pending invite. if not api.USERS.by_clerk_id("clerk_staff"): api.USERS.create(TENANT, "clerk_staff", "staff@thelawnstandard.com", "staff", status="active") if not api.USERS.pending_by_email("invitee@thelawnstandard.com"): api.USERS.invite(TENANT, "invitee@thelawnstandard.com", "staff", invited_by=1) # A widget-origin quote → lead → booking with long-format line items (Leads + Overview + popup). rw = uuid.uuid4().hex[:12] api.METER.record(TENANT, "5534 Mayberry St, Omaha, NE 68106", api.SINGLE, lawn_sqft=4200.0, result_id=rw, zip="68106", method="lidar+rgb", confidence="high") api.LEADS.record(TENANT, "widget", "5534 Mayberry St, Omaha, NE 68106", name="Sam Homeowner", email="sam@example.com", phone="402-555-0100", zip="68106", result_id=rw) api.LEADS.record(TENANT, "booking", "5534 Mayberry St, Omaha, NE 68106", name="Sam Homeowner", email="sam@example.com", zip="68106", lawn_sqft=4200.0, price_total=84.0, result_id=rw, detail=json.dumps({"original_sqft": 4200.0, "bundle": {"id": "spring", "name": "Spring Package", "cadence": "seasonal"}, "pricing": {"line_items": [{"service": "mow", "label": "Mowing", "price": 42.0}, {"service": "fert", "label": "Fertilizer", "price": 42.0}]}})) api.BOOKING_ITEMS.record_many(TENANT, rw, [ {"service": "mow", "label": "Mowing", "price": 42.0, "purchased": True}, {"service": "fert", "label": "Fertilizer", "price": 42.0, "purchased": True}, {"service": "aerate", "label": "Aeration", "price": 120.0, "purchased": False}]) # Backdated runs so the Overview volume sparkline shows a trend (raw insert — record() can't backdate). import random from datetime import date, timedelta zips = ["68106", "68134", "68164", "68007", "68135"] for i in range(1, 15): d = (date.today() - timedelta(days=i)).isoformat() + "T12:00:00.000Z" for _ in range(random.randint(0, 4)): oc = random.choices(["ok", "rejected", "error"], weights=[85, 10, 5])[0] api.METER._db.execute( "INSERT INTO runs (tenant_id, surface, address, address_norm, zip, billable, " "method, confidence, lawn_sqft, duration_s, outcome, created_at) " "VALUES (?, ?, ?, ?, ?, ?, 'lidar+rgb', 'high', ?, ?, ?, ?)", (TENANT, random.choice(["single", "batch"]), f"seed {i}", f"seed {i}", random.choice(zips), 1, random.uniform(2500, 9000), random.uniform(18, 55), oc, d)) api.METER._db.commit() print(f"\n Seeded → http://127.0.0.1:8000/dashboard?token={TOKEN} (widget: /widget?key=pub-dev)") print(" Leads → click an address (batch: view+drag-edit+history/revert; booking: view).") print(f" results: {r1} (fresh), {r2} (has a tenant edit), {rw} (widget booking)\n") if __name__ == "__main__": seed() import uvicorn uvicorn.run(api.app, host="127.0.0.1", port=8000, log_level="warning")