All files / web/src/db/schema app-settings.ts

100% Statements 107/107
100% Branches 0/0
100% Functions 0/0
100% Lines 107/107

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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 1082x 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 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  
import { real, sqliteTable, text } from 'drizzle-orm/sqlite-core'
 
/**
 * App-wide settings table
 *
 * Singleton table with a fixed key for global application configuration.
 * These settings affect all users/students in the app.
 */
export const appSettings = sqliteTable('app_settings', {
  /**
   * Setting ID - use 'default' for the singleton row
   */
  id: text('id').primaryKey().default('default'),
 
  /**
   * BKT confidence threshold for skill classification.
   *
   * Controls when BKT estimates are trusted:
   * - Lower values (0.3) = more aggressive, ~7 attempts needed
   * - Higher values (0.5) = more conservative, ~14 attempts needed
   *
   * Skills with confidence below this threshold are classified as 'learning'
   * regardless of their pKnown value.
   */
  bktConfidenceThreshold: real('bkt_confidence_threshold').notNull().default(0.3),
 
  /**
   * Active TTS voice for audio help.
   *
   * Matches an OpenAI TTS voice name (e.g. 'nova', 'shimmer', 'alloy').
   * Clips are stored at data/audio/{voice}/{filename}.
   */
  audioVoice: text('audio_voice').notNull().default('nova'),
 
  /**
   * Voice chain configuration (JSON-encoded).
   *
   * An ordered array of VoiceSource objects defining the fallback chain
   * for audio playback. Each entry is either:
   *   { type: 'pregenerated', name: '<voice>' }
   *   { type: 'browser-tts' }
   */
  voiceChain: text('voice_chain').default(
    JSON.stringify([{ type: 'pregenerated', name: 'nova' }, { type: 'browser-tts' }])
  ),
 
  /**
   * Term count scaling configuration (JSON-encoded).
   *
   * Controls how many terms per problem a student gets based on comfort level.
   * When null, uses hardcoded defaults from DEFAULT_TERM_COUNT_SCALING.
   * When set, contains a TermCountScalingConfig JSON object with per-mode
   * floor/ceiling ranges.
   */
  termCountScaling: text('term_count_scaling'),
 
  /**
   * Pricing configuration (JSON-encoded).
   *
   * Stores active Stripe price IDs and amounts for each billing interval.
   * When null, falls back to STRIPE_FAMILY_MONTHLY_PRICE_ID / STRIPE_FAMILY_ANNUAL_PRICE_ID env vars.
   * When set, contains a PricingConfig JSON object with family plan pricing.
   *
   * Shape: { family: { monthly: { amount, priceId }, annual: { amount, priceId } } }
   */
  pricing: text('pricing'),
 
  /**
   * Notification channels configuration (JSON-encoded).
   *
   * Controls which notification channels are enabled globally and their settings.
   * When null, notifications are disabled.
   */
  notificationChannels: text('notification_channels'),
})
 
export type AppSettings = typeof appSettings.$inferSelect
export type NewAppSettings = typeof appSettings.$inferInsert
 
/**
 * Default app settings values
 */
export const DEFAULT_APP_SETTINGS: AppSettings = {
  id: 'default',
  bktConfidenceThreshold: 0.3,
  audioVoice: 'nova',
  voiceChain: JSON.stringify([{ type: 'pregenerated', name: 'nova' }, { type: 'browser-tts' }]),
  termCountScaling: null,
  pricing: null,
  notificationChannels: null,
}
 
/**
 * Shape of the pricing JSON stored in app_settings.pricing
 */
export interface PricingConfig {
  family: {
    monthly: { amount: number; priceId: string }
    annual: { amount: number; priceId: string }
  }
}
 
export interface NotificationChannelsConfig {
  webPush: { enabled: boolean }
  email: { enabled: boolean; fromName?: string; replyTo?: string }
  inApp: { enabled: boolean }
}