# Backlog

Source of truth for the product wishlist. The admin dashboard at
`/admin-dashboard` renders this file. When a PR closes a wishlist item,
move that item to the "🚀 Shipped" section with `**PR:** #<N>` (or
`**Commit:** <hash>` for direct-to-main work) in the same PR. Prune
Shipped items older than 90 days.

Each item: `## Title`, then `**Why:**`, `**Sketch:**`, `**Size:**`
(and `**PR:**` / `**Commit:**` once shipped).

---

## 🔨 In progress

<!-- items currently being worked, with branch or PR# if open -->

_Nothing in flight._

---

## 🎯 Up next

<!-- top 1–3 items queued for the next sprint -->

## Multi-fence-style options in estimator

**Why:** Jake's domain expertise from years in fence sales — homeowners
convert better when shown 2-3 fence-style options side-by-side
(comparison shopping) vs. a single recommendation. Currently the
estimator only supports a single `selectedFence` string; this is about
price-shopping ("show me wood vs. aluminum vs. chain link"), not about
modeling real mixed-style yards.

**Sketch:**
- Tier 1 (preferred): multi-select up to 3 fence types, auto-split the
  total footage evenly, render a per-type row in the estimate output.
  ~4 to 6 hours.
- Tier 2: per-type ft allocation via sliders/inputs. ~1 to 2 days.
- Tier 3: per-section assignment with a color-coded canvas. ~2 to 3 days.
- Per Jake's clarification, lean Tier 1 — the goal is "give people
  options to choose from," not accurately model mixed fences.
- Touches: `selectedFence` (single string) → `selectedFences` (array
  of `{key, ft}`), `selectFence()` and tile click handlers (toggle
  in/out instead of swap), `generateEstimate()` and pricing math (loop
  over selected), estimate output display (per-type breakdown), lead
  form / GHL webhook payload (array of types).
- Per-customer config flag possible: only multi-product accounts get
  multi-select; 2-product accounts keep single-pick UI.

**Size:** Tier 1 = 4–6 hours; Tier 3 = 2–3 days.

---

## 📋 Wishlist

## Record demo Loom for AI SMS sales agent

**Why:** The AI SMS sales agent (Make-based v3 architecture, in build as of 2026-05-28) was originally planned to drop a Loom video link into outbound SMS conversations as a soft-touch demo asset alongside the booking link and prescription URL. Deferred from initial launch — Jake's not recording a Loom pre-go-live. Without the video the agent flow leans harder on the prescription URL + booking link, which is workable but loses a conversion path for SMS recipients who'd rather watch than book or click through. Worth adding back once the agent is live and we can measure whether SMS replies asking for "show me" make it worth the production effort.

**Sketch:**
- Record a 2 to 4 minute Loom showing: homeowner lands on `/e/<slug>`, draws a fence on a property, sees a real price, contractor receives the lead. Lean concrete (real customer site), not abstract.
- Host the Loom on Jake's Loom account; capture the public share URL.
- Add `loom_link` to the Make scenario's link bank alongside `prescription_url` and `booking_link`.
- Update SMS templates that currently text-only-fallback to insert the video link in 1-2 high-fit moments (e.g., after the recipient asks "what does it look like" or after a booking decline).
- Consider a fallback: if the Loom 404s in 6 months, the agent should degrade gracefully to text + booking link.

**Size:** 1 hour to record + edit + host. Maybe 30 minutes on the Make-scenario side to thread it into the templates.

## Auto-detect survey scale on upload

