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 108 | 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 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 }
}
|