← 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,'<')}</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 →