#!/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(); }