project-bifrost-platform/scripts/seed-demo.js
Jonathan Hvid fe27811d16 chore(demo): seed-demo.js + utc fix for last_seen_at
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>
2026-05-11 15:04:11 +02:00

138 lines
6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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