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>
This commit is contained in:
parent
f6e7337c5e
commit
fe27811d16
3 changed files with 147 additions and 3 deletions
|
|
@ -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",
|
||||
|
|
|
|||
138
scripts/seed-demo.js
Normal file
138
scripts/seed-demo.js
Normal file
|
|
@ -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();
|
||||
|
|
@ -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 {
|
|||
<span class="greeting-italic">{greeting}</span>
|
||||
</h1>
|
||||
<p class="greeting-sub body-md">
|
||||
You've been a council member for <em>{tenure}</em>. The team is reading every note you leave.
|
||||
You've been a member for <em>{tenure}</em>. The team is reading every note you leave.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue