[object Object]

← back to Ads Dashboard

Cycle 5: daily orders/revenue trend (30d) in /api/acquisition + bar panel; GSC probed (gated: API disabled + property grant)

f15240ce1b6075a0334bfa93e3de7c617d78d94e · 2026-08-02 20:00:08 -0700 · Steve Abrams

Files touched

Diff

commit f15240ce1b6075a0334bfa93e3de7c617d78d94e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 2 20:00:08 2026 -0700

    Cycle 5: daily orders/revenue trend (30d) in /api/acquisition + bar panel; GSC probed (gated: API disabled + property grant)
---
 public/index.html | 15 +++++++++++++++
 server.js         | 10 ++++++++--
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index be239a5..1ac0cf7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -50,6 +50,12 @@
 <main>
   <div class="grid" id="kpis"></div>
 
+  <div class="section-h">Daily orders <span style="text-transform:none;font-weight:400">(net, last 30d)</span></div>
+  <div class="card">
+    <div id="trend" style="display:flex;align-items:flex-end;gap:3px;height:120px;overflow-x:auto"></div>
+    <div class="empty" id="trendEmpty" hidden></div>
+  </div>
+
   <div class="section-h">Campaigns</div>
   <div class="controls">
     <label>Sort <select id="sort">
@@ -145,6 +151,15 @@ async function boot(){
     $('acqEmpty').textContent = 'Acquisition data unavailable: ' + (acq.reason||'no data') + ' (source: /api/acquisition)';
   }
 
+  // daily trend bars
+  const tr = (acq.trend||[]);
+  if (tr.length) {
+    $('trendEmpty').hidden = true;
+    const max = Math.max(...tr.map(d=>d.orders), 1);
+    $('trend').innerHTML = tr.map(d=>`<div title="${d.date}: ${d.orders} orders, $${d.revenue.toLocaleString()}"
+      style="flex:1;min-width:8px;height:${Math.round(d.orders/max*100)}%;background:var(--accent);border-radius:3px 3px 0 0;opacity:.85"></div>`).join('');
+  } else { $('trend').innerHTML=''; $('trendEmpty').hidden=false; $('trendEmpty').textContent='No daily data (source: /api/acquisition).'; }
+
   // UTM campaigns (attribution)
   const camps = (acq.campaigns||[]);
   if (camps.length) {
diff --git a/server.js b/server.js
index fafd11e..a27d1e0 100644
--- a/server.js
+++ b/server.js
@@ -65,12 +65,14 @@ async function fetchAcquisition() {
   // exclude cancelled — not a real acquisition; count refunds separately, net them from revenue
   const orders = raw.filter(o => !o.cancelled_at);
   const REFUNDED = new Set(['refunded', 'partially_refunded', 'voided']);
-  const bySource = {}, byChannel = {}, byCampaign = {};
+  const bySource = {}, byChannel = {}, byCampaign = {}, byDay = {};
   let revenue = 0, utmTagged = 0, refundedCount = 0, capped = raw.length >= 250;
   const round2 = (n) => Math.round(n * 100) / 100;
   const dates = [];
   for (const o of orders) {
     if (o.created_at) dates.push(o.created_at);
+    const day = (o.created_at || '').slice(0, 10);
+    if (day) { byDay[day] = byDay[day] || { orders: 0, revenue: 0 }; byDay[day].orders++; }
     const isRefund = REFUNDED.has(o.financial_status);
     if (isRefund) refundedCount++;
     const rev = isRefund ? 0 : Number(o.total_price || 0); // net of refunds/voids
@@ -80,6 +82,7 @@ async function fetchAcquisition() {
     const s = o.source_name || '(none)';
     bySource[s] = (bySource[s] || 0) + 1;
     revenue += rev;
+    if (day && byDay[day]) byDay[day].revenue += rev;
     // UTM attribution from the landing_site query string
     let camp = null, src = '?', med = '?';
     try {
@@ -104,12 +107,15 @@ async function fetchAcquisition() {
     .sort((a, b) => b.orders - a.orders);
   const from = dates.length ? dates.reduce((a, b) => a < b ? a : b).slice(0, 10) : null;
   const to = dates.length ? dates.reduce((a, b) => a > b ? a : b).slice(0, 10) : null;
+  const trend = Object.entries(byDay)
+    .map(([date, v]) => ({ date, orders: v.orders, revenue: round2(v.revenue) }))
+    .sort((a, b) => a.date < b.date ? -1 : 1);
   const data = {
     present: true, source: 'shopify-orders (net of refunds, excl. cancelled)', shop: SHOP,
     generated_at: new Date().toISOString(),
     window: capped ? `${from} → ${to} (capped at 250 orders — window may be shorter than ${WINDOW_DAYS}d)` : `${from} → ${to} (last ${WINDOW_DAYS}d)`,
     totals: { orders: orders.length, revenue: round2(revenue), utm_tagged: utmTagged, refunded_netted: refundedCount },
-    channels, campaigns, by_source: bySource,
+    channels, campaigns, trend, by_source: bySource,
   };
   _acqCache = { at: Date.now(), data };
   return data;

← bf73874 Cycle 4 fix (Cody): 30d window + net-of-refunds revenue + un  ·  back to Ads Dashboard  ·  Cycle 5 fix (Cody): expose capped flag + trend caption (net- 6402086 →