[object Object]

← back to Stars of Design

starsofdesign: GET /api/stats — JSON counts (designers/firms/claimed/links/likes/comments/candidates) with shared module-level pool; mirrors asim /api/spotted/stats pattern; consumed by status-loop + external dashboards

728ecab21839893e045290aeab5f9e757a2776fb · 2026-05-12 18:09:29 -0700 · Steve Abrams

Files touched

Diff

commit 728ecab21839893e045290aeab5f9e757a2776fb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 12 18:09:29 2026 -0700

    starsofdesign: GET /api/stats — JSON counts (designers/firms/claimed/links/likes/comments/candidates) with shared module-level pool; mirrors asim /api/spotted/stats pattern; consumed by status-loop + external dashboards
---
 routes/public.js | 40 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/routes/public.js b/routes/public.js
index 86ea5a7..f7daf2c 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -1,4 +1,5 @@
 const express = require('express');
+const { Pool } = require('pg');
 const router = express.Router();
 
 const data = require('../lib/data');
@@ -7,6 +8,13 @@ const firmsLib = require('../lib/firms');
 const feedLib = require('../lib/feed');
 const { sortDesigners, SORTS } = require('../lib/sort');
 
+// Shared pool just for the small JSON stats endpoint — the lib/* modules
+// each carry their own pool and we don't want to leak handles per request.
+const statsPool = new Pool({
+  connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp&user=stevestudio2',
+  max: 2,
+});
+
 const SITE_TITLE = 'Stars of Design';
 const META_DESC = 'Stars of Design — a curated profile directory of the world\'s most influential interior designers, decade by decade, with the wallcoverings that match each designer\'s signature style.';
 
@@ -180,6 +188,38 @@ router.get('/api/clients/:id/comments', async (req, res, next) => {
   } catch (e) { next(e); }
 });
 
+// JSON stats — counts surfaced by the homepage + cross-consumed by status-loop
+// and external dashboards. Public, no auth, returns DW-client tier only
+// (excluding Manufacturer Reps which are gated out everywhere else too).
+router.get('/api/stats', async (_req, res, next) => {
+  try {
+    const r = await statsPool.query(`
+      SELECT
+        (SELECT count(*) FROM sod_designers WHERE role IS NULL OR role <> 'Manufacturer Rep') AS designers,
+        (SELECT count(*) FROM sod_designers WHERE claimed_at IS NOT NULL AND (role IS NULL OR role <> 'Manufacturer Rep')) AS designers_claimed,
+        (SELECT count(*) FROM sod_firms) AS firms,
+        (SELECT count(*) FROM sod_firms WHERE claimed_at IS NOT NULL) AS firms_claimed,
+        (SELECT count(*) FROM sod_links) AS links,
+        (SELECT count(*) FROM sod_likes) AS likes,
+        (SELECT count(*) FROM sod_comments WHERE is_hidden = false) AS comments,
+        (SELECT count(*) FROM sod_email_candidates) AS candidates_total,
+        (SELECT count(*) FROM sod_email_candidates WHERE status = 'new') AS candidates_unreviewed`);
+    const row = r.rows[0] || {};
+    res.json({
+      designers:           Number(row.designers || 0),
+      designers_claimed:   Number(row.designers_claimed || 0),
+      firms:               Number(row.firms || 0),
+      firms_claimed:       Number(row.firms_claimed || 0),
+      links:               Number(row.links || 0),
+      likes:               Number(row.likes || 0),
+      comments:            Number(row.comments || 0),
+      candidates_total:    Number(row.candidates_total || 0),
+      candidates_unreviewed:Number(row.candidates_unreviewed || 0),
+      as_of:               new Date().toISOString(),
+    });
+  } catch (e) { next(e); }
+});
+
 router.post('/api/clients/:id/like', async (req, res, next) => {
   try {
     const id = parseInt(req.params.id, 10);

← 346478c starsofdesign: seed data/vendor-domains.txt (~80 lines) — ve  ·  back to Stars of Design  ·  starsofdesign: GET /favicon.ico fallback — returns public/fa f17b258 →