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