Lucas's Project Guide

Project: Roof Inspection Lead Generation Page Category: Web Development (Flask) Last updated: April 23

Note: This guide reflects the latest state of your project repo. It may not match the most up-to-date version if you've worked since.

Phase 1: Form layout + mobile-friendly inputs

Agent-assisted OK. Pure view/markup. No business logic changes.

Objective

Cap the form width on desktop so it stops looking like a stretched table, and use the right input types/attributes so mobile browsers show the correct keyboard and offer autofill.

Instructions

Hints

Width wrapper pattern (drop this around the existing .card):

<div class="container py-5">
  <div class="row justify-content-center">
    <div class="col-md-8 col-lg-6">
      <div class="card shadow">
        <!-- existing card-body stays as-is -->
      </div>
    </div>
  </div>
</div>

col-md-8 = 8/12 width on tablets, col-lg-6 = 6/12 on desktop. Phones stay full-width automatically. justify-content-center centers the column in the row.

Updated input pattern (apply the matching attributes to each field):

<div class="mb-3">
  <label for="name" class="form-label">Full Name</label>
  <input
    id="name"
    name="name"
    type="text"
    class="form-control"
    autocomplete="name"
    required
  />
</div>

<div class="mb-3">
  <label for="phone" class="form-label">Phone Number</label>
  <input
    id="phone"
    name="phone"
    type="tel"
    class="form-control"
    autocomplete="tel"
    inputmode="tel"
    required
  />
</div>

<div class="mb-3">
  <label for="email" class="form-label">Email</label>
  <input
    id="email"
    name="email"
    type="email"
    class="form-control"
    autocomplete="email"
    inputmode="email"
    required
  />
</div>

<div class="mb-3">
  <label for="address" class="form-label">Address</label>
  <input
    id="address"
    name="address"
    type="text"
    class="form-control"
    autocomplete="street-address"
    required
  />
</div>

<div class="mb-3">
  <label for="insurance" class="form-label">Home Insurance Provider</label>
  <input
    id="insurance"
    name="insurance"
    type="text"
    class="form-control"
    autocomplete="organization"
    required
  />
</div>

Why these attributes matter:

  • type="tel" / type="email" → the phone keyboard is numbers-first; the email keyboard has @ and . on the main row.
  • autocomplete="..." → iOS/Android offer to fill the field from the user's contact card or saved autofill. Single biggest mobile UX win you can add in one line.
  • inputmode="..." → hints to the browser which keyboard to show even when type="text".

Optional — get help from your agent:

Take my home.html and confirmation.html. Wrap the card in a
centered column that caps at col-lg-6. Don't change any input
names or ids — app.py reads them by name. Also add the
autocomplete and inputmode attributes from the guide.

Phase 2: Useful confirmation screen

Objective

Right now confirmation.html just says "Thank you, Lucas!" When someone prints it, they get a receipt with no detail. Show them every field they submitted so the printed page is actually useful.

Instructions

Hints

Refactor the POST branch of your home route:

if request.method == "POST":
    lead = {
        "name": request.form.get("name", "").strip(),
        "phone": request.form.get("phone", "").strip(),
        "email": request.form.get("email", "").strip(),
        "address": request.form.get("address", "").strip(),
        "insurance": request.form.get("insurance", "").strip(),
        "reason": request.form.get("reason", "").strip(),
    }

    if not all(lead.values()):
        return render_template(
            "home.html",
            title="Roof Inspection Form",
            error="Please fill out every field.",
        )

    save_lead(**lead)
    return render_template("confirmation.html", lead=lead)

save_lead(**lead) unpacks the dict into keyword arguments. Your existing save_lead(name, phone, email, address, insurance, reason) signature already matches the dict keys, so nothing in logic.py changes.

In confirmation.html (inside the card body):

<h1 class="card-title mb-3">Thank you, {{ lead.name }}!</h1>
<p class="text-muted mb-4">
  Your roof inspection request has been submitted successfully. We'll be in
  touch at {{ lead.phone }} or {{ lead.email }}.
</p>

<dl class="row text-start mb-4">
  <dt class="col-sm-4">Name</dt>
  <dd class="col-sm-8">{{ lead.name }}</dd>

  <dt class="col-sm-4">Phone</dt>
  <dd class="col-sm-8">{{ lead.phone }}</dd>

  <dt class="col-sm-4">Email</dt>
  <dd class="col-sm-8">{{ lead.email }}</dd>

  <dt class="col-sm-4">Address</dt>
  <dd class="col-sm-8">{{ lead.address }}</dd>

  <dt class="col-sm-4">Insurance</dt>
  <dd class="col-sm-8">{{ lead.insurance }}</dd>

  <dt class="col-sm-4">Reason</dt>
  <dd class="col-sm-8">{{ lead.reason }}</dd>
