@vue-skuilder/standalone-ui 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -9,11 +9,11 @@
9
9
  href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons"
10
10
  rel="stylesheet"
11
11
  />
12
- <script type="module" crossorigin src="/assets/index-DuGLD7Ss.js"></script>
12
+ <script type="module" crossorigin src="/assets/index-CDRvg5QJ.js"></script>
13
13
  <link rel="modulepreload" crossorigin href="/assets/dist-DCANvFNh.js">
14
- <link rel="modulepreload" crossorigin href="/assets/dist-CDhtG9sH.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/dist-D6tRtHge.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/MarkdownRenderer-kStoDRNE-lsnQ9xYB.js">
16
- <link rel="modulepreload" crossorigin href="/assets/common-ui.es-Dt1DQMFY.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/common-ui.es-RbbhjMGz.js">
17
17
  <link rel="stylesheet" crossorigin href="/assets/common-ui-BY2Ut5jf.css">
18
18
  <link rel="stylesheet" crossorigin href="/assets/index-BjQg_GYs.css">
19
19
  </head>
@@ -359,7 +359,7 @@ above:
359
359
  ${g.rows.map(t=>` ${t.id}-${t.key}
360
360
  `)}`;return logger.debug(`Getting ${u} cards centered around elo: ${t}:
361
361
 
362
- `+C),S}async getCourseConfig(){let t=await getCredentialledCourseConfig(this.id);if(t)return t;throw Error(`Course config not found for course ID: ${this.id}`)}async updateCourseConfig(t){logger.debug(`Updating: ${JSON.stringify(t)}`);try{return await updateCredentialledCourseConfig(this.id,t)}catch(t){throw logger.error(`Error updating course config in course DB: ${t}`),t}}async updateCardElo(t,c){if(!c)throw Error(`Cannot update card elo with null or undefined value for card ID: ${t}`);try{return{ok:!0,id:t,rev:(await this.updateQueue.update(t,t=>(logger.debug(`Replacing ${JSON.stringify(t.elo)} with ${JSON.stringify(c)}`),t.elo=c,t)))._rev}}catch(c){throw logger.error(`Failed to update card elo for card ID: ${t}`,c),Error(`Failed to update card elo for card ID: ${t}`)}}async getAppliedTags(t){let c=await getAppliedTags(this.id,t);if(c)return c;throw Error(`Failed to find tags for card ${this.id}-${t}`)}async getAppliedTagsBatch(t){if(t.length===0)return new Map;let c=await this.db.query(`getTags`,{keys:t,include_docs:!1}),u=new Map;for(let c of t)u.set(c,[]);for(let t of c.rows){let c=t.key,d=t.value?.name;d&&u.has(c)&&u.get(c).push(d)}return u}async getAllCardIds(){return(await this.db.allDocs({startkey:`CARD-`,endkey:`CARD-￰`,include_docs:!1})).rows.map(t=>t.id)}async addTagToCard(t,c,u){return await addTagToCard(this.id,t,c,(await this._getCurrentUser()).getUsername(),u)}async removeTagFromCard(t,c){return await removeTagFromCard(this.id,t,c)}async createTag(t,c){return await createTag(this.id,t,c)}async getTag(t){return await getTag(this.id,t)}async updateTag(t){if(t.course!==this.id)throw Error(`Tag ${JSON.stringify(t)} does not belong to course ${this.id}`);return await updateTag(t)}async getCourseTagStubs(){return getCourseTagStubs(this.id)}async addNote(t,c,u,d,m,g,b=blankCourseElo()){try{let S=await addNote55(this.id,t,c,u,d,m,g,b);return S.ok?S.cardCreationFailed?(logger.warn(`[courseDB.addNote] Note added but card creation failed: ${S.cardCreationError}`),{status:Status.error,message:`Note was added but no cards were created: ${S.cardCreationError}`,id:S.id}):{status:Status.ok,message:``,id:S.id}:{status:Status.error,message:`Unexpected error adding note`}}catch(t){let c=t;return logger.error(`[addNote] error ${c.name}
362
+ `+C),S}async getCourseConfig(){let t=await getCredentialledCourseConfig(this.id);if(t)return t;throw Error(`Course config not found for course ID: ${this.id}`)}async updateCourseConfig(t){logger.debug(`Updating: ${JSON.stringify(t)}`);try{return await updateCredentialledCourseConfig(this.id,t)}catch(t){throw logger.error(`Error updating course config in course DB: ${t}`),t}}async updateCardElo(t,c){if(!c)throw Error(`Cannot update card elo with null or undefined value for card ID: ${t}`);try{return{ok:!0,id:t,rev:(await this.updateQueue.update(t,t=>(logger.debug(`Replacing ${JSON.stringify(t.elo)} with ${JSON.stringify(c)}`),t.elo=c,t)))._rev}}catch(c){throw logger.error(`Failed to update card elo for card ID: ${t}`,c),Error(`Failed to update card elo for card ID: ${t}`)}}async getAppliedTags(t){let c=await getAppliedTags(this.id,t);if(c)return c;throw Error(`Failed to find tags for card ${this.id}-${t}`)}async getAppliedTagsBatch(t){if(t.length===0)return new Map;let c=await this.db.query(`getTags`,{keys:t,include_docs:!1}),u=new Map;for(let c of t)u.set(c,[]);for(let t of c.rows){let c=t.key,d=t.value?.name;d&&u.has(c)&&u.get(c).push(d)}return u}async getAllCardIds(){let t=`${DocTypePrefixes.CARD}-`;return(await this.db.allDocs({startkey:t,endkey:`${t}\uFFF0`,include_docs:!1})).rows.map(t=>t.id)}async addTagToCard(t,c,u){return await addTagToCard(this.id,t,c,(await this._getCurrentUser()).getUsername(),u)}async removeTagFromCard(t,c){return await removeTagFromCard(this.id,t,c)}async createTag(t,c){return await createTag(this.id,t,c)}async getTag(t){return await getTag(this.id,t)}async updateTag(t){if(t.course!==this.id)throw Error(`Tag ${JSON.stringify(t)} does not belong to course ${this.id}`);return await updateTag(t)}async getCourseTagStubs(){return getCourseTagStubs(this.id)}async addNote(t,c,u,d,m,g,b=blankCourseElo()){try{let S=await addNote55(this.id,t,c,u,d,m,g,b);return S.ok?S.cardCreationFailed?(logger.warn(`[courseDB.addNote] Note added but card creation failed: ${S.cardCreationError}`),{status:Status.error,message:`Note was added but no cards were created: ${S.cardCreationError}`,id:S.id}):{status:Status.ok,message:``,id:S.id}:{status:Status.error,message:`Unexpected error adding note`}}catch(t){let c=t;return logger.error(`[addNote] error ${c.name}
363
363
  reason: ${c.reason}
364
364
  message: ${c.message}`),{status:Status.error,message:`Error adding note to course. ${t.reason||c.message}`}}}async getCourseDoc(t,c){return await this.db.get(t,c??{})}async getCourseDocs(t,c={}){return await this.db.allDocs({...c,keys:t})}getNavigationStrategy(t){if(logger.debug(`[courseDB] Getting navigation strategy: ${t}`),t==``){let t={_id:`NAVIGATION_STRATEGY-ELO`,docType:`NAVIGATION_STRATEGY`,name:`ELO`,description:`ELO-based navigation strategy for ordering content by difficulty`,implementingClass:`elo`,course:this.id,serializedData:``};return Promise.resolve(t)}else return this.db.get(t)}async getAllNavigationStrategies(){let t=DocTypePrefixes.NAVIGATION_STRATEGY;return(await this.db.allDocs({startkey:t,endkey:`${t}\uFFF0`,include_docs:!0})).rows.map(t=>t.doc)}async addNavigationStrategy(t){return logger.debug(`[courseDB] Adding navigation strategy: ${t._id}`),this.invalidateNavigatorCache(),this.remoteDB.put(t).then(()=>{})}updateNavigationStrategy(t,c){return logger.debug(`[courseDB] Updating navigation strategy: ${t}`),logger.debug(JSON.stringify(c)),Promise.resolve()}async createNavigator(t){try{let c=await this.getAllNavigationStrategies();if(c.length===0)return logger.debug(`[courseDB] No strategy documents found, using default Pipeline(Composite(ELO, SRS), [eloDistanceFilter])`),createDefaultPipeline(t,this);let{pipeline:u,generatorStrategies:d,filterStrategies:m,warnings:g}=await new PipelineAssembler().assemble({strategies:c,user:t,course:this});for(let t of g)logger.warn(`[PipelineAssembler] ${t}`);return u?(logger.debug(`[courseDB] Using assembled pipeline with ${d.length} generator(s) and ${m.length} filter(s)`),u):(logger.debug(`[courseDB] Pipeline assembly failed, using default pipeline`),createDefaultPipeline(t,this))}catch(t){let c=t instanceof Error?`${t.message}
