The previous seed conflated status='shipping' with 'about to ship' — 'Audit log export' was status=shipping target='Next week' which is actually a queued release, not a live one. Refined so: status='shipping' → live in production now status='in_beta' → not yet live, GA target set status='exploring' → on the long horizon status='considering' → not committed Updated distribution: 2 shipping / 2 in_beta / 3 exploring / 2 considering. travelledStop = (1 + 0.5) / 9 ≈ 0.17, so the gradient visibly transitions from travelled to ahead right at the 'you are here' marker — the visual story matches the data. Targets rewritten to read in this new register: - Traceability layer Live since March - Document ingestion Live since late May ← .rr-current - Audit log export GA next week (now in_beta) - Agentic query mode July - Contextual memory Q3 2026 - Multi-organisation graphs Q3 2026 - Multi-tenant isolation Q4 2026 - Federated learning hooks 2027 (considering) - Open evaluation framework 2027 (considering) Descriptions rewritten so the In motion strip pulls a meaningful first sentence from item #2 — 'Indexing PDF, Word, and plain text with proper chunking.' shipped_at backdated on items 1-2 only (60 days / 7 days ago), so the .rr-current marker lands on the most recently-shipped item (Document ingestion), not the about-to-GA in_beta item. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
383 lines
20 KiB
JavaScript
383 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
||
// Phase 2 demo seed — produces the visual state described in SPEC §Phase 2:
|
||
// 4 cab members with title/pull_quote/focus_tags/member_number, 1 active
|
||
// pulse (2 of 4 voted), 4 roadmap items (1 shipping / 1 beta / 2 exploring),
|
||
// 3 contributions with reactions, 4 dispatches at staggered ages, 1 hero
|
||
// event + 1 studio hours + 1 working session + 2 past events.
|
||
//
|
||
// Destructive in scope: wipes the data tables it owns then re-inserts.
|
||
// Users (created by seed.js) are kept; new ones are added with INSERT OR
|
||
// IGNORE. Idempotent: re-running produces the same demo state.
|
||
|
||
import Database from 'better-sqlite3';
|
||
import bcrypt from 'bcryptjs';
|
||
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');
|
||
|
||
// ── Wipe the tables this seed owns ─────────────────────────────────
|
||
db.exec(`
|
||
DELETE FROM activity;
|
||
DELETE FROM attendance;
|
||
DELETE FROM reactions;
|
||
DELETE FROM replies;
|
||
DELETE FROM contributions;
|
||
DELETE FROM roadmap_attributions;
|
||
DELETE FROM roadmap_items;
|
||
DELETE FROM votes;
|
||
DELETE FROM pulses;
|
||
DELETE FROM dispatches;
|
||
DELETE FROM events;
|
||
`);
|
||
|
||
// ── Locate canonical users from seed.js ────────────────────────────
|
||
const allUsers = db.prepare("SELECT id, name, role FROM users WHERE active = 1").all();
|
||
const byRole = (r) => allUsers.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);
|
||
}
|
||
|
||
// ── Add 3 additional CAB members, then populate metadata on all 4 ──
|
||
const ROUNDS = 10;
|
||
const hash = bcrypt.hashSync('cab123', ROUNDS);
|
||
|
||
function kebab(s) {
|
||
return s.toLowerCase()
|
||
.normalize('NFKD').replace(/[̀-ͯ]/g, '')
|
||
.replace(/[^a-z0-9]+/g, '-')
|
||
.replace(/^-+|-+$/g, '');
|
||
}
|
||
|
||
const newCabs = [
|
||
{ name: 'Anna Kjær', email: 'anna@virk3.dk', org: 'Virksomhed 3' },
|
||
{ name: 'Søren Vedel', email: 'soren@virk4.dk', org: 'Virksomhed 4' },
|
||
{ name: 'Henriette Rask',email: 'henriette@virk5.dk',org: 'Virksomhed 5' },
|
||
{ name: 'Mads Lindberg', email: 'mads@virk6.dk', org: 'Virksomhed 6' },
|
||
{ name: 'Camilla Storm', email: 'camilla@virk7.dk', org: 'Virksomhed 7' },
|
||
{ name: 'Frederik Lund', email: 'frederik@virk8.dk', org: 'Virksomhed 8' },
|
||
];
|
||
|
||
const insertUser = db.prepare(`
|
||
INSERT OR IGNORE INTO users (email, password_hash, name, organisation, role, slug, cab_joined_date)
|
||
VALUES (?, ?, ?, ?, 'cab', ?, NULL)
|
||
`);
|
||
for (const c of newCabs) {
|
||
insertUser.run(c.email, hash, c.name, c.org, kebab(c.name));
|
||
}
|
||
|
||
// Allocate member_numbers in member-since order, tiebreak id asc.
|
||
// We backdate cab_joined_date first, then let allocateMemberNumber pick it up.
|
||
// Lars: 0 weeks ago (most senior), then 2 / 4 / 6 weeks for the others.
|
||
const cabRows = db.prepare("SELECT id, email, name FROM users WHERE role = 'cab' AND active = 1 ORDER BY id").all();
|
||
const tenureWeeks = {
|
||
'lars@virk2.dk': 24,
|
||
'anna@virk3.dk': 14,
|
||
'soren@virk4.dk': 12,
|
||
'henriette@virk5.dk': 10,
|
||
'mads@virk6.dk': 8,
|
||
'camilla@virk7.dk': 6,
|
||
'frederik@virk8.dk': 3,
|
||
};
|
||
|
||
const setCabMeta = db.prepare(`
|
||
UPDATE users
|
||
SET cab_joined_date = date('now', ?),
|
||
title = ?,
|
||
pull_quote = ?,
|
||
focus_tags = ?,
|
||
last_seen_at = datetime('now', '-2 minutes')
|
||
WHERE id = ?
|
||
`);
|
||
|
||
const cabMeta = {
|
||
'lars@virk2.dk': {
|
||
title: 'Senior Adviser, Operational Risk',
|
||
pull_quote: 'A model is only as auditable as the chain of evidence behind it. That chain is the work.',
|
||
focus_tags: ['Risk', 'Audit trail', 'GDPR'],
|
||
},
|
||
'anna@virk3.dk': {
|
||
title: 'Director of Digital Services',
|
||
pull_quote: 'Municipalities can\'t outsource sovereignty. We need tools that assume that.',
|
||
focus_tags: ['Public sector', 'Sovereignty'],
|
||
},
|
||
'soren@virk4.dk': {
|
||
title: 'Head of Data Engineering',
|
||
pull_quote: 'Make it boring to deploy and surprising to query.',
|
||
focus_tags: ['Infrastructure', 'Telemetry'],
|
||
},
|
||
'henriette@virk5.dk': {
|
||
title: 'Lead Counsel, Compliance',
|
||
pull_quote: 'I\'ve never trusted a system I couldn\'t cross-examine.',
|
||
focus_tags: ['Legal', 'Policy', 'EU AI Act'],
|
||
},
|
||
'mads@virk6.dk': {
|
||
title: 'Chief Strategy Officer',
|
||
pull_quote: 'Healthcare runs on consent — and consent runs on trust.',
|
||
focus_tags: ['Healthcare', 'Consent', 'Governance'],
|
||
},
|
||
'camilla@virk7.dk': {
|
||
title: 'Head of Cyber Resilience',
|
||
pull_quote: 'Cyber resilience is not a feature — it is the substrate.',
|
||
focus_tags: ['Defence', 'Resilience'],
|
||
},
|
||
'frederik@virk8.dk': {
|
||
title: 'Director of Public Innovation',
|
||
pull_quote: 'Public innovation succeeds when it is measurably better, not just newer.',
|
||
focus_tags: ['Public sector', 'Measurement'],
|
||
},
|
||
};
|
||
|
||
for (const u of cabRows) {
|
||
const m = cabMeta[u.email] ?? null;
|
||
if (!m) continue;
|
||
const weeks = tenureWeeks[u.email] ?? 0;
|
||
setCabMeta.run(`-${weeks} weeks`, m.title, m.pull_quote, JSON.stringify(m.focus_tags), u.id);
|
||
}
|
||
|
||
// Allocate member_numbers using the SQL backfill from migration 0004,
|
||
// but only for cab rows that still lack one (idempotent).
|
||
db.exec(`
|
||
WITH ranked AS (
|
||
SELECT id,
|
||
ROW_NUMBER() OVER (ORDER BY COALESCE(cab_joined_date, created_at) ASC, id ASC) AS rn
|
||
FROM users WHERE role = 'cab'
|
||
)
|
||
UPDATE users
|
||
SET member_number = (SELECT rn FROM ranked WHERE ranked.id = users.id)
|
||
WHERE role = 'cab' AND member_number IS NULL;
|
||
`);
|
||
|
||
// Mark Mette + Jonathan recently seen too (for the avatar pile on /events)
|
||
db.prepare("UPDATE users SET last_seen_at = datetime('now', '-2 minutes') WHERE id IN (?, ?)").run(mette.id, jon.id);
|
||
|
||
// Re-fetch cab rows ordered by member_number
|
||
const cabs = db.prepare("SELECT id, name, email, member_number FROM users WHERE role = 'cab' AND active = 1 ORDER BY member_number ASC").all();
|
||
console.log(` cab members: ${cabs.map(c => `${c.name} #${c.member_number}`).join(', ')}`);
|
||
|
||
// ── Helper: SQL datetime string at offset seconds from now ─────────
|
||
const nowIso = (offsetSeconds = 0) => {
|
||
const d = new Date(Date.now() + offsetSeconds * 1000);
|
||
return d.toISOString().replace('T', ' ').slice(0, 19);
|
||
};
|
||
|
||
// ── Poll attached to a dispatch (the decision) — open, 2/4 voted ──
|
||
// Polls are no longer standalone; they attach to a dispatch via pulse_id.
|
||
// We create the pulse first, capture its id, and stamp it on the dispatch
|
||
// when we INSERT it further down.
|
||
const pulseOptions = [
|
||
'Locking down on-prem deployment first',
|
||
'Pushing the traceability layer to GA',
|
||
'Going wide on document ingestion',
|
||
'Building the agentic query loop',
|
||
];
|
||
const decisionPulseId = 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(pulseOptions),
|
||
nowIso(-3600), nowIso(5 * 24 * 3600), 'open', jon.id,
|
||
).lastInsertRowid;
|
||
|
||
// 2 votes — Lars and Anna
|
||
db.prepare('INSERT INTO votes (pulse_id, user_id, option_index, voted_at) VALUES (?,?,?,?)')
|
||
.run(decisionPulseId, cabs[0].id, 1, nowIso(-2 * 3600));
|
||
db.prepare('INSERT INTO votes (pulse_id, user_id, option_index, voted_at) VALUES (?,?,?,?)')
|
||
.run(decisionPulseId, cabs[1].id, 1, nowIso(-30 * 60));
|
||
|
||
// ── Roadmap: 9 items, status meaning 'currently live' rather than
|
||
// 'shipping soon'. Items 1-2 are live in production; items 3-4 are in
|
||
// beta even if 'audit log export' has a near-term GA target. Travelled
|
||
// stop = (1 + 0.5) / 9 ≈ 0.17, putting the 'you are here' marker at
|
||
// the visible transition between travelled and ahead tones on the path.
|
||
const roadmap = [
|
||
{ title: 'Traceability layer', description: 'Every inference call writes a signed audit record. Shaped by Lars in our March session.', status: 'shipping', target: 'Live since March', display_order: 1, shipped_at: nowIso(-60 * 24 * 3600), attributed: [cabs[0].id], metadata_text: 'Shaped by Lars in our March session' },
|
||
{ title: 'Document ingestion', description: "Indexing PDF, Word, and plain text with proper chunking. Pilot-tested with Mette's team.", status: 'shipping', target: 'Live since late May', display_order: 2, shipped_at: nowIso(-7 * 24 * 3600), attributed: [cabs[1].id, cabs[2].id], metadata_text: "Pilot-tested with Mette's team" },
|
||
{ title: 'Audit log export', description: 'Stream the signed records to your own S3 or on-prem object store.', status: 'in_beta', target: 'GA next week', display_order: 3, shipped_at: null, attributed: [cabs[3].id], metadata_text: 'Builds on traceability layer' },
|
||
{ title: 'Agentic query mode', description: 'Multi-step retrieval over locked, on-prem document stores. Currently testing with two pilot organisations.', status: 'in_beta', target: 'July', display_order: 4, shipped_at: null, attributed: [cabs[1].id], metadata_text: 'Request beta access →' },
|
||
{ title: 'Contextual memory', description: 'Sessions that remember constraints between calls without leaking context across organisational boundaries.', status: 'exploring', target: 'Q3 2026', display_order: 5, shipped_at: null, attributed: [cabs[3].id], metadata_text: '2 council requests' },
|
||
{ title: 'Multi-organisation graphs', description: 'Permission-controlled knowledge spaces for departments within a single deployment.', status: 'exploring', target: 'Q3 2026', display_order: 6, shipped_at: null, attributed: [cabs[4].id], metadata_text: 'Open question on key custody' },
|
||
{ title: 'Multi-tenant isolation', description: 'Cryptographic separation between sub-organisations on shared infrastructure.', status: 'exploring', target: 'Q4 2026', display_order: 7, shipped_at: null, attributed: [cabs[5].id], metadata_text: null },
|
||
{ title: 'Federated learning hooks', description: 'Let aligned organisations train on shared signal without sharing the underlying data.', status: 'considering', target: '2027', display_order: 8, shipped_at: null, attributed: [], metadata_text: 'Council input wanted' },
|
||
{ title: 'Open evaluation framework', description: 'A public benchmark suite for compliant-AI use in regulated industries.', status: 'considering', target: '2027', display_order: 9, shipped_at: null, attributed: [], metadata_text: 'Long-term direction' },
|
||
];
|
||
|
||
const insertRoad = db.prepare(`
|
||
INSERT INTO roadmap_items (title, description, status, target, display_order, shipped_at, metadata_text)
|
||
VALUES (?,?,?,?,?,?,?)
|
||
`);
|
||
const insertAttr = db.prepare('INSERT INTO roadmap_attributions (roadmap_item_id, user_id) VALUES (?,?)');
|
||
for (const r of roadmap) {
|
||
const id = Number(insertRoad.run(r.title, r.description, r.status, r.target, r.display_order, r.shipped_at, r.metadata_text).lastInsertRowid);
|
||
for (const uid of r.attributed) insertAttr.run(id, uid);
|
||
}
|
||
|
||
// ── Contributions: 3 entries; most recent gets 3 reactions ─────────
|
||
const contribs = [
|
||
{ user_id: cabs[2].id, type: 'idea',
|
||
body_md: 'What if we let council members write **inline annotations** on the roadmap items they shaped? A trail of "here\'s what I pushed for."',
|
||
when: nowIso(-2 * 60 * 60), reactors: [cabs[0].id, cabs[1].id, jon.id] },
|
||
{ user_id: cabs[1].id, type: 'question',
|
||
body_md: 'How will the traceability layer handle documents with conflicting metadata across versions?',
|
||
when: nowIso(-2 * 24 * 60 * 60) },
|
||
{ user_id: cabs[0].id, type: 'inspiration',
|
||
body_md: 'A piece on **institutional memory** in regulated industries — it lines up almost exactly with what we\'re trying to build.',
|
||
when: nowIso(-9 * 24 * 60 * 60) },
|
||
];
|
||
const insertContrib = db.prepare(`
|
||
INSERT INTO contributions (user_id, type, body_md, created_at) VALUES (?,?,?,?)
|
||
`);
|
||
const insertReact = db.prepare('INSERT INTO reactions (user_id, contribution_id) VALUES (?,?)');
|
||
for (const c of contribs) {
|
||
const id = Number(insertContrib.run(c.user_id, c.type, c.body_md, c.when).lastInsertRowid);
|
||
for (const uid of c.reactors ?? []) insertReact.run(uid, id);
|
||
}
|
||
|
||
// ── Dispatches: 4 published at staggered ages ──────────────────────
|
||
const dispatchSeed = [
|
||
{ kind: 'decision', ageDays: 2,
|
||
title: 'We are deprioritising public-cloud parity for Q3',
|
||
excerpt: 'After three weeks of pilot feedback, the team is locking the platform to on-prem and Hetzner sovereign cloud for the next quarter.',
|
||
body: `After three weeks of pilot feedback — the kind of feedback that only happens when people are actually trying to deploy this thing — we are deprioritising public-cloud parity for Q3.
|
||
|
||
The signal was unambiguous. Every council member we spoke to in May has the same constraint: the data cannot leave their network. AWS, Azure, GCP are non-starters not because of price but because of jurisdiction.
|
||
|
||
So for Q3 the platform supports two deployment targets only — on-prem inside the customer's own VPC, and our Hetzner sovereign cloud in Helsinki. Everything else is parked. The agentic query work moves up a quarter to fill the gap.
|
||
|
||
We'll revisit public cloud in Q4 once the on-prem story is boring.`,
|
||
},
|
||
{ kind: 'behind_the_scenes', ageDays: 5,
|
||
title: 'A morning at the council kickoff',
|
||
excerpt: 'Four members in the room, two on video. The whiteboard ended up with three lists: must-have, would-be-nice, do-not-build.',
|
||
body: `Four council members in the room, two on video. The kickoff meeting was meant to be 90 minutes. It went four hours.
|
||
|
||
The whiteboard ended up with three lists: must-have, would-be-nice, and — the most interesting one — do-not-build.
|
||
|
||
Henriette pushed back hard on the "agent that emails on your behalf" pattern. "I don't want a system speaking on my legal team's behalf. Ever." That note alone reshaped a whole feature.
|
||
|
||
Photos to come, with permission.`,
|
||
},
|
||
{ kind: 'update', ageDays: 9,
|
||
title: 'Document ingestion is now feature-complete in beta',
|
||
excerpt: 'PDF, Word, plain text. Chunking, metadata, deduplication, basic OCR. Three pilots have run it against their corpora.',
|
||
body: `Document ingestion is feature-complete in beta. PDF, Word, plain text. Chunking, metadata extraction, deduplication, and basic OCR for scanned PDFs.
|
||
|
||
Three pilots have now run it against their internal corpora — biggest was 47,000 documents, smallest was 380. Both worked. The 47k run took 8 hours and surfaced some neat edge cases (mostly around tables that span pages).
|
||
|
||
Next week we open it to the full pilot group. We'll need notes.`,
|
||
},
|
||
{ kind: 'note', ageDays: 12,
|
||
title: 'Welcome to the council',
|
||
excerpt: 'A short note to mark the start. This page will fill up with decisions, half-built ideas, and things we have changed our minds about.',
|
||
body: `This page will fill up with decisions, half-built ideas, and things we have changed our minds about.
|
||
|
||
It is not a blog. It is the studio talking to the room — short, dated, signed.`,
|
||
},
|
||
];
|
||
|
||
const fenjas = db.prepare("SELECT id FROM users WHERE role = 'fenja' AND active = 1 ORDER BY id").all();
|
||
const insertDispatch = db.prepare(`
|
||
INSERT INTO dispatches (title, body, excerpt, kind, author_id, status, published_at, created_at, updated_at, pulse_id)
|
||
VALUES (?,?,?,?,?,'published',?,?,?,?)
|
||
`);
|
||
for (let i = 0; i < dispatchSeed.length; i += 1) {
|
||
const d = dispatchSeed[i];
|
||
const when = nowIso(-d.ageDays * 24 * 60 * 60);
|
||
const authorId = fenjas[i % fenjas.length].id;
|
||
// Attach the decision-pulse to the decision dispatch — this is the demo
|
||
// case for polls-as-articles. Other dispatches stay poll-free.
|
||
const attachedPulse = d.kind === 'decision' ? decisionPulseId : null;
|
||
insertDispatch.run(d.title, d.body, d.excerpt, d.kind, authorId, when, when, when, attachedPulse);
|
||
}
|
||
|
||
// ── Events: 1 hero dinner, 1 studio hours, 1 working session, 2 past
|
||
const insertEvent = db.prepare(`
|
||
INSERT INTO events (slug, title, kind, description, location, starts_at, ends_at, capacity, photo_url, audience, duration_label, action_label, notes_url, created_by)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||
`);
|
||
|
||
insertEvent.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',
|
||
nowIso(38 * 24 * 3600), null, 12, null,
|
||
'Members only', null, null, null,
|
||
jon.id,
|
||
);
|
||
const dinnerSlug = 'kickoff-dinner-2026-06';
|
||
|
||
insertEvent.run(
|
||
'studio-hours-2026-05', 'Studio hours with Jonathan', 'office_hours',
|
||
'30-minute slots. Open agenda. Drop in when you\'ve got something to talk through.',
|
||
'Virtual (link sent after RSVP)',
|
||
nowIso(14 * 24 * 3600), null, null, null,
|
||
'Council members', '30 minutes', null, null,
|
||
jon.id,
|
||
);
|
||
|
||
insertEvent.run(
|
||
'working-session-2026-06', 'Working session: traceability UX', 'working_session',
|
||
'Three-person session to walk through the new traceability UI before it lands in beta.',
|
||
'Studio, Refshalevej · Copenhagen',
|
||
nowIso(21 * 24 * 3600), null, 6, null,
|
||
'Council + Fenja team', '90 minutes', 'RSVP →', null,
|
||
jon.id,
|
||
);
|
||
|
||
insertEvent.run(
|
||
'apr-roundtable-2026', 'April roundtable', 'summit',
|
||
'A half-day session anchored around the EU AI Act compliance roadmap.',
|
||
'Studio, Refshalevej · Copenhagen',
|
||
nowIso(-21 * 24 * 3600), null, 10, null,
|
||
'Members only', null, null, 'https://example.invalid/notes/april',
|
||
jon.id,
|
||
);
|
||
const aprilSlug = 'apr-roundtable-2026';
|
||
|
||
insertEvent.run(
|
||
'march-launch-dinner', 'Launch dinner', 'dinner',
|
||
'The first dinner. Where the council was formally introduced.',
|
||
'Aamanns Etablissement · Copenhagen',
|
||
nowIso(-52 * 24 * 3600), null, 10, null,
|
||
'Members only', null, null, null,
|
||
jon.id,
|
||
);
|
||
const marchSlug = 'march-launch-dinner';
|
||
|
||
// Past-event RSVPs (drives the "attended_count" on past cards)
|
||
const insertRsvp = db.prepare(`
|
||
INSERT INTO attendance (user_id, meeting_slug, kind, status, updated_at)
|
||
VALUES (?, ?, 'event', 'yes', datetime('now'))
|
||
`);
|
||
for (const c of cabs) insertRsvp.run(c.id, aprilSlug);
|
||
for (const c of cabs.slice(0, 3)) insertRsvp.run(c.id, marchSlug);
|
||
|
||
// Hero dinner: 1 confirmed so far (Lars) — keeps the avatar pile small at first
|
||
insertRsvp.run(cabs[0].id, dinnerSlug);
|
||
|
||
// ── Activity rows for the (now hidden but still-written) feed ──────
|
||
const insertActivity = db.prepare(`
|
||
INSERT INTO activity (actor_id, kind, subject_type, subject_id, created_at)
|
||
VALUES (?,?,?,?,?)
|
||
`);
|
||
insertActivity.run(jon.id, 'pulse_opened', 'pulse', decisionPulseId, nowIso(-3600));
|
||
insertActivity.run(cabs[0].id,'voted', 'pulse', decisionPulseId, nowIso(-2 * 3600));
|
||
insertActivity.run(cabs[1].id,'voted', 'pulse', decisionPulseId, nowIso(-30 * 60));
|
||
insertActivity.run(cabs[0].id,'rsvped', 'event', db.prepare("SELECT id FROM events WHERE slug = ?").get(dinnerSlug).id, nowIso(-8 * 3600));
|
||
|
||
console.log(` pulse #${decisionPulseId} open, 2 of ${cabs.length} voted`);
|
||
console.log(' roadmap: 9 items (2 shipping / 2 in_beta / 3 exploring / 2 considering)');
|
||
console.log(' contributions: 3 (most recent has 3 reactions)');
|
||
console.log(' dispatches: 4 published (2/5/9/12 days ago)');
|
||
console.log(' events: dinner + studio hours + working session, 2 past');
|
||
db.close();
|