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 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,8 @@ const chatbot = require('./chatbot');
|
||||
const coach = require('./coach'); // coaching view, nudges, digest, prospects, link stats
|
||||
const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward
|
||||
const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails
|
||||
const traffic = require('./traffic'); // public page views by referring domain (admin Traffic tab)
|
||||
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch']);
|
||||
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
|
||||
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
|
||||
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
|
||||
@@ -319,6 +321,7 @@ async function boot() {
|
||||
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer });
|
||||
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
|
||||
legacy.init({ dataDir: DATA_DIR });
|
||||
traffic.init({ dataDir: DATA_DIR });
|
||||
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
||||
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
|
||||
@@ -603,6 +606,8 @@ const server = http.createServer(async (req, res) => {
|
||||
const u = new URL(req.url, 'http://x');
|
||||
const p = u.pathname;
|
||||
|
||||
// -- traffic log: public page views by referring domain (admin > Traffic)
|
||||
if (req.method === 'GET' && (TRAFFIC_PAGES.has(p) || /^\/(join|from|wall)\/[^/]+$/.test(p))) traffic.hit(p, req.headers.referer, req.headers['user-agent']);
|
||||
// -- join links: /join/<memberId or share code> — LAST-touch cookie (Marty,
|
||||
// 2026-09-10): the link a visitor opened most recently is the sponsor shown
|
||||
// and used, and it locks the moment the account is created (accounts.ensure
|
||||
@@ -1093,6 +1098,29 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, await tank.adminView());
|
||||
}
|
||||
// -- admin Traffic tab: page views + join-page views + signups + $20 buyers, by referring
|
||||
// domain / first-touch source, by landing page, by angle and by day (Marty, 2026-09-12)
|
||||
if (p === '/api/admin/traffic' && req.method === 'GET') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const days = Math.min(365, Math.max(1, Number(u.searchParams.get('days')) || 30));
|
||||
const since = Date.now() - days * 86400000, sinceDay = new Date(since).toISOString().slice(0, 10);
|
||||
const hosts = {}, paths = {}, daily = {}, angles = {};
|
||||
const H = k => (hosts[k] = hosts[k] || { source: k, hits: 0, joinViews: 0, signups: 0, registered: 0, buyers: 0 });
|
||||
const Dy = k => (daily[k] = daily[k] || { day: k, hits: 0, signups: 0 });
|
||||
for (const r of await traffic.rows(sinceDay)) { H(r.host).hits += r.n; paths[r.path] = (paths[r.path] || 0) + r.n; Dy(r.day).hits += r.n; }
|
||||
for (const v of await coach.viewsSince(since)) { H(v.ref || 'direct').joinViews += 1; const a = angles[v.angle || 'plain'] = angles[v.angle || 'plain'] || { angle: v.angle || 'plain', views: 0, signups: 0 }; a.views += 1; }
|
||||
const buyerIds = new Set(); for (const ev of chain.recentEvents(1e9)) if (ev.type === 'BuyerCounted' && ev.newBuyerId) buyerIds.add(ev.newBuyerId);
|
||||
for (const a of await accounts.listAll(5000)) {
|
||||
if (!a.created || a.created < since) continue;
|
||||
const h = H(a.joinedRef || 'direct'); h.signups += 1; if (a.memberId) h.registered += 1; if (a.memberId && buyerIds.has(a.memberId)) h.buyers += 1;
|
||||
Dy(new Date(a.created).toISOString().slice(0, 10)).signups += 1;
|
||||
const k = a.joinedVia || 'plain'; angles[k] = angles[k] || { angle: k, views: 0, signups: 0 }; angles[k].signups += 1;
|
||||
}
|
||||
const sources = Object.values(hosts).sort((x, y) => (y.hits + y.joinViews + y.signups * 10) - (x.hits + x.joinViews + x.signups * 10));
|
||||
return json(res, 200, { days, sources, paths: Object.entries(paths).map(([path, hits]) => ({ path, hits })).sort((x, y) => y.hits - x.hits),
|
||||
angles: Object.values(angles).sort((x, y) => y.views - x.views), daily: Object.values(daily).sort((x, y) => x.day < y.day ? -1 : 1),
|
||||
totals: { hits: sources.reduce((n, s) => n + s.hits, 0), joinViews: sources.reduce((n, s) => n + s.joinViews, 0), signups: sources.reduce((n, s) => n + s.signups, 0), buyers: sources.reduce((n, s) => n + s.buyers, 0) } });
|
||||
}
|
||||
if (p === '/api/my/coach' && req.method === 'GET') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
|
||||
Reference in New Issue
Block a user