365
365
  ${t.stack}`:JSON.stringify(t);throw logger.error(`[courseDB] Error creating navigator: ${c}`),t}}setEphemeralHints(t){this._pendingHints=t}async getWeightedCards(t){let c=await this._getCurrentUser();try{let{navigator:u}=await this._getCachedNavigator(c);return this._pendingHints&&(u.setEphemeralHints(this._pendingHints),this._pendingHints=null),await u.getWeightedCards(t)}catch(t){throw logger.error(`[courseDB] Error getting weighted cards: ${t}`),t}}async _getCachedNavigator(t){let c=t.getUsername(),u=Date.now();if(this._cachedNavigator&&this._cachedNavigator.userId===c&&u-this._cachedNavigator.builtAt<this._navigatorTtlMs)return{navigator:this._cachedNavigator.navigator,cacheStatus:`hit`};let d=await this.createNavigator(t);return this._cachedNavigator={navigator:d,userId:c,builtAt:u},{navigator:d,cacheStatus:`miss`}}invalidateNavigatorCache(){this._cachedNavigator=null}async getCardsCenteredAtELO(t={limit:99,elo:`user`},c){let u;if(t.elo===`user`){let t=await this._getCurrentUser();u=-1;try{u=EloToNumber((await t.getCourseRegistrationsDoc()).courses.find(t=>t.courseID===this.id).elo)}catch{u=1e3}}else if(t.elo===`random`){let t=await GET_CACHED(`elo-bounds-${this.id}`,()=>this.getELOBounds());u=Math.round(t.low+Math.random()*(t.high-t.low))}else u=t.elo;let d=Math.max(2e3,t.limit*4),m=Date.now(),g=`hit`;if(!this._eloPoolCache||m-this._eloPoolCache.fetchedAt>this._eloPoolTtlMs){let t=await this.getCardsByELO(u,d);t.length>0&&(this._eloPoolCache={rows:t,fetchedAt:m}),g=`miss`}let rankAgainstCurrentElo=()=>{let t=this._eloPoolCache?.rows??[];return(c?t.filter(t=>c(t)):t).map(t=>({...t})).sort((t,c)=>Math.abs((t.elo??u)-u)-Math.abs((c.elo??u)-u))},b=rankAgainstCurrentElo();if(b.length<t.limit&&(g===`hit`||!this._eloPoolCache)){let t=await this.getCardsByELO(u,d);t.length>0&&(this._eloPoolCache={rows:t,fetchedAt:m}),b=rankAgainstCurrentElo(),g=`refresh`}let S=[];for(;S.length<t.limit&&b.length>0;){let t=randIntWeightedTowardZero(b.length),c=b.splice(t,1)[0];S.push(c)}return S.map(t=>({courseID:this.id,cardID:t.cardID,contentSourceType:`course`,contentSourceID:this.id,elo:t.elo,status:`new`}))}async searchCards(t){logger.log(`[CourseDB ${this.id}] Searching for: "${t}"`);let c;try{c=await this.db.find({selector:{docType:`DISPLAYABLE_DATA`,"data.0.data":{$regex:`.*${t}.*`}}}),logger.log(`[CourseDB ${this.id}] Regex search on data[0].data successful`)}catch(u){logger.log(`[CourseDB ${this.id}] Regex search failed, falling back to manual search:`,u);let d=await this.db.find({selector:{docType:`DISPLAYABLE_DATA`}});logger.log(`[CourseDB ${this.id}] Retrieved ${d.docs.length} documents for manual filtering`),c={docs:d.docs.filter(c=>{let u=JSON.stringify(c).toLowerCase().includes(t.toLowerCase());return u&&logger.log(`[CourseDB ${this.id}] Manual match found in document: ${c._id}`),u})}}if(logger.log(`[CourseDB ${this.id}] Found ${c.docs.length} displayable data documents`),c.docs.length===0){let t=await this.db.find({selector:{docType:`DISPLAYABLE_DATA`},limit:5});logger.log(`[CourseDB ${this.id}] Sample displayable data:`,t.docs.map(t=>({id:t._id,docType:t.docType,dataStructure:t.data?Object.keys(t.data):`no data field`,dataContent:t.data,fullDoc:t})))}let u=[];for(let t of c.docs){let c=await this.db.find({selector:{docType:`CARD`,id_displayable_data:{$in:[t._id]}}});logger.log(`[CourseDB ${this.id}] Displayable data ${t._id} linked to ${c.docs.length} cards`),u.push(...c.docs)}return logger.log(`[CourseDB ${this.id}] Total cards found: ${u.length}`),u}async find(t){return this.db.find(t)}}}}),init_classroomDB2=__esm({"src/impl/couch/classroomDB.ts"(){"use strict";init_factory(),init_logger(),init_pouchdb_setup(),init_couch(),init_courseDB(),classroomLookupDBTitle=`classdb-lookup`,CLASSROOM_CONFIG=`ClassroomConfig`,ClassroomDBBase=class{constructor(){_defineProperty$2(this,`_id`,void 0),_defineProperty$2(this,`_db`,void 0),_defineProperty$2(this,`_cfg`,void 0),_defineProperty$2(this,`_initComplete`,!1),_defineProperty$2(this,`_content_prefix`,`content`)}get _content_searchkeys(){return getStartAndEndKeys2(this._content_prefix)}async getAssignedContent(){return logger.info(`Getting assigned content...`),(await this._db.allDocs({startkey:this._content_prefix,endkey:this._content_prefix+`￰`,include_docs:!0})).rows.map(t=>t.doc)}getContentId(t){return t.type===`tag`?`${this._content_prefix}-${t.courseID}-${t.tagID}`:`${this._content_prefix}-${t.courseID}`}get ready(){return this._initComplete}getConfig(){return this._cfg}},StudentClassroomDB=class _StudentClassroomDB extends ClassroomDBBase{constructor(t,c){super(),_defineProperty$2(this,`userMessages`,void 0),_defineProperty$2(this,`_user`,void 0),this._id=t,this._user=c}async init(){let t=`classdb-student-${this._id}`;this._db=new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL+`://`+ENV.COUCHDB_SERVER_URL+t,createPouchDBConfig());try{this._cfg=await this._db.get(CLASSROOM_CONFIG),this.userMessages=this._db.changes({since:`now`,live:!0,include_docs:!0}),this._initComplete=!0;return}catch(t){throw Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(t)}`)}}static async factory(t,c){let u=new _StudentClassroomDB(t,c);return await u.init(),u}setChangeFcn(t){this.userMessages.on(`change`,t)}async getWeightedCards(t){let c=[],u=(await this._user.getPendingReviews()).filter(t=>t.scheduledFor===`classroom`&&t.schedulingAgentId===this._id);for(let t of u)c.push({cardId:t.cardId,courseId:t.courseId,score:1,reviewID:t._id,provenance:[{strategy:`classroom`,strategyName:`Classroom`,strategyId:`CLASSROOM`,action:`generated`,score:1,reason:`Classroom scheduled review`}]});let d=await this._user.getActiveCards(),m=new Set(d.map(t=>t.cardID)),g=hooks.utc(),b=(await this.getAssignedContent()).filter(t=>g.isAfter(hooks.utc(t.activeOn,REVIEW_TIME_FORMAT2)));logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(b)}`);for(let u of b)if(u.type===`course`){let{cards:d}=await new CourseDB(u.courseID,async()=>this._user).getWeightedCards(t);for(let t of d)m.has(t.cardId)||c.push({...t,provenance:[...t.provenance,{strategy:`classroom`,strategyName:`Classroom`,strategyId:`CLASSROOM`,action:`passed`,score:t.score,reason:`Assigned via classroom from course ${u.courseID}`}]})}else if(u.type===`tag`){let t=await getTag(u.courseID,u.tagID);for(let d of t.taggedCards)m.has(d)||c.push({cardId:d,courseId:u.courseID,score:1,provenance:[{strategy:`classroom`,strategyName:`Classroom`,strategyId:`CLASSROOM`,action:`generated`,score:1,reason:`Classroom assigned tag: ${u.tagID}, new card`}]})}else u.type===`card`&&(m.has(u.cardID)||c.push({cardId:u.cardID,courseId:u.courseID,score:1,provenance:[{strategy:`classroom`,strategyName:`Classroom`,strategyId:`CLASSROOM`,action:`generated`,score:1,reason:`Classroom assigned card, new card`}]}));return logger.info(`[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ${c.length} total (reviews + new)`),{cards:c.sort((t,c)=>c.score-t.score).slice(0,t)}}},TeacherClassroomDB=class _TeacherClassroomDB extends ClassroomDBBase{constructor(t){super(),_defineProperty$2(this,`_stuDb`,void 0),this._id=t}async init(){let t=`classdb-teacher-${this._id}`,c=`classdb-student-${this._id}`;this._db=new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL+`://`+ENV.COUCHDB_SERVER_URL+t,createPouchDBConfig()),this._stuDb=new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL+`://`+ENV.COUCHDB_SERVER_URL+c,createPouchDBConfig());try{return this._db.get(CLASSROOM_CONFIG).then(t=>{this._cfg=t,this._initComplete=!0}).then(()=>{})}catch(t){throw Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(t)}`)}}static async factory(t){let c=new _TeacherClassroomDB(t);return await c.init(),c}async removeContent(t){let c=this.getContentId(t);try{let t=await this._db.get(c);await this._db.remove(t),this._db.replicate.to(this._stuDb,{doc_ids:[c]})}catch(t){logger.error(`Failed to remove content:`,c,t)}}async assignContent(t){let c,u=this.getContentId(t);return c=t.type===`tag`?await this._db.put({courseID:t.courseID,tagID:t.tagID,type:`tag`,_id:u,assignedBy:t.assignedBy,assignedOn:hooks.utc(),activeOn:t.activeOn||hooks.utc()}):await this._db.put({courseID:t.courseID,type:`course`,_id:u,assignedBy:t.assignedBy,assignedOn:hooks.utc(),activeOn:t.activeOn||hooks.utc()}),c.ok?(this._db.replicate.to(this._stuDb,{doc_ids:[u]}),!0):!1}},ClassroomLookupDB=()=>new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL+`://`+ENV.COUCHDB_SERVER_URL+classroomLookupDBTitle,{skip_setup:!0})}}),init_adminDB2=__esm({"src/impl/couch/adminDB.ts"(){"use strict";init_pouchdb_setup(),init_factory(),init_couch(),init_classroomDB2(),init_courseLookupDB(),init_logger(),AdminDB=class{constructor(){_defineProperty$2(this,`usersDB`,void 0),this.usersDB=new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL+`://`+ENV.COUCHDB_SERVER_URL+`_users`,createPouchDBConfig())}async getUsers(){return(await this.usersDB.allDocs({include_docs:!0,...getStartAndEndKeys2(`org.couchdb.user:`)})).rows.map(t=>t.doc)}async getCourses(){let t=await CourseLookup.allCourseWare();return await Promise.all(t.map(t=>getCredentialledCourseConfig(t._id)))}async removeCourse(t){let c=await CourseLookup.delete(t),u=await getCredentialledCourseConfig(t);u.deleted=!0;let d=await updateCredentialledCourseConfig(t,u);return{ok:c.ok&&d.ok,id:c.id,rev:c.rev}}async getClassrooms(){let t=(await ClassroomLookupDB().allDocs({include_docs:!0})).rows.map(t=>t.doc.uuid);logger.debug(t.join(`, `));let c=[];for(let u=0;u<t.length;u++)try{let d=await TeacherClassroomDB.factory(t[u]);c.push(d)}catch(c){let d=c;d.error&&d.error===`not_found`&&logger.warn(`db ${t[u]} not found`)}return c.map(t=>({...t.getConfig(),_id:t._id}))}}}}),CourseSyncService_exports={},__export(CourseSyncService_exports,{CourseSyncService:()=>CourseSyncService}),init_CourseSyncService=__esm({"src/impl/couch/CourseSyncService.ts"(){"use strict";var t;init_pouchdb_setup(),init_couch(),init_logger(),DEFAULT_REPLICATION={batchSize:1e3,batchesLimit:5},CourseSyncService=(t=class _CourseSyncService{constructor(){_defineProperty$2(this,`entries`,new Map),_defineProperty$2(this,`replicationOptions`,DEFAULT_REPLICATION)}static getInstance(){return _CourseSyncService.instance||(_CourseSyncService.instance=new _CourseSyncService),_CourseSyncService.instance}configure(t){t.replication&&(this.replicationOptions={...DEFAULT_REPLICATION,...t.replication},logger.info(`[CourseSyncService] Replication configured: batch_size=${this.replicationOptions.batchSize}, batches_limit=${this.replicationOptions.batchesLimit}`))}static resetInstance(){if(_CourseSyncService.instance){for(let[,t]of _CourseSyncService.instance.entries)t.localDB&&t.localDB.close().catch(()=>{});_CourseSyncService.instance.entries.clear()}_CourseSyncService.instance=null}async ensureSynced(t,c){let u=this.entries.get(t);if(u?.status.state===`ready`&&u.localDB){if(!await this.isLocalEpochStale(t,u.localDB))return;logger.info(`[CourseSyncService] Remote DB epoch changed for course ${t} \u2014 destroying stale local replica`);try{await u.localDB.destroy()}catch{}u.localDB=null,u.readyPromise=null}if(u?.status.state===`disabled`)return;if(u?.readyPromise)return u.readyPromise;let d={localDB:null,status:{state:`not-started`},readyPromise:null};return this.entries.set(t,d),d.readyPromise=this.performSync(t,d,c),d.readyPromise}getLocalDB(t){let c=this.entries.get(t);return c?.status.state===`ready`&&c.localDB?c.localDB:null}isReady(t){return this.entries.get(t)?.status.state===`ready`}getStatus(t){return this.entries.get(t)?.status??{state:`not-started`}}async performSync(t,c,u){try{if(!u&&(c.status={state:`checking-config`},!await this.checkLocalSyncEnabled(t))){c.status={state:`disabled`},c.readyPromise=null,logger.debug(`[CourseSyncService] Local sync disabled for course ${t}`);return}c.status={state:`syncing`};let d=this.localDBName(t),m=new pouchdb_setup_default(d);await this.isLocalEpochStale(t,m)&&(logger.info(`[CourseSyncService] Stale local DB detected for course ${t} \u2014 destroying before sync`),await m.destroy(),m=new pouchdb_setup_default(d)),c.localDB=m;let g=this.getRemoteDB(t),b=Date.now();logger.info(`[CourseSyncService] Starting one-shot replication for course ${t} (batch_size=${this.replicationOptions.batchSize}, batches_limit=${this.replicationOptions.batchesLimit})`);let S=await this.replicate(g,m),C=Date.now()-b;logger.info(`[CourseSyncService] Replication complete for course ${t}: ${S.docs_written} docs in ${C}ms`),c.status={state:`warming-views`};let w=Date.now();await this.warmViewIndices(m);let T=Date.now()-w;logger.info(`[CourseSyncService] View indices warmed for course ${t} in ${T}ms`),c.status={state:`ready`,docsReplicated:S.docs_written,syncTimeMs:C,viewWarmTimeMs:T}}catch(u){let d=u instanceof Error?u.message:String(u);if(logger.error(`[CourseSyncService] Sync failed for course ${t}: ${d}`),c.status={state:`error`,error:d},c.readyPromise=null,c.localDB){try{await c.localDB.destroy()}catch{}c.localDB=null}}}async checkLocalSyncEnabled(t){try{return(await this.getRemoteDB(t).get(`CourseConfig`)).localSync?.enabled===!0}catch(c){return logger.warn(`[CourseSyncService] Could not read CourseConfig for ${t}, assuming local sync disabled: ${c}`),!1}}replicate(t,c){return new Promise((u,d)=>{pouchdb_setup_default.replicate(t,c,{batch_size:this.replicationOptions.batchSize,batches_limit:this.replicationOptions.batchesLimit}).on(`complete`,t=>{u(t)}).on(`error`,t=>{d(t)})})}async warmViewIndices(t){for(let c of[`elo`,`getTags`])try{await t.query(c,{limit:1}),logger.debug(`[CourseSyncService] Warmed view index: ${c}`)}catch(t){logger.debug(`[CourseSyncService] Could not warm view ${c}: ${t}`)}}async isLocalEpochStale(t,c){try{let u=await this.getRemoteDB(t).get(`db-epoch`),d=null;try{d=await c.get(`db-epoch`)}catch{return!0}return u.epoch!==d.epoch}catch{return!1}}getRemoteDB(t){return getCourseDB2(t)}localDBName(t){return`coursedb-local-${t}`}},_defineProperty$2(t,`instance`,null),t)}}),init_auth=__esm({"src/impl/couch/auth.ts"(){"use strict";init_factory(),init_logger()}}),CouchDBSyncStrategy_exports={},__export(CouchDBSyncStrategy_exports,{CouchDBSyncStrategy:()=>CouchDBSyncStrategy}),init_CouchDBSyncStrategy=__esm({"src/impl/couch/CouchDBSyncStrategy.ts"(){"use strict";init_factory(),init_types_legacy(),init_logger(),init_common(),init_pouchdb_setup(),init_couch(),init_auth(),log3=t=>{logger.info(t)},CouchDBSyncStrategy=class{constructor(){_defineProperty$2(this,`syncHandle`,void 0)}setupRemoteDB(t){return t===GuestUsername||t.startsWith(GuestUsername)?getLocalUserDB(t):this.getUserDB(t)}getWriteDB(t){return t===GuestUsername||t.startsWith(GuestUsername)?getLocalUserDB(t):this.getUserDB(t)}startSync(t,c){t!==c&&(this.syncHandle=pouchdb_setup_default.sync(t,c,{live:!0,retry:!0}))}stopSync(){this.syncHandle&&(this.syncHandle.cancel(),this.syncHandle=void 0)}canCreateAccount(){return!0}canAuthenticate(){return!0}async createAccount(t,c){let u=await this.getCurrentUsername(),d=u.startsWith(GuestUsername);d&&logger.info(`Creating account for funnel user ${u} -> ${t}`);try{let m=await this.getRemoteCouchRootDB().signUp(t,c);if(m.ok){log3(`CREATEACCOUNT: Successfully created account for ${t}`);try{let t=await this.getRemoteCouchRootDB().logOut();log3(`CREATEACCOUNT: logged out: ${t.ok}`)}catch{}let m=await this.getRemoteCouchRootDB().logIn(t,c);if(log3(`CREATEACCOUNT: logged in as new user: ${m.ok}`),m.ok){if(d){logger.info(`Migrating data from funnel account ${u} to ${t}`);let c=await this.migrateFunnelData(u,t);c.success||logger.warn(`Migration failed: ${c.error}`)}return{status:Status.ok,error:void 0}}else return{status:Status.error,error:`Failed to log in after account creation`}}else return logger.warn(`Signup not OK: ${JSON.stringify(m)}`),{status:Status.error,error:`Account creation failed`}}catch(t){return t.reason===`Document update conflict.`?{status:Status.error,error:`This username is taken!`}:(logger.error(`Error on signup: ${JSON.stringify(t)}`),{status:Status.error,error:t.message||`Unknown error during account creation`})}}async authenticate(t,c){try{return(await this.getRemoteCouchRootDB().logIn(t,c)).ok?(log3(`Successfully logged in as ${t}`),{ok:!0}):(log3(`Login failed for ${t}`),{ok:!1,error:`Invalid username or password`})}catch(c){return logger.error(`Authentication error for ${t}:`,c),{ok:!1,error:c.message||`Authentication failed`}}}async logout(){try{let t=await this.getRemoteCouchRootDB().logOut();return{ok:t.ok,error:t.ok?void 0:`Logout failed`}}catch(t){return logger.error(`Logout error:`,t),{ok:!1,error:t.message||`Logout failed`}}}async getCurrentUsername(){logger.log(`[funnel] CouchDBSyncStrategy.getCurrentUsername() called`);try{let t=await getLoggedInUsername();return logger.log(`[funnel] getLoggedInUsername() returned:`,t),t}catch(t){logger.log(`[funnel] getLoggedInUsername() failed, calling accomodateGuest()`),logger.log(`[funnel] Error was:`,t);let c=accomodateGuest();return logger.log(`[funnel] accomodateGuest() returned:`,c),c.username}}async migrateFunnelData(t,c){try{logger.info(`Starting data migration from ${t} to ${c}`);let u=getLocalUserDB(t),d=getLocalUserDB(c),m=await u.allDocs({include_docs:!0});logger.info(`Found ${m.rows.length} documents in funnel account`);let g=m.rows.filter(t=>!t.id.startsWith(`_design/`)).map(t=>({...t.doc,_rev:void 0}));return g.length>0?(await d.bulkDocs(g),logger.info(`Successfully migrated ${g.length} documents from ${t} to ${c}`)):logger.info(`No documents to migrate from funnel account`),{success:!0}}catch(t){return logger.error(`Migration failed:`,t),{success:!1,error:t instanceof Error?t.message:`Unknown error`}}}getRemoteCouchRootDB(){let t=ENV.COUCHDB_SERVER_PROTOCOL+`://`+ENV.COUCHDB_SERVER_URL+`skuilder`;try{return new pouchdb_setup_default(t,{skip_setup:!0})}catch(t){throw logger.error(`Failed to initialize remote CouchDB connection:`,t),Error(`Failed to initialize CouchDB: ${JSON.stringify(t)}`)}}getUserDB(t){let c=!1,u=`userdb-${hexEncode(t)}`;return log3(`Fetching user database: ${u} (${t})`),new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL+`://`+ENV.COUCHDB_SERVER_URL+u,createPouchDBConfig())}}}}),init_couch=__esm({"src/impl/couch/index.ts"(){"use strict";init_factory(),init_types_legacy(),init_logger(),init_pouchdb_setup(),init_contentSource(),init_adminDB2(),init_classroomDB2(),init_courseAPI(),init_courseDB(),init_CourseSyncService(),init_CouchDBSyncStrategy(),isBrowser=typeof window<`u`,isBrowser&&(window.process=import_browser.default),GUEST_LOCAL_DB=`userdb-${GuestUsername}`,new pouchdb_setup_default(GUEST_LOCAL_DB),pouchDBincludeCredentialsConfig={fetch(t,c){return c.credentials=`include`,pouchdb_setup_default.fetch(t,c)}},REVIEW_TIME_FORMAT2=`YYYY-MM-DD--kk:mm:ss-SSS`}}),init_BaseUserDB=__esm({"src/impl/common/BaseUserDB.ts"(){"use strict";var t;init_core(),init_util(),init_types_legacy(),init_logger(),init_userDBHelpers(),init_updateQueue(),init_user_course_relDB(),init_couch(),log4=t=>{logger.info(t)},BaseUser=(t=class _BaseUser{static Dummy(t){return new _BaseUser(`Me`,t)}getUsername(){return this._username}isLoggedIn(){return!this._username.startsWith(GuestUsername)}remote(){return this.remoteDB}async createAccount(t,c){if(!this.syncStrategy.canCreateAccount())throw Error(`Account creation not supported by current sync strategy`);if(!this._username.startsWith(GuestUsername))throw Error(`Cannot create a new account while logged in:
