#!/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}
All screens · Pricing
""" 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)}

{title}

Signed in as {c['user']} · Sign out
{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)}
{c['brand_html']}
{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"""
New leads
7
+3 this week
Open pipeline
18
4 contacted
Mailing list
412
8 opted out (30d)
Open invoices
2
Next due Fri

Recent leads

View all
{rows}
NameSourceStatus

Upcoming outreach

Compose

{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"""

Lead inbox

{rows}
NameSource / UTMNeedStatus
""" 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']}

UTM attribution

Source{s.split(' / ')[0]}
Mediumcpc
Campaign{s.split(' / ')[-1]}
Landing/{c['contact'].split('#')[0] if '#' in c['contact'] else 'contact.html'}
First touchMar 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 source
Google
18 leads
Top campaign
spring-promo
11 leads

Leads by week

{bars}

By source / campaign

SourceCampaignLeadsWon
googleemergency123
facebookspring-promo112
direct(none)94
postcardmarch-mailer61
yelpprofile41
""" 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 = """

Mailing list

Import New campaign
NameEmailEmailSMSPostcard
Jordan Leejordan@example.com
Sam Ortizsam@example.com
Avery Chenavery@example.com
Morgan Diazmorgan@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"""
Email SMS Postcard

Compose

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"""
Delivered
178
of 186
Opens
64%
114 opens
Clicks
18%
32 clicks
Unsubscribes
2
1 bounce

{c['campaign']}

Sent
RecipientOpenedClickedStatus
jordan@example.comYesYesEngaged
sam@example.comYesOpened
avery@example.comDelivered
bad@example.comBounced
""" return shell(c, "Email engagement", "report", body, "Opens, clicks, bounces, unsubscribes per campaign.") def page_postcard(c: dict) -> str: body = f"""

Size

Copy

Use in campaign

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"""

Direct mail campaign

Edit design

Print + postage pass-through billed separately.

""" 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"""

Blog posts

New post
{rows}
TitleDateStatus
""" 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

""" 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"""

Invoices

Create invoice
{rows}
NumberCustomerAmountStatus
""" 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"""

Create invoice

Stripe processing fees billed separately. Email notification requires Email & SMS add-on.

Create & send
""" 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

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 = """
""" return shell(c, "Social accounts", "accounts", body, "OAuth connect / reconnect when tokens expire; health + disconnect.") def page_social(c: dict) -> str: body = f"""

Compose

AI assist

Facebook preview

""" 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"""

March 2026

New post
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('&','&')}

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"""

Email preferences

Manage how {c['brand_html']} contacts you.

Unsubscribe from all marketing

""" 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.

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"""

Under construction

We're finishing something new for {c['brand_html']}. Leave your email and we'll notify you.

""" 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()