diff --git a/bin/events.js b/bin/events.js new file mode 100644 index 0000000..c13c9a9 --- /dev/null +++ b/bin/events.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node +// ───────────────────────────────────────────────────────────── +// bin/events.js — read the engagement-event log. +// +// Records every landmark event (login, timeline_view) with the +// user's email, device fields parsed from the UA, and the session +// ID at time-of-event. +// +// Usage: +// node bin/events.js list [--type ] [--limit ] +// node bin/events.js summary # per-user counts +// node bin/events.js for # full history for one user +// node bin/events.js stats # totals + device breakdown +// ───────────────────────────────────────────────────────────── +import { q } from '../src/db.js'; + +const args = process.argv.slice(2); +const cmd = args[0]; +const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; + +function help() { + console.log('Usage:'); + console.log(' events list [--type ] [--limit ]'); + console.log(' events summary # per-user counts'); + console.log(' events for # event history for a user'); + console.log(' events stats # totals + device breakdown'); + process.exit(1); +} + +function iso(t) { return new Date(t).toISOString(); } +function shortSid(s) { return s ? `[${s.slice(0, 8)}…]` : '[—]'; } + +function parseFlag(name) { + const i = args.indexOf(name); + return i >= 0 ? args[i + 1] : null; +} + +function parseMeta(s) { + if (!s) return null; + try { return JSON.parse(s); } catch { return s; } +} + +function metaCompact(m) { + if (!m) return ''; + if (typeof m !== 'object') return String(m); + return Object.entries(m).map(([k, v]) => `${k}=${v}`).join(' '); +} + +switch (cmd) { + case 'list': { + const type = parseFlag('--type'); + const limit = Number(parseFlag('--limit') || 200); + const rows = type + ? q.listEventsByType.all(type, limit) + : q.listEvents.all(limit); + if (rows.length === 0) { console.log('(no events yet)'); break; } + for (const r of rows) { + const dev = [r.device_type, r.os, r.browser].filter(Boolean).join('/') || '?'; + const meta = metaCompact(parseMeta(r.meta)); + console.log( + ` ${iso(r.occurred_at)} ${r.event_type.padEnd(14)} ${r.email.padEnd(28)} ${dev.padEnd(24)} ${shortSid(r.session_id)} ${meta}` + ); + } + console.log(`\n${rows.length} event${rows.length === 1 ? '' : 's'} shown.`); + break; + } + + case 'summary': { + const rows = q.summariseEvents.all(); + if (rows.length === 0) { console.log('(no events yet)'); break; } + console.log(' LOGINS TIMELINE LAST SEEN EMAIL'); + for (const r of rows) { + const lg = String(r.logins).padStart(6); + const tv = String(r.timeline_views).padStart(8); + console.log(` ${lg} ${tv} ${iso(r.last_seen)} ${r.email}`); + } + console.log(`\n${rows.length} unique user${rows.length === 1 ? '' : 's'}.`); + break; + } + + case 'for': { + const arg = args[1]; + if (!arg || !EMAIL_RE.test(arg)) help(); + const email = arg.trim().toLowerCase(); + const rows = q.listEventsForEmail.all(email); + if (rows.length === 0) { console.log(`(no events for ${email})`); break; } + console.log(`Events for ${email}:`); + for (const r of rows) { + const dev = [r.device_type, r.os, r.browser].filter(Boolean).join('/') || '?'; + const meta = metaCompact(parseMeta(r.meta)); + console.log(` ${iso(r.occurred_at)} ${r.event_type.padEnd(14)} ${dev.padEnd(24)} ${shortSid(r.session_id)} ${meta}`); + } + console.log(`\n${rows.length} event${rows.length === 1 ? '' : 's'}.`); + break; + } + + case 'stats': { + const byType = q.countEventsByType.all(); + if (byType.length === 0) { console.log('(no events yet)'); break; } + console.log(' EVENT TYPE TOTAL UNIQUE USERS'); + for (const r of byType) { + console.log(` ${r.event_type.padEnd(14)} ${String(r.total).padStart(5)} ${String(r.unique_users).padStart(12)}`); + } + const dev = q.deviceBreakdown.all(); + if (dev.length > 0) { + console.log('\n DEVICE TYPE COUNT'); + for (const r of dev) { + console.log(` ${r.device_type.padEnd(14)} ${String(r.n).padStart(5)}`); + } + } + break; + } + + default: + help(); +}