scripts/seed-demo.js — one open Pulse with realistic context, marks "Traceability layer" as shipping with shipped_at -2 days and attributes it to the cab user, two events (dinner in 5 weeks, office hours in 2 weeks), six hand-crafted activity rows mixing all 5 activity kinds so the ticker has something to scroll on first load. Idempotent: skips if any pulses exist. Backdates Lars's cab_joined_date so the greeting renders "2 years, 4 months". Wired to db:setup and db:seed:demo. Also fixes a parse bug on /pulse: SQL stores last_seen_at as 'YYYY-MM-DD HH:MM:SS' UTC, but new Date(string) parses that as local time — on a non-UTC server the freshness check was wrong by the server's offset. Coerce to UTC ISO before parsing. Manual smoke as Lars now shows two member chips in "online now"; admin tabs all render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
6 KiB
JavaScript
138 lines
6 KiB
JavaScript
#!/usr/bin/env node
|
||
// Demo seed for first-load credibility: one open pulse, one shipped roadmap
|
||
// item attributed to the cab user, one dinner + one office hours event, and
|
||
// a handful of hand-crafted activity rows so the ticker has something to
|
||
// scroll on a fresh demo.
|
||
//
|
||
// Idempotent: skips if a pulse already exists. Run AFTER scripts/seed.js
|
||
// and scripts/seed-roadmap.js (or via `pnpm db:setup`).
|
||
|
||
import Database from 'better-sqlite3';
|
||
import { join, dirname } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const dbPath = process.env.BIFROST_DB_PATH ?? join(__dirname, '..', 'bifrost.db');
|
||
const db = new Database(dbPath);
|
||
db.pragma('foreign_keys = ON');
|
||
|
||
const existing = db.prepare('SELECT COUNT(*) AS n FROM pulses').get().n;
|
||
if (existing > 0) {
|
||
console.log(` demo data already present (${existing} pulse(s)) — skipping.`);
|
||
db.close();
|
||
process.exit(0);
|
||
}
|
||
|
||
const users = db.prepare("SELECT id, name, role FROM users WHERE active = 1").all();
|
||
const byRole = (r) => users.find(u => u.role === r);
|
||
const mette = byRole('pilot');
|
||
const lars = byRole('cab');
|
||
const jon = byRole('fenja');
|
||
|
||
if (!mette || !lars || !jon) {
|
||
console.error(' seed.js users not found — run `pnpm db:seed` first.');
|
||
process.exit(1);
|
||
}
|
||
|
||
// Backdate Lars's cab membership to give realistic tenure on /pulse
|
||
db.prepare(`UPDATE users SET cab_joined_date = date('now', '-2 years', '-4 months') WHERE id = ?`).run(lars.id);
|
||
// Mark all three as recently seen so the "online now" chip strip has content
|
||
// (current viewer is excluded from "others online" — see /pulse)
|
||
db.prepare(`UPDATE users SET last_seen_at = datetime('now', '-2 minutes') WHERE id IN (?, ?, ?)`)
|
||
.run(lars.id, mette.id, jon.id);
|
||
|
||
const nowIso = (offsetSeconds = 0) => {
|
||
const d = new Date(Date.now() + offsetSeconds * 1000);
|
||
return d.toISOString().replace('T', ' ').slice(0, 19);
|
||
};
|
||
|
||
// ── Pulse: open now, closes in 5 days ────────────────────────────────
|
||
const opensAt = nowIso(-3600); // opened an hour ago
|
||
const closesAt = nowIso(5 * 24 * 3600); // closes in 5 days
|
||
const options = [
|
||
'Locking down on-prem deployment first',
|
||
'Pushing the traceability layer to GA',
|
||
'Going wide on document ingestion',
|
||
'Building the agentic query loop',
|
||
];
|
||
|
||
const pulseId = db.prepare(`
|
||
INSERT INTO pulses (question, context, options, opens_at, closes_at, status, created_by)
|
||
VALUES (?,?,?,?,?,?,?)
|
||
`).run(
|
||
'Which milestone should we anchor Q3 around?',
|
||
'Council input on this directly shapes what the team works on in July–September. Read the roadmap before voting.',
|
||
JSON.stringify(options),
|
||
opensAt,
|
||
closesAt,
|
||
'open',
|
||
jon.id,
|
||
).lastInsertRowid;
|
||
|
||
// Lars votes for the traceability option
|
||
db.prepare('INSERT INTO votes (pulse_id, user_id, option_index, voted_at) VALUES (?,?,?,?)')
|
||
.run(pulseId, lars.id, 1, nowIso(-2 * 3600));
|
||
|
||
// ── Roadmap: mark "Traceability layer" as shipping, attribute to Lars ──
|
||
const traceability = db.prepare("SELECT id FROM roadmap_items WHERE title LIKE 'Traceability%'").get();
|
||
if (traceability) {
|
||
db.prepare(`UPDATE roadmap_items SET status = 'shipping', shipped_at = datetime('now', '-2 days'), target = 'Live now' WHERE id = ?`).run(traceability.id);
|
||
db.prepare('INSERT OR IGNORE INTO roadmap_attributions (roadmap_item_id, user_id) VALUES (?,?)').run(traceability.id, lars.id);
|
||
}
|
||
|
||
// ── Events ────────────────────────────────────────────────────────────
|
||
const dinnerStart = nowIso(38 * 24 * 3600); // ~5.5 weeks out
|
||
db.prepare(`
|
||
INSERT INTO events (slug, title, kind, description, location, starts_at, capacity, created_by)
|
||
VALUES (?,?,?,?,?,?,?,?)
|
||
`).run(
|
||
'kickoff-dinner-2026-06',
|
||
'Council kickoff dinner',
|
||
'dinner',
|
||
'A private dinner at the studio. Conversation about what we ship next, no slides.',
|
||
'Studio, Refshalevej · Copenhagen',
|
||
dinnerStart,
|
||
12,
|
||
jon.id,
|
||
);
|
||
const dinnerId = db.prepare("SELECT id FROM events WHERE slug = 'kickoff-dinner-2026-06'").get().id;
|
||
|
||
const officeHoursStart = nowIso(14 * 24 * 3600); // 2 weeks out
|
||
db.prepare(`
|
||
INSERT INTO events (slug, title, kind, description, location, starts_at, created_by)
|
||
VALUES (?,?,?,?,?,?,?)
|
||
`).run(
|
||
'office-hours-2026-05',
|
||
'Office hours with the founder',
|
||
'office_hours',
|
||
'30-minute one-on-one slots. Open agenda. Book one or just drop by.',
|
||
'Virtual (link sent after RSVP)',
|
||
officeHoursStart,
|
||
jon.id,
|
||
);
|
||
const officeHoursId = db.prepare("SELECT id FROM events WHERE slug = 'office-hours-2026-05'").get().id;
|
||
|
||
// ── Activity rows ─────────────────────────────────────────────────────
|
||
// Mix of real (Lars's vote, Jonathan's publish, Jonathan's ship) and
|
||
// hand-crafted demo rows so the ticker has six items to scroll.
|
||
const insertActivity = db.prepare(`
|
||
INSERT INTO activity (actor_id, kind, subject_type, subject_id, created_at)
|
||
VALUES (?,?,?,?,?)
|
||
`);
|
||
|
||
insertActivity.run(jon.id, 'pulse_opened', 'pulse', pulseId, nowIso(-3600));
|
||
insertActivity.run(lars.id, 'voted', 'pulse', pulseId, nowIso(-2 * 3600));
|
||
if (traceability) {
|
||
insertActivity.run(jon.id, 'roadmap_shipped', 'roadmap', traceability.id, nowIso(-2 * 24 * 3600));
|
||
}
|
||
insertActivity.run(lars.id, 'rsvped', 'event', dinnerId, nowIso(-8 * 3600));
|
||
insertActivity.run(mette.id,'rsvped', 'event', officeHoursId, nowIso(-30 * 60));
|
||
insertActivity.run(jon.id, 'booked_office_hours', 'event', officeHoursId, nowIso(-1 * 24 * 3600));
|
||
|
||
console.log(' demo data seeded:');
|
||
console.log(` pulse #${pulseId} (open, closes in 5 days)`);
|
||
if (traceability) console.log(` roadmap #${traceability.id} → shipping, attributed to ${lars.name}`);
|
||
console.log(` events: kickoff-dinner-2026-06, office-hours-2026-05`);
|
||
console.log(` activity: 6 rows`);
|
||
|
||
db.close();
|