[object Object]

← back to Quadrille Showroom

Scope the sample-request submit endpoint (age rides along)

cc9d428cfff929a2a1eba2351ab07d8f5c4953c8 · 2026-07-02 07:44:28 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit cc9d428cfff929a2a1eba2351ab07d8f5c4953c8
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 2 07:44:28 2026 -0700

    Scope the sample-request submit endpoint (age rides along)
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 SAMPLE-REQUEST-SPEC.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 111 insertions(+)

diff --git a/SAMPLE-REQUEST-SPEC.md b/SAMPLE-REQUEST-SPEC.md
new file mode 100644
index 0000000..24c7d99
--- /dev/null
+++ b/SAMPLE-REQUEST-SPEC.md
@@ -0,0 +1,111 @@
+# Sample-Request Submit — Scope (v1)
+
+**Status:** SCOPED — not yet built. Some pieces are Steve-gated (marked 🔒).
+**Origin:** the Sample Tray captures products + optional age but has **no submit** —
+nothing is ever sent anywhere. Age (`visitorAge`, `showroom.js:57`) is localStorage-only.
+This spec adds the real sample-request submission; age rides along with it.
+
+**Context that shapes the design:**
+- No database — everything is file-backed JSON (`showroom-products.json`, `thumb-cache/`).
+- Local-only tool — localhost bypasses auth (`server.js:48-53`, DTD verdict C). Kiosk
+  capture, not an internet lead form — but v1 collects PII (see 🔒 Consent), so treat it
+  as PII-bearing regardless.
+
+---
+
+## 1. Endpoint — `POST /api/sample-request`
+
+Express 5, `express.json()` already mounted (`server.js:45`). Add a body-size cap:
+change to `express.json({ limit: '32kb' })`.
+
+**Request body**
+```jsonc
+{
+  "samples": [ { "sku": "…", "pattern_name": "…", "color": "…" } ],  // required, 1..50
+  "age":     42,                 // optional int 0..120 or null (mirrors client clamp 3915)
+  "contact": {
+    "name":  "…",                // optional string, ≤120 chars
+    "email": "…",                // optional, RFC-lite regex, ≤160
+    "phone": "…"                 // optional, ≤40, digits/spaces/+()-
+  },
+  "notes":   "…"                 // optional free text, ≤2000 chars
+}
+```
+
+**Validation (reject 400 on fail)**
+- `samples` present, array, length 1..50; each item has a string `sku`.
+- `age` — `null` OR integer 0..120.
+- `contact.email` — if present, passes a lightweight format check.
+- Trim + length-cap every string field; strip control chars.
+- Body > 32 kb → 413 (handled by the json limit).
+
+**Response** — `{ ok:true, id, created_at }`
+- `id` = `sr_` + short sha1(created_at + samples) slice.
+- `created_at` = **server-stamped** ISO (never trust a client clock).
+
+---
+
+## 2. Storage — file **and** email
+
+### 2a. Local JSONL (always, primary)
+Append one line to `data/sample-requests.jsonl`. `mkdir -p`-safe, same pattern as
+`THUMB_CACHE` (`server.js:162-163`). Record = the validated body + `id` + `created_at`
++ `ip` (already available as `req.ip`). Append-only; never rewrite the file.
+
+### 2b. Email via George 🔒 (outward-facing — gated)
+On successful persist, fire one email to the DW sales inbox with the request summary
+(samples list, age, contact, notes, timestamp).
+- Send via the **George** Gmail tooling (`mcp__george__gmail_send` / george skill),
+  from the **steve-office** account per global rules.
+- **🔒 GATED — needs your go before it sends live:**
+  - **Recipient address?** (default guess: the DW sales / info inbox — confirm which.)
+  - Enabling an outbound send is an outward-facing action; wire it behind an env flag
+    (`SAMPLE_REQUEST_EMAIL=off` by default) so the endpoint ships capturing-to-file
+    immediately and the email switches on only when you say go.
+- **Failure isolation:** email failure must NOT fail the request — file persist is the
+  source of truth; email is best-effort, logged on error.
+
+---
+
+## 3. Front-end — tray submit flow
+
+Add to `#sample-tray` (`showroom.html:299`, wired near `updateSampleTray` ~`showroom.js:4137`):
+- Fields: **name, email, phone** (all optional), the existing **age** input, a **notes**
+  textarea.
+- A **"Request Samples"** button — disabled when `sampleTray.length === 0`.
+- On click: POST tray items + `_qh.visitorAge` + contact + notes.
+  - Success → clear tray + fields, clear `qh_visitor_age`, show inline confirmation.
+  - Failure → keep everything, show a retry message (never lose the visitor's input).
+- **🔒 Consent line** (because PII is now collected): one line under the button, e.g.
+  *"We'll use your details only to send the samples you requested."* — confirm the exact
+  wording you want; this is the minimum notice for PII capture.
+
+---
+
+## 4. Admin read-back — `GET /api/sample-requests`
+
+Behind auth; reads `sample-requests.jsonl` (newest first). Plus a small admin page
+(`/admin/sample-requests` or a section in an existing admin surface) rendering each
+request as a **card** with:
+- The samples (SKU + pattern + color chip), age, contact, notes.
+- **🕓 created date + time chip** per your standing admin-card rule —
+  `toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})`,
+  full ISO in the `title=`.
+- Newest/Oldest sort reads the same `created_at`.
+
+---
+
+## 5. Build order (what ships when)
+
+| Step | Piece | Gated? |
+|------|-------|--------|
+| 1 | `POST /api/sample-request` + JSONL persist + validation | no — build now |
+| 2 | Tray form + Request Samples button + confirmation UX | no — build now |
+| 3 | Admin `GET` + card list with date/time chip | no — build now |
+| 4 | Consent-line wording | 🔒 needs your exact copy |
+| 5 | Email via George (env-flag off by default) | 🔒 needs recipient + your go to send live |
+
+Steps 1–3 are fully buildable + verifiable (Playwright/`/5x`) without touching anything
+outward-facing. Steps 4–5 wait on your two answers (consent wording + recipient inbox).
+
+**Cost:** $0 (all local). George email, once enabled, is $0 (Gmail API).

← d6a840c chore: refactor + v1.2.2 (session close)  ·  back to Quadrille Showroom  ·  Build sample-request submit: endpoint + tray form + admin ca 38c4172 →