[object Object]

← back to Ads Dashboard

auto-save: 2026-08-02T18:47:17 (1 files) — server.js

370f7232a182759932a84709b407a917c6f55eed · 2026-08-02 18:47:24 -0700 · Steve Abrams

Files touched

Diff

commit 370f7232a182759932a84709b407a917c6f55eed
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 2 18:47:24 2026 -0700

    auto-save: 2026-08-02T18:47:17 (1 files) — server.js
---
 server.js | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 68 insertions(+)

diff --git a/server.js b/server.js
index ee0027e..3b1746c 100644
--- a/server.js
+++ b/server.js
@@ -22,6 +22,68 @@ const AUTH_PASS = process.env.ADS_PASS || 'DW2024!';
 const PUBLIC_DIR = path.join(__dirname, 'public');
 const DATA_DIR = path.join(__dirname, 'data');
 
+// ── minimal .env loader (prod-only secrets live here, gitignored) ────────────
+(function loadEnv() {
+  try {
+    for (const line of fs.readFileSync(path.join(__dirname, '.env'), 'utf8').split('\n')) {
+      const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
+      if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
+    }
+  } catch { /* no .env — fine in dev */ }
+})();
+
+// ── Shopify acquisition (DW real data — order source_name / referring_site) ──
+const SHOP = process.env.DW_SHOP || 'designer-laboratory-sandbox.myshopify.com';
+const ORDERS_TOKEN = process.env.SHOPIFY_ORDERS_TOKEN || '';
+let _acqCache = { at: 0, data: null };
+
+function classifyChannel(referrer, sourceName) {
+  const h = String(referrer || '').toLowerCase();
+  if (!h) return sourceName === 'web' ? 'Direct / None' : (sourceName || 'Other');
+  if (/google\./.test(h)) return 'Google';
+  if (/bing\./.test(h)) return 'Bing';
+  if (/shop\.app|shopify/.test(h)) return 'Shop app';
+  if (/chatgpt|openai/.test(h)) return 'ChatGPT';
+  if (/facebook|fb\.|meta\./.test(h)) return 'Facebook';
+  if (/instagram/.test(h)) return 'Instagram';
+  if (/pinterest/.test(h)) return 'Pinterest';
+  if (/bing|duckduck|yahoo/.test(h)) return 'Other search';
+  if (/t\.co|twitter|x\.com/.test(h)) return 'X / Twitter';
+  return 'Other referral';
+}
+
+async function fetchAcquisition() {
+  if (!ORDERS_TOKEN) return { present: false, reason: 'SHOPIFY_ORDERS_TOKEN not set on this host', source: 'shopify-orders' };
+  if (_acqCache.data && Date.now() - _acqCache.at < 10 * 60 * 1000) return _acqCache.data;
+  const url = `https://${SHOP}/admin/api/2024-10/orders.json?status=any&limit=250&fields=id,created_at,source_name,referring_site,total_price`;
+  const r = await fetch(url, { headers: { 'X-Shopify-Access-Token': ORDERS_TOKEN } });
+  if (!r.ok) return { present: false, reason: `shopify ${r.status}`, source: 'shopify-orders' };
+  const j = await r.json();
+  const orders = j.orders || [];
+  const bySource = {}, byChannel = {};
+  let revenue = 0;
+  for (const o of orders) {
+    const ch = classifyChannel(o.referring_site, o.source_name);
+    byChannel[ch] = byChannel[ch] || { orders: 0, revenue: 0 };
+    byChannel[ch].orders++; byChannel[ch].revenue += Number(o.total_price || 0);
+    const s = o.source_name || '(none)';
+    bySource[s] = (bySource[s] || 0) + 1;
+    revenue += Number(o.total_price || 0);
+  }
+  const channels = Object.entries(byChannel)
+    .map(([channel, v]) => ({ channel, orders: v.orders, revenue: Math.round(v.revenue * 100) / 100 }))
+    .sort((a, b) => b.orders - a.orders);
+  const data = {
+    present: true, source: 'shopify-orders', shop: SHOP,
+    generated_at: new Date().toISOString(),
+    window: `last ${orders.length} orders`,
+    totals: { orders: orders.length, revenue: Math.round(revenue * 100) / 100 },
+    channels, by_source: bySource,
+  };
+  _acqCache = { at: Date.now(), data };
+  return data;
+}
+
 // ── Host → brand map ────────────────────────────────────────────────────────
 // Add a hostname here (and register a vhost) — this is the PUBLIC_HOSTS registry.
 // NOTE: ads.agentabrams.com is NOT ours — it belongs to `agent-ad-network`
@@ -152,6 +214,12 @@ const server = http.createServer((req, res) => {
     return sendJSON(res, 200, Array.isArray(c) ? { campaigns: c } : c);
   }
   if (pathname === '/api/brand') return sendJSON(res, 200, brand);
+  if (pathname === '/api/acquisition') {
+    if (brand.key !== 'dw') return sendJSON(res, 200, { present: false, reason: 'acquisition data is DW-only' });
+    return fetchAcquisition()
+      .then(d => sendJSON(res, 200, d))
+      .catch(e => sendJSON(res, 200, { present: false, reason: 'error: ' + e.message, source: 'shopify-orders' }));
+  }
 
   // static
   let rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');

← 891af5c scope to DW-only: remove ads.agentabrams.com from BRANDS (it  ·  back to Ads Dashboard  ·  Cycle 3: real acquisition panel from Shopify order referrers d022604 →