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 whentype="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 3: One-click follow-up mailto links (Stretch Goal 2)
Agent-assisted OK. This is HTML/frontend code.
Objective
From /owner/leads you can see every lead. Now add a Follow up button per row that opens a pre-filled email draft in your default mail client (Gmail in the browser, Apple Mail, Outlook — whatever's set). This covers your Stretch Goal 2 with zero SMTP setup.
Why mailto: instead of an SMTP send route?
Your original Stretch Goal 2 spec said "send a follow-up email from the dashboard." mailto: is the simpler version — the user's own email client opens with to, subject, and body pre-filled, and they can edit/send from there. For outbound sales-style follow-ups this is actually what you want: the reply lands in your sent folder and threads with any response. No API keys, no deliverability headaches.
Instructions
Hints
Add to the <thead> <tr>, after <th>Reason</th>:
<th>Follow up</th>
Add as the last <td> inside the {% for lead in leads %} loop (your leads are sqlite3.Row objects, so keep using lead["field"] to match the rest of your template):
<td>
{% set subject = "Your roof inspection request" %} {% set body %} Hi {{
lead["name"] }}, Thanks for requesting a roof inspection at {{ lead["address"]
}}. I'd love to set up a time to come take a look. A few quick questions to
confirm: - Best time to reach you at {{ lead["phone"] }}? - Is {{
lead["insurance"] }} still your current home insurance? - You mentioned: "{{
lead["reason"] }}" — anything else I should know before I come out? Talk soon,
Lucas {% endset %}
<a
class="btn btn-sm btn-primary"
href="mailto:{{ lead['email'] }}?subject={{ subject|urlencode }}&body={{ body|urlencode }}"
>
Follow up
</a>
</td>
A few things worth understanding:
{% set body %}...{% endset %}lets you assign a multi-line string in Jinja.|urlencodeconverts spaces, newlines, and special characters into the%20-style encodingmailto:needs. Without it, a newline or quote breaks the link.mailto:...?subject=...&body=...is a standard URL. The default email client opens with a pre-filled draft; you can still edit before hitting send.
Test it: visit /owner/leads?key=YOUR_KEY, click a Follow up button. Your email client should open with the lead's email in To, your subject, and the templated body ready to edit.
Optional — get help from your agent:
In my owner_leads.html, add a Follow up column. Each cell is a Bootstrap button that opens a mailto: link with the lead's email pre-filled, my subject "Your roof inspection request", and a multi-line body that references the lead's name, address, phone, insurance, and reason. Use Jinja's urlencode filter. My template uses lead["field"] syntax — match that.
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:
- Fill out the form at
/locally. - Watch your terminal — you should see no errors.
- 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.