OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface / preview.mjs

114 lines · javascript ref:
// 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)