File size: 7,643 Bytes
ca81019
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df692ec
 
 
ca81019
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df692ec
 
 
ca81019
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df692ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7507549
df692ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca81019
 
 
 
 
 
 
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""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")