#!/usr/bin/env python3
"""Generate planned UX mock pages for client proposals from features.md catalog."""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CREDIT_HTML = (
'
Made by '
''
"AI ML Operations, LLC
"
)
CLIENTS = {
"eds-plumbing-and-sewer": {
"brand": "Ed's Plumbing & Sewer",
"brand_html": "Ed's Plumbing & Sewer",
"short": "Ed's Portal",
"user": "Ed's Admin",
"email": "info@thegreatestplumber.com",
"phone": "(630) 663-1444",
"area": "Darien & Chicagoland",
"theme": "",
"home": "index.html",
"about": "about.html",
"contact": "contact.html",
"industry": "plumbing",
"leads": [
("Jordan Lee", "google / emergency", "New", "Clogged main line"),
("Sam Ortiz", "facebook / spring-promo", "Contacted", "Water heater quote"),
("Avery Chen", "direct / contact-form", "New", "Sump pump install"),
("Morgan Diaz", "postcard / march-mailer", "Won", "Fixture upgrade"),
("Riley Park", "google / brand", "Lost", "Gas line repair"),
],
"invoices": [
("INV-1042", "Jordan Lee", "$480.00", "Open"),
("INV-1041", "Morgan Diaz", "$1,250.00", "Paid"),
("INV-1040", "Sam Ortiz", "$320.00", "Open"),
],
"blog_posts": [
("When to call for a sewer backup", "Mar 12, 2026", "Signs your main line needs attention before it floods the basement."),
("Water heater lifespan checklist", "Feb 28, 2026", "How to know if repair or replacement saves more money."),
("Winter sump pump prep", "Jan 15, 2026", "Keep ejector and sump systems ready for melt season."),
],
"campaign": "Spring drain cleaning reminder",
"social_caption": "Clogged drain? We clear it fast — 24/7 emergency service in Darien & Chicagoland.\n\nCall (630) 663-1444",
"postcard_headline": "Drain trouble?",
"postcard_sub": "Local plumbers · 24/7 emergency · (630) 663-1444",
},
"paint-to-please": {
"brand": "Paint to Please",
"brand_html": "Paint to Please",
"short": "Bill's Portal",
"user": "Bill",
"email": "hello@painttoplease.example",
"phone": "(630) XXX-XXXX",
"area": "Wheaton, IL",
"theme": " theme-paint",
"home": "index.html",
"about": "index.html#about",
"contact": "index.html#contact",
"industry": "painting",
"leads": [
("Ann Brooks", "yelp / profile", "New", "Living room + trim"),
("Phyllis Voss", "google / brand", "Contacted", "Cabinet refinish"),
("Luann Meyer", "facebook / before-after", "New", "Whole-home interior"),
("Clara Hunt", "direct / estimate-form", "Won", "Fireplace + bookcase"),
("Gina Delgado", "postcard / wheaton-mailer", "Lost", "Wallpaper removal"),
],
"invoices": [
("INV-208", "Clara Hunt", "$2,400.00", "Paid"),
("INV-209", "Ann Brooks", "$850.00", "Open"),
("INV-207", "Phyllis Voss", "$1,100.00", "Open"),
],
"blog_posts": [
("Trim painting tips that last", "Mar 10, 2026", "Prep and cut-in habits that keep baseboards crisp."),
("Cabinet paint vs refinish", "Feb 20, 2026", "When a spray finish beats a full replace."),
("Choosing white without regret", "Jan 8, 2026", "Warm vs cool whites for Wheaton interiors."),
],
"campaign": "Spring interior refresh offer",
"social_caption": "Fresh walls, careful trim, zero shortcuts — interior painting in Wheaton.\n\nBook an estimate: (630) XXX-XXXX",
"postcard_headline": "Walls tired?",
"postcard_sub": "Interior painting · Wheaton · Paint to Please",
},
}
def banner(c: dict, extra: str = "") -> str:
return f"""
Proposal mockup {c['brand_html']} — UX examples (not production){extra}
"""
def nav(c: dict, active: str) -> str:
def a(key: str, href: str, label: str) -> str:
cls = "active" if key == active else ""
return f'{label} '
return f""""""
def shell(c: dict, title: str, active: str, body: str, note: str = "") -> str:
note_html = f'UX note: {note}
' if note else ""
return f"""
{title} · Portal · {c['brand_html']}
{banner(c)}
{nav(c, active)}
{note_html}
{body}
{CREDIT_HTML}
"""
def public_shell(c: dict, title: str, body: str, note: str = "") -> str:
note_html = f'UX note: {note}
' if note else ""
return f"""
{title} · {c['brand_html']}
{banner(c)}
{note_html}
{body}
{CREDIT_HTML}
"""
def page_login(c: dict) -> str:
return f"""
Sign in · Portal · {c['brand_html']}
{banner(c)}
UX note: Private client login (Django auth). Not public.
{c['short']}
{c['brand_html']} · leads, campaigns, and billing in one place.
← Back to proposal gallery
{CREDIT_HTML}
"""
def page_dashboard(c: dict) -> str:
rows = "".join(
f'{n} {s} {st} '
for n, s, st, _ in c["leads"][:3]
)
body = f"""
Open pipeline
18
4 contacted
Mailing list
412
8 opted out (30d)
Open invoices
2
Next due Fri
{c['campaign']} — Email · scheduled Fri 10:00 · 186 recipients
Job reminder SMS — Draft · 42 recipients
Neighborhood promo — Postcard · queued · 120 pieces
"""
return shell(c, "Dashboard", "dashboard", body, "Portal home — glanceable widgets for leads, list health, and scheduled work.")
def page_leads(c: dict) -> str:
rows = "".join(
f'{n} {s} {need} {st} '
for n, s, st, need in c["leads"]
)
body = f"""
Name Source / UTM Need Status
{rows}
"""
return shell(c, "Lead inbox", "leads", body, "Statuses, source attribution, quick filter. Contact form submissions land here.")
def page_lead_detail(c: dict) -> str:
n, s, st, need = c["leads"][0]
body = f"""
{n} {st}
Need: {need}
Email: jordan@example.com
Phone: (630) 555-0142
Area: {c['area']}
Internal notes
Save notes
UTM attribution
Source {s.split(' / ')[0]}
Medium cpc
Campaign {s.split(' / ')[-1]}
Landing /{c['contact'].split('#')[0] if '#' in c['contact'] else 'contact.html'}
First touch Mar 18, 2026 · 9:14am
Linked contact consent: email ✓ · SMS ✓ · postcard —
"""
return shell(c, "Lead detail", "leads", body, "Notes, UTM attribution, linked contact consent.")
def page_analytics(c: dict) -> str:
bars = "".join(f'
' for h in (40, 65, 35, 80, 55, 70, 90))
body = f"""
Leads (30d)
42
+12% vs prior
Top campaign
spring-promo
11 leads
By source / campaign
Source Campaign Leads Won
google emergency 12 3
facebook spring-promo 11 2
direct (none) 9 4
postcard march-mailer 6 1
yelp profile 4 1
"""
return shell(c, "UTM analytics", "analytics", body, "Included with every client portal — leads by source/campaign over time.")
def page_contacts(c: dict) -> str:
body = """
Name Email Email SMS Postcard
Jordan Lee jordan@example.com ✓ ✓ —
Sam Ortiz sam@example.com ✓ — ✓
Avery Chen avery@example.com ✓ ✓ ✓
Morgan Diaz morgan@example.com — ✓ ✓
"""
return shell(c, "Mailing list", "contacts", body, "Contacts with per-channel consent (email / SMS / postcard).")
def page_contacts_import(c: dict) -> str:
body = """
1 Upload
2 Map columns
3 Consent defaults
4 Review duplicates
Upload CSV / Excel
Drop file here or
browse .csv, .xlsx · max 5MB mock
After upload: map name/email/phone, set default consent, merge duplicates.
Continue (mock)
"""
return shell(c, "Import contacts", "contacts", body, "CSV/Excel upload, column mapping, consent defaults, duplicate merge.")
def page_campaign(c: dict) -> str:
body = f"""
Preview
Subject: A quick note from {c['brand_html']}
Hi Jordan,
Just a friendly reminder we're here when you need us in {c['area']}.
— {c['brand_html']}
"""
return shell(c, "Campaign composer", "campaign", body, "Email, SMS, or postcard — select recipients, schedule, send.")
def page_campaign_report(c: dict) -> str:
body = f"""
{c['campaign']} Sent
Recipient Opened Clicked Status
jordan@example.com Yes Yes Engaged
sam@example.com Yes — Opened
avery@example.com — — Delivered
bad@example.com — — Bounced
"""
return shell(c, "Email engagement", "report", body, "Opens, clicks, bounces, unsubscribes per campaign.")
def page_postcard(c: dict) -> str:
body = f"""
Size
4×6
Standard
6×9
Large
6×11
Jumbo
Square
6×6
Live preview — front
{c['postcard_headline']}
{c['postcard_sub']}
"""
return shell(c, "Postcard designer", "postcard", body, "Size picker, reusable media library, live front/back preview.")
def page_direct_mail(c: dict) -> str:
body = f"""
"""
return shell(c, "Direct mail campaign", "directmail", body, "Recipient selection from contacts with postcard consent; vendor send.")
def page_blog_portal(c: dict) -> str:
rows = "".join(
f'{t} {d} {"Published" if i==0 else "Draft"} '
for i, (t, d, _) in enumerate(c["blog_posts"])
)
body = f"""
"""
return shell(c, "Blog list", "blog", body, "Client manages drafts and published posts.")
def page_blog_edit(c: dict) -> str:
t, _, excerpt = c["blog_posts"][0]
slug = "-".join("".join(ch if ch.isalnum() else " " for ch in t.lower()).split()[:6])
body = f"""
Edit post
Save draft
Publish
"""
return shell(c, "Blog editor", "blog", body, "Create and edit posts; publish or keep as draft.")
def page_invoices(c: dict) -> str:
rows = "".join(
f'{num} {who} {amt} {st} '
for num, who, amt, st in c["invoices"]
)
body = f"""
Number Customer Amount Status {rows}
"""
return shell(c, "Invoices", "invoices", body, "Paid / open / past due. Requires Email & SMS for pay-link notifications.")
def page_invoice_create(c: dict) -> str:
body = f"""
"""
return shell(c, "Create invoice", "invoices", body, "Amount, line items, customer email; pay link via email.")
def page_invoice_detail(c: dict) -> str:
num, who, amt, st = c["invoices"][0]
body = f"""
{num} {st}
Customer: {who}
Email: jordan@example.com
Amount: {amt}
Created: Mar 18, 2026
Pay link: public-pay.html
Resend email
Activity
Mar 18 · Invoice created
Mar 18 · Pay-link email sent
Mar 19 · Customer opened email
"""
return shell(c, "Invoice detail", "invoices", body, "Status and payment history; resend pay link.")
def page_social_accounts(c: dict) -> str:
body = """
f
Facebook Page Connected · token healthy
IG
Instagram Reconnect required · token expired
in
LinkedIn Not connected · Add via OAuth
"""
return shell(c, "Social accounts", "accounts", body, "OAuth connect / reconnect when tokens expire; health + disconnect.")
def page_social(c: dict) -> str:
body = f"""
Facebook preview
{c['social_caption']}
Like · Comment · Share
"""
return shell(c, "Social composer", "social", body, "Compose once; live FB / IG / LinkedIn previews update as you type.")
def page_social_calendar(c: dict) -> str:
days = []
for i in range(1, 36):
has = i in (5, 12, 18, 24)
cls = "day has-post" if has else "day"
dot = '
' if has else ""
label = i if i <= 31 else ""
days.append(f'{label}{dot}
')
body = f"""
Sun
Mon
Tue
Wed
Thu
Fri
Sat
{''.join(days)}
Dots = scheduled posts. Click through to composer in production.
"""
return shell(c, "Social calendar", "calendar", body, "Schedule and queue upcoming posts.")
def page_social_ai(c: dict) -> str:
body = f"""
AI social generator Ollama · local
Assistant
Describe the post. I'll draft captions you can send to the composer.
You
Spring promo for local customers, friendly, include phone.
Assistant
Draft A:
{c['social_caption'].replace('&','&')}
Send
How it works
Runs against your local LLM via Ollama — no cloud LLM required.
Requires Social consolidation so drafts hand off to compose → preview → schedule.
Mock only — production streams from your Ollama host.
"""
return shell(c, "AI social generator", "ai", body, "Prompt → draft variants → insert into social composer. Ollama local.")
def page_public_blog(c: dict) -> str:
items = "".join(
f'{d}
{t} {ex}
'
for t, d, ex in c["blog_posts"]
)
body = f'Blog Tips from {c["brand_html"]}
{items}
'
return public_shell(c, "Blog", body, "Public readers view posts; client manages them in the portal.")
def page_public_blog_post(c: dict) -> str:
t, d, ex = c["blog_posts"][0]
body = f"""
{d} · All posts
{t}
{ex}
This is a mock article body for the proposal. In production, posts are managed from the client portal blog editor and rendered on the public site.
Contact {c['brand_html']} if you need help sooner.
"""
return public_shell(c, t, body)
def page_public_unsubscribe(c: dict) -> str:
body = f"""
"""
return public_shell(c, "Unsubscribe", body, "One-click email unsubscribe and per-channel preference.")
def page_public_pay(c: dict) -> str:
num, who, amt, _ = c["invoices"][0]
body = f"""
UX note: Stripe Checkout (hosted). Email add-on delivered this link.
Invoice {num} · {who}
Pay {c['brand_html']}
{amt}
Secure payment via Stripe. Card details never touch our servers in this mock.
Pay with card
Back to site
"""
return f"""
Pay invoice · {c['brand_html']}
{banner(c)}
{body}
{CREDIT_HTML}
"""
def page_public_404(c: dict) -> str:
body = f"""
404
Page not found
That URL doesn't exist on {c['brand_html']}.
Go home
Contact
"""
return public_shell(c, "404", body, "Branded error page with path home or to contact.")
def page_under_construction(c: dict) -> str:
body = f"""
"""
return public_shell(c, "Under construction", body, "Pre-launch / maintenance holding page with notify-me email.")
PAGES = {
"portal-login.html": page_login,
"portal-dashboard.html": page_dashboard,
"portal-leads.html": page_leads,
"portal-lead-detail.html": page_lead_detail,
"portal-analytics.html": page_analytics,
"portal-contacts.html": page_contacts,
"portal-contacts-import.html": page_contacts_import,
"portal-campaign.html": page_campaign,
"portal-campaign-report.html": page_campaign_report,
"portal-postcard-designer.html": page_postcard,
"portal-direct-mail.html": page_direct_mail,
"portal-blog.html": page_blog_portal,
"portal-blog-edit.html": page_blog_edit,
"portal-invoices.html": page_invoices,
"portal-invoice-create.html": page_invoice_create,
"portal-invoice-detail.html": page_invoice_detail,
"portal-social-accounts.html": page_social_accounts,
"portal-social.html": page_social,
"portal-social-calendar.html": page_social_calendar,
"portal-social-ai.html": page_social_ai,
"public-blog.html": page_public_blog,
"public-blog-post.html": page_public_blog_post,
"public-unsubscribe.html": page_public_unsubscribe,
"public-pay.html": page_public_pay,
"public-404.html": page_public_404,
"public-under-construction.html": page_under_construction,
}
def main() -> None:
for slug, cfg in CLIENTS.items():
out = ROOT / "proposals" / slug / "mockup"
out.mkdir(parents=True, exist_ok=True)
for name, fn in PAGES.items():
path = out / name
path.write_text(fn(cfg), encoding="utf-8")
print("wrote", path.relative_to(ROOT))
if __name__ == "__main__":
main()