Spaces:
Running
Running
| """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") | |