[object Object]

← back to Wallco Ai

wallco.ai · /admin/dashboard with catalog + demand + cost-burn metrics

5c636e266c419ed036b24bb1375d1816f9099457 · 2026-05-11 22:50:01 -0700 · SteveStudio2

- new admin-only route returns single-page dashboard via single
  json_build_object PG round-trip (~30ms total)
- 3 sections: Catalog (designs · 24h/7d · mockups · inspiration pool)
  · Demand (leads · 7d/24h · chat sessions · votes) · Cost burn
  (Replicate SDXL + room renders + totals)
- by-category breakdown chips, capitalized
- gated through isAdmin (loopback + JWT cookie via src/admin-gate)
- linked from Go-Live page nav

Fixes during build:
- wallco_generator_runs uses 'started_at' not 'created_at'
- mockup count uses jsonb_object_keys() not jsonb_path_query_array (PG14+ vs 15)

Current snapshot: 173 designs · 5 mockup designs (10 room renders) · 0
leads · 3 chat sessions · $2.56 lifetime Replicate spend.

Files touched

Diff

commit 5c636e266c419ed036b24bb1375d1816f9099457
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 11 22:50:01 2026 -0700

    wallco.ai · /admin/dashboard with catalog + demand + cost-burn metrics
    
    - new admin-only route returns single-page dashboard via single
      json_build_object PG round-trip (~30ms total)
    - 3 sections: Catalog (designs · 24h/7d · mockups · inspiration pool)
      · Demand (leads · 7d/24h · chat sessions · votes) · Cost burn
      (Replicate SDXL + room renders + totals)
    - by-category breakdown chips, capitalized
    - gated through isAdmin (loopback + JWT cookie via src/admin-gate)
    - linked from Go-Live page nav
    
    Fixes during build:
    - wallco_generator_runs uses 'started_at' not 'created_at'
    - mockup count uses jsonb_object_keys() not jsonb_path_query_array (PG14+ vs 15)
    
    Current snapshot: 173 designs · 5 mockup designs (10 room renders) · 0
    leads · 3 chat sessions · $2.56 lifetime Replicate spend.
---
 server.js | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 123 insertions(+)

diff --git a/server.js b/server.js
index a8c697e..85e4c1f 100644
--- a/server.js
+++ b/server.js
@@ -286,6 +286,129 @@ setInterval(() => load(true), 30000);
 // check as a CRITICAL bypass (anyone could mint a token with {role:"admin"}).
 const { isAdmin, requireAdmin } = require('./src/admin-gate');
 
