@playcademy/sandbox 0.4.2-beta.1 → 0.4.2-beta.3
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/cli.js +1134 -200
- package/dist/constants.js +1 -0
- package/dist/mocks/timeback.d.ts +1 -0
- package/dist/server.js +1132 -199
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -249,6 +249,7 @@ var init_timeback2 = __esm(() => {
|
|
|
249
249
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
250
250
|
GET_XP: "/integrations/timeback/xp",
|
|
251
251
|
GET_MASTERY: "/integrations/timeback/mastery",
|
|
252
|
+
GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
|
|
252
253
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
253
254
|
ADVANCE_COURSE: "/integrations/timeback/advance-course",
|
|
254
255
|
UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
|
|
@@ -1077,7 +1078,7 @@ var package_default;
|
|
|
1077
1078
|
var init_package = __esm(() => {
|
|
1078
1079
|
package_default = {
|
|
1079
1080
|
name: "@playcademy/sandbox",
|
|
1080
|
-
version: "0.4.2-beta.
|
|
1081
|
+
version: "0.4.2-beta.3",
|
|
1081
1082
|
description: "Local development server for Playcademy game development",
|
|
1082
1083
|
type: "module",
|
|
1083
1084
|
exports: {
|
|
@@ -11319,10 +11320,11 @@ var init_table6 = __esm(() => {
|
|
|
11319
11320
|
});
|
|
11320
11321
|
|
|
11321
11322
|
// ../data/src/domains/timeback/table.ts
|
|
11322
|
-
var gameTimebackIntegrations, gameTimebackAssessmentTests;
|
|
11323
|
+
var gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications;
|
|
11323
11324
|
var init_table7 = __esm(() => {
|
|
11324
11325
|
init_pg_core();
|
|
11325
11326
|
init_table5();
|
|
11327
|
+
init_table3();
|
|
11326
11328
|
gameTimebackIntegrations = pgTable("game_timeback_integrations", {
|
|
11327
11329
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
11328
11330
|
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
@@ -11347,6 +11349,21 @@ var init_table7 = __esm(() => {
|
|
|
11347
11349
|
}, (table3) => [
|
|
11348
11350
|
uniqueIndex("game_timeback_assessment_tests_integration_qti_idx").on(table3.integrationId, table3.qtiTestIdentifier)
|
|
11349
11351
|
]);
|
|
11352
|
+
gameTimebackMetricDiscrepancyVerifications = pgTable("game_timeback_metric_discrepancy_verifications", {
|
|
11353
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
11354
|
+
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
11355
|
+
courseId: text("course_id").notNull(),
|
|
11356
|
+
studentId: text("student_id").notNull(),
|
|
11357
|
+
runId: uuid("run_id").notNull(),
|
|
11358
|
+
activityId: text("activity_id"),
|
|
11359
|
+
verifiedByUserId: text("verified_by_user_id").references(() => users.id, {
|
|
11360
|
+
onDelete: "set null"
|
|
11361
|
+
}),
|
|
11362
|
+
verifiedAt: timestamp("verified_at", { withTimezone: true }).notNull().defaultNow()
|
|
11363
|
+
}, (table3) => [
|
|
11364
|
+
uniqueIndex("game_timeback_metric_discrepancy_verifications_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId),
|
|
11365
|
+
index("game_timeback_metric_discrepancy_verifications_course_idx").on(table3.gameId, table3.courseId, table3.verifiedAt)
|
|
11366
|
+
]);
|
|
11350
11367
|
});
|
|
11351
11368
|
|
|
11352
11369
|
// ../data/src/tables.index.ts
|
|
@@ -11360,6 +11377,7 @@ __export(exports_tables_index, {
|
|
|
11360
11377
|
games: () => games,
|
|
11361
11378
|
gameVisibilityEnum: () => gameVisibilityEnum,
|
|
11362
11379
|
gameTypeEnum: () => gameTypeEnum,
|
|
11380
|
+
gameTimebackMetricDiscrepancyVerifications: () => gameTimebackMetricDiscrepancyVerifications,
|
|
11363
11381
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
11364
11382
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
11365
11383
|
gameScoresRelations: () => gameScoresRelations,
|
|
@@ -28248,6 +28266,7 @@ var init_constants3 = __esm(() => {
|
|
|
28248
28266
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
28249
28267
|
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
28250
28268
|
GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
|
|
28269
|
+
GET_HIGHEST_GRADE_MASTERED: `/api${TIMEBACK_ROUTES.GET_HIGHEST_GRADE_MASTERED}`,
|
|
28251
28270
|
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
|
|
28252
28271
|
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`,
|
|
28253
28272
|
UNENROLL_COURSE: `/api${TIMEBACK_ROUTES.UNENROLL_COURSE}`
|
|
@@ -29128,6 +29147,7 @@ var init_utils6 = __esm(() => {
|
|
|
29128
29147
|
};
|
|
29129
29148
|
});
|
|
29130
29149
|
init_constants7();
|
|
29150
|
+
init_constants7();
|
|
29131
29151
|
if (process.env.DEBUG === "true") {
|
|
29132
29152
|
process.env.TERM = "dumb";
|
|
29133
29153
|
}
|
|
@@ -29361,6 +29381,75 @@ function formatDateYMDInTimezone(timeZone, date3 = new Date) {
|
|
|
29361
29381
|
const d = parts2.find((p) => p.type === "day").value;
|
|
29362
29382
|
return `${y}-${m}-${d}`;
|
|
29363
29383
|
}
|
|
29384
|
+
function getUtcInstantForMidnight(date3, timeZone) {
|
|
29385
|
+
const parts2 = new Intl.DateTimeFormat("en-US", {
|
|
29386
|
+
timeZone,
|
|
29387
|
+
year: "numeric",
|
|
29388
|
+
month: "2-digit",
|
|
29389
|
+
day: "2-digit"
|
|
29390
|
+
}).formatToParts(date3).reduce((acc, p) => {
|
|
29391
|
+
if (p.type !== "literal") {
|
|
29392
|
+
acc[p.type] = p.value;
|
|
29393
|
+
}
|
|
29394
|
+
return acc;
|
|
29395
|
+
}, {});
|
|
29396
|
+
const year = Number(parts2.year);
|
|
29397
|
+
const month = Number(parts2.month);
|
|
29398
|
+
const day = Number(parts2.day);
|
|
29399
|
+
for (let dayOffset = -1;dayOffset <= 1; dayOffset++) {
|
|
29400
|
+
const testYear = year;
|
|
29401
|
+
const testMonth = month;
|
|
29402
|
+
const testDay = day + dayOffset;
|
|
29403
|
+
for (let utcHour = 0;utcHour < 24; utcHour++) {
|
|
29404
|
+
const testDate = new Date(Date.UTC(testYear, testMonth - 1, testDay, utcHour, 0, 0, 0));
|
|
29405
|
+
const testParts = new Intl.DateTimeFormat("en-US", {
|
|
29406
|
+
timeZone,
|
|
29407
|
+
year: "numeric",
|
|
29408
|
+
month: "2-digit",
|
|
29409
|
+
day: "2-digit",
|
|
29410
|
+
hour: "2-digit",
|
|
29411
|
+
minute: "2-digit",
|
|
29412
|
+
hour12: false
|
|
29413
|
+
}).formatToParts(testDate).reduce((acc, p) => {
|
|
29414
|
+
if (p.type !== "literal") {
|
|
29415
|
+
acc[p.type] = p.value;
|
|
29416
|
+
}
|
|
29417
|
+
return acc;
|
|
29418
|
+
}, {});
|
|
29419
|
+
const yearMatch = Number(testParts.year) === year;
|
|
29420
|
+
const monthMatch = Number(testParts.month) === month;
|
|
29421
|
+
const dayMatch = Number(testParts.day) === day;
|
|
29422
|
+
const hourValue = Number(testParts.hour);
|
|
29423
|
+
const hourMatch = hourValue === 0 || hourValue === 24;
|
|
29424
|
+
const minuteMatch = Number(testParts.minute) === 0;
|
|
29425
|
+
if (yearMatch && monthMatch && dayMatch && hourMatch && minuteMatch) {
|
|
29426
|
+
return testDate;
|
|
29427
|
+
}
|
|
29428
|
+
}
|
|
29429
|
+
}
|
|
29430
|
+
throw new Error(`Could not find midnight for ${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")} in timezone ${timeZone}`);
|
|
29431
|
+
}
|
|
29432
|
+
function getDayBoundariesInTimezone(date3, timezone) {
|
|
29433
|
+
const startOfDay = getUtcInstantForMidnight(date3, timezone);
|
|
29434
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
29435
|
+
timeZone: timezone,
|
|
29436
|
+
year: "numeric",
|
|
29437
|
+
month: "2-digit",
|
|
29438
|
+
day: "2-digit"
|
|
29439
|
+
});
|
|
29440
|
+
const parts2 = formatter.formatToParts(date3).reduce((acc, p) => {
|
|
29441
|
+
if (p.type !== "literal") {
|
|
29442
|
+
acc[p.type] = p.value;
|
|
29443
|
+
}
|
|
29444
|
+
return acc;
|
|
29445
|
+
}, {});
|
|
29446
|
+
const year = Number(parts2.year);
|
|
29447
|
+
const month = Number(parts2.month);
|
|
29448
|
+
const day = Number(parts2.day);
|
|
29449
|
+
const nextDayNoon = new Date(Date.UTC(year, month - 1, day + 1, 12, 0, 0, 0));
|
|
29450
|
+
const endOfDay = getUtcInstantForMidnight(nextDayNoon, timezone);
|
|
29451
|
+
return { startOfDay, endOfDay };
|
|
29452
|
+
}
|
|
29364
29453
|
// ../utils/src/url.ts
|
|
29365
29454
|
function buildPath(path, params) {
|
|
29366
29455
|
const url = new URL(path, "http://n");
|
|
@@ -29404,6 +29493,42 @@ function formatGradeLabel(grade) {
|
|
|
29404
29493
|
}
|
|
29405
29494
|
}
|
|
29406
29495
|
}
|
|
29496
|
+
function isTimebackDiscrepancyQueueWindow(value) {
|
|
29497
|
+
return typeof value === "string" && TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES.has(value);
|
|
29498
|
+
}
|
|
29499
|
+
function isTimebackDiscrepancyQueueMetric(value) {
|
|
29500
|
+
return typeof value === "string" && TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES.has(value);
|
|
29501
|
+
}
|
|
29502
|
+
function parseTimebackDiscrepancyQueueWindow(value) {
|
|
29503
|
+
const window2 = value?.trim();
|
|
29504
|
+
return isTimebackDiscrepancyQueueWindow(window2) ? window2 : DEFAULT_TIMEBACK_DISCREPANCY_QUEUE_WINDOW;
|
|
29505
|
+
}
|
|
29506
|
+
function parseTimebackDiscrepancyQueueMetrics(values) {
|
|
29507
|
+
const metrics = [];
|
|
29508
|
+
const seen = new Set;
|
|
29509
|
+
for (const value of values) {
|
|
29510
|
+
const metric = value.trim();
|
|
29511
|
+
if (isTimebackDiscrepancyQueueMetric(metric) && !seen.has(metric)) {
|
|
29512
|
+
seen.add(metric);
|
|
29513
|
+
metrics.push(metric);
|
|
29514
|
+
}
|
|
29515
|
+
}
|
|
29516
|
+
return metrics;
|
|
29517
|
+
}
|
|
29518
|
+
var TIMEBACK_DISCREPANCY_QUEUE_WINDOWS, TIMEBACK_DISCREPANCY_QUEUE_METRICS, DEFAULT_TIMEBACK_DISCREPANCY_QUEUE_WINDOW = "this-week", TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES, TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES;
|
|
29519
|
+
var init_timeback3 = __esm(() => {
|
|
29520
|
+
TIMEBACK_DISCREPANCY_QUEUE_WINDOWS = [
|
|
29521
|
+
"today",
|
|
29522
|
+
"yesterday",
|
|
29523
|
+
"this-week",
|
|
29524
|
+
"last-week",
|
|
29525
|
+
"all",
|
|
29526
|
+
"custom"
|
|
29527
|
+
];
|
|
29528
|
+
TIMEBACK_DISCREPANCY_QUEUE_METRICS = ["xp", "mastery", "time", "score"];
|
|
29529
|
+
TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_WINDOWS);
|
|
29530
|
+
TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_METRICS);
|
|
29531
|
+
});
|
|
29407
29532
|
|
|
29408
29533
|
// ../../node_modules/.bun/drizzle-zod@0.7.1+e9f18f9688af15ce/node_modules/drizzle-zod/index.mjs
|
|
29409
29534
|
function isColumnType(column2, columnTypes) {
|
|
@@ -29904,7 +30029,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
29904
30029
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
29905
30030
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
29906
30031
|
}
|
|
29907
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
30032
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29908
30033
|
var init_schemas4 = __esm(() => {
|
|
29909
30034
|
init_drizzle_zod();
|
|
29910
30035
|
init_esm();
|
|
@@ -30145,6 +30270,11 @@ var init_schemas4 = __esm(() => {
|
|
|
30145
30270
|
studentId: exports_external.string().min(1),
|
|
30146
30271
|
enrollmentId: exports_external.string().min(1)
|
|
30147
30272
|
});
|
|
30273
|
+
VerifyTimebackMetricDiscrepancyRequestSchema = exports_external.object({
|
|
30274
|
+
studentId: exports_external.string().min(1),
|
|
30275
|
+
runId: exports_external.string().uuid(),
|
|
30276
|
+
activityId: exports_external.string().min(1).optional()
|
|
30277
|
+
});
|
|
30148
30278
|
InsertAssessmentTestSchema = createInsertSchema(gameTimebackAssessmentTests).omit({
|
|
30149
30279
|
id: true,
|
|
30150
30280
|
createdAt: true
|
|
@@ -30277,183 +30407,6 @@ var init_timeback_admin_util = __esm(() => {
|
|
|
30277
30407
|
init_errors();
|
|
30278
30408
|
});
|
|
30279
30409
|
|
|
30280
|
-
// ../api-core/src/utils/timeback-game-metrics-comparison.util.ts
|
|
30281
|
-
function createMetricRow(definition) {
|
|
30282
|
-
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
30283
|
-
if (timebackValue === undefined && gameValue === undefined) {
|
|
30284
|
-
return null;
|
|
30285
|
-
}
|
|
30286
|
-
if (gameValue === undefined) {
|
|
30287
|
-
return {
|
|
30288
|
-
metric,
|
|
30289
|
-
kind,
|
|
30290
|
-
status: "not_reported_by_game",
|
|
30291
|
-
...timebackValue !== undefined ? { timebackValue } : {}
|
|
30292
|
-
};
|
|
30293
|
-
}
|
|
30294
|
-
if (timebackValue === undefined) {
|
|
30295
|
-
return {
|
|
30296
|
-
metric,
|
|
30297
|
-
kind,
|
|
30298
|
-
status: "not_recorded_by_timeback",
|
|
30299
|
-
gameValue
|
|
30300
|
-
};
|
|
30301
|
-
}
|
|
30302
|
-
const delta = gameValue - timebackValue;
|
|
30303
|
-
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
30304
|
-
return {
|
|
30305
|
-
metric,
|
|
30306
|
-
kind,
|
|
30307
|
-
status: isDiscrepant ? "discrepant" : "matched",
|
|
30308
|
-
timebackValue,
|
|
30309
|
-
gameValue,
|
|
30310
|
-
delta
|
|
30311
|
-
};
|
|
30312
|
-
}
|
|
30313
|
-
function createRunComparison(activity, gameRun) {
|
|
30314
|
-
const runId = activity.runId ?? "";
|
|
30315
|
-
if (!gameRun) {
|
|
30316
|
-
return {
|
|
30317
|
-
runId,
|
|
30318
|
-
status: "not_reported",
|
|
30319
|
-
discrepancyCount: 0,
|
|
30320
|
-
rows: []
|
|
30321
|
-
};
|
|
30322
|
-
}
|
|
30323
|
-
const rows = [
|
|
30324
|
-
createMetricRow({
|
|
30325
|
-
metric: "xp",
|
|
30326
|
-
kind: "number",
|
|
30327
|
-
timebackValue: activity.xpDelta,
|
|
30328
|
-
gameValue: gameRun.totalXp,
|
|
30329
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
30330
|
-
}),
|
|
30331
|
-
createMetricRow({
|
|
30332
|
-
metric: "mastery",
|
|
30333
|
-
kind: "number",
|
|
30334
|
-
timebackValue: activity.masteredUnitsDelta,
|
|
30335
|
-
gameValue: gameRun.masteredUnits,
|
|
30336
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
30337
|
-
}),
|
|
30338
|
-
createMetricRow({
|
|
30339
|
-
metric: "time",
|
|
30340
|
-
kind: "time",
|
|
30341
|
-
timebackValue: activity.timeDeltaSeconds,
|
|
30342
|
-
gameValue: gameRun.activeTimeSeconds,
|
|
30343
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
30344
|
-
}),
|
|
30345
|
-
createMetricRow({
|
|
30346
|
-
metric: "score",
|
|
30347
|
-
kind: "percent",
|
|
30348
|
-
timebackValue: activity.score,
|
|
30349
|
-
gameValue: gameRun.score,
|
|
30350
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
30351
|
-
})
|
|
30352
|
-
].filter((row) => row !== null);
|
|
30353
|
-
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
30354
|
-
return {
|
|
30355
|
-
runId,
|
|
30356
|
-
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
30357
|
-
discrepancyCount,
|
|
30358
|
-
rows
|
|
30359
|
-
};
|
|
30360
|
-
}
|
|
30361
|
-
function summarizeGameRunMetricsComparison(comparison) {
|
|
30362
|
-
return {
|
|
30363
|
-
runId: comparison.runId,
|
|
30364
|
-
status: comparison.status,
|
|
30365
|
-
discrepancyCount: comparison.discrepancyCount,
|
|
30366
|
-
...comparison.reason ? { reason: comparison.reason } : {}
|
|
30367
|
-
};
|
|
30368
|
-
}
|
|
30369
|
-
function buildGameRunMetricComparisons(activities, course, response) {
|
|
30370
|
-
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
30371
|
-
const comparisons = new Map;
|
|
30372
|
-
if (activitiesWithRunIds.length === 0) {
|
|
30373
|
-
return comparisons;
|
|
30374
|
-
}
|
|
30375
|
-
if (!response.supported) {
|
|
30376
|
-
for (const activity of activitiesWithRunIds) {
|
|
30377
|
-
comparisons.set(activity.runId, {
|
|
30378
|
-
runId: activity.runId,
|
|
30379
|
-
status: "unavailable",
|
|
30380
|
-
discrepancyCount: 0,
|
|
30381
|
-
reason: response.reason,
|
|
30382
|
-
rows: []
|
|
30383
|
-
});
|
|
30384
|
-
}
|
|
30385
|
-
return comparisons;
|
|
30386
|
-
}
|
|
30387
|
-
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
30388
|
-
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
30389
|
-
for (const activity of activitiesWithRunIds) {
|
|
30390
|
-
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
30391
|
-
}
|
|
30392
|
-
return comparisons;
|
|
30393
|
-
}
|
|
30394
|
-
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
30395
|
-
init_src();
|
|
30396
|
-
});
|
|
30397
|
-
|
|
30398
|
-
// ../api-core/src/utils/timeback-mastery-completion.util.ts
|
|
30399
|
-
async function upsertMasteryCompletionEntry(params) {
|
|
30400
|
-
const { client, courseId, studentId, appName, action } = params;
|
|
30401
|
-
const ids = deriveSourcedIds(courseId);
|
|
30402
|
-
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
30403
|
-
const resultId = `${lineItemId}:${studentId}:completion`;
|
|
30404
|
-
if (action === "complete") {
|
|
30405
|
-
await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
|
|
30406
|
-
sourcedId: lineItemId,
|
|
30407
|
-
title: "Mastery Completion",
|
|
30408
|
-
status: ONEROSTER_STATUS.active,
|
|
30409
|
-
course: { sourcedId: ids.course },
|
|
30410
|
-
...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
|
|
30411
|
-
});
|
|
30412
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
30413
|
-
sourcedId: resultId,
|
|
30414
|
-
status: ONEROSTER_STATUS.active,
|
|
30415
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
30416
|
-
student: { sourcedId: studentId },
|
|
30417
|
-
score: 100,
|
|
30418
|
-
scoreDate: new Date().toISOString(),
|
|
30419
|
-
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
30420
|
-
inProgress: "false",
|
|
30421
|
-
metadata: {
|
|
30422
|
-
isMasteryCompletion: true,
|
|
30423
|
-
adminAction: true,
|
|
30424
|
-
appName
|
|
30425
|
-
}
|
|
30426
|
-
});
|
|
30427
|
-
} else {
|
|
30428
|
-
try {
|
|
30429
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
30430
|
-
sourcedId: resultId,
|
|
30431
|
-
status: ONEROSTER_STATUS.active,
|
|
30432
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
30433
|
-
student: { sourcedId: studentId },
|
|
30434
|
-
score: 0,
|
|
30435
|
-
scoreDate: new Date().toISOString(),
|
|
30436
|
-
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
30437
|
-
inProgress: "true",
|
|
30438
|
-
metadata: {
|
|
30439
|
-
isMasteryCompletion: true,
|
|
30440
|
-
adminAction: true,
|
|
30441
|
-
appName
|
|
30442
|
-
}
|
|
30443
|
-
});
|
|
30444
|
-
} catch {
|
|
30445
|
-
logger16.debug("No completion entry to revoke", { studentId, courseId });
|
|
30446
|
-
}
|
|
30447
|
-
}
|
|
30448
|
-
}
|
|
30449
|
-
var logger16;
|
|
30450
|
-
var init_timeback_mastery_completion_util = __esm(() => {
|
|
30451
|
-
init_src2();
|
|
30452
|
-
init_constants4();
|
|
30453
|
-
init_utils6();
|
|
30454
|
-
logger16 = log.scope("timeback-mastery-completion");
|
|
30455
|
-
});
|
|
30456
|
-
|
|
30457
30410
|
// ../api-core/src/utils/timeback.util.ts
|
|
30458
30411
|
function isRecord2(value) {
|
|
30459
30412
|
return typeof value === "object" && value !== null;
|
|
@@ -30601,6 +30554,13 @@ function groupCaliperEventsByRun(events) {
|
|
|
30601
30554
|
}
|
|
30602
30555
|
return groups;
|
|
30603
30556
|
}
|
|
30557
|
+
function findCaliperEventGroupContainingExternalId(events, externalId) {
|
|
30558
|
+
const targetExternalId = externalId.trim();
|
|
30559
|
+
if (!targetExternalId) {
|
|
30560
|
+
return;
|
|
30561
|
+
}
|
|
30562
|
+
return [...groupCaliperEventsByRun(events).values()].find((group) => group.some((event) => event.externalId === targetExternalId));
|
|
30563
|
+
}
|
|
30604
30564
|
function mapCaliperEventGroupToActivity(events, relevantCourseIds) {
|
|
30605
30565
|
if (events.length === 0) {
|
|
30606
30566
|
return null;
|
|
@@ -30774,6 +30734,371 @@ var init_timeback_util = __esm(() => {
|
|
|
30774
30734
|
]);
|
|
30775
30735
|
});
|
|
30776
30736
|
|
|
30737
|
+
// ../api-core/src/utils/timeback-discrepancy-queue.util.ts
|
|
30738
|
+
function parseDateInputParts(value) {
|
|
30739
|
+
const parts2 = value.split("-");
|
|
30740
|
+
return {
|
|
30741
|
+
year: Number(parts2[0]),
|
|
30742
|
+
month: Number(parts2[1]),
|
|
30743
|
+
day: Number(parts2[2])
|
|
30744
|
+
};
|
|
30745
|
+
}
|
|
30746
|
+
function addDateInputDays(value, days) {
|
|
30747
|
+
const { year, month, day } = parseDateInputParts(value);
|
|
30748
|
+
const date3 = new Date(Date.UTC(year, month - 1, day + days, 12));
|
|
30749
|
+
return [
|
|
30750
|
+
String(date3.getUTCFullYear()),
|
|
30751
|
+
String(date3.getUTCMonth() + 1).padStart(2, "0"),
|
|
30752
|
+
String(date3.getUTCDate()).padStart(2, "0")
|
|
30753
|
+
].join("-");
|
|
30754
|
+
}
|
|
30755
|
+
function getDateInputDayOfWeek(value) {
|
|
30756
|
+
const { year, month, day } = parseDateInputParts(value);
|
|
30757
|
+
return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();
|
|
30758
|
+
}
|
|
30759
|
+
function getDateInputBoundary(value, boundary, timezone2) {
|
|
30760
|
+
const { year, month, day } = parseDateInputParts(value);
|
|
30761
|
+
const anchor = new Date(Date.UTC(year, month - 1, day, 12));
|
|
30762
|
+
const { startOfDay, endOfDay } = getDayBoundariesInTimezone(anchor, timezone2);
|
|
30763
|
+
return boundary === "end" ? new Date(endOfDay.getTime() - 1) : startOfDay;
|
|
30764
|
+
}
|
|
30765
|
+
function getWeekStartDateInput(date3, timezone2) {
|
|
30766
|
+
const today = formatDateYMDInTimezone(timezone2, date3);
|
|
30767
|
+
const dayOfWeek = getDateInputDayOfWeek(today);
|
|
30768
|
+
return addDateInputDays(today, -dayOfWeek);
|
|
30769
|
+
}
|
|
30770
|
+
function parseDateBoundary(value, boundary, timezone2) {
|
|
30771
|
+
if (!value) {
|
|
30772
|
+
return null;
|
|
30773
|
+
}
|
|
30774
|
+
if (DATE_INPUT_RE.test(value)) {
|
|
30775
|
+
return getDateInputBoundary(value, boundary, timezone2);
|
|
30776
|
+
}
|
|
30777
|
+
const parsed = new Date(value);
|
|
30778
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
30779
|
+
}
|
|
30780
|
+
function getTimebackDiscrepancyQueueDateRange(options) {
|
|
30781
|
+
const now2 = options.now ?? new Date;
|
|
30782
|
+
const timezone2 = options.timezone ?? "UTC";
|
|
30783
|
+
if (options.window === "all") {
|
|
30784
|
+
return {};
|
|
30785
|
+
}
|
|
30786
|
+
if (options.window === "today") {
|
|
30787
|
+
return { startDate: getDayBoundariesInTimezone(now2, timezone2).startOfDay.toISOString() };
|
|
30788
|
+
}
|
|
30789
|
+
if (options.window === "yesterday") {
|
|
30790
|
+
const yesterdayInput = addDateInputDays(formatDateYMDInTimezone(timezone2, now2), -1);
|
|
30791
|
+
return {
|
|
30792
|
+
startDate: getDateInputBoundary(yesterdayInput, "start", timezone2).toISOString(),
|
|
30793
|
+
endDate: getDateInputBoundary(yesterdayInput, "end", timezone2).toISOString()
|
|
30794
|
+
};
|
|
30795
|
+
}
|
|
30796
|
+
if (options.window === "this-week") {
|
|
30797
|
+
return {
|
|
30798
|
+
startDate: getDateInputBoundary(getWeekStartDateInput(now2, timezone2), "start", timezone2).toISOString()
|
|
30799
|
+
};
|
|
30800
|
+
}
|
|
30801
|
+
if (options.window === "last-week") {
|
|
30802
|
+
const thisWeekStartInput = getWeekStartDateInput(now2, timezone2);
|
|
30803
|
+
const lastWeekStartInput = addDateInputDays(thisWeekStartInput, -7);
|
|
30804
|
+
const lastWeekEndInput = addDateInputDays(thisWeekStartInput, -1);
|
|
30805
|
+
return {
|
|
30806
|
+
startDate: getDateInputBoundary(lastWeekStartInput, "start", timezone2).toISOString(),
|
|
30807
|
+
endDate: getDateInputBoundary(lastWeekEndInput, "end", timezone2).toISOString()
|
|
30808
|
+
};
|
|
30809
|
+
}
|
|
30810
|
+
const startDate = parseDateBoundary(options.startDate, "start", timezone2);
|
|
30811
|
+
const endDate = parseDateBoundary(options.endDate, "end", timezone2);
|
|
30812
|
+
return {
|
|
30813
|
+
...startDate ? { startDate: startDate.toISOString() } : {},
|
|
30814
|
+
...endDate ? { endDate: endDate.toISOString() } : {}
|
|
30815
|
+
};
|
|
30816
|
+
}
|
|
30817
|
+
function getCaliperActorSourcedId(event) {
|
|
30818
|
+
const actorId = typeof event.actor.id === "string" ? event.actor.id.trim() : "";
|
|
30819
|
+
if (!actorId) {
|
|
30820
|
+
return;
|
|
30821
|
+
}
|
|
30822
|
+
const normalized = actorId.replace(/\/$/, "");
|
|
30823
|
+
const segment = normalized.split("/").at(-1);
|
|
30824
|
+
if (!segment) {
|
|
30825
|
+
return;
|
|
30826
|
+
}
|
|
30827
|
+
try {
|
|
30828
|
+
return decodeURIComponent(segment);
|
|
30829
|
+
} catch {
|
|
30830
|
+
return segment;
|
|
30831
|
+
}
|
|
30832
|
+
}
|
|
30833
|
+
function getTimebackDiscrepancyVerificationKey(params) {
|
|
30834
|
+
return `${params.studentId}:${params.runId.toLowerCase()}`;
|
|
30835
|
+
}
|
|
30836
|
+
function getTimebackMetricDiscrepancyVerificationForActivity(params) {
|
|
30837
|
+
if (params.comparison?.status !== "discrepant" || !params.activity.runId) {
|
|
30838
|
+
return;
|
|
30839
|
+
}
|
|
30840
|
+
return params.verificationsByKey.get(getTimebackDiscrepancyVerificationKey({
|
|
30841
|
+
studentId: params.studentId,
|
|
30842
|
+
runId: params.activity.runId
|
|
30843
|
+
}));
|
|
30844
|
+
}
|
|
30845
|
+
function mapCaliperEventsToStudentGameplayActivities(events, relevantCourseIds) {
|
|
30846
|
+
if (relevantCourseIds.size === 0) {
|
|
30847
|
+
return [];
|
|
30848
|
+
}
|
|
30849
|
+
const gameplayEventsByStudent = new Map;
|
|
30850
|
+
for (const event of events) {
|
|
30851
|
+
const isGameplayEvent = event.type === "ActivityEvent" || event.type === "TimeSpentEvent";
|
|
30852
|
+
const studentId = isGameplayEvent && !isCaliperRemediationOrCompletionEvent(event) ? getCaliperActorSourcedId(event) : undefined;
|
|
30853
|
+
if (studentId) {
|
|
30854
|
+
const existing = gameplayEventsByStudent.get(studentId);
|
|
30855
|
+
if (existing) {
|
|
30856
|
+
existing.push(event);
|
|
30857
|
+
} else {
|
|
30858
|
+
gameplayEventsByStudent.set(studentId, [event]);
|
|
30859
|
+
}
|
|
30860
|
+
}
|
|
30861
|
+
}
|
|
30862
|
+
const groups = [];
|
|
30863
|
+
for (const [studentId, studentEvents] of gameplayEventsByStudent) {
|
|
30864
|
+
const activities = [...groupCaliperEventsByRun(studentEvents).values()].map((group) => mapCaliperEventGroupToActivity(group, relevantCourseIds)).filter((activity) => Boolean(activity)).toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
30865
|
+
if (activities.length > 0) {
|
|
30866
|
+
groups.push({ studentId, activities });
|
|
30867
|
+
}
|
|
30868
|
+
}
|
|
30869
|
+
return groups.toSorted((a, b) => {
|
|
30870
|
+
const aLatest = a.activities[0]?.occurredAt ?? "";
|
|
30871
|
+
const bLatest = b.activities[0]?.occurredAt ?? "";
|
|
30872
|
+
return bLatest.localeCompare(aLatest);
|
|
30873
|
+
});
|
|
30874
|
+
}
|
|
30875
|
+
function mapCaliperEventsToCompletedStudentGameplayActivities(events, relevantCourseIds, options = {}) {
|
|
30876
|
+
const runIds = options.runIds ? new Set([...options.runIds].map((runId) => runId.toLowerCase())) : undefined;
|
|
30877
|
+
return mapCaliperEventsToStudentGameplayActivities(events, relevantCourseIds).filter((group) => !options.studentId || group.studentId === options.studentId).map((group) => ({
|
|
30878
|
+
studentId: group.studentId,
|
|
30879
|
+
activities: group.activities.filter((activity) => {
|
|
30880
|
+
const runId = activity.runId?.toLowerCase();
|
|
30881
|
+
return activity.kind === "activity" && runId !== undefined && isValidUUID(runId) && (!runIds || runIds.has(runId));
|
|
30882
|
+
})
|
|
30883
|
+
})).filter((group) => group.activities.length > 0);
|
|
30884
|
+
}
|
|
30885
|
+
function getTimebackActivityRunIds(groups) {
|
|
30886
|
+
const runIds = [];
|
|
30887
|
+
const seen = new Set;
|
|
30888
|
+
for (const group of groups) {
|
|
30889
|
+
for (const activity of group.activities) {
|
|
30890
|
+
const runId = activity.runId?.toLowerCase();
|
|
30891
|
+
if (runId && isValidUUID(runId) && !seen.has(runId)) {
|
|
30892
|
+
seen.add(runId);
|
|
30893
|
+
runIds.push(runId);
|
|
30894
|
+
}
|
|
30895
|
+
}
|
|
30896
|
+
}
|
|
30897
|
+
return runIds;
|
|
30898
|
+
}
|
|
30899
|
+
function mergeHydratedMetricDiscrepancyQueueItems(options) {
|
|
30900
|
+
const hydratedRunIds = new Set([...options.hydratedRunIds].map((runId) => runId.toLowerCase()));
|
|
30901
|
+
const hydratedItems = options.hydratedItems.map((item) => ({
|
|
30902
|
+
...item,
|
|
30903
|
+
comparisonSource: "hydrated"
|
|
30904
|
+
}));
|
|
30905
|
+
const preliminaryFallbackItems = options.preliminaryItems.filter((item) => {
|
|
30906
|
+
const runId = item.activity.runId?.toLowerCase();
|
|
30907
|
+
return runId === undefined || !hydratedRunIds.has(runId);
|
|
30908
|
+
}).map((item) => ({
|
|
30909
|
+
...item,
|
|
30910
|
+
comparisonSource: "preliminary"
|
|
30911
|
+
}));
|
|
30912
|
+
return [...hydratedItems, ...preliminaryFallbackItems].toSorted((a, b) => b.activity.occurredAt.localeCompare(a.activity.occurredAt));
|
|
30913
|
+
}
|
|
30914
|
+
function selectTimebackMetricDiscrepancyQueueItems(candidates, options) {
|
|
30915
|
+
const discrepancyMetricScopesSet = new Set(options.discrepancyMetricScopes);
|
|
30916
|
+
return candidates.filter((candidate) => candidate.activity.kind === "activity").filter((candidate) => candidate.gameMetricsComparison.status === "discrepant").filter((candidate) => !options.studentId || candidate.student.studentId === options.studentId).filter((candidate) => discrepancyMetricScopesSet.size === 0 || candidate.gameMetricsComparison.rows.some((row) => row.status === "discrepant" && discrepancyMetricScopesSet.has(row.metric))).filter((candidate) => options.includeVerified || !candidate.verification).toSorted((a, b) => b.activity.occurredAt.localeCompare(a.activity.occurredAt));
|
|
30917
|
+
}
|
|
30918
|
+
var DATE_INPUT_RE;
|
|
30919
|
+
var init_timeback_discrepancy_queue_util = __esm(() => {
|
|
30920
|
+
init_src4();
|
|
30921
|
+
init_timeback_util();
|
|
30922
|
+
DATE_INPUT_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
30923
|
+
});
|
|
30924
|
+
|
|
30925
|
+
// ../api-core/src/utils/timeback-game-metrics-comparison.util.ts
|
|
30926
|
+
function createMetricRow(definition) {
|
|
30927
|
+
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
30928
|
+
if (timebackValue === undefined && gameValue === undefined) {
|
|
30929
|
+
return null;
|
|
30930
|
+
}
|
|
30931
|
+
if (gameValue === undefined) {
|
|
30932
|
+
return {
|
|
30933
|
+
metric,
|
|
30934
|
+
kind,
|
|
30935
|
+
status: "not_reported_by_game",
|
|
30936
|
+
...timebackValue !== undefined ? { timebackValue } : {}
|
|
30937
|
+
};
|
|
30938
|
+
}
|
|
30939
|
+
if (timebackValue === undefined) {
|
|
30940
|
+
return {
|
|
30941
|
+
metric,
|
|
30942
|
+
kind,
|
|
30943
|
+
status: "not_recorded_by_timeback",
|
|
30944
|
+
gameValue
|
|
30945
|
+
};
|
|
30946
|
+
}
|
|
30947
|
+
const delta = gameValue - timebackValue;
|
|
30948
|
+
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
30949
|
+
return {
|
|
30950
|
+
metric,
|
|
30951
|
+
kind,
|
|
30952
|
+
status: isDiscrepant ? "discrepant" : "matched",
|
|
30953
|
+
timebackValue,
|
|
30954
|
+
gameValue,
|
|
30955
|
+
delta
|
|
30956
|
+
};
|
|
30957
|
+
}
|
|
30958
|
+
function createRunComparison(activity, gameRun) {
|
|
30959
|
+
const runId = activity.runId ?? "";
|
|
30960
|
+
if (!gameRun) {
|
|
30961
|
+
return {
|
|
30962
|
+
runId,
|
|
30963
|
+
status: "not_reported",
|
|
30964
|
+
discrepancyCount: 0,
|
|
30965
|
+
rows: []
|
|
30966
|
+
};
|
|
30967
|
+
}
|
|
30968
|
+
const rows = [
|
|
30969
|
+
createMetricRow({
|
|
30970
|
+
metric: "xp",
|
|
30971
|
+
kind: "number",
|
|
30972
|
+
timebackValue: activity.xpDelta,
|
|
30973
|
+
gameValue: gameRun.totalXp,
|
|
30974
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
30975
|
+
}),
|
|
30976
|
+
createMetricRow({
|
|
30977
|
+
metric: "time",
|
|
30978
|
+
kind: "time",
|
|
30979
|
+
timebackValue: activity.timeDeltaSeconds,
|
|
30980
|
+
gameValue: gameRun.activeTimeSeconds,
|
|
30981
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
30982
|
+
}),
|
|
30983
|
+
createMetricRow({
|
|
30984
|
+
metric: "score",
|
|
30985
|
+
kind: "percent",
|
|
30986
|
+
timebackValue: activity.score,
|
|
30987
|
+
gameValue: gameRun.score,
|
|
30988
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
30989
|
+
}),
|
|
30990
|
+
createMetricRow({
|
|
30991
|
+
metric: "mastery",
|
|
30992
|
+
kind: "number",
|
|
30993
|
+
timebackValue: activity.masteredUnitsDelta ?? 0,
|
|
30994
|
+
gameValue: gameRun.masteredUnits,
|
|
30995
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
30996
|
+
})
|
|
30997
|
+
].filter((row) => row !== null);
|
|
30998
|
+
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
30999
|
+
return {
|
|
31000
|
+
runId,
|
|
31001
|
+
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
31002
|
+
discrepancyCount,
|
|
31003
|
+
rows
|
|
31004
|
+
};
|
|
31005
|
+
}
|
|
31006
|
+
function summarizeGameRunMetricsComparison(comparison) {
|
|
31007
|
+
return {
|
|
31008
|
+
runId: comparison.runId,
|
|
31009
|
+
status: comparison.status,
|
|
31010
|
+
discrepancyCount: comparison.discrepancyCount,
|
|
31011
|
+
...comparison.reason ? { reason: comparison.reason } : {}
|
|
31012
|
+
};
|
|
31013
|
+
}
|
|
31014
|
+
function buildGameRunMetricComparisons(activities, course, response) {
|
|
31015
|
+
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
31016
|
+
const comparisons = new Map;
|
|
31017
|
+
if (activitiesWithRunIds.length === 0) {
|
|
31018
|
+
return comparisons;
|
|
31019
|
+
}
|
|
31020
|
+
if (!response.supported) {
|
|
31021
|
+
for (const activity of activitiesWithRunIds) {
|
|
31022
|
+
comparisons.set(activity.runId, {
|
|
31023
|
+
runId: activity.runId,
|
|
31024
|
+
status: "unavailable",
|
|
31025
|
+
discrepancyCount: 0,
|
|
31026
|
+
reason: response.reason,
|
|
31027
|
+
rows: []
|
|
31028
|
+
});
|
|
31029
|
+
}
|
|
31030
|
+
return comparisons;
|
|
31031
|
+
}
|
|
31032
|
+
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
31033
|
+
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
31034
|
+
for (const activity of activitiesWithRunIds) {
|
|
31035
|
+
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
31036
|
+
}
|
|
31037
|
+
return comparisons;
|
|
31038
|
+
}
|
|
31039
|
+
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
31040
|
+
init_src();
|
|
31041
|
+
});
|
|
31042
|
+
|
|
31043
|
+
// ../api-core/src/utils/timeback-mastery-completion.util.ts
|
|
31044
|
+
async function upsertMasteryCompletionEntry(params) {
|
|
31045
|
+
const { client, courseId, studentId, appName, action } = params;
|
|
31046
|
+
const ids = deriveSourcedIds(courseId);
|
|
31047
|
+
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
31048
|
+
const resultId = `${lineItemId}:${studentId}:completion`;
|
|
31049
|
+
if (action === "complete") {
|
|
31050
|
+
await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
|
|
31051
|
+
sourcedId: lineItemId,
|
|
31052
|
+
title: "Mastery Completion",
|
|
31053
|
+
status: ONEROSTER_STATUS.active,
|
|
31054
|
+
course: { sourcedId: ids.course },
|
|
31055
|
+
...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
|
|
31056
|
+
});
|
|
31057
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31058
|
+
sourcedId: resultId,
|
|
31059
|
+
status: ONEROSTER_STATUS.active,
|
|
31060
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31061
|
+
student: { sourcedId: studentId },
|
|
31062
|
+
score: 100,
|
|
31063
|
+
scoreDate: new Date().toISOString(),
|
|
31064
|
+
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
31065
|
+
inProgress: "false",
|
|
31066
|
+
metadata: {
|
|
31067
|
+
isMasteryCompletion: true,
|
|
31068
|
+
adminAction: true,
|
|
31069
|
+
appName
|
|
31070
|
+
}
|
|
31071
|
+
});
|
|
31072
|
+
} else {
|
|
31073
|
+
try {
|
|
31074
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31075
|
+
sourcedId: resultId,
|
|
31076
|
+
status: ONEROSTER_STATUS.active,
|
|
31077
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31078
|
+
student: { sourcedId: studentId },
|
|
31079
|
+
score: 0,
|
|
31080
|
+
scoreDate: new Date().toISOString(),
|
|
31081
|
+
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
31082
|
+
inProgress: "true",
|
|
31083
|
+
metadata: {
|
|
31084
|
+
isMasteryCompletion: true,
|
|
31085
|
+
adminAction: true,
|
|
31086
|
+
appName
|
|
31087
|
+
}
|
|
31088
|
+
});
|
|
31089
|
+
} catch {
|
|
31090
|
+
logger16.debug("No completion entry to revoke", { studentId, courseId });
|
|
31091
|
+
}
|
|
31092
|
+
}
|
|
31093
|
+
}
|
|
31094
|
+
var logger16;
|
|
31095
|
+
var init_timeback_mastery_completion_util = __esm(() => {
|
|
31096
|
+
init_src2();
|
|
31097
|
+
init_constants4();
|
|
31098
|
+
init_utils6();
|
|
31099
|
+
logger16 = log.scope("timeback-mastery-completion");
|
|
31100
|
+
});
|
|
31101
|
+
|
|
30777
31102
|
// ../api-core/src/services/timeback-admin.service.ts
|
|
30778
31103
|
class TimebackAdminService {
|
|
30779
31104
|
deps;
|
|
@@ -30782,6 +31107,10 @@ class TimebackAdminService {
|
|
|
30782
31107
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
30783
31108
|
static MAX_STUDENT_ACTIVITY_OFFSET = 1000;
|
|
30784
31109
|
static MAX_RECENT_ACTIVITY_EVENT_FETCH = 4000;
|
|
31110
|
+
static DISCREPANCY_QUEUE_EVENT_FETCH_LIMIT = 4000;
|
|
31111
|
+
static DISCREPANCY_QUEUE_RUN_EVENT_FETCH_LIMIT = 4000;
|
|
31112
|
+
static DISCREPANCY_QUEUE_RUN_HYDRATION_CONCURRENCY = 4;
|
|
31113
|
+
static DISCREPANCY_QUEUE_COMPARISON_CONCURRENCY = 4;
|
|
30785
31114
|
static ANALYTICS_CONCURRENCY = 8;
|
|
30786
31115
|
static MASTERABLE_UNITS_CONCURRENCY = 4;
|
|
30787
31116
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -31029,16 +31358,53 @@ class TimebackAdminService {
|
|
|
31029
31358
|
}));
|
|
31030
31359
|
return comparisons;
|
|
31031
31360
|
}
|
|
31361
|
+
async getMetricDiscrepancyVerificationsByKey(options) {
|
|
31362
|
+
const runIds = TimebackAdminService.normalizeRunIds(options.runIds);
|
|
31363
|
+
if (runIds.length === 0) {
|
|
31364
|
+
return new Map;
|
|
31365
|
+
}
|
|
31366
|
+
const conditions2 = [
|
|
31367
|
+
eq(gameTimebackMetricDiscrepancyVerifications.gameId, options.gameId),
|
|
31368
|
+
eq(gameTimebackMetricDiscrepancyVerifications.courseId, options.courseId),
|
|
31369
|
+
inArray(gameTimebackMetricDiscrepancyVerifications.runId, runIds)
|
|
31370
|
+
];
|
|
31371
|
+
if (options.studentId) {
|
|
31372
|
+
conditions2.push(eq(gameTimebackMetricDiscrepancyVerifications.studentId, options.studentId));
|
|
31373
|
+
}
|
|
31374
|
+
const rows = await this.deps.db.query.gameTimebackMetricDiscrepancyVerifications.findMany({
|
|
31375
|
+
where: and(...conditions2)
|
|
31376
|
+
});
|
|
31377
|
+
return new Map(rows.map((row) => [
|
|
31378
|
+
getTimebackDiscrepancyVerificationKey({
|
|
31379
|
+
studentId: row.studentId,
|
|
31380
|
+
runId: row.runId
|
|
31381
|
+
}),
|
|
31382
|
+
TimebackAdminService.mapMetricDiscrepancyVerification(row)
|
|
31383
|
+
]));
|
|
31384
|
+
}
|
|
31032
31385
|
async attachGameMetricSummariesToActivities(user, options) {
|
|
31033
31386
|
const comparisons = await this.getGameMetricComparisonsForActivities(user, options);
|
|
31034
31387
|
if (comparisons.size === 0) {
|
|
31035
31388
|
return [...options.activities];
|
|
31036
31389
|
}
|
|
31390
|
+
const verificationsByKey = await this.getMetricDiscrepancyVerificationsByKey({
|
|
31391
|
+
gameId: options.gameId,
|
|
31392
|
+
courseId: options.courseId,
|
|
31393
|
+
studentId: options.studentId,
|
|
31394
|
+
runIds: [...comparisons.values()].filter((comparison) => comparison.status === "discrepant").map((comparison) => comparison.runId)
|
|
31395
|
+
});
|
|
31037
31396
|
return options.activities.map((activity) => {
|
|
31038
31397
|
const comparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31398
|
+
const verification2 = getTimebackMetricDiscrepancyVerificationForActivity({
|
|
31399
|
+
studentId: options.studentId,
|
|
31400
|
+
activity,
|
|
31401
|
+
comparison,
|
|
31402
|
+
verificationsByKey
|
|
31403
|
+
});
|
|
31039
31404
|
return comparison ? {
|
|
31040
31405
|
...activity,
|
|
31041
|
-
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison)
|
|
31406
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison),
|
|
31407
|
+
...verification2 ? { gameMetricsVerification: verification2 } : {}
|
|
31042
31408
|
} : activity;
|
|
31043
31409
|
});
|
|
31044
31410
|
}
|
|
@@ -31129,6 +31495,165 @@ class TimebackAdminService {
|
|
|
31129
31495
|
});
|
|
31130
31496
|
return events;
|
|
31131
31497
|
}
|
|
31498
|
+
async fetchCaliperEventsForGame(client, source, options) {
|
|
31499
|
+
const actorId = options.studentId ? `${client.getBaseUrl().replace(/\/$/, "")}/ims/oneroster/rostering/v1p2/users/${options.studentId}` : undefined;
|
|
31500
|
+
return client.caliper.events.list({
|
|
31501
|
+
limit: options.limit,
|
|
31502
|
+
offset: options.offset,
|
|
31503
|
+
...actorId ? { actorId } : {},
|
|
31504
|
+
...options.startDate ? { startDate: options.startDate } : {},
|
|
31505
|
+
...options.endDate ? { endDate: options.endDate } : {},
|
|
31506
|
+
...options.sessionId ? { sessionId: options.sessionId } : {},
|
|
31507
|
+
...source.sourceMode === "production" ? { sensor: source.sensorUrl } : {},
|
|
31508
|
+
extensions: {
|
|
31509
|
+
gameId: source.gameId
|
|
31510
|
+
}
|
|
31511
|
+
});
|
|
31512
|
+
}
|
|
31513
|
+
static getActivityRunOwners(activityGroups) {
|
|
31514
|
+
const runOwnersById = new Map;
|
|
31515
|
+
for (const group of activityGroups) {
|
|
31516
|
+
for (const activity of group.activities) {
|
|
31517
|
+
const runId = activity.runId?.toLowerCase();
|
|
31518
|
+
if (runId && !runOwnersById.has(runId)) {
|
|
31519
|
+
runOwnersById.set(runId, group.studentId);
|
|
31520
|
+
}
|
|
31521
|
+
}
|
|
31522
|
+
}
|
|
31523
|
+
return runOwnersById;
|
|
31524
|
+
}
|
|
31525
|
+
static dedupeCaliperEvents(events) {
|
|
31526
|
+
const deduped = [];
|
|
31527
|
+
const seen = new Set;
|
|
31528
|
+
for (const event of events) {
|
|
31529
|
+
const key = `${event.id}:${event.externalId}`;
|
|
31530
|
+
if (!seen.has(key)) {
|
|
31531
|
+
seen.add(key);
|
|
31532
|
+
deduped.push(event);
|
|
31533
|
+
}
|
|
31534
|
+
}
|
|
31535
|
+
return deduped;
|
|
31536
|
+
}
|
|
31537
|
+
async fetchCaliperEventsForRun(client, source, options) {
|
|
31538
|
+
const runId = options.runId.toLowerCase();
|
|
31539
|
+
const events = [];
|
|
31540
|
+
let offset = 0;
|
|
31541
|
+
while (true) {
|
|
31542
|
+
const result = await this.fetchCaliperEventsForGame(client, source, {
|
|
31543
|
+
limit: TimebackAdminService.DISCREPANCY_QUEUE_RUN_EVENT_FETCH_LIMIT,
|
|
31544
|
+
offset,
|
|
31545
|
+
studentId: options.studentId
|
|
31546
|
+
});
|
|
31547
|
+
const runEvents = result.events.filter((event) => getCanonicalRunId(event.session)?.toLowerCase() === runId);
|
|
31548
|
+
events.push(...runEvents);
|
|
31549
|
+
const pageStep = result.pagination.limit > 0 ? result.pagination.limit : result.events.length;
|
|
31550
|
+
const nextOffset = offset + pageStep;
|
|
31551
|
+
if (pageStep === 0 || result.events.length === 0 || nextOffset >= result.pagination.total) {
|
|
31552
|
+
break;
|
|
31553
|
+
}
|
|
31554
|
+
offset = nextOffset;
|
|
31555
|
+
}
|
|
31556
|
+
return TimebackAdminService.dedupeCaliperEvents(events);
|
|
31557
|
+
}
|
|
31558
|
+
async hydrateDiscrepancyQueueRunEvents(client, source, options) {
|
|
31559
|
+
if (options.runOwnersById.size === 0) {
|
|
31560
|
+
return { events: [], runIds: new Set };
|
|
31561
|
+
}
|
|
31562
|
+
const hydratedRuns = await TimebackAdminService.runWithConcurrency([...options.runOwnersById.entries()], TimebackAdminService.DISCREPANCY_QUEUE_RUN_HYDRATION_CONCURRENCY, async ([runId, studentId]) => {
|
|
31563
|
+
try {
|
|
31564
|
+
return {
|
|
31565
|
+
runId,
|
|
31566
|
+
events: await this.fetchCaliperEventsForRun(client, source, {
|
|
31567
|
+
runId,
|
|
31568
|
+
studentId
|
|
31569
|
+
})
|
|
31570
|
+
};
|
|
31571
|
+
} catch (error) {
|
|
31572
|
+
logger17.warn("Failed to hydrate Caliper events for discrepancy queue run", {
|
|
31573
|
+
runId,
|
|
31574
|
+
studentId,
|
|
31575
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31576
|
+
});
|
|
31577
|
+
return { runId, events: [] };
|
|
31578
|
+
}
|
|
31579
|
+
});
|
|
31580
|
+
const hydratedRunIds = new Set(hydratedRuns.filter((result) => result.events.length > 0).map((result) => result.runId.toLowerCase()));
|
|
31581
|
+
const baseEventsForHydratedRuns = options.baseEvents.filter((event) => {
|
|
31582
|
+
const runId = getCanonicalRunId(event.session)?.toLowerCase();
|
|
31583
|
+
return runId !== undefined && hydratedRunIds.has(runId);
|
|
31584
|
+
});
|
|
31585
|
+
return {
|
|
31586
|
+
events: TimebackAdminService.dedupeCaliperEvents([
|
|
31587
|
+
...baseEventsForHydratedRuns,
|
|
31588
|
+
...hydratedRuns.flatMap((result) => result.events)
|
|
31589
|
+
]),
|
|
31590
|
+
runIds: hydratedRunIds
|
|
31591
|
+
};
|
|
31592
|
+
}
|
|
31593
|
+
async buildMetricDiscrepancyQueueCandidates(user, options) {
|
|
31594
|
+
const comparisonResults = await TimebackAdminService.runWithConcurrency(options.activityGroups, TimebackAdminService.DISCREPANCY_QUEUE_COMPARISON_CONCURRENCY, async (group) => {
|
|
31595
|
+
try {
|
|
31596
|
+
return {
|
|
31597
|
+
group,
|
|
31598
|
+
comparisons: await this.getGameMetricComparisonsForActivities(user, {
|
|
31599
|
+
gameId: options.gameId,
|
|
31600
|
+
studentId: group.studentId,
|
|
31601
|
+
course: options.course,
|
|
31602
|
+
activities: group.activities,
|
|
31603
|
+
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
31604
|
+
})
|
|
31605
|
+
};
|
|
31606
|
+
} catch (error) {
|
|
31607
|
+
logger17.warn("Failed to compare game metrics for discrepancy queue student", {
|
|
31608
|
+
gameId: options.gameId,
|
|
31609
|
+
courseId: options.courseId,
|
|
31610
|
+
studentId: group.studentId,
|
|
31611
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31612
|
+
});
|
|
31613
|
+
return { group, comparisons: new Map };
|
|
31614
|
+
}
|
|
31615
|
+
});
|
|
31616
|
+
return comparisonResults.flatMap(({ group, comparisons }) => {
|
|
31617
|
+
const student = options.studentsById.get(group.studentId) ?? {
|
|
31618
|
+
studentId: group.studentId,
|
|
31619
|
+
name: "No name specified",
|
|
31620
|
+
email: null
|
|
31621
|
+
};
|
|
31622
|
+
return group.activities.flatMap((activity) => {
|
|
31623
|
+
const runId = activity.runId;
|
|
31624
|
+
if (!runId) {
|
|
31625
|
+
return [];
|
|
31626
|
+
}
|
|
31627
|
+
const gameMetricsComparison = comparisons.get(runId);
|
|
31628
|
+
if (!gameMetricsComparison) {
|
|
31629
|
+
return [];
|
|
31630
|
+
}
|
|
31631
|
+
const verification2 = options.verificationsByKey.get(getTimebackDiscrepancyVerificationKey({
|
|
31632
|
+
studentId: group.studentId,
|
|
31633
|
+
runId
|
|
31634
|
+
}));
|
|
31635
|
+
return [
|
|
31636
|
+
{
|
|
31637
|
+
student,
|
|
31638
|
+
activity,
|
|
31639
|
+
gameMetricsComparison,
|
|
31640
|
+
...verification2 ? { verification: verification2 } : {}
|
|
31641
|
+
}
|
|
31642
|
+
];
|
|
31643
|
+
});
|
|
31644
|
+
});
|
|
31645
|
+
}
|
|
31646
|
+
static mapMetricDiscrepancyVerification(row) {
|
|
31647
|
+
return {
|
|
31648
|
+
gameId: row.gameId,
|
|
31649
|
+
courseId: row.courseId,
|
|
31650
|
+
studentId: row.studentId,
|
|
31651
|
+
runId: row.runId,
|
|
31652
|
+
activityId: row.activityId,
|
|
31653
|
+
verifiedAt: row.verifiedAt.toISOString(),
|
|
31654
|
+
verifiedByUserId: row.verifiedByUserId
|
|
31655
|
+
};
|
|
31656
|
+
}
|
|
31132
31657
|
async listRecentActivityForStudent(client, studentId, source, relevantCourseIds, maxResults = TimebackAdminService.RECENT_ACTIVITY_LIMIT) {
|
|
31133
31658
|
if (relevantCourseIds.size === 0) {
|
|
31134
31659
|
return [];
|
|
@@ -31378,12 +31903,215 @@ class TimebackAdminService {
|
|
|
31378
31903
|
const activitiesWithGameMetrics = await this.attachGameMetricSummariesToActivities(user, {
|
|
31379
31904
|
gameId,
|
|
31380
31905
|
studentId,
|
|
31906
|
+
courseId,
|
|
31381
31907
|
course: { grade: integration.grade, subject: integration.subject },
|
|
31382
31908
|
activities,
|
|
31383
31909
|
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
31384
31910
|
});
|
|
31385
31911
|
return { activities: activitiesWithGameMetrics, hasMore };
|
|
31386
31912
|
}
|
|
31913
|
+
async listMetricDiscrepancies(user, options) {
|
|
31914
|
+
const { gameId, courseId, window: window2, includeVerified } = options;
|
|
31915
|
+
const selectedStudentId = options.studentId?.trim() || undefined;
|
|
31916
|
+
const client = this.requireClient();
|
|
31917
|
+
const dateRange = getTimebackDiscrepancyQueueDateRange({
|
|
31918
|
+
window: window2,
|
|
31919
|
+
startDate: options.startDate,
|
|
31920
|
+
endDate: options.endDate,
|
|
31921
|
+
timezone: PLATFORM_TIMEZONE
|
|
31922
|
+
});
|
|
31923
|
+
const safeEventOffset = Math.max(0, Math.trunc(options.eventOffset));
|
|
31924
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
31925
|
+
const [integration, gameSource, roster] = await Promise.all([
|
|
31926
|
+
this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
31927
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
31928
|
+
}),
|
|
31929
|
+
this.getGameActivitySource(gameId),
|
|
31930
|
+
client.oneroster.enrollments.listByCourse(courseId, {
|
|
31931
|
+
role: "student",
|
|
31932
|
+
includeInactive: true
|
|
31933
|
+
})
|
|
31934
|
+
]);
|
|
31935
|
+
if (!integration) {
|
|
31936
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
31937
|
+
}
|
|
31938
|
+
const studentsById = new Map;
|
|
31939
|
+
for (const entry of roster) {
|
|
31940
|
+
const studentId = entry.enrollment.user.sourcedId;
|
|
31941
|
+
if (studentId && !studentsById.has(studentId)) {
|
|
31942
|
+
const name3 = entry.user ? `${entry.user.givenName} ${entry.user.familyName}`.trim() || "No name specified" : "No name specified";
|
|
31943
|
+
studentsById.set(studentId, {
|
|
31944
|
+
studentId,
|
|
31945
|
+
name: name3,
|
|
31946
|
+
email: entry.user?.email || null
|
|
31947
|
+
});
|
|
31948
|
+
}
|
|
31949
|
+
}
|
|
31950
|
+
const students = [...studentsById.values()].toSorted((a, b) => a.name.localeCompare(b.name) || a.studentId.localeCompare(b.studentId));
|
|
31951
|
+
if (selectedStudentId && !studentsById.has(selectedStudentId)) {
|
|
31952
|
+
students.push({
|
|
31953
|
+
studentId: selectedStudentId,
|
|
31954
|
+
name: selectedStudentId,
|
|
31955
|
+
email: null
|
|
31956
|
+
});
|
|
31957
|
+
}
|
|
31958
|
+
const eventResult = await this.fetchCaliperEventsForGame(client, gameSource, {
|
|
31959
|
+
limit: TimebackAdminService.DISCREPANCY_QUEUE_EVENT_FETCH_LIMIT,
|
|
31960
|
+
offset: safeEventOffset,
|
|
31961
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
31962
|
+
...dateRange
|
|
31963
|
+
});
|
|
31964
|
+
const { events, pagination } = eventResult;
|
|
31965
|
+
let effectiveEventPageLimit = pagination.limit;
|
|
31966
|
+
if (effectiveEventPageLimit <= 0) {
|
|
31967
|
+
effectiveEventPageLimit = events.length > 0 ? events.length : TimebackAdminService.DISCREPANCY_QUEUE_EVENT_FETCH_LIMIT;
|
|
31968
|
+
}
|
|
31969
|
+
const relevantCourseIds = new Set([courseId]);
|
|
31970
|
+
const preliminaryActivityGroups = mapCaliperEventsToCompletedStudentGameplayActivities(events, relevantCourseIds, selectedStudentId ? { studentId: selectedStudentId } : {});
|
|
31971
|
+
const preliminaryRunIds = getTimebackActivityRunIds(preliminaryActivityGroups);
|
|
31972
|
+
const verificationsByKey = await this.getMetricDiscrepancyVerificationsByKey({
|
|
31973
|
+
gameId,
|
|
31974
|
+
courseId,
|
|
31975
|
+
runIds: preliminaryRunIds
|
|
31976
|
+
});
|
|
31977
|
+
const queueFilterOptions = {
|
|
31978
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
31979
|
+
discrepancyMetricScopes: options.discrepancyMetricScopes,
|
|
31980
|
+
includeVerified
|
|
31981
|
+
};
|
|
31982
|
+
const preliminaryCandidates = await this.buildMetricDiscrepancyQueueCandidates(user, {
|
|
31983
|
+
gameId,
|
|
31984
|
+
courseId,
|
|
31985
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31986
|
+
activityGroups: preliminaryActivityGroups,
|
|
31987
|
+
studentsById,
|
|
31988
|
+
verificationsByKey
|
|
31989
|
+
});
|
|
31990
|
+
const preliminaryItems = selectTimebackMetricDiscrepancyQueueItems(preliminaryCandidates, queueFilterOptions);
|
|
31991
|
+
const runOwnersById = TimebackAdminService.getActivityRunOwners(preliminaryItems.map((item) => ({
|
|
31992
|
+
studentId: item.student.studentId,
|
|
31993
|
+
activities: [item.activity]
|
|
31994
|
+
})));
|
|
31995
|
+
const hydratedRunEvents = await this.hydrateDiscrepancyQueueRunEvents(client, gameSource, {
|
|
31996
|
+
baseEvents: events,
|
|
31997
|
+
runOwnersById
|
|
31998
|
+
});
|
|
31999
|
+
const finalActivityGroups = mapCaliperEventsToCompletedStudentGameplayActivities(hydratedRunEvents.events, relevantCourseIds, {
|
|
32000
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
32001
|
+
runIds: hydratedRunEvents.runIds
|
|
32002
|
+
});
|
|
32003
|
+
const finalCandidates = await this.buildMetricDiscrepancyQueueCandidates(user, {
|
|
32004
|
+
gameId,
|
|
32005
|
+
courseId,
|
|
32006
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
32007
|
+
activityGroups: finalActivityGroups,
|
|
32008
|
+
studentsById,
|
|
32009
|
+
verificationsByKey
|
|
32010
|
+
});
|
|
32011
|
+
const hydratedItems = selectTimebackMetricDiscrepancyQueueItems(finalCandidates, queueFilterOptions);
|
|
32012
|
+
const items = mergeHydratedMetricDiscrepancyQueueItems({
|
|
32013
|
+
preliminaryItems,
|
|
32014
|
+
hydratedItems,
|
|
32015
|
+
hydratedRunIds: hydratedRunEvents.runIds
|
|
32016
|
+
});
|
|
32017
|
+
const eventPage = {
|
|
32018
|
+
offset: safeEventOffset,
|
|
32019
|
+
limit: effectiveEventPageLimit,
|
|
32020
|
+
total: pagination.total,
|
|
32021
|
+
hasPrevious: safeEventOffset > 0,
|
|
32022
|
+
hasNext: safeEventOffset + effectiveEventPageLimit < pagination.total
|
|
32023
|
+
};
|
|
32024
|
+
return {
|
|
32025
|
+
gameId,
|
|
32026
|
+
courseId,
|
|
32027
|
+
window: window2,
|
|
32028
|
+
dateRange,
|
|
32029
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
32030
|
+
students,
|
|
32031
|
+
discrepancyMetricScopes: [...options.discrepancyMetricScopes ?? []],
|
|
32032
|
+
eventPage,
|
|
32033
|
+
includeVerified,
|
|
32034
|
+
items
|
|
32035
|
+
};
|
|
32036
|
+
}
|
|
32037
|
+
async verifyMetricDiscrepancy(user, options) {
|
|
32038
|
+
const { gameId, courseId, data } = options;
|
|
32039
|
+
const client = this.requireClient();
|
|
32040
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
32041
|
+
const [integration, gameSource] = await Promise.all([
|
|
32042
|
+
this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
32043
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
32044
|
+
}),
|
|
32045
|
+
this.getGameActivitySource(gameId)
|
|
32046
|
+
]);
|
|
32047
|
+
if (!integration) {
|
|
32048
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
32049
|
+
}
|
|
32050
|
+
await this.assertStudentHasEnrollmentInCourse(client, data.studentId, courseId);
|
|
32051
|
+
const runId = data.runId.toLowerCase();
|
|
32052
|
+
let runEvents;
|
|
32053
|
+
try {
|
|
32054
|
+
runEvents = await this.fetchCaliperEventsForRun(client, gameSource, {
|
|
32055
|
+
runId,
|
|
32056
|
+
studentId: data.studentId
|
|
32057
|
+
});
|
|
32058
|
+
} catch (error) {
|
|
32059
|
+
logger17.warn("Failed to hydrate Caliper events for discrepancy verification", {
|
|
32060
|
+
gameId,
|
|
32061
|
+
courseId,
|
|
32062
|
+
studentId: data.studentId,
|
|
32063
|
+
runId,
|
|
32064
|
+
error: error instanceof Error ? error.message : String(error)
|
|
32065
|
+
});
|
|
32066
|
+
throw new ValidationError("Cannot verify discrepancy until full run data is available");
|
|
32067
|
+
}
|
|
32068
|
+
const activity = mapCaliperEventsToCompletedStudentGameplayActivities(runEvents, new Set([courseId]), {
|
|
32069
|
+
studentId: data.studentId,
|
|
32070
|
+
runIds: new Set([runId])
|
|
32071
|
+
}).flatMap((group) => group.activities).find((runActivity) => runActivity.runId?.toLowerCase() === runId);
|
|
32072
|
+
if (!activity) {
|
|
32073
|
+
throw new ValidationError("Cannot verify discrepancy until full run data is available");
|
|
32074
|
+
}
|
|
32075
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, {
|
|
32076
|
+
gameId,
|
|
32077
|
+
studentId: data.studentId,
|
|
32078
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
32079
|
+
activities: [activity]
|
|
32080
|
+
});
|
|
32081
|
+
const comparison = comparisons.get(activity.runId ?? runId);
|
|
32082
|
+
if (comparison?.status !== "discrepant") {
|
|
32083
|
+
throw new ValidationError("Cannot verify discrepancy because the hydrated run is not discrepant");
|
|
32084
|
+
}
|
|
32085
|
+
const verifiedAt = new Date;
|
|
32086
|
+
const [row] = await this.deps.db.insert(gameTimebackMetricDiscrepancyVerifications).values({
|
|
32087
|
+
gameId,
|
|
32088
|
+
courseId,
|
|
32089
|
+
studentId: data.studentId,
|
|
32090
|
+
runId,
|
|
32091
|
+
activityId: data.activityId ?? null,
|
|
32092
|
+
verifiedByUserId: user.id,
|
|
32093
|
+
verifiedAt
|
|
32094
|
+
}).onConflictDoUpdate({
|
|
32095
|
+
target: [
|
|
32096
|
+
gameTimebackMetricDiscrepancyVerifications.gameId,
|
|
32097
|
+
gameTimebackMetricDiscrepancyVerifications.courseId,
|
|
32098
|
+
gameTimebackMetricDiscrepancyVerifications.studentId,
|
|
32099
|
+
gameTimebackMetricDiscrepancyVerifications.runId
|
|
32100
|
+
],
|
|
32101
|
+
set: {
|
|
32102
|
+
activityId: data.activityId ?? null,
|
|
32103
|
+
verifiedByUserId: user.id,
|
|
32104
|
+
verifiedAt
|
|
32105
|
+
}
|
|
32106
|
+
}).returning();
|
|
32107
|
+
if (!row) {
|
|
32108
|
+
throw new ValidationError("Failed to verify metric discrepancy");
|
|
32109
|
+
}
|
|
32110
|
+
return {
|
|
32111
|
+
status: "ok",
|
|
32112
|
+
verification: TimebackAdminService.mapMetricDiscrepancyVerification(row)
|
|
32113
|
+
};
|
|
32114
|
+
}
|
|
31387
32115
|
async getActivityDetail(user, options) {
|
|
31388
32116
|
const { gameId, studentId, courseId, activityId, runId } = options;
|
|
31389
32117
|
const client = this.requireClient();
|
|
@@ -31398,16 +32126,37 @@ class TimebackAdminService {
|
|
|
31398
32126
|
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
31399
32127
|
}
|
|
31400
32128
|
await this.assertStudentHasEnrollmentInCourse(client, studentId, courseId);
|
|
31401
|
-
const events = await this.fetchCaliperEventsForStudent(client, studentId, gameSource, TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
|
|
31402
32129
|
const relevantCourseIds = new Set([courseId]);
|
|
31403
32130
|
let matchedEvents;
|
|
31404
32131
|
let activity;
|
|
31405
32132
|
if (runId) {
|
|
31406
|
-
const
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
32133
|
+
const normalizedRunId = TimebackAdminService.normalizeRunIds([runId], 1)[0];
|
|
32134
|
+
if (!normalizedRunId) {
|
|
32135
|
+
matchedEvents = [];
|
|
32136
|
+
activity = null;
|
|
32137
|
+
} else {
|
|
32138
|
+
let runEvents;
|
|
32139
|
+
try {
|
|
32140
|
+
runEvents = await this.fetchCaliperEventsForRun(client, gameSource, {
|
|
32141
|
+
runId: normalizedRunId,
|
|
32142
|
+
studentId
|
|
32143
|
+
});
|
|
32144
|
+
} catch (error) {
|
|
32145
|
+
logger17.warn("Failed to load Caliper events for activity detail run", {
|
|
32146
|
+
runId: normalizedRunId,
|
|
32147
|
+
studentId,
|
|
32148
|
+
activityId,
|
|
32149
|
+
error: error instanceof Error ? error.message : String(error)
|
|
32150
|
+
});
|
|
32151
|
+
runEvents = [];
|
|
32152
|
+
}
|
|
32153
|
+
const gameplayEvents = runEvents.filter((event) => (event.type === "ActivityEvent" || event.type === "TimeSpentEvent") && !isCaliperRemediationOrCompletionEvent(event));
|
|
32154
|
+
const matchingActivityEvents = findCaliperEventGroupContainingExternalId(gameplayEvents, activityId);
|
|
32155
|
+
matchedEvents = matchingActivityEvents ?? [];
|
|
32156
|
+
activity = mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
|
|
32157
|
+
}
|
|
31410
32158
|
} else {
|
|
32159
|
+
const events = await this.fetchCaliperEventsForStudent(client, studentId, gameSource, TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
|
|
31411
32160
|
matchedEvents = events.filter((event) => event.externalId === activityId);
|
|
31412
32161
|
if (matchedEvents.length > 0) {
|
|
31413
32162
|
activity = mapCaliperEventToRemediationActivity(matchedEvents[0], relevantCourseIds) ?? mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
|
|
@@ -31425,9 +32174,22 @@ class TimebackAdminService {
|
|
|
31425
32174
|
activities: [activity]
|
|
31426
32175
|
});
|
|
31427
32176
|
const gameMetricsComparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
32177
|
+
const verificationsByKey = gameMetricsComparison?.status === "discrepant" && activity.runId ? await this.getMetricDiscrepancyVerificationsByKey({
|
|
32178
|
+
gameId,
|
|
32179
|
+
courseId,
|
|
32180
|
+
studentId,
|
|
32181
|
+
runIds: [activity.runId]
|
|
32182
|
+
}) : new Map;
|
|
32183
|
+
const verification2 = getTimebackMetricDiscrepancyVerificationForActivity({
|
|
32184
|
+
studentId,
|
|
32185
|
+
activity,
|
|
32186
|
+
comparison: gameMetricsComparison,
|
|
32187
|
+
verificationsByKey
|
|
32188
|
+
});
|
|
31428
32189
|
const activityWithGameMetrics = gameMetricsComparison ? {
|
|
31429
32190
|
...activity,
|
|
31430
|
-
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison)
|
|
32191
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison),
|
|
32192
|
+
...verification2 ? { gameMetricsVerification: verification2 } : {}
|
|
31431
32193
|
} : activity;
|
|
31432
32194
|
return {
|
|
31433
32195
|
activity: activityWithGameMetrics,
|
|
@@ -31718,9 +32480,11 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
31718
32480
|
init_types4();
|
|
31719
32481
|
init_utils6();
|
|
31720
32482
|
init_src4();
|
|
32483
|
+
init_timeback3();
|
|
31721
32484
|
init_errors();
|
|
31722
32485
|
init_timeback_admin_metrics_util();
|
|
31723
32486
|
init_timeback_admin_util();
|
|
32487
|
+
init_timeback_discrepancy_queue_util();
|
|
31724
32488
|
init_timeback_game_metrics_comparison_util();
|
|
31725
32489
|
init_timeback_mastery_completion_util();
|
|
31726
32490
|
init_timeback_util();
|
|
@@ -33560,6 +34324,25 @@ var init_timeback_service = __esm(() => {
|
|
|
33560
34324
|
});
|
|
33561
34325
|
return result;
|
|
33562
34326
|
}
|
|
34327
|
+
async getStudentHighestGradeMastered(timebackId, user, options) {
|
|
34328
|
+
const client = this.requireClient();
|
|
34329
|
+
const db2 = this.deps.db;
|
|
34330
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
34331
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
34332
|
+
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject))
|
|
34333
|
+
});
|
|
34334
|
+
if (!integration) {
|
|
34335
|
+
throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
|
|
34336
|
+
}
|
|
34337
|
+
const result = await client.getHighestGradeMastered(timebackId, options.subject);
|
|
34338
|
+
logger20.debug("Retrieved student highest grade mastered", {
|
|
34339
|
+
timebackId,
|
|
34340
|
+
gameId: options.gameId,
|
|
34341
|
+
subject: options.subject,
|
|
34342
|
+
highestGradeMastered: result.highestGradeMastered
|
|
34343
|
+
});
|
|
34344
|
+
return result;
|
|
34345
|
+
}
|
|
33563
34346
|
};
|
|
33564
34347
|
});
|
|
33565
34348
|
|
|
@@ -34789,6 +35572,19 @@ async function getTimebackTokenResponse(config2) {
|
|
|
34789
35572
|
function getAuthUrl(environment = "production") {
|
|
34790
35573
|
return TIMEBACK_AUTH_URLS5[environment];
|
|
34791
35574
|
}
|
|
35575
|
+
function parseEduBridgeGrade(value) {
|
|
35576
|
+
if (value === null || value === undefined || value.trim() === "") {
|
|
35577
|
+
return null;
|
|
35578
|
+
}
|
|
35579
|
+
const parsed = Number(value);
|
|
35580
|
+
return isTimebackGrade3(parsed) ? parsed : null;
|
|
35581
|
+
}
|
|
35582
|
+
function normalizeHighestGradeMastered(response, subject) {
|
|
35583
|
+
return {
|
|
35584
|
+
subject,
|
|
35585
|
+
highestGradeMastered: parseEduBridgeGrade(response.grades.highestGradeOverall)
|
|
35586
|
+
};
|
|
35587
|
+
}
|
|
34792
35588
|
function handleHttpError(res, errorBody, attempt, retries, url2) {
|
|
34793
35589
|
const error = new TimebackApiError2(res.status, res.statusText, errorBody);
|
|
34794
35590
|
if (res.status >= HTTP_STATUS5.CLIENT_ERROR_MIN && res.status < HTTP_STATUS5.CLIENT_ERROR_MAX) {
|
|
@@ -34961,6 +35757,9 @@ function createCaliperNamespace(client) {
|
|
|
34961
35757
|
if (params.actorEmail) {
|
|
34962
35758
|
query.set("actorEmail", params.actorEmail);
|
|
34963
35759
|
}
|
|
35760
|
+
if (params.sessionId) {
|
|
35761
|
+
query.set("sessionId", params.sessionId);
|
|
35762
|
+
}
|
|
34964
35763
|
if (params.extensions) {
|
|
34965
35764
|
for (const [key, value] of Object.entries(params.extensions)) {
|
|
34966
35765
|
query.set(`extensions.${key}`, value);
|
|
@@ -35133,7 +35932,8 @@ function createEduBridgeNamespace(client) {
|
|
|
35133
35932
|
const analytics = {
|
|
35134
35933
|
getEnrollmentFacts: async (enrollmentId, options) => client["request"](buildPath(`/edubridge/analytics/enrollment/${enrollmentId}`, {
|
|
35135
35934
|
timezone: options?.timezone
|
|
35136
|
-
}), "GET")
|
|
35935
|
+
}), "GET"),
|
|
35936
|
+
getHighestGradeMastered: async (studentId, subject) => client["request"](`/edubridge/analytics/highestGradeMastered/${encodeURIComponent(studentId)}/${encodeURIComponent(subject)}`, "GET")
|
|
35137
35937
|
};
|
|
35138
35938
|
return {
|
|
35139
35939
|
enrollments,
|
|
@@ -36875,6 +37675,11 @@ class TimebackClient {
|
|
|
36875
37675
|
...options?.include?.perCourse && { courses }
|
|
36876
37676
|
};
|
|
36877
37677
|
}
|
|
37678
|
+
async getHighestGradeMastered(studentId, subject) {
|
|
37679
|
+
await this._ensureAuthenticated();
|
|
37680
|
+
const response = await this.edubridge.analytics.getHighestGradeMastered(studentId, subject);
|
|
37681
|
+
return normalizeHighestGradeMastered(response, subject);
|
|
37682
|
+
}
|
|
36878
37683
|
async getStudentXp(studentId, options) {
|
|
36879
37684
|
await this._ensureAuthenticated();
|
|
36880
37685
|
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
@@ -37323,7 +38128,7 @@ function buildTimebackClient() {
|
|
|
37323
38128
|
}
|
|
37324
38129
|
return;
|
|
37325
38130
|
}
|
|
37326
|
-
var
|
|
38131
|
+
var init_timeback4 = __esm(() => {
|
|
37327
38132
|
init_src2();
|
|
37328
38133
|
init_dist3();
|
|
37329
38134
|
init_config();
|
|
@@ -37331,7 +38136,7 @@ var init_timeback3 = __esm(() => {
|
|
|
37331
38136
|
|
|
37332
38137
|
// src/infrastructure/api/clients/index.ts
|
|
37333
38138
|
var init_clients = __esm(() => {
|
|
37334
|
-
|
|
38139
|
+
init_timeback4();
|
|
37335
38140
|
});
|
|
37336
38141
|
|
|
37337
38142
|
// src/infrastructure/api/providers/auth.provider.ts
|
|
@@ -92377,7 +93182,7 @@ async function seedTimebackIntegrations(db2, gameId, courses) {
|
|
|
92377
93182
|
}
|
|
92378
93183
|
return seededCount;
|
|
92379
93184
|
}
|
|
92380
|
-
var
|
|
93185
|
+
var init_timeback5 = __esm(() => {
|
|
92381
93186
|
init_tables_index();
|
|
92382
93187
|
init_config();
|
|
92383
93188
|
});
|
|
@@ -92468,7 +93273,7 @@ var init_games = __esm(() => {
|
|
|
92468
93273
|
init_tables_index();
|
|
92469
93274
|
init_constants();
|
|
92470
93275
|
init_logging();
|
|
92471
|
-
|
|
93276
|
+
init_timeback5();
|
|
92472
93277
|
});
|
|
92473
93278
|
|
|
92474
93279
|
// src/database/seed/index.ts
|
|
@@ -92494,7 +93299,7 @@ var init_seed = __esm(() => {
|
|
|
92494
93299
|
init_tables_index();
|
|
92495
93300
|
init_constants();
|
|
92496
93301
|
init_games();
|
|
92497
|
-
|
|
93302
|
+
init_timeback5();
|
|
92498
93303
|
init_games();
|
|
92499
93304
|
});
|
|
92500
93305
|
|
|
@@ -94197,12 +95002,13 @@ var init_session_controller = __esm(() => {
|
|
|
94197
95002
|
});
|
|
94198
95003
|
|
|
94199
95004
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
94200
|
-
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
95005
|
+
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
94201
95006
|
var init_timeback_controller = __esm(() => {
|
|
94202
95007
|
init_esm();
|
|
94203
95008
|
init_schemas_index();
|
|
94204
95009
|
init_src2();
|
|
94205
95010
|
init_src4();
|
|
95011
|
+
init_timeback3();
|
|
94206
95012
|
init_errors();
|
|
94207
95013
|
init_utils11();
|
|
94208
95014
|
logger45 = log.scope("TimebackController");
|
|
@@ -94541,6 +95347,33 @@ var init_timeback_controller = __esm(() => {
|
|
|
94541
95347
|
include
|
|
94542
95348
|
});
|
|
94543
95349
|
});
|
|
95350
|
+
getStudentHighestGradeMastered = requireDeveloper(async (ctx) => {
|
|
95351
|
+
const timebackId = ctx.params.timebackId;
|
|
95352
|
+
if (!timebackId) {
|
|
95353
|
+
throw ApiError.badRequest("Missing timebackId parameter");
|
|
95354
|
+
}
|
|
95355
|
+
const gameId = ctx.url.searchParams.get("gameId");
|
|
95356
|
+
if (!gameId) {
|
|
95357
|
+
throw ApiError.badRequest("Missing required gameId query parameter");
|
|
95358
|
+
}
|
|
95359
|
+
const subjectParam = ctx.url.searchParams.get("subject");
|
|
95360
|
+
if (!subjectParam) {
|
|
95361
|
+
throw ApiError.badRequest("Missing required subject query parameter");
|
|
95362
|
+
}
|
|
95363
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
95364
|
+
throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
95365
|
+
}
|
|
95366
|
+
logger45.debug("Getting student highest grade mastered", {
|
|
95367
|
+
requesterId: ctx.user.id,
|
|
95368
|
+
timebackId,
|
|
95369
|
+
gameId,
|
|
95370
|
+
subject: subjectParam
|
|
95371
|
+
});
|
|
95372
|
+
return ctx.services.timeback.getStudentHighestGradeMastered(timebackId, ctx.user, {
|
|
95373
|
+
gameId,
|
|
95374
|
+
subject: subjectParam
|
|
95375
|
+
});
|
|
95376
|
+
});
|
|
94544
95377
|
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
94545
95378
|
const gameId = ctx.params.gameId;
|
|
94546
95379
|
const courseId = ctx.params.courseId;
|
|
@@ -94642,6 +95475,65 @@ var init_timeback_controller = __esm(() => {
|
|
|
94642
95475
|
runId
|
|
94643
95476
|
});
|
|
94644
95477
|
});
|
|
95478
|
+
listMetricDiscrepancies = requireGameManagementAccess(async (ctx) => {
|
|
95479
|
+
const gameId = ctx.params.gameId;
|
|
95480
|
+
const courseId = ctx.params.courseId;
|
|
95481
|
+
const requestedEventOffset = Number(ctx.url.searchParams.get("eventOffset"));
|
|
95482
|
+
const requestedWindow = ctx.url.searchParams.get("window");
|
|
95483
|
+
const startDate = ctx.url.searchParams.get("startDate") ?? undefined;
|
|
95484
|
+
const endDate = ctx.url.searchParams.get("endDate") ?? undefined;
|
|
95485
|
+
const studentId = ctx.url.searchParams.get("studentId")?.trim() || undefined;
|
|
95486
|
+
const discrepancyMetricScopes = parseTimebackDiscrepancyQueueMetrics(ctx.url.searchParams.getAll("discrepancyMetric"));
|
|
95487
|
+
const eventOffset = Number.isFinite(requestedEventOffset) && requestedEventOffset > 0 ? Math.trunc(requestedEventOffset) : 0;
|
|
95488
|
+
const window2 = parseTimebackDiscrepancyQueueWindow(requestedWindow);
|
|
95489
|
+
const includeVerified = ctx.url.searchParams.get("includeVerified") === "true";
|
|
95490
|
+
if (!gameId || !courseId) {
|
|
95491
|
+
throw ApiError.badRequest("Missing gameId or courseId path parameter");
|
|
95492
|
+
}
|
|
95493
|
+
logger45.debug("Listing metric discrepancies", {
|
|
95494
|
+
requesterId: ctx.user.id,
|
|
95495
|
+
gameId,
|
|
95496
|
+
courseId,
|
|
95497
|
+
window: window2,
|
|
95498
|
+
startDate,
|
|
95499
|
+
endDate,
|
|
95500
|
+
studentId,
|
|
95501
|
+
discrepancyMetricScopes,
|
|
95502
|
+
includeVerified,
|
|
95503
|
+
eventOffset
|
|
95504
|
+
});
|
|
95505
|
+
return ctx.services.timebackAdmin.listMetricDiscrepancies(ctx.user, {
|
|
95506
|
+
gameId,
|
|
95507
|
+
courseId,
|
|
95508
|
+
window: window2,
|
|
95509
|
+
startDate,
|
|
95510
|
+
endDate,
|
|
95511
|
+
studentId,
|
|
95512
|
+
discrepancyMetricScopes,
|
|
95513
|
+
includeVerified,
|
|
95514
|
+
eventOffset
|
|
95515
|
+
});
|
|
95516
|
+
});
|
|
95517
|
+
verifyMetricDiscrepancy = requireDeveloper(async (ctx) => {
|
|
95518
|
+
const gameId = ctx.params.gameId;
|
|
95519
|
+
const courseId = ctx.params.courseId;
|
|
95520
|
+
const body2 = await parseRequestBody(ctx.request, VerifyTimebackMetricDiscrepancyRequestSchema);
|
|
95521
|
+
if (!gameId || !courseId) {
|
|
95522
|
+
throw ApiError.badRequest("Missing gameId or courseId path parameter");
|
|
95523
|
+
}
|
|
95524
|
+
logger45.debug("Verifying metric discrepancy", {
|
|
95525
|
+
requesterId: ctx.user.id,
|
|
95526
|
+
gameId,
|
|
95527
|
+
courseId,
|
|
95528
|
+
studentId: body2.studentId,
|
|
95529
|
+
runId: body2.runId
|
|
95530
|
+
});
|
|
95531
|
+
return ctx.services.timebackAdmin.verifyMetricDiscrepancy(ctx.user, {
|
|
95532
|
+
gameId,
|
|
95533
|
+
courseId,
|
|
95534
|
+
data: body2
|
|
95535
|
+
});
|
|
95536
|
+
});
|
|
94645
95537
|
grantXp = requireDeveloper(async (ctx) => {
|
|
94646
95538
|
const body2 = await parseRequestBody(ctx.request, GrantTimebackXpRequestSchema);
|
|
94647
95539
|
logger45.debug("Granting manual XP", {
|
|
@@ -94879,11 +95771,14 @@ var init_timeback_controller = __esm(() => {
|
|
|
94879
95771
|
unenrollCourse,
|
|
94880
95772
|
getStudentXp,
|
|
94881
95773
|
getStudentMastery,
|
|
95774
|
+
getStudentHighestGradeMastered,
|
|
94882
95775
|
getRoster,
|
|
94883
95776
|
getStudentOverview,
|
|
94884
95777
|
getGameMetrics,
|
|
94885
95778
|
getStudentActivity,
|
|
94886
95779
|
getActivityDetail,
|
|
95780
|
+
listMetricDiscrepancies,
|
|
95781
|
+
verifyMetricDiscrepancy,
|
|
94887
95782
|
grantXp,
|
|
94888
95783
|
adjustTime,
|
|
94889
95784
|
adjustMastery,
|
|
@@ -95087,6 +95982,10 @@ async function getMockTimebackUser(db2, gameId) {
|
|
|
95087
95982
|
const timebackId = config.timeback.timebackId || "mock-student-00000001";
|
|
95088
95983
|
return getMockTimebackData(db2, timebackId, gameId);
|
|
95089
95984
|
}
|
|
95985
|
+
function getMockHighestGradeMastered(enrollments, subject) {
|
|
95986
|
+
const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
|
|
95987
|
+
return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
|
|
95988
|
+
}
|
|
95090
95989
|
async function buildMockUserResponse(db2, user, gameId) {
|
|
95091
95990
|
const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
|
|
95092
95991
|
if (gameId) {
|
|
@@ -95117,7 +96016,7 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
95117
96016
|
timeback: timeback3
|
|
95118
96017
|
};
|
|
95119
96018
|
}
|
|
95120
|
-
var
|
|
96019
|
+
var init_timeback6 = __esm(() => {
|
|
95121
96020
|
init_drizzle_orm();
|
|
95122
96021
|
init_utils11();
|
|
95123
96022
|
init_tables_index();
|
|
@@ -95135,7 +96034,7 @@ var init_users = __esm(() => {
|
|
|
95135
96034
|
init_tables_index();
|
|
95136
96035
|
init_api();
|
|
95137
96036
|
init_error_handler();
|
|
95138
|
-
|
|
96037
|
+
init_timeback6();
|
|
95139
96038
|
usersRouter = new Hono2;
|
|
95140
96039
|
usersRouter.get("/me", async (c2) => {
|
|
95141
96040
|
const user = c2.get("user");
|
|
@@ -95760,15 +96659,16 @@ function hashCode(str) {
|
|
|
95760
96659
|
return Math.abs(hash);
|
|
95761
96660
|
}
|
|
95762
96661
|
var timebackRouter;
|
|
95763
|
-
var
|
|
96662
|
+
var init_timeback7 = __esm(() => {
|
|
95764
96663
|
init_dist4();
|
|
95765
96664
|
init_controllers();
|
|
95766
96665
|
init_errors();
|
|
95767
96666
|
init_utils11();
|
|
95768
96667
|
init_schemas_index();
|
|
96668
|
+
init_timeback3();
|
|
95769
96669
|
init_api();
|
|
95770
96670
|
init_error_handler();
|
|
95771
|
-
|
|
96671
|
+
init_timeback6();
|
|
95772
96672
|
timebackRouter = new Hono2;
|
|
95773
96673
|
timebackRouter.post("/populate-student", async (c2) => c2.json({
|
|
95774
96674
|
status: "no_record"
|
|
@@ -95934,6 +96834,39 @@ var init_timeback6 = __esm(() => {
|
|
|
95934
96834
|
}
|
|
95935
96835
|
return handle2(timeback2.getStudentMastery)(c2);
|
|
95936
96836
|
});
|
|
96837
|
+
timebackRouter.get("/student-highest-grade-mastered/:timebackId", async (c2) => {
|
|
96838
|
+
const user = c2.get("user");
|
|
96839
|
+
if (!user) {
|
|
96840
|
+
const error2 = ApiError.unauthorized("Must be logged in to get student highest grade mastered");
|
|
96841
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96842
|
+
}
|
|
96843
|
+
if (shouldMockTimeback()) {
|
|
96844
|
+
const url2 = new URL(c2.req.url);
|
|
96845
|
+
const subject = url2.searchParams.get("subject");
|
|
96846
|
+
const contextGameId = c2.get("gameId");
|
|
96847
|
+
const gameId = url2.searchParams.get("gameId") || (typeof contextGameId === "string" ? contextGameId : undefined);
|
|
96848
|
+
if (!subject) {
|
|
96849
|
+
const error2 = ApiError.badRequest("Missing required subject query parameter");
|
|
96850
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96851
|
+
}
|
|
96852
|
+
if (!gameId) {
|
|
96853
|
+
const error2 = ApiError.badRequest("Missing required gameId query parameter");
|
|
96854
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96855
|
+
}
|
|
96856
|
+
if (!isTimebackSubject2(subject)) {
|
|
96857
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subject}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
96858
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96859
|
+
}
|
|
96860
|
+
const db2 = c2.get("db");
|
|
96861
|
+
const enrollments = await getMockEnrollments(db2);
|
|
96862
|
+
const gameScopedEnrollments = filterEnrollmentsByGame(enrollments, gameId);
|
|
96863
|
+
return c2.json({
|
|
96864
|
+
subject,
|
|
96865
|
+
highestGradeMastered: getMockHighestGradeMastered(gameScopedEnrollments, subject)
|
|
96866
|
+
});
|
|
96867
|
+
}
|
|
96868
|
+
return handle2(timeback2.getStudentHighestGradeMastered)(c2);
|
|
96869
|
+
});
|
|
95937
96870
|
});
|
|
95938
96871
|
|
|
95939
96872
|
// src/routes/integrations/lti.ts
|
|
@@ -96058,7 +96991,7 @@ var init_routes = __esm(() => {
|
|
|
96058
96991
|
init_users();
|
|
96059
96992
|
init_games2();
|
|
96060
96993
|
init_leaderboard();
|
|
96061
|
-
|
|
96994
|
+
init_timeback7();
|
|
96062
96995
|
init_lti();
|
|
96063
96996
|
});
|
|
96064
96997
|
|
|
@@ -98311,7 +99244,8 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
|
|
|
98311
99244
|
port,
|
|
98312
99245
|
url: `http://localhost:${port}/api`,
|
|
98313
99246
|
startedAt: Date.now(),
|
|
98314
|
-
projectRoot: process.cwd()
|
|
99247
|
+
projectRoot: process.cwd(),
|
|
99248
|
+
gameId: server.gameId
|
|
98315
99249
|
});
|
|
98316
99250
|
const totalCourses = project?.timebackCourses?.length ?? 0;
|
|
98317
99251
|
const excludedCount = options.timebackExcludedCourses ? options.timebackExcludedCourses.split(",").filter(Boolean).length : 0;
|