@vue-skuilder/db 0.1.26 → 0.1.27
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/core/index.d.cts +51 -1
- package/dist/core/index.d.ts +51 -1
- package/dist/core/index.js +337 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +335 -2
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +367 -20
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +367 -20
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +358 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +358 -4
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +337 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +335 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/UserDBDebugger.ts +466 -0
- package/src/core/index.ts +3 -0
- package/src/core/navigators/filters/hierarchyDefinition.ts +4 -3
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.1.
|
|
7
|
+
"version": "0.1.27",
|
|
8
8
|
"description": "Database layer for vue-skuilder",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
51
|
-
"@vue-skuilder/common": "0.1.
|
|
51
|
+
"@vue-skuilder/common": "0.1.27",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"vite": "^7.0.0",
|
|
63
63
|
"vitest": "^4.0.15"
|
|
64
64
|
},
|
|
65
|
-
"stableVersion": "0.1.
|
|
65
|
+
"stableVersion": "0.1.27"
|
|
66
66
|
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { logger } from '../util/logger';
|
|
2
|
+
import { getDataLayer } from '../factory';
|
|
3
|
+
import { DocType, DocTypePrefixes } from './types/types-legacy';
|
|
4
|
+
import { filterAllDocsByPrefix } from '../impl/common/userDBHelpers';
|
|
5
|
+
import type { UserDBInterface } from './interfaces/userDB';
|
|
6
|
+
import type { ScheduledCard, CourseRegistration } from './types/user';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// USER DATABASE DEBUGGER
|
|
10
|
+
// ============================================================================
|
|
11
|
+
//
|
|
12
|
+
// Console-accessible debug API for inspecting user database (PouchDB/CouchDB).
|
|
13
|
+
//
|
|
14
|
+
// Exposed as `window.skuilder.userdb` for interactive exploration.
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// window.skuilder.userdb.showUser()
|
|
18
|
+
// window.skuilder.userdb.showScheduledReviews()
|
|
19
|
+
// window.skuilder.userdb.showCourseRegistrations()
|
|
20
|
+
// window.skuilder.userdb.showCardHistory('cardId')
|
|
21
|
+
// window.skuilder.userdb.queryByType('SCHEDULED_CARD')
|
|
22
|
+
// window.skuilder.userdb.dbInfo()
|
|
23
|
+
// window.skuilder.userdb.export()
|
|
24
|
+
//
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the user database instance safely
|
|
29
|
+
*/
|
|
30
|
+
function getUserDB(): UserDBInterface | null {
|
|
31
|
+
try {
|
|
32
|
+
const provider = getDataLayer();
|
|
33
|
+
return provider.getUserDB();
|
|
34
|
+
} catch {
|
|
35
|
+
logger.info('[UserDB Debug] Data layer not initialized yet.');
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get raw PouchDB instance for advanced queries
|
|
42
|
+
* This accesses the internal localDB property
|
|
43
|
+
*/
|
|
44
|
+
function getRawDB(): PouchDB.Database | null {
|
|
45
|
+
const userDB = getUserDB();
|
|
46
|
+
if (!userDB) return null;
|
|
47
|
+
|
|
48
|
+
// Access the internal localDB property
|
|
49
|
+
// This is a bit of a hack but necessary for raw queries
|
|
50
|
+
const rawDB = (userDB as any).localDB;
|
|
51
|
+
if (!rawDB) {
|
|
52
|
+
logger.info('[UserDB Debug] Unable to access raw database instance.');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return rawDB;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format a timestamp for display
|
|
61
|
+
*/
|
|
62
|
+
function formatTimestamp(isoString: string): string {
|
|
63
|
+
const date = new Date(isoString);
|
|
64
|
+
return date.toLocaleString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Console API object exposed on window.skuilder.userdb
|
|
69
|
+
*/
|
|
70
|
+
export const userDBDebugAPI = {
|
|
71
|
+
/**
|
|
72
|
+
* Show current user information
|
|
73
|
+
*/
|
|
74
|
+
showUser(): void {
|
|
75
|
+
const userDB = getUserDB();
|
|
76
|
+
if (!userDB) return;
|
|
77
|
+
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.group('👤 User Information');
|
|
80
|
+
logger.info(`Username: ${userDB.getUsername()}`);
|
|
81
|
+
logger.info(`Logged in: ${userDB.isLoggedIn() ? 'Yes ✅' : 'No (Guest) ❌'}`);
|
|
82
|
+
|
|
83
|
+
userDB.getConfig()
|
|
84
|
+
.then((config) => {
|
|
85
|
+
logger.info('Configuration:');
|
|
86
|
+
logger.info(JSON.stringify(config, null, 2));
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
logger.info(`Error loading config: ${err.message}`);
|
|
90
|
+
})
|
|
91
|
+
.finally(() => {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.groupEnd();
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Show scheduled reviews
|
|
99
|
+
*/
|
|
100
|
+
async showScheduledReviews(courseId?: string): Promise<void> {
|
|
101
|
+
const userDB = getUserDB();
|
|
102
|
+
if (!userDB) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const reviews = await userDB.getPendingReviews(courseId);
|
|
106
|
+
|
|
107
|
+
// eslint-disable-next-line no-console
|
|
108
|
+
console.group(`📅 Scheduled Reviews${courseId ? ` (${courseId})` : ''}`);
|
|
109
|
+
logger.info(`Total: ${reviews.length}`);
|
|
110
|
+
|
|
111
|
+
if (reviews.length > 0) {
|
|
112
|
+
// Group by course
|
|
113
|
+
const byCourse = new Map<string, ScheduledCard[]>();
|
|
114
|
+
for (const review of reviews) {
|
|
115
|
+
if (!byCourse.has(review.courseId)) {
|
|
116
|
+
byCourse.set(review.courseId, []);
|
|
117
|
+
}
|
|
118
|
+
byCourse.get(review.courseId)!.push(review);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const [course, courseReviews] of byCourse) {
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.group(`Course: ${course} (${courseReviews.length} reviews)`);
|
|
124
|
+
|
|
125
|
+
// Sort by review time
|
|
126
|
+
const sorted = courseReviews.sort((a, b) => {
|
|
127
|
+
const timeA = typeof a.reviewTime === 'string' ? a.reviewTime : a.reviewTime.toISOString();
|
|
128
|
+
const timeB = typeof b.reviewTime === 'string' ? b.reviewTime : b.reviewTime.toISOString();
|
|
129
|
+
return new Date(timeA).getTime() - new Date(timeB).getTime();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Show first 10
|
|
133
|
+
for (const review of sorted.slice(0, 10)) {
|
|
134
|
+
const reviewTimeStr = typeof review.reviewTime === 'string'
|
|
135
|
+
? review.reviewTime
|
|
136
|
+
: review.reviewTime.toISOString();
|
|
137
|
+
logger.info(
|
|
138
|
+
` ${review.cardId.slice(0, 12)}... @ ${formatTimestamp(reviewTimeStr)} ` +
|
|
139
|
+
`[${review.scheduledFor}/${review.schedulingAgentId}]`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sorted.length > 10) {
|
|
144
|
+
logger.info(` ... and ${sorted.length - 10} more`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.groupEnd();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.groupEnd();
|
|
154
|
+
} catch (err: any) {
|
|
155
|
+
logger.info(`Error loading scheduled reviews: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Show course registrations
|
|
161
|
+
*/
|
|
162
|
+
async showCourseRegistrations(): Promise<void> {
|
|
163
|
+
const userDB = getUserDB();
|
|
164
|
+
if (!userDB) return;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const registrations = await userDB.getActiveCourses();
|
|
168
|
+
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.group('📚 Course Registrations');
|
|
171
|
+
logger.info(`Total: ${registrations.length}`);
|
|
172
|
+
|
|
173
|
+
if (registrations.length > 0) {
|
|
174
|
+
// eslint-disable-next-line no-console
|
|
175
|
+
console.table(
|
|
176
|
+
registrations.map((reg: CourseRegistration) => ({
|
|
177
|
+
courseId: reg.courseID,
|
|
178
|
+
status: reg.status || 'active',
|
|
179
|
+
elo: typeof reg.elo === 'number'
|
|
180
|
+
? reg.elo.toFixed(0)
|
|
181
|
+
: reg.elo?.global?.score?.toFixed(0) || 'N/A',
|
|
182
|
+
}))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// eslint-disable-next-line no-console
|
|
187
|
+
console.groupEnd();
|
|
188
|
+
} catch (err: any) {
|
|
189
|
+
logger.info(`Error loading course registrations: ${err.message}`);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Show card history for a specific card
|
|
195
|
+
*/
|
|
196
|
+
async showCardHistory(cardId: string): Promise<void> {
|
|
197
|
+
const rawDB = getRawDB();
|
|
198
|
+
if (!rawDB) return;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// Card history docs use prefix 'cardH'
|
|
202
|
+
const result = await filterAllDocsByPrefix(rawDB, DocTypePrefixes[DocType.CARDRECORD]);
|
|
203
|
+
|
|
204
|
+
// Filter for this specific card
|
|
205
|
+
const cardHistories = result.rows
|
|
206
|
+
.filter((row: any) => row.doc && row.doc.cardID === cardId)
|
|
207
|
+
.map((row: any) => row.doc);
|
|
208
|
+
|
|
209
|
+
// eslint-disable-next-line no-console
|
|
210
|
+
console.group(`🎴 Card History: ${cardId}`);
|
|
211
|
+
logger.info(`Total interactions: ${cardHistories.length}`);
|
|
212
|
+
|
|
213
|
+
if (cardHistories.length > 0) {
|
|
214
|
+
// Sort by timestamp
|
|
215
|
+
const sorted = cardHistories.sort((a: any, b: any) =>
|
|
216
|
+
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Show recent history
|
|
220
|
+
// eslint-disable-next-line no-console
|
|
221
|
+
console.table(
|
|
222
|
+
sorted.slice(0, 20).map((doc: any) => ({
|
|
223
|
+
time: formatTimestamp(doc.timestamp),
|
|
224
|
+
outcome: doc.outcome || 'N/A',
|
|
225
|
+
duration: doc.duration ? `${(doc.duration / 1000).toFixed(1)}s` : 'N/A',
|
|
226
|
+
courseId: doc.courseId,
|
|
227
|
+
}))
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (sorted.length > 20) {
|
|
231
|
+
logger.info(`... and ${sorted.length - 20} more interactions`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// eslint-disable-next-line no-console
|
|
236
|
+
console.groupEnd();
|
|
237
|
+
} catch (err: any) {
|
|
238
|
+
logger.info(`Error loading card history: ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Query documents by type
|
|
244
|
+
*/
|
|
245
|
+
async queryByType(docType: keyof typeof DocType, limit: number = 50): Promise<void> {
|
|
246
|
+
const rawDB = getRawDB();
|
|
247
|
+
if (!rawDB) return;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const prefix = DocTypePrefixes[DocType[docType]];
|
|
251
|
+
if (!prefix) {
|
|
252
|
+
logger.info(`Unknown document type: ${docType}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const result = await filterAllDocsByPrefix(rawDB, prefix);
|
|
257
|
+
|
|
258
|
+
// eslint-disable-next-line no-console
|
|
259
|
+
console.group(`📄 Documents: ${docType}`);
|
|
260
|
+
logger.info(`Total: ${result.rows.length}`);
|
|
261
|
+
logger.info(`Prefix: ${prefix}`);
|
|
262
|
+
|
|
263
|
+
if (result.rows.length > 0) {
|
|
264
|
+
logger.info('Sample documents:');
|
|
265
|
+
const samples = result.rows.slice(0, Math.min(limit, result.rows.length));
|
|
266
|
+
|
|
267
|
+
for (const row of samples) {
|
|
268
|
+
logger.info(`\n${row.id}:`);
|
|
269
|
+
logger.info(JSON.stringify(row.doc, null, 2));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (result.rows.length > limit) {
|
|
273
|
+
logger.info(`\n... and ${result.rows.length - limit} more documents`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// eslint-disable-next-line no-console
|
|
278
|
+
console.groupEnd();
|
|
279
|
+
} catch (err: any) {
|
|
280
|
+
logger.info(`Error querying documents: ${err.message}`);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Show database info and statistics
|
|
286
|
+
*/
|
|
287
|
+
async dbInfo(): Promise<void> {
|
|
288
|
+
const rawDB = getRawDB();
|
|
289
|
+
if (!rawDB) return;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const info = await rawDB.info();
|
|
293
|
+
|
|
294
|
+
// eslint-disable-next-line no-console
|
|
295
|
+
console.group('ℹ️ Database Information');
|
|
296
|
+
logger.info(`Database name: ${info.db_name}`);
|
|
297
|
+
logger.info(`Total documents: ${info.doc_count}`);
|
|
298
|
+
logger.info(`Update sequence: ${info.update_seq}`);
|
|
299
|
+
// disk_size may not be available in all PouchDB implementations
|
|
300
|
+
if ('disk_size' in info) {
|
|
301
|
+
logger.info(`Disk size: ${((info as any).disk_size || 0) / 1024 / 1024} MB`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Count documents by type
|
|
305
|
+
logger.info('\nDocument counts by type:');
|
|
306
|
+
const allDocs = await rawDB.allDocs({ include_docs: false });
|
|
307
|
+
const typeCounts = new Map<string, number>();
|
|
308
|
+
|
|
309
|
+
for (const row of allDocs.rows) {
|
|
310
|
+
// Extract prefix from document ID
|
|
311
|
+
let prefix = 'other';
|
|
312
|
+
for (const [type, typePrefix] of Object.entries(DocTypePrefixes)) {
|
|
313
|
+
if (row.id.startsWith(typePrefix)) {
|
|
314
|
+
prefix = type;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
typeCounts.set(prefix, (typeCounts.get(prefix) || 0) + 1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// eslint-disable-next-line no-console
|
|
322
|
+
console.table(
|
|
323
|
+
Array.from(typeCounts.entries())
|
|
324
|
+
.sort((a, b) => b[1] - a[1])
|
|
325
|
+
.map(([type, count]) => ({ type, count }))
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// eslint-disable-next-line no-console
|
|
329
|
+
console.groupEnd();
|
|
330
|
+
} catch (err: any) {
|
|
331
|
+
logger.info(`Error getting database info: ${err.message}`);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* List all document types
|
|
337
|
+
*/
|
|
338
|
+
listDocTypes(): void {
|
|
339
|
+
// eslint-disable-next-line no-console
|
|
340
|
+
console.group('📋 Available Document Types');
|
|
341
|
+
logger.info('Use with queryByType(type):');
|
|
342
|
+
|
|
343
|
+
for (const [type, prefix] of Object.entries(DocTypePrefixes)) {
|
|
344
|
+
logger.info(` ${type.padEnd(30)} → prefix: "${prefix}"`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// eslint-disable-next-line no-console
|
|
348
|
+
console.groupEnd();
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Export database contents (limited, for debugging)
|
|
353
|
+
*/
|
|
354
|
+
async export(includeContent: boolean = false): Promise<string> {
|
|
355
|
+
const rawDB = getRawDB();
|
|
356
|
+
const userDB = getUserDB();
|
|
357
|
+
if (!rawDB || !userDB) return '{}';
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const data: any = {
|
|
361
|
+
username: userDB.getUsername(),
|
|
362
|
+
loggedIn: userDB.isLoggedIn(),
|
|
363
|
+
timestamp: new Date().toISOString(),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (includeContent) {
|
|
367
|
+
// Get all documents
|
|
368
|
+
const allDocs = await rawDB.allDocs({ include_docs: true });
|
|
369
|
+
data.documents = allDocs.rows.map((row: any) => ({
|
|
370
|
+
id: row.id,
|
|
371
|
+
doc: row.doc,
|
|
372
|
+
}));
|
|
373
|
+
data.totalDocs = allDocs.rows.length;
|
|
374
|
+
} else {
|
|
375
|
+
// Just get counts
|
|
376
|
+
const allDocs = await rawDB.allDocs({ include_docs: false });
|
|
377
|
+
data.totalDocs = allDocs.rows.length;
|
|
378
|
+
|
|
379
|
+
const typeCounts = new Map<string, number>();
|
|
380
|
+
for (const row of allDocs.rows) {
|
|
381
|
+
let prefix = 'other';
|
|
382
|
+
for (const [type, typePrefix] of Object.entries(DocTypePrefixes)) {
|
|
383
|
+
if (row.id.startsWith(typePrefix)) {
|
|
384
|
+
prefix = type;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
typeCounts.set(prefix, (typeCounts.get(prefix) || 0) + 1);
|
|
389
|
+
}
|
|
390
|
+
data.docCounts = Object.fromEntries(typeCounts);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const json = JSON.stringify(data, null, 2);
|
|
394
|
+
logger.info('[UserDB Debug] Database info exported. Copy the returned string or use:');
|
|
395
|
+
logger.info(' copy(window.skuilder.userdb.export())');
|
|
396
|
+
if (!includeContent) {
|
|
397
|
+
logger.info(' For full content export: window.skuilder.userdb.export(true)');
|
|
398
|
+
}
|
|
399
|
+
return json;
|
|
400
|
+
} catch (err: any) {
|
|
401
|
+
logger.info(`Error exporting database: ${err.message}`);
|
|
402
|
+
return '{}';
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Execute raw PouchDB query
|
|
408
|
+
*/
|
|
409
|
+
async raw(queryFn: (db: PouchDB.Database) => Promise<any>): Promise<void> {
|
|
410
|
+
const rawDB = getRawDB();
|
|
411
|
+
if (!rawDB) return;
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const result = await queryFn(rawDB);
|
|
415
|
+
logger.info('[UserDB Debug] Query result:');
|
|
416
|
+
logger.info(result);
|
|
417
|
+
} catch (err: any) {
|
|
418
|
+
logger.info(`[UserDB Debug] Query error: ${err.message}`);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Show help
|
|
424
|
+
*/
|
|
425
|
+
help(): void {
|
|
426
|
+
logger.info(`
|
|
427
|
+
🔧 UserDB Debug API
|
|
428
|
+
|
|
429
|
+
Commands:
|
|
430
|
+
.showUser() Show current user info and config
|
|
431
|
+
.showScheduledReviews(courseId?) Show scheduled reviews (optionally filter by course)
|
|
432
|
+
.showCourseRegistrations() Show all course registrations
|
|
433
|
+
.showCardHistory(cardId) Show interaction history for a card
|
|
434
|
+
.queryByType(docType, limit?) Query documents by type (e.g., 'SCHEDULED_CARD')
|
|
435
|
+
.listDocTypes() List all available document types
|
|
436
|
+
.dbInfo() Show database info and statistics
|
|
437
|
+
.export(includeContent?) Export database info (true = include all docs)
|
|
438
|
+
.raw(queryFn) Execute raw PouchDB query
|
|
439
|
+
.help() Show this help message
|
|
440
|
+
|
|
441
|
+
Examples:
|
|
442
|
+
window.skuilder.userdb.showUser()
|
|
443
|
+
window.skuilder.userdb.showScheduledReviews('course123')
|
|
444
|
+
window.skuilder.userdb.queryByType('SCHEDULED_CARD', 10)
|
|
445
|
+
window.skuilder.userdb.raw(db => db.allDocs({ limit: 5 }))
|
|
446
|
+
`);
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// WINDOW MOUNT
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Mount the debug API on window.skuilder.userdb
|
|
456
|
+
*/
|
|
457
|
+
export function mountUserDBDebugger(): void {
|
|
458
|
+
if (typeof window === 'undefined') return;
|
|
459
|
+
|
|
460
|
+
const win = window as any;
|
|
461
|
+
win.skuilder = win.skuilder || {};
|
|
462
|
+
win.skuilder.userdb = userDBDebugAPI;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Auto-mount when module is loaded
|
|
466
|
+
mountUserDBDebugger();
|
package/src/core/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ const DEFAULT_MIN_COUNT = 3;
|
|
|
38
38
|
* A filter strategy that gates cards based on prerequisite mastery.
|
|
39
39
|
*
|
|
40
40
|
* Cards are locked until the user masters all prerequisite tags.
|
|
41
|
-
* Locked cards receive score
|
|
41
|
+
* Locked cards receive score * 0.01 (strong penalty, not hard filter).
|
|
42
42
|
*
|
|
43
43
|
* Mastery is determined by:
|
|
44
44
|
* - User's ELO for the tag exceeds threshold (or avgElo if not specified)
|
|
@@ -198,7 +198,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
198
198
|
/**
|
|
199
199
|
* CardFilter.transform implementation.
|
|
200
200
|
*
|
|
201
|
-
* Apply prerequisite gating to cards. Cards with locked tags receive score
|
|
201
|
+
* Apply prerequisite gating to cards. Cards with locked tags receive score * 0.01.
|
|
202
202
|
*/
|
|
203
203
|
async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
|
|
204
204
|
// Get mastery state
|
|
@@ -215,7 +215,8 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
215
215
|
unlockedTags,
|
|
216
216
|
masteredTags
|
|
217
217
|
);
|
|
218
|
-
const
|
|
218
|
+
const LOCKED_PENALTY = 0.01;
|
|
219
|
+
const finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
|
|
219
220
|
const action = isUnlocked ? 'passed' : 'penalized';
|
|
220
221
|
|
|
221
222
|
gated.push({
|