From ef088601c356c4f21302ff2d01d08822af7580c2 Mon Sep 17 00:00:00 2001 From: martbost Date: Sat, 12 Sep 2026 07:51:01 -0500 Subject: [PATCH] Admin Traffic tab: referring domains, landing pages, angles, by day traffic.js logs public page views by referring domain (30 s buffered; page_hits table or traffic.json), coach exposes all join-page views, and /api/admin/traffic merges page views, join-page views, signups, registrations and $20+ buyers by first-touch source for 7/30/90/365-day ranges. Also: line-tree chips no longer truncate own-position labels. Co-Authored-By: Claude Fable 5.1 --- coach.js | 10 +++++-- db.js | 4 +++ public/admin.html | 14 ++++++++++ public/assets/admin.js | 17 +++++++++++- public/assets/my.js | 2 +- server.js | 28 ++++++++++++++++++++ traffic.js | 60 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 traffic.js diff --git a/coach.js b/coach.js index f8aedde..7cf0170 100644 --- a/coach.js +++ b/coach.js @@ -104,7 +104,8 @@ const J = { async addView(token, angle, ts, ref) { this.db.views.push({ token, angle, ts, ref: ref || '' }); if (this.db.views.length > 50000) this.db.views = this.db.views.slice(-40000); this.save(); }, async addClick(campaignId, src, ts) { this.db.clicks = this.db.clicks || []; this.db.clicks.push({ campaignId, src, ts }); if (this.db.clicks.length > 50000) this.db.clicks = this.db.clicks.slice(-40000); this.save(); }, async clicksFor(ids) { return (this.db.clicks || []).filter(c => ids.includes(c.campaignId)); }, - async views(tokens, since) { return this.db.views.filter(v => tokens.includes(v.token) && v.ts >= since); } + async views(tokens, since) { return this.db.views.filter(v => tokens.includes(v.token) && v.ts >= since); }, + async viewsSince(since) { return this.db.views.filter(v => v.ts >= since); } }; const D = { async lastNudge(email) { const r = await db.q('SELECT rung, ts FROM nudges WHERE email=?', [email]); return r[0] ? { rung: r[0].rung, ts: Number(r[0].ts) } : null; }, @@ -131,6 +132,10 @@ const D = { if (!tokens.length) return []; const rows = await db.q('SELECT token, angle, ts, ref FROM join_views WHERE ts>=? AND token IN (' + tokens.map(() => '?').join(',') + ')', [since, ...tokens]); return rows.map(r => ({ token: r.token, angle: r.angle, ts: Number(r.ts), ref: r.ref || '' })); + }, + async viewsSince(since) { + const rows = await db.q('SELECT token, angle, ts, ref FROM join_views WHERE ts>=?', [since]); + return rows.map(r => ({ token: r.token, angle: r.angle, ts: Number(r.ts), ref: r.ref || '' })); } }; const impl = () => db.enabled() ? D : J; @@ -249,4 +254,5 @@ function init(opts) { DATA_DIR = opts.dataDir; chain = opts.chain; accounts = opts.accounts; mailer = opts.mailer; J.load(); } -module.exports = { init, RUNGS, rungFor, describe, coachView, prospects, saveProspect, removeProspect, STATUSES, recordView, linkStats, nudgeTick, recordClick, clickSources, refHost }; +async function viewsSince(since) { return impl().viewsSince(since); } +module.exports = { init, RUNGS, rungFor, describe, coachView, prospects, saveProspect, removeProspect, STATUSES, recordView, linkStats, nudgeTick, recordClick, clickSources, refHost, viewsSince }; diff --git a/db.js b/db.js index 0001f9d..f8ca8b1 100644 --- a/db.js +++ b/db.js @@ -144,6 +144,10 @@ async function bootstrap() { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); await q(`CREATE TABLE IF NOT EXISTS nudges (email VARCHAR(190) PRIMARY KEY, rung INT NOT NULL, ts BIGINT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); await q(`CREATE TABLE IF NOT EXISTS digests (email VARCHAR(190) PRIMARY KEY, ts BIGINT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); + await q(`CREATE TABLE IF NOT EXISTS page_hits ( + day CHAR(10) NOT NULL, host VARCHAR(80) NOT NULL, path VARCHAR(40) NOT NULL, n INT NOT NULL DEFAULT 0, + PRIMARY KEY (day, host, path) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // admin Traffic tab: public page views by referring domain await q(`CREATE TABLE IF NOT EXISTS adoptions ( id INT AUTO_INCREMENT PRIMARY KEY, adoptee VARCHAR(190) NOT NULL, adopter VARCHAR(190) NOT NULL, diff --git a/public/admin.html b/public/admin.html index 297e3e2..0204a9c 100644 --- a/public/admin.html +++ b/public/admin.html @@ -90,6 +90,7 @@ + @@ -251,6 +252,19 @@ +