diff --git a/src/components/Avatar.astro b/src/components/Avatar.astro
new file mode 100644
index 0000000..ec582ae
--- /dev/null
+++ b/src/components/Avatar.astro
@@ -0,0 +1,43 @@
+---
+import { pigmentForId, initialsFromName } from '../lib/format';
+
+interface Props {
+ id: number;
+ name: string;
+ size?: number;
+ tinted?: boolean;
+}
+
+const { id, name, size = 32, tinted = true } = Astro.props;
+const pigment = pigmentForId(id);
+
+const style = tinted
+ ? `--avatar-bg: ${pigment.token}; --avatar-size: ${size}px;`
+ : `--avatar-bg: var(--surface-container); --avatar-size: ${size}px;`;
+---
+{initialsFromName(name)}
+
+
diff --git a/src/components/DispatchesSection.astro b/src/components/DispatchesSection.astro
new file mode 100644
index 0000000..2be0e2c
--- /dev/null
+++ b/src/components/DispatchesSection.astro
@@ -0,0 +1,240 @@
+---
+import Avatar from './Avatar.astro';
+import { getLatestPublishedDispatches } from '../lib/db';
+import {
+ dispatchSlug, dispatchKindLabel, dispatchKindPigment,
+ dispatchExcerptParas, relativeTime,
+} from '../lib/format';
+
+interface Props {
+ limit?: number;
+}
+
+const { limit = 4 } = Astro.props;
+const dispatches = getLatestPublishedDispatches(limit);
+
+const featured = dispatches[0] ?? null;
+const earlier = dispatches.slice(1);
+---
+{featured && (
+
+
+
+
+
+
+ {featured.author_name}
+ {featured.author_title && ยท {featured.author_title}}
+ {relativeTime(featured.published_at ?? featured.created_at)}
+
+ {dispatchKindLabel(featured.kind)}
+
+
+
+ {featured.title}
+
+ {(() => {
+ const { lead, trail } = dispatchExcerptParas(featured);
+ return (
+
+
{lead}
+ {trail &&
{trail}
}
+
+ );
+ })()}
+
+ Read the full dispatch โ
+
+
+ {earlier.length > 0 && (
+ <>
+
+ Earlier
+
+ >
+ )}
+
+)}
+
+
diff --git a/src/lib/format.ts b/src/lib/format.ts
index 8d64ccc..5795cf8 100644
Binary files a/src/lib/format.ts and b/src/lib/format.ts differ
diff --git a/tests/dispatches.test.ts b/tests/dispatches.test.ts
new file mode 100644
index 0000000..03798a4
--- /dev/null
+++ b/tests/dispatches.test.ts
@@ -0,0 +1,102 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import type Database from 'better-sqlite3';
+import {
+ createDispatch, publishDispatch, archiveDispatch,
+ getDispatchById, getLatestPublishedDispatches, getAdjacentDispatches,
+ createUser,
+} from '../src/lib/db.js';
+import { dispatchSlug, parseDispatchSlug } from '../src/lib/format.js';
+
+const db = (globalThis as typeof globalThis & { __bifrost_db?: Database.Database }).__bifrost_db!;
+
+let admin: number;
+
+beforeEach(() => {
+ db.exec(`
+ DELETE FROM votes;
+ DELETE FROM pulses;
+ DELETE FROM activity;
+ DELETE FROM dispatches;
+ DELETE FROM users;
+ `);
+ admin = createUser({ email: 'a@x.test', password_hash: 'x', name: 'Admin', organisation: 'Fenja', role: 'fenja' });
+});
+
+describe('Dispatch status transitions', () => {
+ it('draft create leaves published_at null', () => {
+ const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' });
+ const d = getDispatchById(id)!;
+ expect(d.status).toBe('draft');
+ expect(d.published_at).toBeNull();
+ });
+
+ it('published create stamps published_at', () => {
+ const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'decision', author_id: admin, status: 'published' });
+ const d = getDispatchById(id)!;
+ expect(d.status).toBe('published');
+ expect(d.published_at).not.toBeNull();
+ });
+
+ it('publishDispatch on a draft stamps published_at', () => {
+ const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' });
+ publishDispatch(id);
+ const d = getDispatchById(id)!;
+ expect(d.status).toBe('published');
+ expect(d.published_at).not.toBeNull();
+ });
+
+ it('publishDispatch is idempotent โ re-publishing keeps the original published_at', () => {
+ const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' });
+ publishDispatch(id);
+ const first = getDispatchById(id)!.published_at;
+ // Force a tick so a re-stamp would visibly differ
+ publishDispatch(id);
+ const second = getDispatchById(id)!.published_at;
+ expect(second).toBe(first);
+ });
+
+ it('archiveDispatch preserves published_at and only flips status', () => {
+ const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'update', author_id: admin, status: 'published' });
+ const before = getDispatchById(id)!.published_at;
+ archiveDispatch(id);
+ const d = getDispatchById(id)!;
+ expect(d.status).toBe('archived');
+ expect(d.published_at).toBe(before);
+ });
+
+ it('getLatestPublishedDispatches excludes drafts and archived', () => {
+ createDispatch({ title: 'A', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' });
+ const pubId = createDispatch({ title: 'B', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'published' });
+ const archId = createDispatch({ title: 'C', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'published' });
+ archiveDispatch(archId);
+ const out = getLatestPublishedDispatches(10);
+ expect(out.map(d => d.id)).toEqual([pubId]);
+ });
+
+ it('getAdjacentDispatches returns prev/next in published order', () => {
+ const ids: number[] = [];
+ for (let i = 0; i < 3; i += 1) {
+ const id = createDispatch({ title: `T${i}`, body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'published' });
+ // Backdate so ordering is deterministic
+ db.prepare("UPDATE dispatches SET published_at = datetime('now', ?) WHERE id = ?").run(`-${(3 - i)} days`, id);
+ ids.push(id);
+ }
+ const { prev, next } = getAdjacentDispatches(ids[1]);
+ expect(prev?.id).toBe(ids[0]);
+ expect(next?.id).toBe(ids[2]);
+ });
+});
+
+describe('dispatch slug', () => {
+ it('produces {id}-kebab(title)', () => {
+ expect(dispatchSlug({ id: 7, title: 'A Decision About Telemetry' })).toBe('7-a-decision-about-telemetry');
+ expect(dispatchSlug({ id: 12, title: 'Behind the Scenes: April' })).toBe('12-behind-the-scenes-april');
+ });
+
+ it('parseDispatchSlug pulls the leading id', () => {
+ expect(parseDispatchSlug('7-a-decision-about-telemetry')).toBe(7);
+ expect(parseDispatchSlug('99')).toBe(99);
+ expect(parseDispatchSlug('not-a-slug')).toBeNull();
+ expect(parseDispatchSlug('')).toBeNull();
+ });
+});