pinned / server /exportBoard.ts
igorktech's picture
Pinned demo: two recorded boards, agent off
6f91ce5 verified
Raw History Blame Contribute Delete
3.13 kB
// 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 <dataDir> <boardId> <out.json>
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 <dataDir> <boardId> <out.json>')
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<string, unknown>[]
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}`)