← 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
M src/admin.jsA tests/debug-kb.spec.jsA tests/debug-kb2.spec.jsA tests/debug-kb3.spec.js
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 => ({'<':'<','>':'>','&':'&','"':'"',"'":'''}[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 →