// Export a board for the demo fixtures (server/demoBoards): every record a visitor needs, plus the // board row, from a saved DATA_DIR (e.g. a live run kept with LIVE_KEEP_DATA=1). // Usage: node server/exportBoard.ts import { existsSync } from 'node:fs' import { writeFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { DatabaseSync } from 'node:sqlite' import { boardDb, closeBoardDb, findBoard, setBoardDbPath } from './boardDb.ts' import { safeId } from './rooms.ts' const [dataDir, boardId, out] = process.argv.slice(2) if (!dataDir || !boardId || !out) { console.error('Usage: node server/exportBoard.ts ') process.exit(1) } // the room's tldraw records, straight from its SQLite file like the live check's room.py reads them const roomFile = join(resolve(dataDir), 'rooms', `${safeId(boardId)}.db`) if (!existsSync(roomFile)) { console.error(`no room file at ${roomFile}`) process.exit(1) } const room = new DatabaseSync(roomFile, { readOnly: true }) const records = room .prepare('SELECT state FROM documents') .all() .map((r) => { const state = (r as { state: string | Uint8Array }).state return JSON.parse(typeof state === 'string' ? state : new TextDecoder().decode(state)) as { typeName?: string } }) // putShapes writes shapes and bindings; pages and assets go straight into the room .filter((r) => r.typeName === 'shape' || r.typeName === 'binding' || r.typeName === 'asset' || r.typeName === 'page') room.close() setBoardDbPath(join(resolve(dataDir), 'board.db')) const board = findBoard(boardId) if (!board) { console.error(`no board row for ${boardId} in ${dataDir}/board.db`) process.exit(1) } const rows = (sql: string) => boardDb().prepare(sql).all(safeId(boardId)) as Record[] const exported = { title: board.title, question: board.question, context: board.context, goals: board.goals, records, // per-board state the widgets read back: votes (card faces carry keepBy/discardBy too, but the // REST answers come from decisions) and links people removed so the agent never redraws them decisions: rows('SELECT paper_id, member_id, decision FROM decisions WHERE board_id = ? ORDER BY at, rowid').map( (r) => ({ paperId: String(r.paper_id), memberId: String(r.member_id), decision: String(r.decision) }) ), // the people who voted or wrote notes, so their names show on the demo board members: rows('SELECT member_id, display_name FROM members WHERE board_id = ? ORDER BY joined_at, rowid').map( (r) => ({ memberId: String(r.member_id), displayName: String(r.display_name) }) ), removedLinks: rows( 'SELECT from_paper, to_paper, kind, removed_by FROM removed_links WHERE board_id = ? ORDER BY at, rowid' ).map((r) => ({ from: String(r.from_paper), to: String(r.to_paper), kind: String(r.kind), removedBy: String(r.removed_by) })), } closeBoardDb() writeFileSync(out, JSON.stringify(exported, null, '\t') + '\n') console.log(`${boardId}: ${records.length} records, ${exported.decisions.length} decisions, ${exported.removedLinks.length} removed links → ${out}`)