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:
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
<button data-pane="campaigns" type="button"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>All campaigns</button>
|
||||
<button data-pane="members" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="7" r="3.2"/><circle cx="5" cy="17" r="2.6"/><circle cx="19" cy="17" r="2.6"/><path d="M12 10v3M12 13l-5 2M12 13l5 2"/></svg>Members</button>
|
||||
<button data-pane="reports" type="button"><svg viewBox="0 0 24 24"><path d="M12 3l9 16H3z"/><path d="M12 10v4M12 17v.5"/></svg>Reports<span class="pill" id="repBadge" hidden></span></button>
|
||||
<button data-pane="traffic" type="button"><svg viewBox="0 0 24 24"><path d="M3 17l6-6 4 4 8-8"/><path d="M14 7h7v7"/></svg>Traffic</button>
|
||||
<button data-pane="pnl" type="button"><svg viewBox="0 0 24 24"><path d="M4 19V5M4 19h16"/><path d="M8 15l3-4 3 2 5-6"/></svg>P&L</button>
|
||||
<button data-pane="settings" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>Settings</button>
|
||||
</nav>
|
||||
@@ -251,6 +252,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane" id="pane-traffic" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Where visitors come from</h3><span class="sub" id="trfSub"></span></div>
|
||||
<div class="chips" id="trfRange" style="margin:0 0 10px"><button type="button" class="chip-t" data-days="7">7 days</button><button type="button" class="chip-t on" data-days="30">30 days</button><button type="button" class="chip-t" data-days="90">90 days</button><button type="button" class="chip-t" data-days="365">Year</button></div>
|
||||
<p class="muted small" style="margin:0 0 10px">Page views are public-page loads by referring domain (crawlers skipped; our own pages and no referrer count as direct). Join-page views are invite-link opens. Signups, registered and $20+ buyers are accounts whose first-touch source was that domain; legacy arrivals show as legacy:brand:domain.</p>
|
||||
<div class="tablewrap"><table class="adm-table" id="trfSources"></table></div>
|
||||
</div>
|
||||
<div class="grid c2">
|
||||
<div class="card"><div class="card-head"><h3>Landing pages</h3><span class="sub">page views by page</span></div><div class="tablewrap"><table class="adm-table" id="trfPaths"></table></div></div>
|
||||
<div class="card"><div class="card-head"><h3>Angles</h3><span class="sub">join-page hook copy</span></div><div class="tablewrap"><table class="adm-table" id="trfAngles"></table></div></div>
|
||||
</div>
|
||||
<div class="card"><div class="card-head"><h3>By day</h3><span class="sub">page views, signups</span></div><div class="tablewrap"><table class="adm-table" id="trfDaily"></table></div></div>
|
||||
</div>
|
||||
<div class="pane" id="pane-pnl" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Profit and loss</h3><span class="sub">read from the chain index; periods are by block (about 43,200 Polygon blocks a day)</span></div>
|
||||
|
||||
+16
-1
@@ -45,7 +45,7 @@
|
||||
|
||||
// ── panes ──
|
||||
const TITLES = { overview: 'Overview', house: 'House ads', campaigns: 'All campaigns', members: 'Members', reports: 'Reports', pnl: 'Profit and loss', settings: 'Settings' };
|
||||
const loaders = { overview: loadOverview, house: loadHouse, campaigns: loadCampaigns, members: loadMembers, reports: loadReports, pnl: loadPnl, settings: loadSettings };
|
||||
const loaders = { overview: loadOverview, house: loadHouse, campaigns: loadCampaigns, members: loadMembers, reports: loadReports, traffic: loadTraffic, pnl: loadPnl, settings: loadSettings };
|
||||
function setPane(name) {
|
||||
if (!TITLES[name]) name = 'overview';
|
||||
document.querySelectorAll('.pane').forEach(p => { p.hidden = p.id !== 'pane-' + name; });
|
||||
@@ -310,6 +310,21 @@
|
||||
let pnlDays = 30;
|
||||
const pol = w => { try { return (Number(BigInt(w || '0') / 10n ** 14n) / 10000).toLocaleString(undefined, { maximumFractionDigits: 2 }); } catch (e) { return '0'; } };
|
||||
const usdOf = (w, px) => { try { return '$' + ((Number(BigInt(w || '0') / 10n ** 14n) / 10000) * px).toLocaleString(undefined, { maximumFractionDigits: 0 }); } catch (e) { return '$0'; } };
|
||||
// ── traffic: referring domains / sources, landing pages, angles, by day ──
|
||||
let trfDays = 30;
|
||||
document.querySelectorAll('#trfRange [data-days]').forEach(b => b.addEventListener('click', () => { trfDays = Number(b.dataset.days); document.querySelectorAll('#trfRange [data-days]').forEach(x => x.classList.toggle('on', x === b)); loadTraffic().catch(e => IAP.status(e.message, 'bad')); }));
|
||||
async function loadTraffic() {
|
||||
const d = await (await fetch('/api/admin/traffic?days=' + trfDays)).json();
|
||||
if (d.error) throw new Error(d.error);
|
||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
const n = v => Number(v || 0).toLocaleString();
|
||||
$('trfSub').textContent = 'last ' + d.days + ' days · ' + n(d.totals.hits) + ' page views · ' + n(d.totals.joinViews) + ' join-page views · ' + n(d.totals.signups) + ' signups · ' + n(d.totals.buyers) + ' buyers';
|
||||
$('trfSources').innerHTML = '<tr><th>Source</th><th>Page views</th><th>Join-page views</th><th>Signups</th><th>Registered</th><th>$20+ buyers</th></tr>'
|
||||
+ (d.sources.length ? d.sources.map(s => '<tr><td>' + esc(s.source) + '</td><td>' + n(s.hits) + '</td><td>' + n(s.joinViews) + '</td><td>' + n(s.signups) + '</td><td>' + n(s.registered) + '</td><td>' + n(s.buyers) + '</td></tr>').join('') : '<tr><td colspan="6" class="muted">Nothing recorded in this range yet.</td></tr>');
|
||||
$('trfPaths').innerHTML = '<tr><th>Page</th><th>Views</th></tr>' + (d.paths.length ? d.paths.map(p => '<tr><td>' + esc(p.path) + '</td><td>' + n(p.hits) + '</td></tr>').join('') : '<tr><td colspan="2" class="muted">No page views yet.</td></tr>');
|
||||
$('trfAngles').innerHTML = '<tr><th>Angle</th><th>Join-page views</th><th>Signups</th></tr>' + (d.angles.length ? d.angles.map(a => '<tr><td>' + esc(a.angle) + '</td><td>' + n(a.views) + '</td><td>' + n(a.signups) + '</td></tr>').join('') : '<tr><td colspan="3" class="muted">No angle data yet.</td></tr>');
|
||||
$('trfDaily').innerHTML = '<tr><th>Day</th><th>Page views</th><th>Signups</th></tr>' + (d.daily.length ? d.daily.slice().reverse().map(x => '<tr><td>' + esc(x.day) + '</td><td>' + n(x.hits) + '</td><td>' + n(x.signups) + '</td></tr>').join('') : '<tr><td colspan="3" class="muted">Nothing yet.</td></tr>');
|
||||
}
|
||||
async function loadPnl() {
|
||||
const r = await api('/api/admin/pnl?days=' + pnlDays);
|
||||
const px = r.polUsd || 0;
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@
|
||||
const levels = r.levels || [];
|
||||
const total = levels.reduce((n, L) => n + L.members.length, 0);
|
||||
if ($('treeSub')) $('treeSub').textContent = total ? total + ' across 3 levels' : 'share your link to grow';
|
||||
const chip = (m, q) => '<span class="lt-node' + (q ? ' qualified' : (m.email ? '' : ' deep')) + '" title="' + esc(m.name || 'member') + (q ? ' · qualified buyer' : '') + '">' + esc(String(m.name || 'M').replace(/^@/, '').slice(0, 12)) + '</span>';
|
||||
const chip = (m, q) => '<span class="lt-node' + (q ? ' qualified' : (m.email ? '' : ' deep')) + '" title="' + esc(m.name || 'member') + (q ? ' · qualified buyer' : '') + '">' + esc(String(m.name || 'M').replace(/^@/, '').slice(0, m.own ? 20 : 14)) + '</span>';
|
||||
const rowFor = lvl => {
|
||||
const L = levels.find(x => x.level === lvl); const members = L ? L.members : [];
|
||||
// gold = the contract counted this member as one of your qualifying buyers (not "the first N chips")
|
||||
|
||||
@@ -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.' });
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Page-hit log for the admin Traffic tab (Marty, 2026-09-12): which referring domains send
|
||||
// visitors to the public pages, per day and per landing page. Counters are buffered in memory
|
||||
// and flushed every 30 s: MySQL table page_hits (day, host, path, n) when the DB is on, else
|
||||
// DATA_DIR/traffic.json. Obvious crawlers are skipped. Our own pages as referrer = 'direct'.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('./db');
|
||||
let DATA_DIR = null, buf = {}, jsonStore = null, timer = null;
|
||||
const BOT = /bot|crawl|spider|slurp|facebookexternalhit|preview|monitor|curl\/|wget|python-requests|headless/i;
|
||||
|
||||
function init(opts) {
|
||||
DATA_DIR = opts.dataDir;
|
||||
if (!timer) { timer = setInterval(() => flush().catch(() => {}), 30 * 1000); timer.unref(); }
|
||||
}
|
||||
function host(referer) {
|
||||
try { const h = new URL(String(referer || '')).hostname.replace(/^www\./, '').toLowerCase(); return (!h || /instantadpay\.com$/.test(h)) ? 'direct' : h.slice(0, 80); } catch (e) { return 'direct'; }
|
||||
}
|
||||
function family(p) {
|
||||
if (p === '/' || p === '') return 'home';
|
||||
if (p.startsWith('/join/')) return 'join';
|
||||
if (p.startsWith('/from/')) return p.slice(1, 40); // from/faucetwave, from/tieroneads
|
||||
if (p.startsWith('/wall/')) return 'wall';
|
||||
return p.replace(/^\//, '').slice(0, 40) || 'home'; // ledger, contract, shorts, plays ...
|
||||
}
|
||||
const day = ts => new Date(ts).toISOString().slice(0, 10);
|
||||
function hit(p, referer, ua) {
|
||||
if (BOT.test(String(ua || ''))) return;
|
||||
const k = day(Date.now()) + '|' + host(referer) + '|' + family(p);
|
||||
buf[k] = (buf[k] || 0) + 1;
|
||||
}
|
||||
function loadJson() {
|
||||
if (jsonStore) return jsonStore;
|
||||
try { jsonStore = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'traffic.json'), 'utf8')); } catch (e) { jsonStore = {}; }
|
||||
return jsonStore;
|
||||
}
|
||||
async function flush() {
|
||||
const pending = buf; buf = {};
|
||||
const keys = Object.keys(pending); if (!keys.length) return;
|
||||
if (db.enabled()) {
|
||||
for (const k of keys) { const [d, h, f] = k.split('|'); await db.q('INSERT INTO page_hits (day,host,path,n) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE n=n+VALUES(n)', [d, h, f, pending[k]]); }
|
||||
return;
|
||||
}
|
||||
const J = loadJson();
|
||||
for (const k of keys) J[k] = (J[k] || 0) + pending[k];
|
||||
// keep the JSON store bounded: drop days older than 400
|
||||
const cutoff = day(Date.now() - 400 * 86400000);
|
||||
for (const k of Object.keys(J)) if (k.slice(0, 10) < cutoff) delete J[k];
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'traffic.json'), JSON.stringify(J));
|
||||
}
|
||||
// rows since a day (inclusive): [{day, host, path, n}]
|
||||
async function rows(sinceDay) {
|
||||
await flush();
|
||||
if (db.enabled()) {
|
||||
const r = await db.q('SELECT day, host, path, n FROM page_hits WHERE day>=?', [sinceDay]);
|
||||
return r.map(x => ({ day: x.day, host: x.host, path: x.path, n: Number(x.n) }));
|
||||
}
|
||||
const J = loadJson();
|
||||
return Object.keys(J).filter(k => k.slice(0, 10) >= sinceDay).map(k => { const [d, h, p] = k.split('|'); return { day: d, host: h, path: p, n: J[k] }; });
|
||||
}
|
||||
module.exports = { init, hit, flush, rows, host, family };
|
||||
Reference in New Issue
Block a user