**Why:** Today when a homeowner uploads a survey to measure, they have to manually mark two points and type the real-world distance between them. Friction step that confuses non-technical users and is easy to do wrong (mark the wrong segment, mistype the distance, leading to a silently wrong estimate). Most surveys already have the scale on them somewhere — printed ratio ("1" = 30'"), a scale bar graphic, or dimensional callouts on property lines.

**Sketch:**
- On survey upload, send the image to a vision model (Claude vision) with a structured prompt: "Find the scale bar, ratio statement, or any dimension label. Return pixel coordinates of the segment endpoints and the real-world distance it represents." JSON out.
- Compute pixels-per-foot from that, auto-fill the scale fields.
- Confirmation UX is non-negotiable: overlay the detected segment on the image with a "We measured this section at X ft, looks right?" one-click confirm. Wrong silent scale = whole estimate off by a fixed multiplier.
- Fallback to existing manual flow if confidence is low or no scale found.
- Edge case: phone photos of paper surveys at an angle have perspective distortion that breaks pixel-based scaling. Best accuracy on PDFs / flat scans.

**Size:** 2 to 3 days for MVP that pre-fills correctly on ~70-80% of clean surveys.

## Per-product per-width gate price overrides + editor

**Why:** Today (PR #75 + linear-scaling follow-up) multi-width gate prices are derived at runtime by scaling each product's anchor price linearly per foot. That covers the typical case where 5 ft costs 25% more than 4 ft. Customers will eventually want explicit per-cell overrides — manufacturer step pricing, specialty hardware tiers, or non-linear breaks (e.g., 10 ft drive jumps disproportionately because of a wider track kit). Right now there's no way for a contractor to say "5 ft black vinyl-coated chain link is $400, not the $465 the formula gives."

**Sketch:**
- Schema: add `gates_by_width` on each product (mirror of `gates_by_height`, keyed by width → `{ "4": {single, double}, "5": {single, double}, ... }`).
- Resolver: new Step 0 in `gatePriceFor` checks `prod.gates_by_width[width][type]` before falling through to scaled `gates_by_height`. Existing scaling stays as the fallback.
- Editor: extend the per-product "Custom gate pricing" disclosure in `NEMProductsEditor` with a second table keyed by width (rows = enabled widths, cols = single/double). Blank cell = use scaled default.
- Customer-setup wizard: defer (most customers will land via the runtime-scaling default and only need overrides later via /my/updates).

**Size:** 1 day.

## Per-customer pixel trackers in estimator header

**Why:** Customers running their own paid ads need their FB Pixel / GA4 / GTM / TikTok Pixel firing on the homeowner-facing estimator pages so their attribution works. Today we only fire NEM's tracking; customer ads are flying blind on conversions from `/e/<slug>`.

**Sketch:**
- Per-customer config columns (or `pricing_json` extension): `fb_pixel_id`, `ga4_measurement_id`, `gtm_container_id`, `tiktok_pixel_id`. Whitelisted so customers paste only the ID, not arbitrary `<script>` — eliminates XSS risk.
- Template injects each vendor's standard snippet into `estimate.html` at runtime when the corresponding ID is set.
- Surface a "Tracking" tab on /my/updates with one input per pixel, plus a docs link to "where do I find my Pixel ID?" for each vendor.
- Fire standard events: PageView on load, Lead on estimate submit (with lead value if available).

**Size:** 1 day. Mostly the per-vendor snippet research + the /my/updates UI.

## Per-customer Microsoft Clarity drill-down on /admin

**Why:** Right now Clarity gives us aggregate behavior across all customers. To diagnose churn or onboarding friction per customer, we need to filter Clarity by their `/e/<slug>` or `/my/updates` traffic in one click from /admin.

**Sketch:**
- Confirm Clarity's filtered-dashboard URL syntax (path filter + time range). Likely a deep-link like `https://clarity.microsoft.com/projects/view/<project-id>/dashboard?date=Last%2030%20days&Path=%2Fe%2F<slug>`.
- Add a "View in Clarity" button to each customer row / detail panel on /admin and to the per-customer activity view on /admin-dashboard (already shipped).
- Two buttons per customer: homeowner traffic (`/e/<slug>`) and customer-side traffic (`/my/updates` filtered by user).
- No API integration needed — pure deep-link.

**Size:** 2 hours. Mostly verifying the URL filter syntax works.

## Compose-and-send customer emails from /admin

**Why:** Manual customer outreach today means opening Gmail, composing from scratch, finding the customer's email, and forgetting which email was sent when. A compose pane on /admin scoped to one customer speeds up support / save-flow outreach and creates an audit trail.

**Sketch:**
- "Email customer" button on each customer row / detail panel on /admin.
- Compose UI: subject + body, From `jake@nextestimateagency.com`, plain text or simple rich text. Templates dropdown (e.g., "follow-up after first login", "post-trial check-in").
- POST to a new Netlify function `admin-send-email.js` that calls Resend with the customer's email as recipient and writes a row to a new `customer_emails_sent` audit table (customer_id, subject, body, sent_at, sent_by).
- Surface "Last contacted: N days ago" on the customer row as a derived field from this table.
- Follow-up: reply tracking via Resend webhooks → thread on /admin.

**Size:** half-day for v1 (compose + send + audit row). Reply tracking is a follow-up.

## Drop the post-setup scheduling-call step

**Why:** Post-setup pages and emails currently ask the customer to
schedule a call with Jake. That made sense when Jake had to manually
configure their estimator. Wave 5's self-serve editor removes that
need; the only manual step that remains is the GHL webhook setup,
which Jake can do without a sync call.

**Sketch:**
- Audit `thank-you-fence.html`, post-customer-setup pages, and the
  onboarding email sequence for "schedule a call" copy and Calendly
  links. Replace with a self-serve walkthrough video or written
  checklist.
- For GHL: confirm Jake gets an internal alert when a new customer
  needs the webhook configured. If not, add one.

**Size:** 2 to 3 hours, mostly content.

## Re-evaluate pre-filled defaults on setup and /my/updates

**Why:** Both `customer-setup.html` and `/my/updates` ship with
defaults inherited from the seed template. Some are appropriate
(industry-standard warranty badges); some may be misleading or
encourage customers to keep generic copy. Worth a deliberate audit
on what should be empty vs. pre-filled vs. example-as-placeholder.

**Sketch:**
- Walk every form field. Categorize each default as:
  (a) safe pre-fill,
  (b) placeholder hint not pre-filled,
  (c) actively misleading - replace with empty or smart default.
- Some defaults should become smart-defaulted from the customer's
  website (overlaps with Auto-pull branding from URL below).

**Size:** half-day.

## Buyer's-remorse / churn-reduction research and implementation

**Why:** Open question on what specifically to do, but the goal is
to reduce post-purchase regret in the 0-30 day window and lock in
early engagement. The drip-email work shipped (days 1-6) addresses
part of this; the remaining piece is a deliberate research pass on
retention literature to identify the next 3-5 changes to ship.

**Sketch:**
- Research pass on SaaS retention literature for the 0-30-day window.
  Common patterns: explicit "you're now a member" reinforcement
  emails, quick-win activation tasks, money-back-guarantee
  positioning, founder-personal-touch outreach.
- Output: a short concrete plan of 3-5 specific changes to ship.
- Likely changes touch the post-Stripe-checkout flow, the first-week
  email cadence (extending the existing drip), and possibly a "your
  first 7 days" checklist on /my/updates that nudges activation.

**Size:** half-day research, then variable implementation.

## Auto-pull branding from a customer's website URL

**Why:** Jake flagged this as "needed soon" on 2026-04-25. High-leverage
for trial conversion: customer pastes their site URL and the form
prefills logo + brand color, dropping setup time from ~10 minutes to
under 2.

**Sketch:**
- Customer pastes their website URL on the setup form (already
  collected as `website_url` on `customer-setup.html`).
- Netlify function fetches the URL server-side and parses:
  - logo: `<meta property="og:image">`, then `<link rel="icon">`,
    then the first `<img>` near the header
  - brand color: dominant non-grey color from the logo (k-means or
    similar), with a fallback to the first hex value in inline
    style attributes near the header
- Returns JSON to the form, which prefills `logo_url` and
  `brand_color`. Customer can override either before submit.
- The same endpoint can also populate the /my/updates branding tab
  (Wave 5 self-edit, shipped) when an existing customer hits an
  "auto-fill from my website" button later.

**Size:** 1 day.

## Service area polygon → competitor research email

**Why:** Bonus value during the trial. Customer draws their service
area, a background job runs deep research on competitors in that area,
and they get an email summarizing what they're up against.

**Sketch:**
- When the polygon save RPC succeeds (Wave 3 Phase A path), enqueue
  a job. Storage option: a `competitor_research_jobs` table with a
  `status` enum, processed by a scheduled Netlify function.
- Job payload: polygon centroid + bounding radius, customer email,
  business name, slug.
- Worker calls Maps Places API for fence contractors in the area,
  optionally augments with web search.
- LLM (Claude API) summarizes top 3-5 competitors: pricing signals,
  review themes, service gaps the customer could exploit.
- Resend email to the customer with the summary, signed off by Jake.

**Size:** 2 to 3 days, mostly the LLM/research integration.

---

## 💤 Parked

_Nothing parked._

---

## 🚀 Shipped (last 90 days)

<!-- newest first; prune items older than 90 days -->

## Quiz funnel: GHL integration + resume URL + paid guard

`/funnel` quiz-to-prescription flow is now wired end-to-end into Jake's CRM. Stripe paid handler hardened against duplicate fires (idempotency guard on `maybeMarkFunnelLeadPaid`); GHL dispatch refactored from one branched URL to three event-specific URLs (`FUNNEL_GHL_WEBHOOK_LEAD/PAID/ABANDONED`) since GHL inbound webhooks on Jake's plan are 1:1 with workflows; quiz answers ship as flat `q1..q7` top-level fields for direct GHL Update-Contact mapping; new public `get-funnel-lead` GET endpoint + `?r=<contact_id>` hydration in `funnel.html` lets the AI appointment setter drop personalized resume links into outbound SMS/email without orphaning the original contact_id. End-to-end smoke-tested against production: LEAD POST + resume URL + direct-fire PAID/ABANDONED webhooks + Supabase status transitions all green.

**PR:** #85

## apply_customer_setup merges instead of replaces branding/pricing

The `apply_customer_setup` RPC now shallow-merges `branding_json` and `pricing_json` (`existing || incoming`) instead of full-replacing them, mirroring the pattern already used by `update_my_customer_data`. Closes the latent clobber bug where every field outside the slim 5-stage wizard schema (gate_widths_*, gate_label_*, company_phones, financing_url, products list, etc.) would be wiped on resubmit. Verified end-to-end against AAGS: all 21 branding keys + 6 pricing keys survived a sentinel wizard call, sentinels overrode existing values, then restored the originals via a second RPC call.

**PR:** #76

## Multi-width gate offerings (contractor enable + homeowner pick)

Contractors can now enable any combination of single + double gate widths via `branding_json.gate_widths_single / _double` (e.g. `[3, 4, 6]` singles, `[6, 8, 10]` doubles). When 2+ widths are enabled, tapping the type button reveals a chip row of widths; homeowner picks one to enter placement. Single-width customers (every legacy customer + AAGS today) see no UI change — the button still reads "+ Single gate (4 ft)" and skips the chip step. Lead payload ships a per-width `widths` array so GHL workflows can see exactly which sizes the homeowner picked. Per-width prices via `gate_prices_single / _double` override the flat `gate_cost / gate_double` defaults. `gate_label_single / _double` from PR #66 stays in place; deferred to a separate cleanup PR once AAGS data migrates to the new fields.

**PR:** #75

## Product photo + spec bullets on lead PDF

Selected fence type's photo and on-page spec bullets now render on a dedicated page 2 of both the homeowner-download and lead-email PDFs, so the contractor reads specs and sees the actual fence alongside the estimate. Triggered by AAGS feedback ("the estimate you sent did not include the image").

**PR:** #64

## Split-test infra + /admin-split-tests dashboard

Variant-tracking RPC + admin scoreboard so /lp/v3 changes ship as A/B tests instead of direct edits to the canonical file.

**PR:** #37

## Estimate map: commit polyline before entering gate mode

Bug fix where the draw polyline would not persist when transitioning into gate-placement mode.

**PR:** #33

## Reddit Pixel + Conversions API

Server + client tracking for paid Reddit traffic, matched to Stripe events.

**PR:** #32

## /lp/v3 demo walkthrough on first scenario pick

4-step interactive walkthrough fires the first time a homeowner picks a scenario, then never again.

**PR:** #31

## /lp/v3 trial modal + dual-webhook submit

Trial CTAs now route through a lead modal that fires both the lead webhook and an InitiateCheckout event.

**PR:** #27

## /lp/v3 book-a-call flow

One-piece-per-card capture (first name → email → phone) using Typeform best practices; pinned progress bar.

**PR:** #26

## Field Manual visual overhaul (Phase 1) on the estimator

Typography, contrast, and tap-target refresh across estimate.html.

**PR:** #24

## /lp/v2 drawing UX overhaul

Tap-coach finger animation, modal clip fix, touch-only overlay corrections, dead-click cleanup in the property picker.

**PR:** #22

## /lp/v2 cold-traffic landing page

WebP photos, mobile-first layout, real-mode photos, pain-card fix; deployed at /lp/v2.

**PR:** #17

## Customer journey overhaul: trial-aware thank-you + dashboard tour + lead self-hide

Trial-state-aware thank-you pages, an in-product dashboard tour, and a way for customers to hide test/junk leads from /my/updates.

**PR:** #14

## Field Manual visual refresh on customer-setup + /my/updates

Visual upgrade aligning customer-facing dashboards with the new design system.

**PR:** #11

## Auto-resize customer-uploaded images on upload

Client-side canvas resize on every customer-upload site (branding logo, per-product photos in both customer-setup and my-updates), with a defensive shim if the resize script fails to load.

**Commit:** e77f7b6

## Estimate PDF generation + lead-email attachment

Server-side branded PDF for every lead, attached to the customer email; "Download PDF" button on /my/updates past leads; three real bugs fixed post-rollout.

**Commit:** 77f4a16

## Customer-setup 5-stage wizard

Rebuilt the one-long-form intake into a multi-step wizard with localStorage persistence per stage and tailored instructions per stage.

**Commit:** bd5f254

## Onboarding drip emails (days 1-6 of trial)

Trial-only drip with rebuilt voice + structure per the retention playbook; first-name personalization, yearly-buyer welcome variant, preview-inbox mode for QA.

**Commit:** ed151ca

## Support-ticket lifecycle emails

Acknowledge + resolution emails on /my/updates ticket status changes; resolution note inlined.

**Commit:** d49d735

## Trial end-date column on /admin

Stripe webhook writes `trial_end_at` to customers; visible per-customer column, color-coded if within 3 days.

**Commit:** 49e2e1e

## Admin per-customer activity view

Last-30-day drill-down on /admin: sign-ins, customer edits with editor role, and lead counts per customer.

**Commit:** a51c16b

## Self-serve trial signup with Stripe + Resend (Wave 3 Phase D)

Customer can sign up for a 7-day trial without Jake's involvement; Stripe creates the subscription, Resend sends the welcome.

**Commit:** 74d5cd2
