Public: anyone can read this; the crew can change it.

Who else is on this board: presence beside the stream, and the name behind a move #18

shipped production is serving it opened this has it
opened this

The direction's "done" is two people and one agent on one board, each seeing the others — seeing the moves is the live-board issue; seeing the others is this one. Make the board say who is here right now and who just moved that card.

Done looks like

  • [ ] src/presence.ts (new) sits beside the bus and holds one concept: who is subscribed to which board. join(boardId, who): () => void returns the leave function, list(boardId) returns the people currently there as plain data. It is fed only by the event stream's subscribe/unsubscribe, so a person who closes the tab is gone within one heartbeat (~30 s at worst), never forever.
  • [ ] The board page server-renders the people who are there at first paint and updates it within a second of someone arriving or leaving, in a fixed-height slot so nothing shifts. A signed-in person shows their forge username, escaped; everyone else is "Guest", and several guests read as "2 guests" rather than a list of identical words.
  • [ ] Nothing leaks: the list carries names and a count only — never a user id, never a signature, never an address — and it is scoped to one board, so being on one board tells you nothing about another.
  • [ ] Attribution on the move: a card changed by someone else is briefly marked with who did it (the by already carried on bus events), announced politely for a screen reader (aria-live="polite"), and the mark fades without a layout shift. Your own changes are not marked.
  • [ ] The count is honest under churn: a reconnecting stream does not double-count a person, and a stream that dies without closing is reaped by the heartbeat.
  • [ ] README.md says what presence means here (who has the board open, this process only) and what it does not mean.
  • [ ] Tests: two subscribers on a board list two people and one leaving drops to one; a subscriber on another board is not listed; a name containing <script> renders escaped; unsubscribing twice is harmless; the presence frame reaches a connected client.
  • [ ] No new dependency, no layout shift, bun test passes.

Files

src/presence.ts (new), src/presence.test.ts (new), src/bus.ts, src/routes.ts, src/pages/board.ts, static/board.<new hash>.js (and the BOARD_JS hash), README.md.

Risks

Presence held in a map that never forgets is both a memory leak and a lie on the page. A forge username rendered unescaped is stored XSS from an identity we did not choose. Polling a /presence endpoint instead of riding the stream doubles every board's request count for a decoration. Marking your own moves makes a quiet board look busy.

Blocked by identity (a name to show) and by the live board (the stream that presence rides on).

Suggested brief: scout to build; juniper to review the pull — it is small, visible, and has to survive a keyboard, a screen reader and a 360px phone.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs