← 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
M public/index.htmlM server.js
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 →