diff options
| author | benj <benj@rse8.com> | 2026-04-10 11:13:34 +0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2026-04-10 11:13:34 +0800 |
| commit | 493746b14c1251a45b061d2e3edd9160c929d2b9 (patch) | |
| tree | 1607cceb94c1aac1a17a01bb5c0d71b97342e892 /web/ui/scripts/seed.js | |
| parent | c041641634650c31e03c70dcad132fd94cb08e63 (diff) | |
| download | tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.gz tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.bz2 tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.lz tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.xz tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.tar.zst tidyindex-493746b14c1251a45b061d2e3edd9160c929d2b9.zip | |
a basic ui and landing web interface for tidyindex.com
Diffstat (limited to '')
| -rw-r--r-- | web/ui/scripts/seed.js | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/web/ui/scripts/seed.js b/web/ui/scripts/seed.js new file mode 100644 index 0000000..f4d27c2 --- /dev/null +++ b/web/ui/scripts/seed.js @@ -0,0 +1,191 @@ +/** + * Populate the dashboard SQLite database with fake usage data against + * the first account in the DB so the Usage tab isn't empty. + * + * Run with: `npm run seed` (which uses node --env-file=.env). + * + * If there's no account yet, we create an anonymous one and also mint + * two keys so the dashboard has something to show when you first log in. + */ + +import Database from 'better-sqlite3'; +import { mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { createHash, randomBytes, randomUUID } from 'node:crypto'; + +const DB_PATH = resolve(process.env.DATABASE_PATH || './data/dashboard.db'); +mkdirSync(dirname(DB_PATH), { recursive: true }); + +const db = new Database(DB_PATH); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +// Make sure the schema exists (same as src/lib/server/db.ts). +db.exec(` + CREATE TABLE IF NOT EXISTS accounts ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE, + plan TEXT NOT NULL DEFAULT 'free', + created_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS api_keys ( + id TEXT PRIMARY KEY, + account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + key_hash TEXT NOT NULL UNIQUE, + key_prefix TEXT NOT NULL, + name TEXT NOT NULL, + scopes TEXT NOT NULL DEFAULT '["*"]', + active INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + last_used_at INTEGER + ); + CREATE INDEX IF NOT EXISTS idx_api_keys_account ON api_keys(account_id); + CREATE TABLE IF NOT EXISTS magic_links ( + id TEXT PRIMARY KEY, + account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + token TEXT NOT NULL UNIQUE, + expires_at INTEGER NOT NULL, + used INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_magic_links_token ON magic_links(token); + CREATE TABLE IF NOT EXISTS usage_events ( + id TEXT PRIMARY KEY, + api_key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, + dataset TEXT NOT NULL, + timestamp INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_usage_key ON usage_events(api_key_id); + CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_events(timestamp); + CREATE INDEX IF NOT EXISTS idx_usage_dataset ON usage_events(dataset); +`); + +const now = Date.now(); + +const ALPHABET = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +function mintKey() { + const bytes = randomBytes(32); + let random = ''; + for (let i = 0; i < 32; i++) { + random += ALPHABET[bytes[i] % ALPHABET.length]; + } + const key = `ti_${random}`; + const hash = createHash('sha256').update(key).digest('hex'); + const prefix = key.slice(0, 8); + return { key, hash, prefix }; +} + +// --- find or create an account --- + +/** @typedef {{ id: string, plan: string }} AccountRow */ +/** @typedef {{ id: string, name: string }} KeyRow */ + +let account = /** @type {AccountRow | undefined} */ ( + db + .prepare(`SELECT id, plan FROM accounts ORDER BY created_at ASC LIMIT 1`) + .get() +); + +if (!account) { + const id = randomUUID(); + db.prepare( + `INSERT INTO accounts (id, email, plan, created_at) VALUES (?, NULL, 'dev', ?)` + ).run(id, now - 1000 * 60 * 60 * 24 * 14); // created 2 weeks ago + account = { id, plan: 'dev' }; + console.log(`created demo account ${id}`); +} else { + console.log(`seeding against existing account ${account.id}`); +} + +// --- make sure there are at least two active keys --- + +const existingKeys = /** @type {KeyRow[]} */ ( + db + .prepare(`SELECT id, name FROM api_keys WHERE account_id = ? AND active = 1`) + .all(account.id) +); + +/** @type {KeyRow[]} */ +const keyRows = + existingKeys.length >= 2 + ? existingKeys + : (() => { + /** @type {KeyRow[]} */ + const created = []; + for (const name of ['production ingest', 'research notebook']) { + const { hash, prefix } = mintKey(); + const id = randomUUID(); + db.prepare( + `INSERT INTO api_keys + (id, account_id, key_hash, key_prefix, name, scopes, active, created_at) + VALUES (?, ?, ?, ?, ?, '["*"]', 1, ?)` + ).run(id, account.id, hash, prefix, name, now - 1000 * 60 * 60 * 24 * 10); + created.push({ id, name }); + } + return [...existingKeys, ...created]; + })(); + +// --- wipe usage events we seeded previously and re-generate --- + +db.prepare( + `DELETE FROM usage_events WHERE api_key_id IN + (SELECT id FROM api_keys WHERE account_id = ?)` +).run(account.id); + +// Generate roughly 2,400 events distributed over the past 30 days, heavily +// weighted toward irs-990 and sec-edgar (the flagship datasets). +const weights = { + 'irs-990': 28, + 'sec-edgar': 22, + 'sec-13f': 14, + 'fec-contributions': 10, + 'nih-reporter': 8, + 'pacer': 6, + 'fda-faers': 4, + 'osha': 3, + 'usaspending': 3, + 'cfpb-complaints': 2 +}; +const weighted = /** @type {string[]} */ ([]); +for (const [slug, w] of Object.entries(weights)) { + for (let i = 0; i < w; i++) weighted.push(slug); +} + +const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; +const insertEvent = db.prepare( + `INSERT INTO usage_events (id, api_key_id, dataset, timestamp) VALUES (?, ?, ?, ?)` +); + +const TOTAL = 2400; +const tx = db.transaction(() => { + for (let i = 0; i < TOTAL; i++) { + const dataset = weighted[Math.floor(Math.random() * weighted.length)]; + const keyRow = keyRows[Math.floor(Math.random() * keyRows.length)]; + const age = Math.floor(Math.random() * THIRTY_DAYS); + insertEvent.run(randomUUID(), keyRow.id, dataset, now - age); + } +}); +tx(); + +// Update last_used_at on each key to the most recent event timestamp. +for (const row of keyRows) { + const latest = /** @type {{ ts: number | null } | undefined} */ ( + db + .prepare( + `SELECT MAX(timestamp) AS ts FROM usage_events WHERE api_key_id = ?` + ) + .get(row.id) + ); + if (latest?.ts) { + db.prepare(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`).run( + latest.ts, + row.id + ); + } +} + +console.log(`seeded ${TOTAL} usage events against ${keyRows.length} keys`); +console.log(`db: ${DB_PATH}`); +db.close(); |
