← back to NationalPaperHangers

HANDOFF.md

140 lines

# Handoff — NationalPaperHangers.com

**Built:** 2026-05-05 (Opus 4.7 session, paused at 93% weekly cap)
**Status:** scaffold complete. Boots, but has not yet been smoke-tested live.
**Next operator:** local Ollama is fine for the smoke-test loop below.

## What you're picking up

A working luxury wallcovering installer marketplace + scheduling app at `~/Projects/NationalPaperHangers/`. Installers pay (Stripe subscription, mock-mode until the key lands) for a calendar that lets consumers self-book installs. Schema, server, routes, views, CSS, JS all written. Stripe runs in mock mode (toggles tier locally on checkout). Email runs through George with auto-fallback to console logs.

## Smoke-test (run these in order)

```bash
cd ~/Projects/NationalPaperHangers
cp .env.example .env

# 1. Install
npm install

# 2. Database
createdb national_paper_hangers
psql national_paper_hangers < db/schema.sql
psql national_paper_hangers < db/seed.sql

# 3. (Optional) Regenerate the demo password hash. The seed file ships with
#    a placeholder bcrypt hash. If login fails on demo accounts, run:
node -e "console.log(require('bcrypt').hashSync('demo1234', 10))"
# then UPDATE installers SET password_hash='<new hash>' WHERE email LIKE 'demo+%';

# 4. Boot
npm run dev
# → http://localhost:9765
```

## Manual test plan

Public side:
- [ ] `/` — homepage renders, featured grid populated from seed
- [ ] `/find` — grid-search works (try `?material=hand_painted`, `?segment=hospitality`, `?state=CA`)
- [ ] `/installer/atelier-bond-nyc` — profile + portfolio + sidebar render
- [ ] `/installer/paperworks-collective-la/book` — calendar loads, slot click selects, form submit creates booking → redirects to `/bookings/:uuid`
- [ ] Dark/light toggle in header (☼/☾) toggles + persists across reloads

Auth:
- [ ] `/signup` — creates new installer at tier=basic, status=pending
- [ ] `/login` with seed `demo+bond@example.com` / `demo1234` (after fixing hash above)
- [ ] `/admin` — dashboard shows stats + welcome callout

Admin:
- [ ] `/admin/calendar` — gated on paid tier; for seed Signature accounts (Bond, Paperworks) it loads
- [ ] Add an availability window; reload; persists
- [ ] Add a time-off block; reload; persists
- [ ] `/admin/bookings` — confirmed seed booking shows for Paperworks
- [ ] `/admin/profile` — edit form saves
- [ ] `/admin/billing` — mock checkout flips tier locally

Slot calculator:
- [ ] After Bond's seeded time-off (days 14–17), `/api/installers/atelier-bond-nyc/slots?from=...&to=...` returns no slots in that window
- [ ] After Paperworks' seeded confirmed booking (~7 days out, 10–11am PT), the matching slot is excluded

## Bootstrap data (option C — scrape-lite + self-claim)

Per `DATA_POLICY.md` v1.0:
- Scraper at `scripts/scrape-wia.js` collects ONLY: business name, city/state/country, public website, public bio, accreditations, IG handle. **Never** phone/email/street address/owner names.
- Records land with `claim_status='unclaimed'`, `status='pending'`. Visible on `/find` with an "Unclaimed listing" badge + "Claim this listing" CTA.
- Studios self-claim via `/installer/:slug/claim` — must use email on the studio's own website domain. One-shot 24h token. Verified-email → `/claim/complete` → set password → `claim_status='claimed'`, redirect to `/admin`.
- 3 demo unclaimed listings already in the DB to exercise the UI: bromfield-walls (Boston), marquee-finishes (Atlanta), ridgeway-paper (Seattle).

**To populate real data:**
1. Open `https://www.wallcoveringinstallers.org/` in a browser
2. Use the public locator, copy WIA profile URLs into `scripts/wia-profile-urls.txt`
3. Dry-run: `node scripts/scrape-wia.js`
4. Commit: `node scripts/scrape-wia.js --commit --enrich-ig --max=50`
5. Crawl is rate-limited (3s/req, 200/day cap, robots.txt honored, audited in `scrape_log` table)

## Outstanding work (priority order)