+// ── /admin/dashboard — key metrics for wallco.ai operations
+app.get('/admin/dashboard', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  // Pull metrics in one round-trip via single SQL block.
+  let metrics = {};
+  try {
+    const out = execSync(PSQL_CMD, { encoding: 'utf8', input: `
+SELECT json_build_object(
+  'designs_total',        (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate'),
+  'designs_24h',          (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND created_at > now()-interval '24 hours'),
+  'designs_7d',           (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND created_at > now()-interval '7 days'),
+  'designs_published',    (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND is_published),
+  'by_category',          (SELECT COALESCE(json_object_agg(category, n),'{}') FROM (SELECT category, count(*) n FROM spoon_all_designs WHERE generator='replicate' GROUP BY category) t),
+  'saturation_high',      (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND dominant_hex IS NOT NULL),
+  'mockups_done',         (SELECT count(*) FROM spoon_all_designs WHERE room_mockups IS NOT NULL),
+  'leads_total',          (SELECT count(*) FROM wallco_leads),
+  'leads_7d',             (SELECT count(*) FROM wallco_leads WHERE created_at > now()-interval '7 days'),
+  'leads_24h',            (SELECT count(*) FROM wallco_leads WHERE created_at > now()-interval '24 hours'),
+  'chat_sessions',        (SELECT count(*) FROM wallco_chat_sessions),
+  'chat_msgs_24h',        (SELECT count(*) FROM wallco_chat_messages WHERE created_at > now()-interval '24 hours'),
+  'votes_total',          (SELECT count(*) FROM wallco_votes),
+  'votes_7d',             (SELECT count(*) FROM wallco_votes WHERE created_at > now()-interval '7 days'),
+  'inspiration_pool',     (SELECT count(*) FROM wallco_inspiration_pool),
+  'generator_runs',       (SELECT count(*) FROM wallco_generator_runs),
+  'generator_runs_24h',   (SELECT count(*) FROM wallco_generator_runs WHERE started_at > now()-interval '24 hours')
+);
+    `});
+    metrics = JSON.parse(out.trim());
+  } catch (e) {
+    return res.status(500).type('html').send(`<h1>dashboard query failed</h1><pre>${(e.message||'').replace(/</g,'&lt;')}</pre>`);
+  }
+
+  // Replicate cost: $0.014/SDXL gen × designs_total + $0.014/room mockup × mockups (multi-key JSONB).
+  // Sum keys per design — each room type is one Replicate call.
+  let mockupCount = metrics.mockups_done;
+  try {
+    const raw = execSync(PSQL_CMD, { encoding: 'utf8', input:
+      `SELECT COALESCE(SUM((SELECT count(*) FROM jsonb_object_keys(room_mockups))), 0)
+       FROM spoon_all_designs WHERE room_mockups IS NOT NULL;`
+    }).trim();
+    if (raw) mockupCount = parseInt(raw, 10) || metrics.mockups_done;
+  } catch { /* fall through to metrics.mockups_done */ }
+  const replicateCostSdxl = (metrics.designs_total * 0.014).toFixed(2);
+  const replicateCostRooms = (mockupCount * 0.014).toFixed(2);
+  const replicateCostTotal = (parseFloat(replicateCostSdxl) + parseFloat(replicateCostRooms)).toFixed(2);
+
+  const card = (label, value, sub) => `
+    <div class="m-card">
+      <div class="m-label">${label}</div>
+      <div class="m-value">${value}</div>
+      ${sub ? `<div class="m-sub">${sub}</div>` : ''}
+    </div>`;
+
+  const catBreakdown = Object.entries(metrics.by_category || {})
+    .sort((a,b) => b[1]-a[1])
+    .map(([k,v]) => `<span class="cat-chip"><span class="cat-k">${k}</span> <span class="cat-v">${v}</span></span>`)
+    .join('');
+
+  res.type('html').send(`<!doctype html>
+<html lang="en"><head>
+<meta charset="utf-8"><title>wallco.ai — admin dashboard</title>
+<style>
+  :root { --bg:#0f0e0c; --panel:#1a1816; --line:#2a2825; --ink:#e8e2d6; --ink-faint:#888; --accent:#d2b15c; }
+  * { box-sizing:border-box }
+  body { font:14px/1.5 -apple-system,system-ui,sans-serif; margin:0; padding:32px 40px; background:var(--bg); color:var(--ink); }
+  h1 { font-weight:300; font-size:28px; margin:0 0 6px; letter-spacing:.5px; }
+  .sub { color:var(--ink-faint); font-size:12.5px; margin-bottom:28px; }
+  .sub a { color:var(--accent); text-decoration:none; margin-right:14px; }
+  .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:14px; margin-bottom:28px; }
+  .m-card { background:var(--panel); border:1px solid var(--line); border-radius:6px; padding:16px 18px; }
+  .m-label { color:var(--ink-faint); font-size:11.5px; text-transform:uppercase; letter-spacing:.7px; margin-bottom:8px; }
+  .m-value { font-size:28px; font-weight:300; letter-spacing:.3px; color:var(--ink); }
+  .m-sub { color:var(--accent); font-size:11.5px; margin-top:4px; }
+  .section-h { font-size:13px; text-transform:uppercase; letter-spacing:1px; color:var(--ink-faint); margin:0 0 12px; }
+  .cat-row { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:28px; }
+  .cat-chip { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:6px 12px; font-size:12.5px; }
+  .cat-k { color:var(--ink); text-transform:capitalize; }
+  .cat-v { color:var(--accent); margin-left:6px; font-weight:500; }
+  .footer { color:var(--ink-faint); font-size:11px; margin-top:24px; padding-top:14px; border-top:1px solid var(--line); }
+</style></head>
+<body>
+  <h1>wallco.ai · admin dashboard</h1>
+  <div class="sub">
+    <a href="/admin/golive">go-live</a>
+    <a href="/admin/leads">leads</a>
+    <a href="/designs?review=1">review designs</a>
+    <a href="/admin/inspiration">inspiration</a>
+    · refreshed ${new Date().toLocaleString('en-US',{ timeZone:'America/Los_Angeles' })} PT
+  </div>
+
+  <h2 class="section-h">Catalog</h2>
+  <div class="grid">
+    ${card('Total designs',       metrics.designs_total, `${metrics.designs_published} published`)}
+    ${card('Generated · last 24h', metrics.designs_24h)}
+    ${card('Generated · last 7d',  metrics.designs_7d)}
+    ${card('Room mockups',         metrics.mockups_done, `${mockupCount} total renderings`)}
+    ${card('Inspiration pool',     metrics.inspiration_pool)}
+  </div>
+
+  <h2 class="section-h">By category</h2>
+  <div class="cat-row">${catBreakdown || '<span class="m-sub">no data</span>'}</div>
+
+  <h2 class="section-h">Demand signals</h2>
+  <div class="grid">
+    ${card('Leads · total',       metrics.leads_total)}
+    ${card('Leads · last 7d',     metrics.leads_7d)}
+    ${card('Leads · last 24h',    metrics.leads_24h)}
+    ${card('Chat sessions',       metrics.chat_sessions, `${metrics.chat_msgs_24h} msgs last 24h`)}
+    ${card('Votes',               metrics.votes_total, `${metrics.votes_7d} in last 7d`)}
+  </div>
+
+  <h2 class="section-h">Cost burn</h2>
+  <div class="grid">
+    ${card('Replicate · SDXL',    '$' + replicateCostSdxl, `${metrics.designs_total} gens @ $0.014`)}
+    ${card('Replicate · rooms',   '$' + replicateCostRooms, `${mockupCount} renders @ $0.014`)}
+    ${card('Replicate · TOTAL',   '$' + replicateCostTotal)}
+    ${card('Generator runs',      metrics.generator_runs, `${metrics.generator_runs_24h} last 24h`)}
+  </div>
+
+  <div class="footer">Source: dw_unified.spoon_all_designs + wallco_*. Admin-only route. Cache: live.</div>
+</body></html>`);
+});
+
 // Color-isolate module — palette extract, Sherwin-Williams nearest match,
 // coordinating-color suggestions. Sourced from ~/Projects/_shared/color-isolate.js
 // and copied locally per deploy-portability rule (Kamatera lacks ~/.claude/skills/).

← 468964c wallco.ai · multi-room mockup grid + bedroom for top-5 (tick  ·  back to Wallco Ai  ·  mobile: 44px tap-target floor + iOS auto-zoom fix + 24-viewp 2ad6307 →