77 lines
3 KiB
JavaScript
77 lines
3 KiB
JavaScript
#!/usr/bin/env node
|
|
// ─────────────────────────────────────────────────────────────
|
|
// bin/joins.js — read the Bifrost join-click log.
|
|
//
|
|
// Every press of the final "Join Project Bifrost" CTA is recorded
|
|
// as its own row in the `bifrost_joins` table (who, when, session).
|
|
//
|
|
// Usage:
|
|
// node bin/joins.js list # every click, newest first
|
|
// node bin/joins.js summary # one row per user, with click count
|
|
// node bin/joins.js for <email> # full click history for one user
|
|
// node bin/joins.js stats # totals (clicks + unique users)
|
|
// ─────────────────────────────────────────────────────────────
|
|
import { q } from '../src/db.js';
|
|
|
|
const [, , cmd, arg] = process.argv;
|
|
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
|
|
function help() {
|
|
console.log('Usage:');
|
|
console.log(' joins list # every click, newest first');
|
|
console.log(' joins summary # one row per user');
|
|
console.log(' joins for <email> # click history for a user');
|
|
console.log(' joins stats # totals');
|
|
process.exit(1);
|
|
}
|
|
|
|
function iso(t) { return new Date(t).toISOString(); }
|
|
function shortSid(s) { return s ? `[${s.slice(0, 8)}…]` : ''; }
|
|
|
|
switch (cmd) {
|
|
case 'list': {
|
|
const rows = q.listJoins.all();
|
|
if (rows.length === 0) { console.log('(no clicks yet)'); break; }
|
|
for (const r of rows) {
|
|
console.log(` ${iso(r.clicked_at)} ${r.email.padEnd(32)} ${shortSid(r.session_id)}`);
|
|
}
|
|
console.log(`\n${rows.length} click${rows.length === 1 ? '' : 's'} total.`);
|
|
break;
|
|
}
|
|
|
|
case 'summary': {
|
|
const rows = q.summariseJoins.all();
|
|
if (rows.length === 0) { console.log('(no clicks yet)'); break; }
|
|
console.log(' CLICKS FIRST LAST EMAIL');
|
|
for (const r of rows) {
|
|
const n = String(r.click_count).padStart(6);
|
|
console.log(` ${n} ${iso(r.first_clicked_at)} ${iso(r.last_clicked_at)} ${r.email}`);
|
|
}
|
|
console.log(`\n${rows.length} unique user${rows.length === 1 ? '' : 's'}.`);
|
|
break;
|
|
}
|
|
|
|
case 'for': {
|
|
if (!arg || !EMAIL_RE.test(arg)) help();
|
|
const email = arg.trim().toLowerCase();
|
|
const rows = q.listJoinsForEmail.all(email);
|
|
if (rows.length === 0) { console.log(`(no clicks for ${email})`); break; }
|
|
console.log(`Clicks by ${email}:`);
|
|
for (const r of rows) {
|
|
console.log(` ${iso(r.clicked_at)} ${shortSid(r.session_id)}`);
|
|
}
|
|
console.log(`\n${rows.length} click${rows.length === 1 ? '' : 's'}.`);
|
|
break;
|
|
}
|
|
|
|
case 'stats': {
|
|
const total = q.countJoins.get().n;
|
|
const unique = q.countUniqueJoiners.get().n;
|
|
console.log(` total clicks: ${total}`);
|
|
console.log(` unique users: ${unique}`);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
help();
|
|
}
|