</dl>

<dl> / <dt> / <dd> is the right HTML for label/value pairs — literally called a "description list."

Optional — get help from your agent:

Skip — this is straightforward Jinja.

Phase 4: Auto email notification on new lead (Stretch Goal 3, via Resend)

Mixed. The API call is library work. The email copy is yours.

Objective

When a new lead submits the form, you get an email. No dashboard refreshes required. Resend is a modern transactional email service with a generous free tier (3,000 emails/month) and a clean Python SDK — easier than wiring up Gmail SMTP.

Instructions

Hints

Install:

uv add resend

(You already have python-dotenv — no need to add it again.)

.env (NEVER commit — .env is already in your .gitignore, good):

OWNER_KEY=your_existing_owner_key
RESEND_API_KEY=re_your_key_here
OWNER_EMAIL=lucas@example.com
SENDER_EMAIL=onboarding@resend.dev

.env.example (commit this so anyone cloning the repo knows what to set):

OWNER_KEY=
RESEND_API_KEY=
OWNER_EMAIL=
SENDER_EMAIL=onboarding@resend.dev

notifications.py (new file at project root):

"""Owner notifications — sends an email when a new lead arrives.

Keeps email concerns out of logic.py (DB) and app.py (routes).
"""

import os
import resend

resend.api_key = os.environ.get("RESEND_API_KEY")


def notify_owner(lead):
    owner = os.environ.get("OWNER_EMAIL")
    sender = os.environ.get("SENDER_EMAIL", "onboarding@resend.dev")

    if not resend.api_key or not owner:
        print("[notify_owner] Skipping — RESEND_API_KEY or OWNER_EMAIL not set.")
        return

    body = f"""New roof inspection lead:

Name:      {lead['name']}
Phone:     {lead['phone']}
Email:     {lead['email']}
Address:   {lead['address']}
Insurance: {lead['insurance']}
Reason:    {lead['reason']}
"""

    resend.Emails.send({
        "from": sender,
        "to": [owner],
        "subject": f"New lead: {lead['name']}",
        "text": body,
    })

Note: no load_dotenv() call here — app.py already calls load_dotenv() at import time, so env vars are populated before notifications is imported.

In app.py, add the import and the call:

from notifications import notify_owner

# ...inside the POST branch of home(), after save_lead(**lead):
try:
    notify_owner(lead)
except Exception as e:
    print(f"[app] Email notification failed: {e}")

return render_template("confirmation.html", lead=lead)

The try/except matches your stretch-goals-steps Goal 3: "Wrap email send in try/except so submission still succeeds if email fails." You'd rather miss an email than drop a lead.

Test it:

  1. Fill out the form at / locally.
  2. Watch your terminal — you should see no errors.
  3. Check the inbox at OWNER_EMAIL. If it's not there, check spam and check the terminal for exception text.

Optional — get help from your agent:

Walk me through how load_dotenv() in app.py makes os.environ.get()
work in notifications.py. Why don't I need to call load_dotenv()
in both files? What happens on Render where there's no .env file?

Phase 5 (Bonus): Deploy to Render

Only attempt after Phases 1–4 work locally. Cherry on top — skip if short on time.

Objective

Put your app on the internet at a URL like https://roof-lead-gen.onrender.com so your demo pulls up a real live site instead of localhost. Corresponds to your Stretch Goal 6.

Instructions

Hints

render.yaml:

services:
  - type: web
    name: roof-lead-gen
    env: python
    plan: free
    buildCommand: uv sync --frozen && uv cache prune --ci
    startCommand: uv run gunicorn app:app
    autoDeploy: true
    envVars:
      - key: OWNER_KEY
        sync: false
      - key: RESEND_API_KEY
        sync: false
      - key: OWNER_EMAIL
        sync: false
      - key: SENDER_EMAIL
        sync: false

sync: false means "don't bake the value into git — I'll set it in the Render dashboard." That's how your keys stay out of the repo.

Important — SQLite caveat for demo day:

Render's free tier wipes the disk on every deploy. Your data/leads.db will reset every time you push code or Render restarts your service. For demo day this is fine — submit 2–3 fresh test leads right before your presentation so /owner/leads isn't empty. Don't treat the live site as a real CRM yet.

If you ever want persistent storage, see the Render Disks or hosted Postgres section of the deploy guide.

Optional — get help from your agent:

I've deployed to Render using the guide. The build succeeded but
visiting /owner/leads?key=... returns 403 even though I'm using
the right key. Walk me through checking the OWNER_KEY env var in
the Render dashboard and how to see the live logs.

Checkpoint 3 Readiness

By Friday, May 1st at 3:30pm (Demo Day):

Helpful Resources