1. **DNS for nationalpaperhangers.com** — Cloudflare zone, A → Kamatera, LE SSL, info@ MX/SPF/DKIM/DMARC. Use the `domain-name-agent` subagent or `/domain-setup` skill (Phase A).
2. **Stripe live keys** — paste into the conversation; the global standing rule will route them through `secrets-manager` to `~/Projects/NationalPaperHangers/.env`. Then create products/prices in Stripe and set `STRIPE_PRICE_PRO_MONTH` etc.
3. **George email integration** — verify `info@nationalpaperhangers.com` is configured in George (account=info). Already wired in `lib/email.js`.
4. **3-agent QA loop** — per `feedback_test_every_site_completion.md`: click-through QA agent + graphic-design agent + UX critic before declaring "done".
5. **CAN-SPAM + DNC compliance gate** — before any installer or consumer email blast, run through `comms-compliance` subagent.
6. **Image assets** — portfolio currently uses CSS placeholders (brand initials in serif). Replace with real photos OR stick with placeholders forever (no stock images per `feedback_no_stock_images.md`).
7. **Verification workflow UI** — admin upload form for COI / W-9 / license docs. Schema ready (`insurance_on_file`, `insurance_expires`, `license_*`); UI not yet built.
8. **pm2 + launchd** — `pm2 start ecosystem.config.js && pm2 save`. Add a launchd watchdog `com.steve.nph-hawk` mirroring the pd-hawk pattern.
9. **Production .env** — set `NODE_ENV=production`, real `SESSION_SECRET` (32+ bytes), real `PUBLIC_URL=https://nationalpaperhangers.com`.
10. **Reviews moderation queue** — schema exists (`installer_reviews.published`); admin UI to publish/reject is not built.

## Architecture quick-reference

| File | Role |
|---|---|
| `PROCESS.md` | Build plan + standing-rule compliance |
| `db/schema.sql` | 10 tables incl. `installers`, `installer_availability`, `installer_time_off`, `bookings`, `installer_reviews`, `consumer_leads`, `lead_offers`, `subscription_events`, `installer_portfolio`, `session` |
| `db/seed.sql` | 5 installers, weekly availability, 1 time-off block, 1 confirmed booking |
| `server.js` | Express + sessions on Postgres + EJS + static |
| `lib/db.js` | pg pool |
| `lib/auth.js` | bcrypt + `requireInstaller` + `requirePaidTier` middleware |
| `lib/slots.js` | Available-slot calculator (luxon, intersect availability − time-off − bookings) |
| `lib/email.js` | George Gmail client + booking templates |
| `lib/stripe.js` | Subscription helpers; mocks gracefully when key missing |
| `routes/public.js` | / · /find · /installer/:slug · /installer/:slug/book · /bookings/:uuid · /about · /for-installers · /privacy · /terms |
| `routes/auth.js` | /login · /signup · /logout |
| `routes/admin.js` | /admin/* (gated) — dashboard, calendar, bookings, profile, billing |
| `routes/api.js` | /api/installers/:slug/slots · /api/installers/:slug/book · /api/admin/availability · /api/admin/time-off · /api/admin/bookings.json |
| `routes/webhooks.js` | /webhooks/stripe |
| `views/` | EJS templates, organized by section, with `partials/head` `partials/header` `partials/footer` `admin/partials/admin-header` |
| `public/css/theme.css` | CSS-vars, dark/light, type system |
| `public/css/public.css` | Marketplace pages |
| `public/css/admin.css` | Dashboard pages |
| `public/js/theme-toggle.js` | Light/dark toggle |
| `public/js/calendar-consumer.js` | Slot picker on /book |
| `public/js/calendar-installer.js` | Availability + time-off + upcoming on /admin/calendar |

## Standing-rule compliance status

| Rule | Status |
|---|---|
| Local install (`~/Projects/`) | ✅ |
| Process markdown first | ✅ `PROCESS.md` |
| Dark/light toggle | ✅ |
| Grid-search | ✅ `/find` |
| Own info@ email | ⚠ Code wired; DNS + George config not yet provisioned |
| Standalone PG | ✅ `national_paper_hangers` (NOT in dw_unified) |
| Default .com | ✅ |
| pm2 + launchd | ⚠ ecosystem.config.js written; not yet started |
| No DNS touch on protected list | ✅ |
| 3-agent QA loop before "done" | ⏳ pending phase 1 |
| CAN-SPAM + DNC compliance | ⏳ pending phase 1 |
| No stock images | ✅ Placeholder initials only |

## Known gaps

- **No tests written.** Add at least a slot-calculator unit test before launch.
- **No rate-limiting on public booking API.** Add `express-rate-limit` for `/api/installers/:slug/book` before going live.
- **Booking does not yet handle multi-day installs.** Schema supports it (`scheduled_start`/`scheduled_end` are timestamptz), but the consumer UI only lets you pick a single 60-min slot.
- **No installer-side reschedule UI.** Confirm/decline/complete only.
- **No consumer-side cancel link.** The booking detail page has copy directing them to email; build a self-serve cancel link before launch.
- **No verification ops queue.** New signups land at status=pending and need a manual UPDATE to go active.