diff --git a/scripts/seed-demo.js b/scripts/seed-demo.js
index dbd9286..aeab873 100644
--- a/scripts/seed-demo.js
+++ b/scripts/seed-demo.js
@@ -143,14 +143,17 @@ const nowIso = (offsetSeconds = 0) => {
return d.toISOString().replace('T', ' ').slice(0, 19);
};
-// ── Pulse: open now, closes in 5 days, 2 of 4 voted ────────────────
+// ── 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 pulseId = db.prepare(`
+const decisionPulseId = db.prepare(`
INSERT INTO pulses (question, context, options, opens_at, closes_at, status, created_by)
VALUES (?,?,?,?,?,?,?)
`).run(
@@ -160,11 +163,11 @@ const pulseId = db.prepare(`
nowIso(-3600), nowIso(5 * 24 * 3600), 'open', jon.id,
).lastInsertRowid;
-// 2 votes from cabs[0] and cabs[1]
+// 2 votes — Lars and Anna
db.prepare('INSERT INTO votes (pulse_id, user_id, option_index, voted_at) VALUES (?,?,?,?)')
- .run(pulseId, cabs[0].id, 1, nowIso(-2 * 3600));
+ .run(decisionPulseId, cabs[0].id, 1, nowIso(-2 * 3600));
db.prepare('INSERT INTO votes (pulse_id, user_id, option_index, voted_at) VALUES (?,?,?,?)')
- .run(pulseId, cabs[1].id, 1, nowIso(-30 * 60));
+ .run(decisionPulseId, cabs[1].id, 1, nowIso(-30 * 60));
// ── Roadmap: 1 shipping / 1 beta / 2 exploring, attributions ───────
const roadmap = [
@@ -249,14 +252,17 @@ 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)
- VALUES (?,?,?,?,?,'published',?,?,?)
+ 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;
- insertDispatch.run(d.title, d.body, d.excerpt, d.kind, authorId, when, when, when);
+ // 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
@@ -329,12 +335,12 @@ 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(cabs[0].id,'voted', 'pulse', pulseId, nowIso(-2 * 3600));
-insertActivity.run(cabs[1].id,'voted', 'pulse', pulseId, nowIso(-30 * 60));
+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 #' + pulseId + ' open, 2 of 4 voted');
+console.log(' pulse #' + decisionPulseId + ' open, 2 of 4 voted');
console.log(' roadmap: 1 shipping / 1 beta / 2 exploring');
console.log(' contributions: 3 (most recent has 3 reactions)');
console.log(' dispatches: 4 published (2/5/9/12 days ago)');
diff --git a/src/pages/dispatches/[slug].astro b/src/pages/dispatches/[slug].astro
index e889cfa..41a3abb 100644
--- a/src/pages/dispatches/[slug].astro
+++ b/src/pages/dispatches/[slug].astro
@@ -1,7 +1,10 @@
---
import AppLayout from '../../layouts/AppLayout.astro';
import Avatar from '../../components/Avatar.astro';
-import { getDispatchById, getAdjacentDispatches } from '../../lib/db';
+import {
+ getDispatchWithPoll, getAdjacentDispatches,
+ getPulseById, getUserVote, castVote, recordActivity, countCabMembers,
+} from '../../lib/db';
import {
parseDispatchSlug, dispatchSlug, dispatchKindLabel,
dispatchKindPigment, roleLabel,
@@ -14,15 +17,39 @@ const id = parseDispatchSlug(slugParam);
if (!id) return Astro.redirect('/dispatches');
-const d = getDispatchById(id);
+// Vote POST — handled before main render so we can refresh state
+if (Astro.request.method === 'POST') {
+ const data = await Astro.request.formData();
+ const action = String(data.get('action') ?? '');
+ if (action === 'vote') {
+ const pulseId = Number(data.get('pulse_id'));
+ const optionIndex = Number(data.get('option_index'));
+ const target = getPulseById(pulseId);
+ if (target && target.status === 'open' && Number.isInteger(optionIndex)
+ && optionIndex >= 0 && optionIndex < target.options.length
+ && getUserVote(pulseId, user.id) === null) {
+ castVote(pulseId, user.id, optionIndex);
+ recordActivity(user.id, 'voted', 'pulse', pulseId);
+ }
+ return Astro.redirect(Astro.url.pathname);
+ }
+}
+
+const d = getDispatchWithPoll(id, user.id);
if (!d || d.status !== 'published') return Astro.redirect('/dispatches');
// Canonical-redirect when the slug changes after a rename — id is the authority
const canonical = dispatchSlug(d);
if (slugParam !== canonical) return Astro.redirect(`/dispatches/${canonical}`);
+const totalMembers = countCabMembers();
const { prev, next } = getAdjacentDispatches(d.id);
+function closeDayLabel(closesAt: string): string {
+ const parsed = closesAt.includes('T') ? new Date(closesAt) : new Date(closesAt.replace(' ', 'T') + 'Z');
+ return new Intl.DateTimeFormat('en-GB', { weekday: 'long', timeZone: 'Europe/Copenhagen' }).format(parsed);
+}
+
function parseUtc(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');
@@ -63,6 +90,47 @@ const bodyHtml = renderMd(d.body);
+ {d.poll && (
+
+ )}
+