All files / web/src/db/schema number-line-postcards.ts

100% Statements 75/75
100% Branches 1/1
100% Functions 0/0
100% Lines 75/75

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 762x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 39x 39x 2x 2x 2x 2x  
import { createId } from '@paralleldrive/cuid2'
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
 
// ── Manifest types (serialized as JSON in the manifest column) ──
 
export interface MomentSnapshot {
  viewport: { center: number; pixelsPerUnit: number }
  highlights?: number[]
  indicatorRange?: { from: number; to: number }
  activeGameId?: string | null
  activeExplorationId?: string | null
  conferenceNumbers?: number[]
  timestamp: number // ms since call start
  gameTarget?: { value: number; emoji: string } // Find-the-Number target
  demoProgress?: number // 0-1 revealProgress when exploration active
}
 
export interface RankedMoment {
  rank: number
  caption: string
  category: 'question' | 'discovery' | 'game' | 'exploration' | 'conversation' | 'conference'
  snapshot: MomentSnapshot
  transcriptExcerpt: string // 2-4 lines around the moment
}
 
export interface PostcardManifest {
  callerNumber: number
  callerPersonality: string // from getTraitSummary(n)
  childName: string
  childAge?: number
  childEmoji: string
  moments: RankedMoment[]
  sessionSummary: string // brief agent-generated summary
}
 
// ── Table ──
 
export const numberLinePostcards = sqliteTable(
  'number_line_postcards',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    userId: text('user_id').notNull(),
    playerId: text('player_id'),
    callerNumber: integer('caller_number', { mode: 'number' }).notNull(),
    sessionId: text('session_id'),
 
    /** pending | generating | ready | failed */
    status: text('status').notNull().default('pending'),
 
    /** JSON-serialized PostcardManifest */
    manifest: text('manifest', { mode: 'json' }).$type<PostcardManifest>().notNull(),
 
    imageUrl: text('image_url'),
    thumbnailUrl: text('thumbnail_url'),
 
    isRead: integer('is_read', { mode: 'boolean' }).notNull().default(false),
    taskId: text('task_id'),
 
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
    updatedAt: integer('updated_at', { mode: 'timestamp' }),
  },
  (table) => ({
    userIdx: index('postcards_user_idx').on(table.userId),
    playerIdx: index('postcards_player_idx').on(table.playerId),
    statusIdx: index('postcards_status_idx').on(table.status),
  })
)
 
export type NumberLinePostcard = typeof numberLinePostcards.$inferSelect
export type NewNumberLinePostcard = typeof numberLinePostcards.$inferInsert