← back to Ads Dashboard
combined revenue: /api/revenue merges Shopify + FMPro (Mac2-pushed) + PayPal placeholder = $115k real; KPIs + Revenue-by-stream panel
914f356b1224c448b4f85b6c1a800839bd76da43 · 2026-08-03 07:20:40 -0700 · Steve Abrams
Files touched
A data/fmpro.jsonM public/index.htmlM server.js
Diff
commit 914f356b1224c448b4f85b6c1a800839bd76da43
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 07:20:40 2026 -0700
combined revenue: /api/revenue merges Shopify + FMPro (Mac2-pushed) + PayPal placeholder = $115k real; KPIs + Revenue-by-stream panel
---
data/fmpro.json | 10 ++++++++++
public/index.html | 39 ++++++++++++++++++++++++++++++++++-----
server.js | 26 ++++++++++++++++++++++++++
3 files changed, 70 insertions(+), 5 deletions(-)
diff --git a/data/fmpro.json b/data/fmpro.json
new file mode 100644
index 0000000..d895f45
--- /dev/null
+++ b/data/fmpro.json
@@ -0,0 +1,10 @@
+{
+ "source": "fmpro-invoicing",
+ "present": true,
+ "truncated": false,
+ "window": "07/04/2026–08/03/2026 (last 30d)",
+ "total": 72706.19,
+ "orders": 251,
+ "open_quotes": 368,
+ "generated_at": "2026-08-03T14:19:02.822Z"
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index ff9d26a..4780e69 100644
--- a/public/index.html
+++ b/public/index.html
@@ -50,6 +50,13 @@
<main>
<div class="grid" id="kpis"></div>
+ <div class="section-h">Revenue by stream <span style="text-transform:none;font-weight:400">(trailing 30d, net)</span></div>
+ <div class="card">
+ <table id="revTable"><thead><tr><th>Stream</th><th>Revenue</th><th>Orders</th><th>Status</th></tr></thead>
+ <tbody id="revBody"></tbody></table>
+ <div class="when" id="revNote" style="margin-top:8px"></div>
+ </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>
@@ -101,6 +108,24 @@
const fmtDate = (iso) => { try { return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); } catch { return iso||'—'; } };
const $ = (id)=>document.getElementById(id);
+function renderRevenue(rev){
+ if (!rev || !rev.present){ $('revBody').innerHTML='<tr><td colspan="4" class="empty">Revenue unavailable (source: /api/revenue)</td></tr>'; return; }
+ const fmt$ = n => '$'+Number(n||0).toLocaleString();
+ const rows = (rev.streams||[]).map(s=>{
+ let status;
+ if (!s.present) status = '<span class="badge">'+(s.reason||'not connected')+'</span>';
+ else if (s.stale) status = '<span class="badge" style="border-color:#c0392b;color:#c0392b">⚠ stale '+s.age_hours+'h — as of '+fmtDate(s.as_of)+'</span>';
+ else status = '<span class="badge">live'+(s.as_of?' · '+fmtDate(s.as_of):'')+'</span>';
+ return `<tr${s.present?'':' style="opacity:.55"'}>
+ <td><a href="/api/revenue">${s.stream}</a></td>
+ <td>${s.present?fmt$(s.total):'—'}</td>
+ <td>${s.orders!=null?s.orders:'—'}</td>
+ <td>${status}</td></tr>`;
+ }).join('');
+ $('revBody').innerHTML = rows + `<tr style="font-weight:680"><td>Combined</td><td>${fmt$(rev.combined_total)}</td><td></td><td></td></tr>`;
+ $('revNote').textContent = rev.note || '';
+}
+
let campaigns = [];
async function boot(){
const brand = await fetch('/api/brand').then(r=>r.json());
@@ -110,21 +135,25 @@ async function boot(){
$('hostChip').textContent = location.host;
document.title = brand.short || 'Ad-Ops';
- const [acq, camps] = await Promise.all([
+ const [acq, camps, rev] = await Promise.all([
fetch('/api/acquisition').then(r=>r.json()).catch(()=>({present:false})),
fetch('/api/campaigns').then(r=>r.json()),
+ fetch('/api/revenue').then(r=>r.json()).catch(()=>({present:false})),
]);
+ renderRevenue(rev);
// KPIs — driven by REAL acquisition data (30d Shopify orders), every number hrefs to its source
const topCh = (acq.channels||[])[0];
const tagPct = acq.present && acq.totals.orders ? Math.round((acq.totals.utm_tagged||0)/acq.totals.orders*100) : 0;
+ const shopS = (rev.streams||[]).find(s=>s.stream==='Shopify');
+ const fmS = (rev.streams||[]).find(s=>s.stream==='FMPro invoicing');
const kpis = acq.present ? [
- { h:'Orders (30d)', v: acq.totals.orders, href:'/api/acquisition', when: acq.window },
- { h:'Shopify rev (30d)', v: '$'+acq.totals.revenue.toLocaleString(), href:'/api/acquisition', when: 'Shopify only — FMPro + PayPal separate' },
+ { h:'Revenue (30d, all streams)', v: rev.present ? '$'+rev.combined_total.toLocaleString() : '$'+acq.totals.revenue.toLocaleString(), href:'/api/revenue', when: rev.present ? 'Shopify + FMPro'+((rev.streams||[]).some(s=>s.stream==='PayPal'&&s.present)?' + PayPal':' (PayPal pending)') : 'Shopify only' },
+ { h:'Shopify (30d)', v: '$'+(shopS?shopS.total:acq.totals.revenue).toLocaleString(), href:'/api/acquisition', when: acq.totals.orders+' orders' },
+ { h:'FMPro invoicing (30d)', v: fmS&&fmS.present ? '$'+fmS.total.toLocaleString() : '—', href:'/api/revenue', when: fmS&&fmS.present ? fmS.orders+' booked'+(fmS.stale?' · ⚠ stale':'') : (fmS?fmS.reason:'') },
{ h:'Top channel', v: topCh ? topCh.channel : '—', href:'/api/acquisition', when: topCh ? topCh.orders+' orders' : '' },
- { h:'UTM-tagged', v: tagPct+'%', href:'/api/acquisition', when: (acq.totals.utm_tagged||0)+' of '+acq.totals.orders },
] : [
- { h:'Orders (30d)', v:'—', href:'/api/acquisition', when:'acquisition unavailable' },
+ { h:'Revenue', v:'—', href:'/api/revenue', when:'unavailable' },
];
$('kpis').innerHTML = kpis.map(k=>`<div class="card"><h2>${k.h}</h2>
<div class="kpi"><a href="${k.href}">${k.v}</a></div>
diff --git a/server.js b/server.js
index d339d63..d21ca71 100644
--- a/server.js
+++ b/server.js
@@ -252,6 +252,32 @@ 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/revenue') {
+ if (brand.key !== 'dw') return sendJSON(res, 200, { present: false, reason: 'revenue is DW-only' });
+ return fetchAcquisition().then(acq => {
+ const fm = readJSON('fmpro.json', null);
+ const streams = [];
+ // Shopify — live
+ streams.push({ stream: 'Shopify', present: !!acq.present, total: acq.present ? acq.totals.revenue : 0,
+ orders: acq.present ? acq.totals.orders : 0, window: acq.window || null, as_of: acq.generated_at || null });
+ // FMPro — pushed from Mac2 (aggregate only), stamped with freshness
+ if (fm && fm.present) {
+ const ageH = fm.generated_at ? (Date.now() - Date.parse(fm.generated_at)) / 36e5 : null;
+ streams.push({ stream: 'FMPro invoicing', present: true, total: fm.total, orders: fm.orders,
+ window: fm.window, as_of: fm.generated_at, stale: ageH != null && ageH > 25, age_hours: ageH != null ? Math.round(ageH * 10) / 10 : null });
+ } else {
+ streams.push({ stream: 'FMPro invoicing', present: false, total: 0, reason: fm ? 'truncated fetch' : 'no push yet (Mac2→Kamatera)' });
+ }
+ // PayPal — not connected (source TBD, Steve to confirm)
+ streams.push({ stream: 'PayPal', present: false, total: 0, reason: 'not connected — source pending' });
+ const combined = streams.filter(s => s.present).reduce((a, s) => a + (s.total || 0), 0);
+ return sendJSON(res, 200, {
+ present: true, generated_at: new Date().toISOString(),
+ note: 'Shopify + FMPro are confirmed non-overlapping (Steve 2026-08-03). PayPal not yet connected.',
+ combined_total: Math.round(combined * 100) / 100, streams,
+ });
+ }).catch(e => sendJSON(res, 200, { present: false, reason: 'error: ' + e.message }));
+ }
if (pathname === '/api/acquisition') {
if (brand.key !== 'dw') return sendJSON(res, 200, { present: false, reason: 'acquisition data is DW-only' });
return fetchAcquisition()
← 33b3919 honest relabel: 'Shopify rev (30d)' (FMPro $72.7k + PayPal a
·
back to Ads Dashboard
·
href-drill: daily trend bars now href to /api/acquisition (e c0ed05e →