lawn-estimator-dev / scripts /dev_seed.py
TempuraML's picture
feat: WS5 — service bundles (config builder → widget packages → Overview) (#138)
7507549 unverified
Raw
History Blame Contribute Delete
7.64 kB
"""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")