← back to Ventura Claw Leads
yolo tick 6: /admin/leads.csv export + mobile audit clean
81d7dec43d64633e5fcc945e807c9a477ad84ca1 · 2026-05-06 17:43:45 -0700 · Steve Abrams
CSV EXPORT
- routes/admin.js: GET /admin/leads.csv — auth-gated (requireBusiness), pulls
up to 5000 rows for the current business, RFC 4180 quoting (CRLF,
'' escapes), Content-Disposition: attachment with date-stamped filename.
All fields included (name/email/phone/zip/source/message/delivered_at/
delivery_email/delivery_msg_id/bill_amount_cents) so businesses can
reconcile against their CRM.
- views/admin/leads.ejs: 'Export CSV ↓' button under the lede.
- Verified e2e: unauth → 302 to login. With session → 200 text/csv with
embedded-comma message correctly quoted.
MOBILE AUDIT (qa-vcl-overflow.mjs in session-debrief skill)
- Playwright headless at 380×720 + 768×720 across 10 routes.
- Result: 0 real horizontal overflows. The flagged leaflet-tile elements
on /map are inside an overflow:hidden tile-pane (same false-positive
pattern as NPH; not actionable).
- VCL was scaffolded mobile-first by lifting NPH's 1024 + 768 breakpoints
into public.css from day one — audit confirms no regression.
46/46 tests still pass. CSV download verified on prod.
Files touched
M routes/admin.jsM views/admin/leads.ejs
Diff
commit 81d7dec43d64633e5fcc945e807c9a477ad84ca1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 17:43:45 2026 -0700
yolo tick 6: /admin/leads.csv export + mobile audit clean
CSV EXPORT
- routes/admin.js: GET /admin/leads.csv — auth-gated (requireBusiness), pulls
up to 5000 rows for the current business, RFC 4180 quoting (CRLF,
'' escapes), Content-Disposition: attachment with date-stamped filename.
All fields included (name/email/phone/zip/source/message/delivered_at/
delivery_email/delivery_msg_id/bill_amount_cents) so businesses can
reconcile against their CRM.
- views/admin/leads.ejs: 'Export CSV ↓' button under the lede.
- Verified e2e: unauth → 302 to login. With session → 200 text/csv with
embedded-comma message correctly quoted.
MOBILE AUDIT (qa-vcl-overflow.mjs in session-debrief skill)
- Playwright headless at 380×720 + 768×720 across 10 routes.
- Result: 0 real horizontal overflows. The flagged leaflet-tile elements
on /map are inside an overflow:hidden tile-pane (same false-positive
pattern as NPH; not actionable).
- VCL was scaffolded mobile-first by lifting NPH's 1024 + 768 breakpoints
into public.css from day one — audit confirms no regression.
46/46 tests still pass. CSV download verified on prod.
---
routes/admin.js | 34 ++++++++++++++++++++++++++++++++++
views/admin/leads.ejs | 1 +
2 files changed, 35 insertions(+)
diff --git a/routes/admin.js b/routes/admin.js
index f9399fd..465a3d5 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -92,6 +92,40 @@ router.get('/leads', async (req, res, next) => {
} catch (err) { next(err); }
});
+// Lead inbox CSV export. Useful when a business runs reporting in Excel /
+// Sheets, or when paid-tier subscribers reconcile leads against their CRM.
+// All fields included — name, email, phone, ZIP, message, timestamps, delivery
+// state, billing amount. RFC 4180 quoting (CRLF, "" escapes inner quotes).
+router.get('/leads.csv', async (req, res, next) => {
+ try {
+ const bizId = res.locals.currentBusiness.id;
+ const leads = await db.many(`
+ SELECT id, consumer_name, consumer_email, consumer_phone, message, zip, source,
+ created_at, delivered_at, delivery_email, delivery_msg_id, bill_amount_cents
+ FROM business_interest
+ WHERE business_id = $1
+ ORDER BY created_at DESC
+ LIMIT 5000
+ `, [bizId]);
+ function q(v) {
+ if (v == null) return '';
+ const s = (v instanceof Date) ? v.toISOString() : String(v);
+ if (/[",\r\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
+ return s;
+ }
+ const headers = ['id','created_at','consumer_name','consumer_email','consumer_phone','zip','source','message','delivered_at','delivery_email','delivery_msg_id','bill_amount_cents'];
+ let csv = headers.join(',') + '\r\n';
+ for (const l of leads) {
+ csv += headers.map(h => q(l[h])).join(',') + '\r\n';
+ }
+ const dateStamp = new Date().toISOString().slice(0, 10);
+ res.set('Content-Type', 'text/csv; charset=utf-8');
+ res.set('Content-Disposition', `attachment; filename="vcl-leads-${dateStamp}.csv"`);
+ res.set('Cache-Control', 'no-store');
+ res.send(csv);
+ } catch (err) { next(err); }
+});
+
// ─── Billing ───────────────────────────────────────────────────────────
router.get('/billing', (req, res) => {
res.render('admin/billing', {
diff --git a/views/admin/leads.ejs b/views/admin/leads.ejs
index bdcd2be..a495f06 100644
--- a/views/admin/leads.ejs
+++ b/views/admin/leads.ejs
@@ -5,6 +5,7 @@
<p class="kicker">Leads</p>
<h1 class="display-sm">Inbox</h1>
<p class="lede">Every customer message routed to <%= currentBusiness.business_name %>. Replies via your email client go straight to the customer.</p>
+ <p style="margin-top:8px"><a href="/admin/leads.csv" class="btn btn-ghost btn-sm" download>Export CSV ↓</a></p>
<% if (leads.length === 0) { %>
<p class="muted" style="margin-top:32px">No leads yet. Make sure your <a href="/admin/profile">profile</a> is filled out — the more complete the listing, the more inbound.</p>
← 20a0eb6 yolo tick 5: sort + density slider on /find (per global stan
·
back to Ventura Claw Leads
·
yolo tick 7: bug fix — drop DEFAULT now() from business_inte d4d9dd2 →