// A design harness: render real pages with fixtures, so a look at the result needs no daemon, no
// session and no browser of anybody's. `node preview.mjs <page>` writes preview.html; the shell
// script beside it screenshots that with headless chromium.
//
// ⚠ It imports the SAME functions the server calls. A harness with its own markup would drift, and
// then it would be checking itself rather than the product.
import fs from 'node:fs'
import { timeline, person, following, notifications, thread, settings, rooms, room, graph,
search, mentions, knocks, login } from './lib/pages.js'
const A = 'ab'.repeat(32)
const B = 'cd'.repeat(32)
const profiles = {
// ⚠ `root` included — it is what the mention affordance needs, and a fixture without it hides the
// very control the page is supposed to offer.
[A]: { handle: 'tabertron0', name: 'tabertron', bio: 'the dev box — runs the fleet', root: 'ef'.repeat(32) },
[B]: { handle: 'ashatron0', name: 'ashatron', bio: null, root: '12'.repeat(32) }
}
const rows = [
{ author: A, collection: 'post', name: 'p1', createdAt: '2026-09-07T16:19:00Z',
body: 'testing mentions: hello [@ashatron](favela://' + B + ')', liked: true, reactions: ['like'], reposted: false },
{ author: B, collection: 'post', name: 'p2', createdAt: '2026-09-07T15:58:00Z',
body: '## UPS Supply Chain Shipping\n\n- must have business first\n- Always overpack\n- get ups-scs account registered', reactions: [], liked: false, reposted: true },
{ author: A, collection: 'post', name: 'p3', createdAt: '2026-09-07T15:34:00Z',
body: 'A shorter one, with `code` and a [link](https://example.com).', reactions: ['🔥'], liked: false, reposted: false }
]
const pages = {
timeline: () => timeline(rows, { profiles, unreadable: 0 }),
'timeline-empty': () => timeline([], { profiles, empty: { follows: 0 } }),
'timeline-waiting': () => timeline([], { profiles, empty: { follows: 3 } }),
'following-empty': () => following([], [], profiles),
'rooms-empty': () => rooms([]),
'knocks-empty': () => knocks([]),
'notifications-empty': () => notifications([], profiles),
person: () => person(A, profiles[A], rows.filter((r) => r.author === A), null, { held: true, following: true }),
following: () => following([A, B], [B], profiles),
notifications: () => notifications([
{ kind: 'reaction', author: B, value: 'like', target: { feed: A, collection: 'post', name: 'p1' } },
{ kind: 'reply', author: B, target: { feed: A, collection: 'post', name: 'p1' }, body: 'good point' },
{ kind: 'follow', author: B, target: null }
], profiles),
thread: () => thread(rows[0], [rows[2]], profiles, null, A + '/post/p1'),
settings: () => settings(profiles[A], null, 'tabertron0',
{ key: 'cd'.repeat(32), claims: 3, founded: true, admins: ['x'], writers: [], requests: [], carrying: true }),
// ⛔ The harness covered six of fifteen pages, so an audit of "the design" was an audit of the
// timeline. Every page a READER lands on belongs here; the ones behind a POST (theme, handle,
// namespace) are forms inside settings and are covered by it.
search: () => search('ups', rows.slice(0, 2), 1, [{ author: A, ...profiles[A] }], profiles, null,
[{ handle: 'tabertron0', name: 'tabertron', feed: A, verdict: 'confirmed' }]),
mentions: () => mentions([rows[0]], profiles),
rooms: () => rooms([
{ key: 'ab'.repeat(32), config: { title: 'the fleet' }, members: 3 },
{ key: 'cd'.repeat(32), config: {}, members: 1 }
]),
room: () => room('ab'.repeat(32), {
role: 'admin',
config: { title: 'the fleet', topic: 'running the boxes' },
members: [{ root: 'ef'.repeat(32), role: 'admin' }, { root: '12'.repeat(32), role: 'peer' }],
messages: [
{ author: 'ef'.repeat(32), body: 'ashatron is back up', createdAt: '2026-09-07T16:10:00Z' },
{ author: '12'.repeat(32), body: 'good — the SATA cable was it', createdAt: '2026-09-07T16:12:00Z' }
],
requests: [], listings: []
}, profiles),
knocks: () => knocks([{ feed: B }]),
login: () => login(null, false)
}
pages.rooms = () => rooms([
{ key: 'ab'.repeat(32), config: { title: 'Berlin P2P' }, members: 3 },
{ key: 'cd'.repeat(32), config: {}, members: 1 }
])
pages.room = () => room('ab'.repeat(32), {
role: 'admin',
config: { title: 'Berlin P2P' },
members: [{ root: 'ef'.repeat(32), role: 'admin' }, { root: '12'.repeat(32), role: 'peer' }],
requests: [{ writer: '34'.repeat(32), root: '56'.repeat(32), device: 'x' }],
messages: [
{ author: 'ef'.repeat(32), body: 'first thing said in here' },
{ author: '12'.repeat(32), body: 'a **reply** of sorts' }
]
}, profiles)
pages['room-guest'] = () => room('ab'.repeat(32), {
role: null, config: { title: 'Berlin P2P' }, members: [], requests: [], messages: []
}, profiles)
pages.graph = () => graph({
me: A,
nodes: [
{ id: A, type: 'actor', state: 'resolved' },
{ id: B, type: 'actor', state: 'resolved' },
{ id: 'ef'.repeat(32), type: 'actor', state: 'resolved' },
{ id: A + '/post/p1', type: 'record', state: 'resolved' },
{ id: A + '/post/p2', type: 'record', state: 'resolved' },
{ id: B + '/post/q1', type: 'record', state: 'converged' },
{ id: B + '/post/q2', type: 'record', state: 'resolved' }
],
edges: [
{ kind: 'reaction', from: B, to: A + '/post/p1', value: 'like', state: 'converged' },
{ kind: 'reaction', from: 'ef'.repeat(32), to: A + '/post/p1', value: '🔥', state: 'converged' },
{ kind: 'repost', from: B, to: A + '/post/p2', value: null, state: 'converged' },
{ kind: 'reply', from: A, to: B + '/post/q1', value: null, state: 'converged' },
{ kind: 'follow', from: A, to: B, value: null, state: 'converged' },
{ kind: 'follow', from: B, to: A, value: null, state: 'converged' }
],
projection: 'unknown'
})
const which = process.argv[2] || 'timeline'
if (!pages[which]) { console.error('pages: ' + Object.keys(pages).join(' ')); process.exit(1) }
fs.writeFileSync('preview.html', pages[which]())
console.log('wrote preview.html for ' + which)