@@ -405,7 +405,7 @@ Examples:
405
405
  Card: ${JSON.stringify(t.cardElo)})
406
406
  `)}}else logger.warn(`[EloService] Partial ELO update (per-tag):
407
407
  User ELO update: ${S?`SUCCESS`:`FAILED`}
408
- Card ELO update: ${C?`SUCCESS`:`FAILED`}`),!S&&m[0].status===`rejected`&&logger.error(`[EloService] User ELO update error:`,m[0].reason),!C&&m[1].status===`rejected`&&logger.error(`[EloService] Card ELO update error:`,m[1].reason)}}},init_core(),init_logger(),ResponseProcessor=class{constructor(t,c){_defineProperty$2(this,`srsService`,void 0),_defineProperty$2(this,`eloService`,void 0),this.srsService=t,this.eloService=c}parsePerformance(t){return typeof t==`number`?{globalScore:t,taggedPerformance:null}:isTaggedPerformance(t)?{globalScore:t._global,taggedPerformance:t}:(logger.warn(`[ResponseProcessor] Unexpected performance structure, using neutral score`,{performance:t}),{globalScore:.5,taggedPerformance:null})}async processResponse(t,c,u,d,m,g,b,S,C,w){if(!isQuestionRecord(t))return{nextCardAction:`dismiss-success`,shouldLoadNextCard:!0,isCorrect:!0,shouldClearFeedbackShadow:!0};try{let T=await c,E;return E=t.isCorrect?this.processCorrectResponse(t,T,u,d,m,g,b):this.processIncorrectResponse(t,T,d,m,g,b,S,C,w),t.deferAdvance&&E.shouldLoadNextCard&&(logger.info(`[ResponseProcessor] deferAdvance requested — suppressing navigation, action stashed:`,{nextCardAction:E.nextCardAction}),E={...E,shouldLoadNextCard:!1,deferred:!0}),E}catch(t){throw logger.error(`[ResponseProcessor] Failed to load card history`,{e:t,cardId:b}),t}}processCorrectResponse(t,c,u,d,m,g,b){if(t.priorAttemps===0){m.card.tags.includes(`srs:skip`)||this.srsService.scheduleReview(c,u);let{globalScore:S,taggedPerformance:C}=this.parsePerformance(t.performance);if(C){let t=Object.keys(C).filter(t=>t!==`_global`),c=t.filter(t=>C[t]===null),u=t.filter(t=>C[t]!==null);logger.info(`[ResponseProcessor] per-tag ELO update for ${b}: scored=[${u.join(`, `)}] count-only=[${c.join(`, `)}]`),this.eloService.updateUserAndCardEloPerTag(C,g,b,d,m)}else{let t=.5+S/2;if(c.records.length===1)this.eloService.updateUserAndCardElo(t,g,b,d,m);else{let u=Math.ceil(32/c.records.length);this.eloService.updateUserAndCardElo(t,g,b,d,m,u)}logger.info(`[ResponseProcessor] Processed correct response with SRS scheduling and ELO update`)}return{nextCardAction:`dismiss-success`,shouldLoadNextCard:!0,isCorrect:!0,performanceScore:S,shouldClearFeedbackShadow:!0}}else{logger.info(`[ResponseProcessor] Processed correct response (retry attempt - no scheduling/ELO)`);let{globalScore:c}=this.parsePerformance(t.performance);return{nextCardAction:`marked-failed`,shouldLoadNextCard:!0,isCorrect:!0,performanceScore:c,shouldClearFeedbackShadow:!0}}}processIncorrectResponse(t,c,u,d,m,g,b,S,C){let{taggedPerformance:w}=this.parsePerformance(t.performance);return c.records.length!==1&&t.priorAttemps===0?w?(this.eloService.updateUserAndCardEloPerTag(w,m,g,u,d),logger.info(`[ResponseProcessor] Processed incorrect response with per-tag ELO update (${Object.keys(w).length-1} tags)`)):(this.eloService.updateUserAndCardElo(0,m,g,u,d),logger.info(`[ResponseProcessor] Processed incorrect response with ELO update`)):logger.info(`[ResponseProcessor] Processed incorrect response (no ELO update needed)`),d.records.length>=b?C>=S?(w?this.eloService.updateUserAndCardEloPerTag(w,m,g,u,d):this.eloService.updateUserAndCardElo(0,m,g,u,d),{nextCardAction:`dismiss-failed`,shouldLoadNextCard:!0,isCorrect:!1,shouldClearFeedbackShadow:!0}):{nextCardAction:`marked-failed`,shouldLoadNextCard:!0,isCorrect:!1,shouldClearFeedbackShadow:!0}:{nextCardAction:`none`,shouldLoadNextCard:!1,isCorrect:!1,shouldClearFeedbackShadow:!0}}},init_logger(),CardHydrationService=class{constructor(t,c,u){_defineProperty$2(this,`hydratedCards`,new Map),_defineProperty$2(this,`hydrationInFlight`,new Set),_defineProperty$2(this,`hydrationInProgress`,!1),this.getViewComponent=t,this.getCourseDB=c,this.getItemsToHydrate=u}getHydratedCard(t){return this.hydratedCards.get(t)??null}hasHydratedCard(t){return this.hydratedCards.has(t)}removeCard(t){this.hydratedCards.delete(t)}async ensureHydratedCards(){this.fillHydratedCards()}async waitForCard(t){if(this.hydratedCards.has(t))return this.hydratedCards.get(t);this.hydrationInProgress||this.fillHydratedCards();let c=1e4,u=25,d=0;for(;d<1e4;){if(this.hydratedCards.has(t))return this.hydratedCards.get(t);if(!this.hydrationInFlight.has(t)&&!this.hydrationInProgress)break;await new Promise(t=>setTimeout(t,25)),d+=25}return this.hydratedCards.get(t)??null}get hydratedCount(){return this.hydratedCards.size}getHydratedCardIds(){return Array.from(this.hydratedCards.keys())}async fillHydratedCards(){if(!this.hydrationInProgress){this.hydrationInProgress=!0;try{let t=this.getItemsToHydrate();for(let c of t)if(!(this.hydratedCards.has(c.cardID)||this.hydrationInFlight.has(c.cardID)))try{await this.hydrateCard(c)}catch(t){logger.error(`[CardHydrationService] Error hydrating card ${c.cardID}:`,t)}}finally{this.hydrationInProgress=!1}}}async hydrateCard(t){if(!(this.hydratedCards.has(t.cardID)||this.hydrationInFlight.has(t.cardID))){this.hydrationInFlight.add(t.cardID);try{let c=this.getCourseDB(t.courseID),[u,d]=await Promise.all([c.getCourseDoc(t.cardID),c.getAppliedTagsBatch([t.cardID])]);isCourseElo(u.elo)||(u.elo=toCourseElo(u.elo));let m=this.getViewComponent(u.id_view),g=await Promise.all(u.id_displayable_data.map(t=>c.getCourseDoc(t,{attachments:!0,binary:!0}))),b=[];g.forEach(t=>{t.data.forEach(t=>{b.push(...parseAudioURIs(t.data))})});let S=[...new Set(b)];S.length>0&&(logger.debug(`[CardHydrationService] Prefetching ${S.length} audio files for card ${t.cardID}`),await Promise.allSettled(S.map(prefetchAudio)));let C=g.map(displayableDataToViewData).reverse();this.hydratedCards.set(t.cardID,{item:t,view:m,data:C,tags:d.get(t.cardID)??[]}),logger.debug(`[CardHydrationService] Hydrated card ${t.cardID}`)}finally{this.hydrationInFlight.delete(t.cardID)}}}},ItemQueue=class{constructor(){_defineProperty$2(this,`q`,[]),_defineProperty$2(this,`seenCardIds`,[]),_defineProperty$2(this,`_dequeueCount`,0)}get dequeueCount(){return this._dequeueCount}add(t,c){this.seenCardIds.find(t=>t===c)||(this.seenCardIds.push(c),this.q.push(t))}addAll(t,c){t.forEach(t=>this.add(t,c(t)))}get length(){return this.q.length}peek(t){return this.q[t]}dequeue(t){if(this.q.length!==0){this._dequeueCount++;let c=this.q.splice(0,1)[0];if(t){let u=t(c),d=this.seenCardIds.indexOf(u);d>-1&&this.seenCardIds.splice(d,1)}return c}else return null}replaceAll(t,c){this.q=[],this.seenCardIds=[];for(let u of t){let t=c(u);this.seenCardIds.includes(t)||(this.seenCardIds.push(t),this.q.push(u))}}mergeToFront(t,c,u){let d=0,m=[];for(let g of t){let t=c(g);if(!this.seenCardIds.includes(t))this.seenCardIds.push(t),m.push(g),d++;else if(u?.has(t)){let u=this.q.findIndex(u=>c(u)===t);u>=0&&m.push(...this.q.splice(u,1))}}return this.q.unshift(...m),d}get toString(){return`${typeof this.q[0]}:
408
+ Card ELO update: ${C?`SUCCESS`:`FAILED`}`),!S&&m[0].status===`rejected`&&logger.error(`[EloService] User ELO update error:`,m[0].reason),!C&&m[1].status===`rejected`&&logger.error(`[EloService] Card ELO update error:`,m[1].reason)}}},init_core(),init_logger(),ResponseProcessor=class{constructor(t,c){_defineProperty$2(this,`srsService`,void 0),_defineProperty$2(this,`eloService`,void 0),this.srsService=t,this.eloService=c}parsePerformance(t){return typeof t==`number`?{globalScore:t,taggedPerformance:null}:isTaggedPerformance(t)?{globalScore:t._global,taggedPerformance:t}:(logger.warn(`[ResponseProcessor] Unexpected performance structure, using neutral score`,{performance:t}),{globalScore:.5,taggedPerformance:null})}async processResponse(t,c,u,d,m,g,b,S,C,w){if(!isQuestionRecord(t))return{nextCardAction:`dismiss-success`,shouldLoadNextCard:!0,isCorrect:!0,shouldClearFeedbackShadow:!0};try{let T=await c,E;return E=t.isCorrect?this.processCorrectResponse(t,T,u,d,m,g,b):this.processIncorrectResponse(t,T,d,m,g,b,S,C,w),t.deferAdvance&&E.shouldLoadNextCard&&(logger.info(`[ResponseProcessor] deferAdvance requested — suppressing navigation, action stashed:`,{nextCardAction:E.nextCardAction}),E={...E,shouldLoadNextCard:!1,deferred:!0}),E}catch(t){throw logger.error(`[ResponseProcessor] Failed to load card history`,{e:t,cardId:b}),t}}processCorrectResponse(t,c,u,d,m,g,b){if(t.priorAttemps===0){m.card.tags.includes(`srs:skip`)||this.srsService.scheduleReview(c,u);let{globalScore:S,taggedPerformance:C}=this.parsePerformance(t.performance);if(C){let u=Object.keys(C).filter(t=>t!==`_global`),S=u.filter(t=>C[t]===null),w=u.filter(t=>C[t]!==null);logger.info(`[FirstContactElo] correct first-attempt per-tag ELO update for ${b} (historyLen=${c.records.length}, priorAttemps=${t.priorAttemps}): scored=[${w.join(`, `)}] count-only=[${S.join(`, `)}]`),this.eloService.updateUserAndCardEloPerTag(C,g,b,d,m)}else{let u=.5+S/2;if(c.records.length===1)this.eloService.updateUserAndCardElo(u,g,b,d,m);else{let t=Math.ceil(32/c.records.length);this.eloService.updateUserAndCardElo(u,g,b,d,m,t)}logger.info(`[FirstContactElo] correct first-attempt ELO update (score=${u.toFixed(3)}) for ${b} (historyLen=${c.records.length}, priorAttemps=${t.priorAttemps})`)}return{nextCardAction:`dismiss-success`,shouldLoadNextCard:!0,isCorrect:!0,performanceScore:S,shouldClearFeedbackShadow:!0}}else{logger.info(`[ResponseProcessor] Processed correct response (retry attempt - no scheduling/ELO)`);let{globalScore:c}=this.parsePerformance(t.performance);return{nextCardAction:`marked-failed`,shouldLoadNextCard:!0,isCorrect:!0,performanceScore:c,shouldClearFeedbackShadow:!0}}}processIncorrectResponse(t,c,u,d,m,g,b,S,C){let{taggedPerformance:w}=this.parsePerformance(t.performance),T=!1;return t.priorAttemps===0?(w?(this.eloService.updateUserAndCardEloPerTag(w,m,g,u,d),logger.info(`[FirstContactElo] incorrect first-attempt per-tag ELO update for ${g} (historyLen=${c.records.length}, priorAttemps=${t.priorAttemps}, tags=${Object.keys(w).length-1})`)):(this.eloService.updateUserAndCardElo(0,m,g,u,d),logger.info(`[FirstContactElo] incorrect first-attempt ELO update (score=0) for ${g} (historyLen=${c.records.length}, priorAttemps=${t.priorAttemps})`)),T=!0):logger.info(`[FirstContactElo] incorrect retry \u2014 no ELO update for ${g} (historyLen=${c.records.length}, priorAttemps=${t.priorAttemps})`),d.records.length>=b?C>=S?(T?logger.info(`[FirstContactElo] dismiss-failed \u2014 ELO already updated this response, skipping double penalty for ${g}`):(w?this.eloService.updateUserAndCardEloPerTag(w,m,g,u,d):this.eloService.updateUserAndCardElo(0,m,g,u,d),logger.info(`[FirstContactElo] dismiss-failed final ELO penalty for ${g} (historyLen=${c.records.length}, sessionViews=${C})`)),{nextCardAction:`dismiss-failed`,shouldLoadNextCard:!0,isCorrect:!1,shouldClearFeedbackShadow:!0}):{nextCardAction:`marked-failed`,shouldLoadNextCard:!0,isCorrect:!1,shouldClearFeedbackShadow:!0}:{nextCardAction:`none`,shouldLoadNextCard:!1,isCorrect:!1,shouldClearFeedbackShadow:!0}}},init_logger(),CardHydrationService=class{constructor(t,c,u){_defineProperty$2(this,`hydratedCards`,new Map),_defineProperty$2(this,`hydrationInFlight`,new Set),_defineProperty$2(this,`hydrationInProgress`,!1),this.getViewComponent=t,this.getCourseDB=c,this.getItemsToHydrate=u}getHydratedCard(t){return this.hydratedCards.get(t)??null}hasHydratedCard(t){return this.hydratedCards.has(t)}removeCard(t){this.hydratedCards.delete(t)}async ensureHydratedCards(){this.fillHydratedCards()}async waitForCard(t){if(this.hydratedCards.has(t))return this.hydratedCards.get(t);this.hydrationInProgress||this.fillHydratedCards();let c=1e4,u=25,d=0;for(;d<1e4;){if(this.hydratedCards.has(t))return this.hydratedCards.get(t);if(!this.hydrationInFlight.has(t)&&!this.hydrationInProgress)break;await new Promise(t=>setTimeout(t,25)),d+=25}return this.hydratedCards.get(t)??null}get hydratedCount(){return this.hydratedCards.size}getHydratedCardIds(){return Array.from(this.hydratedCards.keys())}async fillHydratedCards(){if(!this.hydrationInProgress){this.hydrationInProgress=!0;try{let t=this.getItemsToHydrate();for(let c of t)if(!(this.hydratedCards.has(c.cardID)||this.hydrationInFlight.has(c.cardID)))try{await this.hydrateCard(c)}catch(t){logger.error(`[CardHydrationService] Error hydrating card ${c.cardID}:`,t)}}finally{this.hydrationInProgress=!1}}}async hydrateCard(t){if(!(this.hydratedCards.has(t.cardID)||this.hydrationInFlight.has(t.cardID))){this.hydrationInFlight.add(t.cardID);try{let c=this.getCourseDB(t.courseID),[u,d]=await Promise.all([c.getCourseDoc(t.cardID),c.getAppliedTagsBatch([t.cardID])]);isCourseElo(u.elo)||(u.elo=toCourseElo(u.elo));let m=this.getViewComponent(u.id_view),g=await Promise.all(u.id_displayable_data.map(t=>c.getCourseDoc(t,{attachments:!0,binary:!0}))),b=[];g.forEach(t=>{t.data.forEach(t=>{b.push(...parseAudioURIs(t.data))})});let S=[...new Set(b)];S.length>0&&(logger.debug(`[CardHydrationService] Prefetching ${S.length} audio files for card ${t.cardID}`),await Promise.allSettled(S.map(prefetchAudio)));let C=g.map(displayableDataToViewData).reverse();this.hydratedCards.set(t.cardID,{item:t,view:m,data:C,tags:d.get(t.cardID)??[]}),logger.debug(`[CardHydrationService] Hydrated card ${t.cardID}`)}finally{this.hydrationInFlight.delete(t.cardID)}}}},ItemQueue=class{constructor(){_defineProperty$2(this,`q`,[]),_defineProperty$2(this,`seenCardIds`,[]),_defineProperty$2(this,`_dequeueCount`,0)}get dequeueCount(){return this._dequeueCount}add(t,c){this.seenCardIds.find(t=>t===c)||(this.seenCardIds.push(c),this.q.push(t))}addAll(t,c){t.forEach(t=>this.add(t,c(t)))}get length(){return this.q.length}peek(t){return this.q[t]}dequeue(t){if(this.q.length!==0){this._dequeueCount++;let c=this.q.splice(0,1)[0];if(t){let u=t(c),d=this.seenCardIds.indexOf(u);d>-1&&this.seenCardIds.splice(d,1)}return c}else return null}replaceAll(t,c){this.q=[],this.seenCardIds=[];for(let u of t){let t=c(u);this.seenCardIds.includes(t)||(this.seenCardIds.push(t),this.q.push(u))}}mergeToFront(t,c,u){let d=0,m=[];for(let g of t){let t=c(g);if(!this.seenCardIds.includes(t))this.seenCardIds.push(t),m.push(g),d++;else if(u?.has(t)){let u=this.q.findIndex(u=>c(u)===t);u>=0&&m.push(...this.q.splice(u,1))}}return this.q.unshift(...m),d}get toString(){return`${typeof this.q[0]}:
409
409
  `+this.q.map(t=>` ${t.courseID}+${t.cardID}: ${t.status}`).join(`
