[object Object]

← back to Wallco Ai

wallco.ai admin: /admin/leads viewer — table of all wallco_leads + 'Most requested' chips of top 25 designs by hit count · added Leads tab to admin nav

7ffd43bb6693d593011c1a8845a3fddbe3153de4 · 2026-05-11 21:15:04 -0700 · Steve

Files touched

Diff

commit 7ffd43bb6693d593011c1a8845a3fddbe3153de4
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon May 11 21:15:04 2026 -0700

    wallco.ai admin: /admin/leads viewer — table of all wallco_leads + 'Most requested' chips of top 25 designs by hit count · added Leads tab to admin nav
---
 src/admin.js            | 55 +++++++++++++++++++++++++++++++++++++++++
 tests/debug-kb.spec.js  | 51 ++++++++++++++++++++++++++++++++++++++
 tests/debug-kb2.spec.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++
 tests/debug-kb3.spec.js | 47 +++++++++++++++++++++++++++++++++++
 4 files changed, 219 insertions(+)

diff --git a/src/admin.js b/src/admin.js
index 58bc01e..a341b45 100644
--- a/src/admin.js
+++ b/src/admin.js
@@ -151,6 +151,7 @@ function adminHeader(active) {
   const tabs = [
     ['/admin',              'Vendors'],
     ['/admin/inspiration',  'Inspiration'],
+    ['/admin/leads',        'Leads'],
     ['/admin/golive',       'Go-Live'],
     ['/',                   'View site →']
   ];
@@ -280,6 +281,60 @@ function mount(app) {
       </main>`));
   });
 
+  // ── Leads viewer: every wallco_leads row + per-design hit counts
+  app.get('/admin/leads', gate, (req, res) => {
+    const rows = JSON.parse(psql(
+      "SELECT COALESCE(json_agg(t),'[]'::json) FROM (" +
+      "SELECT id, name, email, phone, message, design_id, source, created_at " +
+      "FROM wallco_leads ORDER BY created_at DESC LIMIT 500) t;"
+    ) || '[]');
+    const byDesign = JSON.parse(psql(
+      "SELECT COALESCE(json_agg(t),'[]'::json) FROM (" +
+      "SELECT design_id, COUNT(*) AS hits FROM wallco_leads " +
+      "WHERE design_id IS NOT NULL GROUP BY design_id ORDER BY 2 DESC LIMIT 25) t;"
+    ) || '[]');
+    const total = rows.length;
+    const last7 = rows.filter(r => new Date(r.created_at) > new Date(Date.now() - 7*86400000)).length;
+    const escHtml = s => String(s||'').replace(/[<>&"']/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]));
+    const tableRows = rows.map(r => `
+      <tr>
+        <td style="font-family:ui-monospace,monospace;font-size:11px;color:var(--ink-faint)">${new Date(r.created_at).toLocaleString()}</td>
+        <td style="font-weight:500">${escHtml(r.name)||'<em style="color:var(--ink-faint)">no name</em>'}</td>
+        <td><a href="mailto:${escHtml(r.email)}" style="color:var(--ink)">${escHtml(r.email)}</a></td>
+        <td>${escHtml(r.phone)||''}</td>
+        <td>${r.design_id ? `<a href="/design/${r.design_id}" style="color:var(--gold);text-decoration:none">#${r.design_id} →</a>` : ''}</td>
+        <td style="color:var(--ink-faint);font-size:12px">${escHtml(r.source)||''}</td>
+        <td style="font-size:12px;max-width:280px">${escHtml(r.message)||''}</td>
+      </tr>`).join('');
+    const popRows = byDesign.map(b => `
+      <a href="/design/${b.design_id}" style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card-bg);border:1px solid var(--line);border-radius:999px;text-decoration:none;color:var(--ink);font-size:12px">
+        #${b.design_id}<span style="background:var(--gold);color:var(--accent);padding:1px 7px;border-radius:99px;font-weight:600;font-size:10.5px">${b.hits}</span>
+      </a>`).join('');
+    res.type('html').send(pageShell('Sample leads', `
+      ${adminHeader('/admin/leads').replace('{email}', req.admin.email)}
+      <main class="admin-main">
+        <div class="admin-intro">
+          <h1>Sample requests</h1>
+          <p class="muted">${total} total leads · ${last7} in last 7 days.</p>
+          ${popRows ? `<div style="display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 18px"><span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px;align-self:center">Most requested</span>${popRows}</div>` : ''}
+        </div>
+        ${rows.length === 0
+          ? '<div class="empty-state" style="padding:60px;text-align:center;color:var(--ink-faint)">No sample requests yet. Once visitors hit /samples → POST /api/leads, they show up here.</div>'
+          : `<table style="width:100%;border-collapse:collapse;background:var(--card-bg);border:1px solid var(--line);border-radius:6px;overflow:hidden">
+              <thead><tr style="background:var(--bg);text-align:left">
+                <th style="padding:10px 14px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint)">When</th>
+                <th style="padding:10px 14px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint)">Name</th>
+                <th style="padding:10px 14px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint)">Email</th>
+                <th style="padding:10px 14px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint)">Phone</th>
+                <th style="padding:10px 14px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint)">Design</th>
+                <th style="padding:10px 14px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint)">Source</th>
+                <th style="padding:10px 14px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint)">Message</th>
+              </tr></thead>
+              <tbody style="font-size:13px">${tableRows}</tbody>
+            </table>`}
+      </main>`));
+  });
+
   // ── JSON endpoints
   app.post('/api/admin/inspiration/add', gate, (req, res) => {
     try { addInspiration(req.admin.email, req.body || {}); res.json({ ok: true }); }
diff --git a/tests/debug-kb.spec.js b/tests/debug-kb.spec.js
new file mode 100644
index 0000000..2eb454b
--- /dev/null
+++ b/tests/debug-kb.spec.js
@@ -0,0 +1,51 @@
+const { test, expect } = require('@playwright/test');
+
+test('debug - hover and key behavior', async ({ page }) => {
+  await page.addInitScript(() => {
+    try { localStorage.setItem('wallco-age-skipped', '1'); } catch {}
+  });
+  
+  await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+  await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
+  
+  const cards = page.locator('.design-card.review-card');
+  const count = await cards.count();
+  console.log('Card count:', count);
+  
+  const card4 = cards.nth(4);
+  const id4 = await card4.getAttribute('data-id');
+  console.log('Card 4 data-id:', id4);
+  
+  // Check btn-pair exists
+  const hasPairBtn = await card4.locator('.btn-pair').count();
+  console.log('Has btn-pair:', hasPairBtn);
+  
+  // Check what btn-pair actually clicks
+  const btnPairVisible = await card4.locator('.btn-pair').isVisible();
+  console.log('btn-pair visible:', btnPairVisible);
+  
+  // Try hover
+  await card4.hover();
+  await page.waitForTimeout(300);
+  
+  // Check if kbd-focus was set
+  const hasFocus = await card4.evaluate(el => el.classList.contains('kbd-focus'));
+  console.log('Has kbd-focus after hover:', hasFocus);
+  
+  // Check FOCUSED_CARD in window
+  const focusedId = await page.evaluate(() => {
+    return window.FOCUSED_CARD ? window.FOCUSED_CARD.dataset.id : 'null';
+  });
+  console.log('FOCUSED_CARD id:', focusedId);
+  
+  // Check the mouseover event listener is on document
+  const hasListener = await page.evaluate(() => {
+    // try dispatching mouseover on the card
+    const card = document.querySelectorAll('.design-card.review-card')[4];
+    if (!card) return 'no card at index 4';
+    const event = new MouseEvent('mouseover', { bubbles: true });
+    card.dispatchEvent(event);
+    return window.FOCUSED_CARD ? window.FOCUSED_CARD.dataset.id : 'still null';
+  });
+  console.log('After manual mouseover dispatch, FOCUSED_CARD:', hasListener);
+});
diff --git a/tests/debug-kb2.spec.js b/tests/debug-kb2.spec.js
new file mode 100644
index 0000000..098c85a
--- /dev/null
+++ b/tests/debug-kb2.spec.js
@@ -0,0 +1,66 @@
+const { test, expect } = require('@playwright/test');
+
+test('debug2 - check review mode JS running + FOCUSED_CARD via closure', async ({ page }) => {
+  await page.addInitScript(() => {
+    try { localStorage.setItem('wallco-age-skipped', '1'); } catch {}
+  });
+  
+  await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+  await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
+  
+  // Check if review mode JS markers exist
+  const hasKbdStyle = await page.evaluate(() => {
+    const styles = Array.from(document.querySelectorAll('style'));
+    return styles.some(s => s.textContent.includes('kbd-focus'));
+  });
+  console.log('kbd-focus style injected:', hasKbdStyle);
+  
+  const hasToast = await page.evaluate(() => {
+    // try show toast to see if fn exists
+    return typeof document.querySelectorAll('.design-card.review-card').length;
+  });
+  console.log('cards type:', hasToast);
+  
+  // Check btn-pair is visible (not covered)
+  const card4 = page.locator('.design-card.review-card').nth(4);
+  await card4.scrollIntoViewIfNeeded();
+  await page.waitForTimeout(300);
+  
+  const rect = await card4.evaluate(el => {
+    const r = el.getBoundingClientRect();
+    return { top: r.top, left: r.left, width: r.width, height: r.height };
+  });
+  console.log('Card 4 rect:', JSON.stringify(rect));
+  
+  // Directly set FOCUSED_CARD by injecting into page
+  const setResult = await page.evaluate(() => {
+    // Get the IIFE-scoped FOCUSED_CARD by patching mouseover event
+    // Try dispatching from a card and checking if setFocused was called
+    const card = document.querySelectorAll('.design-card.review-card')[4];
+    if (!card) return 'no card';
+    // Real mouseover from element
+    const e = new MouseEvent('mouseover', { bubbles: true, cancelable: true });
+    document.dispatchEvent(Object.assign(e, { target: card }));
+    return 'dispatched on document';
+  });
+  console.log('Direct dispatch result:', setResult);
+  
+  // After hover, check kbd-focus
+  await card4.hover({ force: true });
+  await page.waitForTimeout(500);
+  const focusAfterForceHover = await card4.evaluate(el => el.classList.contains('kbd-focus'));
+  console.log('kbd-focus after force hover:', focusAfterForceHover);
+  
+  // Try clicking btn-keep to see if it responds
+  const decisionBefore = await card4.getAttribute('data-decision');
+  console.log('decision before btn click:', decisionBefore);
+  await card4.locator('.btn-keep').click();
+  await page.waitForTimeout(500);
+  const decisionAfter = await card4.getAttribute('data-decision');
+  console.log('decision after btn-keep click:', decisionAfter);
+  
+  // Close to avoid Ollama wait
+  await page.evaluate(() => {
+    // try to cancel Ollama call somehow - just set the state back
+  });
+});
diff --git a/tests/debug-kb3.spec.js b/tests/debug-kb3.spec.js
new file mode 100644
index 0000000..36eabc4
--- /dev/null
+++ b/tests/debug-kb3.spec.js
@@ -0,0 +1,47 @@
+const { test, expect } = require('@playwright/test');
+
+test('debug3 - check what hostname sees and review mode rendering', async ({ page }) => {
+  await page.addInitScript(() => {
+    try { localStorage.setItem('wallco-age-skipped', '1'); } catch {}
+  });
+  
+  await page.goto('/designs?review=1', { waitUntil: 'networkidle' });
+  
+  // Check if review-card class exists at all
+  const reviewCardCount = await page.locator('.design-card.review-card').count();
+  console.log('review-card count:', reviewCardCount);
+  
+  // Check for the admin block elements
+  const hasAdminBanner = await page.locator('.admin-banner').count();
+  console.log('admin-banner count:', hasAdminBanner);
+  
+  const hasChatModal = await page.locator('#chat-modal').count();
+  console.log('#chat-modal count:', hasChatModal);
+  
+  const hasPairModal = await page.locator('#pair-modal').count();
+  console.log('#pair-modal count:', hasPairModal);
+  
+  // Check styles for kbd-focus
+  const hasKbdStyle = await page.evaluate(() => {
+    return Array.from(document.querySelectorAll('style'))
+      .some(s => s.textContent.includes('kbd-focus'));
+  });
+  console.log('kbd-focus style exists (post-networkidle):', hasKbdStyle);
+  
+  // Check btn-keep on first card
+  const hasBtnKeep = await page.locator('.design-card.review-card').first().locator('.btn-keep').count();
+  console.log('btn-keep on first card:', hasBtnKeep);
+  
+  // Try hover and wait longer
+  const card4 = page.locator('.design-card.review-card').nth(4);
+  await card4.hover();
+  await page.waitForTimeout(1000);
+  const hasFocusAfterHover = await card4.evaluate(el => el.classList.contains('kbd-focus'));
+  console.log('kbd-focus after 1s hover wait:', hasFocusAfterHover);
+  
+  // Try pressing K and watch what happens to data-decision
+  await page.keyboard.press('k');
+  await page.waitForTimeout(500);
+  const decision = await card4.getAttribute('data-decision');
+  console.log('data-decision after K press:', JSON.stringify(decision));
+});

← 702863d wallco.ai: color-wheel hue swatch row above /designs · 10 bu  ·  back to Wallco Ai  ·  fix review-mode IIFE syntax error + add keyboard-shortcut e2 0afc54c →