diff --git a/package.json b/package.json index 0c4ab95..c3d7f69 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "db:migrate": "node scripts/migrate.js", "db:seed": "node scripts/seed.js", "db:seed:roadmap": "node scripts/seed-roadmap.js", - "db:setup": "node scripts/migrate.js && node scripts/seed.js && node scripts/seed-roadmap.js" + "db:seed:demo": "node scripts/seed-demo.js", + "db:setup": "node scripts/migrate.js && node scripts/seed.js && node scripts/seed-roadmap.js && node scripts/seed-demo.js" }, "dependencies": { "@astrojs/node": "^8.3.0", diff --git a/scripts/seed-demo.js b/scripts/seed-demo.js new file mode 100644 index 0000000..4c33c68 --- /dev/null +++ b/scripts/seed-demo.js @@ -0,0 +1,138 @@ +#!/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(); diff --git a/src/pages/pulse.astro b/src/pages/pulse.astro index ebb562e..ef370ea 100644 --- a/src/pages/pulse.astro +++ b/src/pages/pulse.astro @@ -119,10 +119,15 @@ const shippedCount = countShippedAttributions(user.id); // ── Members in the room ──────────────────────────────────────────── const allMembers = getAllUsersPublic(); +// SQL stores 'YYYY-MM-DD HH:MM:SS' UTC; new Date() would parse as local — coerce to UTC ISO first. +function sqlToUtcDate(s: string): Date { + if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s); + return new Date(s.replace(' ', 'T') + 'Z'); +} const onlineOthers = allMembers.filter(u => u.id !== user.id && u.last_seen_at - && (Date.now() - new Date(u.last_seen_at).getTime()) < 5 * 60_000 + && (Date.now() - sqlToUtcDate(u.last_seen_at).getTime()) < 5 * 60_000 ); const visibleChips = onlineOthers.slice(0, 4); const overflowCount = Math.max(0, onlineOthers.length - visibleChips.length); @@ -154,7 +159,7 @@ function formatEventDate(iso: string): string { {greeting}

- You've been a council member for {tenure}. The team is reading every note you leave. + You've been a member for {tenure}. The team is reading every note you leave.