410
410
  `)}},init_couch(),init_core(),init_recording(),init_Loggable(),init_types_legacy(),init_logger(),CouchDBToStaticPacker=class{constructor(t={}){_defineProperty$2(this,`config`,void 0),_defineProperty$2(this,`sourceDB`,null),this.config={chunkSize:1e3,includeAttachments:!0,...t}}async packCourse(t,c){logger.info(`Starting static pack for course: ${c}`),this.sourceDB=t;let u={version:`1.0.0`,courseId:c,courseName:``,courseConfig:null,lastUpdated:new Date().toISOString(),documentCount:0,chunks:[],indices:[],designDocs:[]},d=await this.extractCourseConfig(t);u.courseName=d.name,u.courseConfig=d,u.designDocs=await this.extractDesignDocs(t);let m=await this.extractDocumentsByType(t),g=new Map;this.config.includeAttachments&&await this.extractAllAttachments(m,g);let b=new Map;for(let[t,c]of Object.entries(m)){let d=this.createChunks(c,t);u.chunks.push(...d),u.documentCount+=c.length,this.prepareChunkData(d,c,b)}let S=new Map;return u.indices=await this.buildIndices(m,u.designDocs,S),{manifest:u,chunks:b,indices:S,attachments:g}}async packCourseToFiles(t,c,u,d){logger.info(`Packing course ${c} to files in ${u}`);let m=await this.packCourse(t,c),g=await this.writePackedDataToFiles(m,u,d);return{manifest:m.manifest,filesWritten:g,attachmentsFound:m.attachments?m.attachments.size:0}}async writePackedDataToFiles(t,c,u){let d=0;await u.ensureDir(c);let m=u.joinPath(c,`manifest.json`);await u.writeJson(m,t.manifest,{spaces:2}),d++,logger.info(`Wrote manifest: ${m}`);let g=u.joinPath(c,`chunks`),b=u.joinPath(c,`indices`);await u.ensureDir(g),await u.ensureDir(b);for(let[c,m]of t.chunks){let t=u.joinPath(g,`${c}.json`);await u.writeJson(t,m),d++}logger.info(`Wrote ${t.chunks.size} chunk files`);for(let[c,m]of t.indices){let t=u.joinPath(b,`${c}.json`);await u.writeJson(t,m,{spaces:2}),d++}if(logger.info(`Wrote ${t.indices.size} index files`),t.attachments&&t.attachments.size>0){for(let[m,g]of t.attachments){let t=u.joinPath(c,m),b=u.dirname(t);await u.ensureDir(b),await u.writeFile(t,g.buffer),d++}logger.info(`Wrote ${t.attachments.size} attachment files`)}return d}async extractCourseConfig(t){try{return await t.get(`CourseConfig`)}catch(t){throw logger.error(`Failed to extract course config:`,t),Error(`Course config not found`)}}async extractDesignDocs(t){return(await t.allDocs({startkey:`_design/`,endkey:`_design/￰`,include_docs:!0})).rows.map(t=>({_id:t.id,views:t.doc.views||{}}))}async extractDocumentsByType(t){let c=await t.allDocs({include_docs:!0}),u={};for(let t of c.rows){if(t.id.startsWith(`_`))continue;let c=t.doc;c.docType&&(u[c.docType]||(u[c.docType]=[]),u[c.docType].push(c))}return u}createChunks(t,c){let u=[],d=t.sort((t,c)=>t._id.localeCompare(c._id));for(let t=0;t<d.length;t+=this.config.chunkSize){let m=d.slice(t,t+this.config.chunkSize),g=`${c}-${String(Math.floor(t/this.config.chunkSize)).padStart(4,`0`)}`;u.push({id:g,docType:c,startKey:m[0]._id,endKey:m[m.length-1]._id,documentCount:m.length,path:`chunks/${g}.json`})}return u}prepareChunkData(t,c,u){let d=c.sort((t,c)=>t._id.localeCompare(c._id));for(let c of t){let t=d.filter(t=>t._id>=c.startKey&&t._id<=c.endKey).map(t=>{let c={...t};return delete c._rev,this.config.includeAttachments&&c._attachments?c._attachments=this.transformAttachmentStubs(c._attachments,c._id):this.config.includeAttachments||delete c._attachments,c});u.set(c.id,t)}}async buildIndices(t,c,u){let d=[];if(t.CARD){let c=await this.buildEloIndex(t.CARD,u);d.push(c)}if(t.TAG){let c=await this.buildTagIndex(t.TAG,u);d.push(c)}for(let t of c)for(let[c,m]of Object.entries(t.views))if(m.map){logger.info(`Processing view: ${t._id}/${c}`);let m=await this.buildViewIndex(c,t,u);m?(d.push(m),logger.info(`Successfully built index: ${m.name}`)):logger.warn(`Skipped view index: ${t._id}/${c}`)}return d}async buildEloIndex(t,c){let u=[];for(let c of t)c.elo?.global?.score&&u.push({elo:c.elo.global.score,cardId:c._id});u.sort((t,c)=>t.elo-c.elo);let d={},m=50;for(let t of u){let c=Math.floor(t.elo/50)*50;d[c]||(d[c]=[]),d[c].push(t.cardId)}return c.set(`elo`,{sorted:u,buckets:d,stats:{min:u[0]?.elo||0,max:u[u.length-1]?.elo||0,count:u.length}}),{name:`elo`,type:`btree`,path:`indices/elo.json`}}async buildTagIndex(t,c){let u={};for(let c of t)u[c.name]={cardIds:c.taggedCards,snippet:c.snippet,count:c.taggedCards.length};let d={};for(let c of t)for(let t of c.taggedCards)d[t]||(d[t]=[]),d[t].push(c.name);return c.set(`tags`,{byTag:u,byCard:d}),{name:`tags`,type:`hash`,path:`indices/tags.json`}}async buildViewIndex(t,c,u){if(!this.sourceDB)return logger.error(`Source database not available for view querying`),null;try{let d=`${c._id.replace(`_design/`,``)}/${t}`;logger.info(`Querying CouchDB view: ${d}`);let m=await this.sourceDB.query(d,{include_docs:!1});if(!m.rows||m.rows.length===0)return logger.warn(`View ${d} returned no results`),null;logger.info(`Successfully queried view ${d}: ${m.rows.length} results`);let g=this.formatViewResults(t,m.rows,c),b=`view-${c._id.replace(`_design/`,``)}-${t}`;return u.set(b,g),{name:b,type:`view`,path:`indices/${b}.json`}}catch(u){return logger.error(`Failed to query view ${c._id}/${t}:`,u),null}}formatViewResults(t,c,u){let d={type:`couchdb-view`,viewName:t,designDoc:u._id,results:c,metadata:{resultCount:c.length,generatedAt:new Date().toISOString()}};switch(t){case`elo`:return this.formatEloViewIndex(c,d);case`getTags`:return this.formatTagsViewIndex(c,d);case`cardsByInexperience`:return this.formatInexperienceViewIndex(c,d);default:return this.formatGenericViewIndex(c,d)}}formatEloViewIndex(t,c){let u=t.sort((t,c)=>typeof t.key==`number`&&typeof c.key==`number`?t.key-c.key:0);return{...c,sorted:u,stats:{min:u[0]?.key||0,max:u[u.length-1]?.key||0,count:u.length}}}formatTagsViewIndex(t,c){let u={};for(let c of t){let t=c.key;typeof t==`string`&&(u[t]||(u[t]=[]),u[t].push(c.id))}return{...c,byTag:u,tagCount:Object.keys(u).length}}formatInexperienceViewIndex(t,c){let u=t.sort((t,c)=>typeof t.key==`number`&&typeof c.key==`number`?t.key-c.key:0);return{...c,sorted:u,stats:{minInexperience:u[0]?.key||0,maxInexperience:u[u.length-1]?.key||0,count:u.length}}}formatGenericViewIndex(t,c){return{...c}}async extractAllAttachments(t,c){logger.info(`Extracting attachments...`);let u=[];for(let c of Object.values(t))u.push(...c);let d=u.filter(t=>t._attachments&&Object.keys(t._attachments).length>0);if(d.length===0){logger.info(`No attachments found`);return}logger.info(`Found ${d.length} documents with attachments`);let m=d.map(t=>this.extractDocumentAttachments(t,c));await Promise.all(m),logger.info(`Extracted ${c.size} attachment files`)}async extractDocumentAttachments(t,c){if(!t._attachments||!this.sourceDB)return;let u=t._id;for(let[d,m]of Object.entries(t._attachments))try{let t=await this.sourceDB.getAttachment(u,d),g;if(t instanceof ArrayBuffer)g=Buffer.from(t);else if(Buffer.isBuffer(t))g=t;else{let c=t;g=Buffer.from(await c.arrayBuffer())}let b=`${d}${this.getFileExtension(m.content_type)}`,S=`attachments/${u}/${b}`;c.set(S,{docId:u,attachmentName:d,filename:b,path:S,contentType:m.content_type,length:m.length||g.length,digest:m.digest,buffer:g}),logger.debug(`Extracted attachment: ${S}`)}catch(t){throw logger.error(`Failed to extract attachment ${u}/${d}:`,t),Error(`Failed to extract attachment ${u}/${d}: ${t}`)}}transformAttachmentStubs(t,c){let u={};for(let[d,m]of Object.entries(t))u[d]={path:`attachments/${c}/${`${d}${this.getFileExtension(m.content_type)}`}`,content_type:m.content_type,length:m.length,digest:m.digest,stub:!1};return u}getFileExtension(t){return{"image/jpeg":`.jpg`,"image/jpg":`.jpg`,"image/png":`.png`,"image/gif":`.gif`,"image/webp":`.webp`,"audio/mpeg":`.mp3`,"audio/mp3":`.mp3`,"audio/wav":`.wav`,"audio/ogg":`.ogg`,"video/mp4":`.mp4`,"video/webm":`.webm`,"application/pdf":`.pdf`,"text/plain":`.txt`,"application/json":`.json`}[t]||``}},init_logger(),DEFAULT_MIGRATION_OPTIONS={chunkBatchSize:100,validateRoundTrip:!1,cleanupOnFailure:!0,timeout:3e5},init_logger(),FileSystemError=class extends Error{constructor(t,c,u,d){super(t),this.operation=c,this.filePath=u,this.cause=d,this.name=`FileSystemError`}},nodeFS2=null;try{typeof window>`u`&&typeof process<`u`&&process.versions?.node&&(nodeFS2=eval(`require`)(`fs`),nodeFS2.promises=nodeFS2.promises||eval(`require`)(`fs`).promises)}catch{}nodeFS3=null,nodePath=null;try{typeof window>`u`&&typeof process<`u`&&process.versions?.node&&(nodeFS3=eval(`require`)(`fs`),nodePath=eval(`require`)(`path`),nodeFS3.promises=nodeFS3.promises||eval(`require`)(`fs`).promises)}catch{}StaticToCouchDBMigrator=class{constructor(t={},c){_defineProperty$2(this,`options`,void 0),_defineProperty$2(this,`progressCallback`,void 0),_defineProperty$2(this,`fs`,void 0),this.options={...DEFAULT_MIGRATION_OPTIONS,...t},this.fs=c}setProgressCallback(t){this.progressCallback=t}async migrateCourse(t,c){let u=Date.now(),d={success:!1,documentsRestored:0,attachmentsRestored:0,designDocsRestored:0,courseConfigRestored:0,errors:[],warnings:[],migrationTime:0};try{logger.info(`Starting migration from ${t} to CouchDB`),this.reportProgress(`manifest`,0,1,`Validating static course...`);let m=await validateStaticCourse(t,this.fs);if(!m.valid)throw d.errors.push(...m.errors),Error(`Static course validation failed: ${m.errors.join(`, `)}`);d.warnings.push(...m.warnings),this.reportProgress(`manifest`,1,1,`Loading course manifest...`);let g=await this.loadManifest(t);logger.info(`Loaded manifest for course: ${g.courseId} (${g.courseName})`),this.reportProgress(`design_docs`,0,g.designDocs.length,`Restoring design documents...`);let b=await this.restoreDesignDocuments(g.designDocs,c);d.designDocsRestored=b.restored,d.errors.push(...b.errors),d.warnings.push(...b.warnings),this.reportProgress(`course_config`,0,1,`Restoring CourseConfig document...`);let S=await this.restoreCourseConfig(g,c);d.courseConfigRestored=S.restored,d.errors.push(...S.errors),d.warnings.push(...S.warnings),this.reportProgress(`course_config`,1,1,`CourseConfig document restored`);let C=this.calculateExpectedCounts(g);this.reportProgress(`documents`,0,g.documentCount,`Aggregating documents from chunks...`);let w=await this.aggregateDocuments(t,g),T=w.filter(t=>t._id!==`CourseConfig`);w.length!==T.length&&d.warnings.push(`Filtered out ${w.length-T.length} CourseConfig document(s) from chunks to prevent conflicts`),this.reportProgress(`documents`,T.length,g.documentCount,`Uploading documents to CouchDB...`);let E=await this.uploadDocuments(T,c);d.documentsRestored=E.restored,d.errors.push(...E.errors),d.warnings.push(...E.warnings);let D=w.filter(t=>t._attachments&&Object.keys(t._attachments).length>0);this.reportProgress(`attachments`,0,D.length,`Uploading attachments...`);let O=await this.uploadAttachments(t,D,c);if(d.attachmentsRestored=O.restored,d.errors.push(...O.errors),d.warnings.push(...O.warnings),this.options.validateRoundTrip){this.reportProgress(`validation`,0,1,`Validating migration...`);let t=await validateMigration(c,C,g);t.valid||(d.warnings.push(`Migration validation found issues`),t.issues.forEach(t=>{t.type===`error`?d.errors.push(`Validation: ${t.message}`):d.warnings.push(`Validation: ${t.message}`)})),this.reportProgress(`validation`,1,1,`Migration validation completed`)}d.success=d.errors.length===0,d.migrationTime=Date.now()-u,logger.info(`Migration completed in ${d.migrationTime}ms`),logger.info(`Documents restored: ${d.documentsRestored}`),logger.info(`Attachments restored: ${d.attachmentsRestored}`),logger.info(`Design docs restored: ${d.designDocsRestored}`),logger.info(`CourseConfig restored: ${d.courseConfigRestored}`),d.errors.length>0&&logger.error(`Migration completed with ${d.errors.length} errors`),d.warnings.length>0&&logger.warn(`Migration completed with ${d.warnings.length} warnings`)}catch(t){d.success=!1,d.migrationTime=Date.now()-u;let m=t instanceof Error?t.message:String(t);if(d.errors.push(`Migration failed: ${m}`),logger.error(`Migration failed:`,t),this.options.cleanupOnFailure)try{await this.cleanupFailedMigration(c)}catch(t){logger.error(`Failed to cleanup after migration failure:`,t),d.warnings.push(`Failed to cleanup after migration failure`)}}return d}async loadManifest(t){try{let c,u;if(this.fs)u=this.fs.joinPath(t,`manifest.json`),c=await this.fs.readFile(u);else if(u=nodeFS3&&nodePath?nodePath.join(t,`manifest.json`):`${t}/manifest.json`,nodeFS3&&this.isLocalPath(t))c=await nodeFS3.promises.readFile(u,`utf8`);else{let t=await fetch(u);if(!t.ok)throw Error(`Failed to fetch manifest: ${t.status} ${t.statusText}`);c=await t.text()}let d=JSON.parse(c);if(!d.version||!d.courseId||!d.chunks)throw Error(`Invalid manifest structure`);return d}catch(t){let c=t instanceof FileSystemError?t.message:`Failed to load manifest: ${t instanceof Error?t.message:String(t)}`;throw Error(c)}}async restoreDesignDocuments(t,c){let u={restored:0,errors:[],warnings:[]};for(let d=0;d<t.length;d++){let m=t[d];this.reportProgress(`design_docs`,d,t.length,`Restoring ${m._id}...`);try{let t;try{t=await c.get(m._id)}catch{}let d={_id:m._id,views:m.views};t?(d._rev=t._rev,logger.debug(`Updating existing design document: ${m._id}`)):logger.debug(`Creating new design document: ${m._id}`),await c.put(d),u.restored++}catch(t){let c=`Failed to restore design document ${m._id}: ${t instanceof Error?t.message:String(t)}`;u.errors.push(c),logger.error(c)}}return this.reportProgress(`design_docs`,t.length,t.length,`Restored ${u.restored} design documents`),u}async aggregateDocuments(t,c){let u=[],d=new Map;for(let m=0;m<c.chunks.length;m++){let g=c.chunks[m];this.reportProgress(`documents`,u.length,c.documentCount,`Loading chunk ${g.id}...`);try{let c=await this.loadChunk(t,g);for(let t of c){if(!t._id){logger.warn(`Document without _id found in chunk ${g.id}, skipping`);continue}d.has(t._id)&&logger.warn(`Duplicate document ID found: ${t._id}, using latest version`),d.set(t._id,t)}}catch(t){throw Error(`Failed to load chunk ${g.id}: ${t instanceof Error?t.message:String(t)}`)}}return u.push(...d.values()),logger.info(`Aggregated ${u.length} unique documents from ${c.chunks.length} chunks`),u}async loadChunk(t,c){try{let u,d;if(this.fs)d=this.fs.joinPath(t,c.path),u=await this.fs.readFile(d);else if(d=nodeFS3&&nodePath?nodePath.join(t,c.path):`${t}/${c.path}`,nodeFS3&&this.isLocalPath(t))u=await nodeFS3.promises.readFile(d,`utf8`);else{let t=await fetch(d);if(!t.ok)throw Error(`Failed to fetch chunk: ${t.status} ${t.statusText}`);u=await t.text()}let m=JSON.parse(u);if(!Array.isArray(m))throw Error(`Chunk file does not contain an array of documents`);return m}catch(t){let c=t instanceof FileSystemError?t.message:`Failed to load chunk: ${t instanceof Error?t.message:String(t)}`;throw Error(c)}}async uploadDocuments(t,c){let u={restored:0,errors:[],warnings:[]},d=this.options.chunkBatchSize;for(let m=0;m<t.length;m+=d){let g=t.slice(m,m+d);this.reportProgress(`documents`,m,t.length,`Uploading batch ${Math.floor(m/d)+1}...`);try{let t=g.map(t=>{let c={...t};return delete c._rev,delete c._attachments,c}),d=await c.bulkDocs(t);for(let t=0;t<d.length;t++){let c=d[t],m=g[t];if(`error`in c){let t=`Failed to upload document ${m._id}: ${c.error} - ${c.reason}`;u.errors.push(t),logger.error(t)}else u.restored++}}catch(t){let c;c=t instanceof Error||t&&typeof t==`object`&&`message`in t?`Failed to upload document batch starting at index ${m}: ${t.message}`:`Failed to upload document batch starting at index ${m}: ${JSON.stringify(t)}`,u.errors.push(c),logger.error(c)}}return this.reportProgress(`documents`,t.length,t.length,`Uploaded ${u.restored} documents`),u}async uploadAttachments(t,c,u){let d={restored:0,errors:[],warnings:[]},m=0;for(let g of c)if(this.reportProgress(`attachments`,m,c.length,`Processing attachments for ${g._id}...`),m++,g._attachments)for(let[c,m]of Object.entries(g._attachments))try{let b=await this.uploadSingleAttachment(t,g._id,c,m,u);b.success?d.restored++:d.errors.push(b.error||`Unknown attachment upload error`)}catch(t){let u=`Failed to upload attachment ${g._id}/${c}: ${t instanceof Error?t.message:String(t)}`;d.errors.push(u),logger.error(u)}return this.reportProgress(`attachments`,c.length,c.length,`Uploaded ${d.restored} attachments`),d}async uploadSingleAttachment(t,c,u,d,m){let g={success:!1,attachmentName:u,docId:c};try{if(!d.path)return g.error=`Attachment metadata missing file path`,g;let b,S;if(this.fs)S=this.fs.joinPath(t,d.path),b=await this.fs.readBinary(S);else if(S=nodeFS3&&nodePath?nodePath.join(t,d.path):`${t}/${d.path}`,nodeFS3&&this.isLocalPath(t))b=await nodeFS3.promises.readFile(S);else{let t=await fetch(S);if(!t.ok)return g.error=`Failed to fetch attachment: ${t.status} ${t.statusText}`,g;b=await t.arrayBuffer()}let C=await m.get(c);await m.putAttachment(c,u,C._rev,b,d.content_type),g.success=!0}catch(t){g.error=t instanceof Error?t.message:String(t)}return g}async restoreCourseConfig(t,c){let u={restored:0,errors:[],warnings:[]};try{if(!t.courseConfig)return u.warnings.push(`No courseConfig found in manifest, skipping CourseConfig document creation`),u;let d={_id:`CourseConfig`,...t.courseConfig,courseID:t.courseId};delete d._rev,await c.put(d),u.restored=1,logger.info(`CourseConfig document created for course: ${t.courseId}`)}catch(t){let c=t instanceof Error?t.message:JSON.stringify(t);u.errors.push(`Failed to restore CourseConfig: ${c}`),logger.error(`CourseConfig restoration failed:`,t)}return u}calculateExpectedCounts(t){let c={};for(let u of t.chunks)c[u.docType]=(c[u.docType]||0)+u.documentCount;return t.designDocs.length>0&&(c._design=t.designDocs.length),c}async cleanupFailedMigration(t){logger.info(`Cleaning up failed migration...`);try{let c=(await t.allDocs()).rows.map(t=>({_id:t.id,_rev:t.value.rev,_deleted:!0}));c.length>0&&(await t.bulkDocs(c),logger.info(`Cleaned up ${c.length} documents from failed migration`))}catch(t){throw logger.error(`Failed to cleanup documents:`,t),t}}reportProgress(t,c,u,d){this.progressCallback&&this.progressCallback({phase:t,current:c,total:u,message:d})}isLocalPath(t){return!t.startsWith(`http://`)&&!t.startsWith(`https://`)}},init_dataDirectory(),init_navigators(),init_Pipeline(),QuotaRoundRobinMixer=class{mix(t,c){if(t.length===0)return[];let u=Math.ceil(c/t.length),d=t.map(t=>[...t.weighted].sort((t,c)=>c.score-t.score).slice(0,u));for(let t=d.length-1;t>0;t--){let c=Math.floor(Math.random()*(t+1));[d[t],d[c]]=[d[c],d[t]]}let m=[],g=0,b=Array(d.length).fill(0);for(;m.length<c&&g<d.length;){g=0;for(let t=0;t<d.length&&!(m.length>=c);t++)b[t]<d[t].length?(m.push(d[t][b[t]]),b[t]++):g++}return m}},init_logger(),init_navigators(),MAX_RUNS2=10,runHistory2=[],mixerDebugAPI={get runs(){return[...runHistory2]},showRun(t=0){if(runHistory2.length===0){logger.info(`[Mixer Debug] No runs captured yet.`);return}let c;if(typeof t==`number`){if(c=runHistory2[t],!c){logger.info(`[Mixer Debug] No run found at index ${t}. History length: ${runHistory2.length}`);return}}else if(c=runHistory2.find(c=>c.runId.endsWith(t)),!c){logger.info(`[Mixer Debug] No run found matching ID '${t}'.`);return}printMixerSummary(c)},showLastMix(){this.showRun(0)},explainSourceBalance(){if(runHistory2.length===0){logger.info(`[Mixer Debug] No runs captured yet.`);return}let t=runHistory2[0];console.group(`⚖️ Source Balance Analysis`),logger.info(`Mixer: ${t.mixerType}`),logger.info(`Requested limit: ${t.requestedLimit}`),t.quotaPerSource&&logger.info(`Quota per source: ${t.quotaPerSource}`),console.group(`Input Distribution:`);for(let c of t.sourceSummaries){let t=c.sourceName||c.sourceId;logger.info(`${t}:`),logger.info(` Provided: ${c.totalCards} cards (${c.reviewCount} reviews, ${c.newCount} new)`),logger.info(` Score range: [${c.scoreRange[0].toFixed(2)}, ${c.scoreRange[1].toFixed(2)}]`)}console.groupEnd(),console.group(`Selection Results:`);for(let c of t.sourceBreakdowns){let t=c.sourceName||c.sourceId;logger.info(`${t}:`),logger.info(` Selected: ${c.totalSelected}/${c.reviewsProvided+c.newProvided} (${c.selectionRate.toFixed(1)}%)`),logger.info(` Reviews: ${c.reviewsSelected}/${c.reviewsProvided}`),logger.info(` New: ${c.newSelected}/${c.newProvided}`),c.reviewsProvided>0&&c.reviewsSelected===0&&logger.info(` ⚠️ Had reviews but none selected!`),c.totalSelected===0&&c.reviewsProvided+c.newProvided>0&&logger.info(` ⚠️ Had cards but none selected!`)}console.groupEnd();let c=t.sourceBreakdowns.map(t=>t.selectionRate),u=c.reduce((t,c)=>t+c,0)/c.length,d=Math.max(...c.map(t=>Math.abs(t-u)));d>20&&(logger.info(`
411
411
  \u26A0\uFE0F Significant imbalance detected (max deviation: ${d.toFixed(1)}%)`),logger.info(`Possible causes:`),logger.info(` - Score range differences between sources`),logger.info(` - One source has much better quality cards`),logger.info(` - Different card availability (reviews vs new)`)),console.groupEnd()},compareScores(){if(runHistory2.length===0){logger.info(`[Mixer Debug] No runs captured yet.`);return}let t=runHistory2[0];console.group(`📊 Score Distribution Comparison`),console.table(t.sourceSummaries.map(t=>({source:t.sourceName||t.sourceId,cards:t.totalCards,min:t.bottomScore.toFixed(3),max:t.topScore.toFixed(3),avg:t.avgScore.toFixed(3),range:(t.topScore-t.bottomScore).toFixed(3)})));let c=t.sourceSummaries.map(t=>t.topScore-t.bottomScore),u=t.sourceSummaries.map(t=>t.avgScore),d=Math.max(...c)-Math.min(...c),m=Math.max(...u)-Math.min(...u);(d>.3||m>.2)&&(logger.info(`