@playcademy/sandbox 0.4.0 → 0.4.1-beta.2
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 +523 -38
- package/dist/constants.js +13 -1
- package/dist/server.js +523 -38
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -243,11 +243,12 @@ var init_platform = __esm(() => {
|
|
|
243
243
|
var PLATFORM_TIMEZONE = "America/New_York";
|
|
244
244
|
|
|
245
245
|
// ../constants/src/timeback.ts
|
|
246
|
-
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
|
|
246
|
+
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
|
|
247
247
|
var init_timeback2 = __esm(() => {
|
|
248
248
|
TIMEBACK_ROUTES = {
|
|
249
249
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
250
250
|
GET_XP: "/integrations/timeback/xp",
|
|
251
|
+
GET_MASTERY: "/integrations/timeback/mastery",
|
|
251
252
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
252
253
|
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
253
254
|
};
|
|
@@ -287,6 +288,17 @@ var init_timeback2 = __esm(() => {
|
|
|
287
288
|
sortOrder: 1,
|
|
288
289
|
lessonType: "quiz"
|
|
289
290
|
};
|
|
291
|
+
TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
|
|
292
|
+
xp: 1,
|
|
293
|
+
mastery: 0,
|
|
294
|
+
score: 2
|
|
295
|
+
};
|
|
296
|
+
TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
|
|
297
|
+
xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
|
|
298
|
+
mastery: 0,
|
|
299
|
+
time: 60,
|
|
300
|
+
score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
|
|
301
|
+
};
|
|
290
302
|
});
|
|
291
303
|
|
|
292
304
|
// ../constants/src/cloudflare.ts
|
|
@@ -1064,7 +1076,7 @@ var package_default;
|
|
|
1064
1076
|
var init_package = __esm(() => {
|
|
1065
1077
|
package_default = {
|
|
1066
1078
|
name: "@playcademy/sandbox",
|
|
1067
|
-
version: "0.4.
|
|
1079
|
+
version: "0.4.1-beta.2",
|
|
1068
1080
|
description: "Local development server for Playcademy game development",
|
|
1069
1081
|
type: "module",
|
|
1070
1082
|
exports: {
|
|
@@ -28234,6 +28246,7 @@ var init_constants3 = __esm(() => {
|
|
|
28234
28246
|
TIMEBACK: {
|
|
28235
28247
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
28236
28248
|
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
28249
|
+
GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
|
|
28237
28250
|
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
|
|
28238
28251
|
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
|
|
28239
28252
|
}
|
|
@@ -29889,7 +29902,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
29889
29902
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
29890
29903
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
29891
29904
|
}
|
|
29892
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema,
|
|
29905
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, 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;
|
|
29893
29906
|
var init_schemas4 = __esm(() => {
|
|
29894
29907
|
init_drizzle_zod();
|
|
29895
29908
|
init_esm();
|
|
@@ -29958,24 +29971,28 @@ var init_schemas4 = __esm(() => {
|
|
|
29958
29971
|
}).optional(),
|
|
29959
29972
|
xpEarned: exports_external.number().optional(),
|
|
29960
29973
|
masteredUnits: exports_external.number().optional(),
|
|
29974
|
+
masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
|
|
29961
29975
|
extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
29976
|
+
}).refine((data) => !(data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined), {
|
|
29977
|
+
message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
|
|
29978
|
+
path: ["masteredUnitsAbsolute"]
|
|
29962
29979
|
});
|
|
29963
|
-
|
|
29980
|
+
GameRunMetricsSchema = exports_external.object({
|
|
29981
|
+
runId: exports_external.string().uuid(),
|
|
29964
29982
|
activityId: exports_external.string().min(1),
|
|
29965
29983
|
activityName: exports_external.string().optional(),
|
|
29966
|
-
totalXp: exports_external.number().nonnegative(),
|
|
29967
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
29968
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
29969
|
-
|
|
29970
|
-
lastCompletedAt: exports_external.string().datetime().optional()
|
|
29984
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
29985
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
29986
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
29987
|
+
score: exports_external.number().min(0).max(100).optional()
|
|
29971
29988
|
});
|
|
29972
29989
|
GameCourseMetricsSchema = exports_external.object({
|
|
29973
29990
|
grade: TimebackGradeSchema,
|
|
29974
29991
|
subject: TimebackSubjectSchema,
|
|
29975
|
-
totalXp: exports_external.number().nonnegative(),
|
|
29976
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
29977
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
29978
|
-
activities: exports_external.array(
|
|
29992
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
29993
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
29994
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
29995
|
+
activities: exports_external.array(GameRunMetricsSchema).optional()
|
|
29979
29996
|
});
|
|
29980
29997
|
GameMetricsResponseSchema = exports_external.object({
|
|
29981
29998
|
studentId: exports_external.string().min(1),
|
|
@@ -30252,6 +30269,124 @@ var init_timeback_admin_util = __esm(() => {
|
|
|
30252
30269
|
init_errors();
|
|
30253
30270
|
});
|
|
30254
30271
|
|
|
30272
|
+
// ../api-core/src/utils/timeback-game-metrics-comparison.util.ts
|
|
30273
|
+
function createMetricRow(definition) {
|
|
30274
|
+
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
30275
|
+
if (timebackValue === undefined && gameValue === undefined) {
|
|
30276
|
+
return null;
|
|
30277
|
+
}
|
|
30278
|
+
if (gameValue === undefined) {
|
|
30279
|
+
return {
|
|
30280
|
+
metric,
|
|
30281
|
+
kind,
|
|
30282
|
+
status: "not_reported_by_game",
|
|
30283
|
+
...timebackValue !== undefined ? { timebackValue } : {}
|
|
30284
|
+
};
|
|
30285
|
+
}
|
|
30286
|
+
if (timebackValue === undefined) {
|
|
30287
|
+
return {
|
|
30288
|
+
metric,
|
|
30289
|
+
kind,
|
|
30290
|
+
status: "not_recorded_by_timeback",
|
|
30291
|
+
gameValue
|
|
30292
|
+
};
|
|
30293
|
+
}
|
|
30294
|
+
const delta = gameValue - timebackValue;
|
|
30295
|
+
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
30296
|
+
return {
|
|
30297
|
+
metric,
|
|
30298
|
+
kind,
|
|
30299
|
+
status: isDiscrepant ? "discrepant" : "matched",
|
|
30300
|
+
timebackValue,
|
|
30301
|
+
gameValue,
|
|
30302
|
+
delta
|
|
30303
|
+
};
|
|
30304
|
+
}
|
|
30305
|
+
function createRunComparison(activity, gameRun) {
|
|
30306
|
+
const runId = activity.runId ?? "";
|
|
30307
|
+
if (!gameRun) {
|
|
30308
|
+
return {
|
|
30309
|
+
runId,
|
|
30310
|
+
status: "not_reported",
|
|
30311
|
+
discrepancyCount: 0,
|
|
30312
|
+
rows: []
|
|
30313
|
+
};
|
|
30314
|
+
}
|
|
30315
|
+
const rows = [
|
|
30316
|
+
createMetricRow({
|
|
30317
|
+
metric: "xp",
|
|
30318
|
+
kind: "number",
|
|
30319
|
+
timebackValue: activity.xpDelta,
|
|
30320
|
+
gameValue: gameRun.totalXp,
|
|
30321
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
30322
|
+
}),
|
|
30323
|
+
createMetricRow({
|
|
30324
|
+
metric: "mastery",
|
|
30325
|
+
kind: "number",
|
|
30326
|
+
timebackValue: activity.masteredUnitsDelta,
|
|
30327
|
+
gameValue: gameRun.masteredUnits,
|
|
30328
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
30329
|
+
}),
|
|
30330
|
+
createMetricRow({
|
|
30331
|
+
metric: "time",
|
|
30332
|
+
kind: "time",
|
|
30333
|
+
timebackValue: activity.timeDeltaSeconds,
|
|
30334
|
+
gameValue: gameRun.activeTimeSeconds,
|
|
30335
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
30336
|
+
}),
|
|
30337
|
+
createMetricRow({
|
|
30338
|
+
metric: "score",
|
|
30339
|
+
kind: "percent",
|
|
30340
|
+
timebackValue: activity.score,
|
|
30341
|
+
gameValue: gameRun.score,
|
|
30342
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
30343
|
+
})
|
|
30344
|
+
].filter((row) => row !== null);
|
|
30345
|
+
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
30346
|
+
return {
|
|
30347
|
+
runId,
|
|
30348
|
+
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
30349
|
+
discrepancyCount,
|
|
30350
|
+
rows
|
|
30351
|
+
};
|
|
30352
|
+
}
|
|
30353
|
+
function summarizeGameRunMetricsComparison(comparison) {
|
|
30354
|
+
return {
|
|
30355
|
+
runId: comparison.runId,
|
|
30356
|
+
status: comparison.status,
|
|
30357
|
+
discrepancyCount: comparison.discrepancyCount,
|
|
30358
|
+
...comparison.reason ? { reason: comparison.reason } : {}
|
|
30359
|
+
};
|
|
30360
|
+
}
|
|
30361
|
+
function buildGameRunMetricComparisons(activities, course, response) {
|
|
30362
|
+
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
30363
|
+
const comparisons = new Map;
|
|
30364
|
+
if (activitiesWithRunIds.length === 0) {
|
|
30365
|
+
return comparisons;
|
|
30366
|
+
}
|
|
30367
|
+
if (!response.supported) {
|
|
30368
|
+
for (const activity of activitiesWithRunIds) {
|
|
30369
|
+
comparisons.set(activity.runId, {
|
|
30370
|
+
runId: activity.runId,
|
|
30371
|
+
status: "unavailable",
|
|
30372
|
+
discrepancyCount: 0,
|
|
30373
|
+
reason: response.reason,
|
|
30374
|
+
rows: []
|
|
30375
|
+
});
|
|
30376
|
+
}
|
|
30377
|
+
return comparisons;
|
|
30378
|
+
}
|
|
30379
|
+
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
30380
|
+
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
30381
|
+
for (const activity of activitiesWithRunIds) {
|
|
30382
|
+
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
30383
|
+
}
|
|
30384
|
+
return comparisons;
|
|
30385
|
+
}
|
|
30386
|
+
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
30387
|
+
init_src();
|
|
30388
|
+
});
|
|
30389
|
+
|
|
30255
30390
|
// ../api-core/src/utils/timeback-mastery-completion.util.ts
|
|
30256
30391
|
async function upsertMasteryCompletionEntry(params) {
|
|
30257
30392
|
const { client, courseId, studentId, appName, action } = params;
|
|
@@ -30329,7 +30464,7 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
30329
30464
|
subject: integration.subject,
|
|
30330
30465
|
courseId: integration.courseId,
|
|
30331
30466
|
orgId: courseToSchool.get(integration.courseId),
|
|
30332
|
-
...enrollment ? {
|
|
30467
|
+
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
30333
30468
|
};
|
|
30334
30469
|
});
|
|
30335
30470
|
}
|
|
@@ -30642,6 +30777,8 @@ class TimebackAdminService {
|
|
|
30642
30777
|
static ANALYTICS_CONCURRENCY = 8;
|
|
30643
30778
|
static MASTERABLE_UNITS_CONCURRENCY = 4;
|
|
30644
30779
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
30780
|
+
static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
|
|
30781
|
+
static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
|
|
30645
30782
|
constructor(deps) {
|
|
30646
30783
|
this.deps = deps;
|
|
30647
30784
|
}
|
|
@@ -30651,13 +30788,42 @@ class TimebackAdminService {
|
|
|
30651
30788
|
}
|
|
30652
30789
|
return this.deps.config.localGameUrls[slug2] ?? deployedUrl;
|
|
30653
30790
|
}
|
|
30654
|
-
static resolveGameMetricsUrl(baseUrl) {
|
|
30791
|
+
static resolveGameMetricsUrl(baseUrl, runIds) {
|
|
30655
30792
|
try {
|
|
30656
|
-
|
|
30793
|
+
const url2 = new URL("/__playcademy/metrics", baseUrl);
|
|
30794
|
+
for (const runId of runIds ?? []) {
|
|
30795
|
+
url2.searchParams.append("runId", runId);
|
|
30796
|
+
}
|
|
30797
|
+
return url2;
|
|
30657
30798
|
} catch {
|
|
30658
30799
|
return null;
|
|
30659
30800
|
}
|
|
30660
30801
|
}
|
|
30802
|
+
static normalizeRunIds(runIds, limit = Number.POSITIVE_INFINITY) {
|
|
30803
|
+
const normalized = [];
|
|
30804
|
+
const seen = new Set;
|
|
30805
|
+
for (const runId of runIds ?? []) {
|
|
30806
|
+
const value = runId.trim().toLowerCase();
|
|
30807
|
+
if (isValidUUID(value) && !seen.has(value)) {
|
|
30808
|
+
seen.add(value);
|
|
30809
|
+
normalized.push(value);
|
|
30810
|
+
if (normalized.length >= limit) {
|
|
30811
|
+
break;
|
|
30812
|
+
}
|
|
30813
|
+
}
|
|
30814
|
+
}
|
|
30815
|
+
return normalized;
|
|
30816
|
+
}
|
|
30817
|
+
static chunkRunIds(runIds) {
|
|
30818
|
+
const chunks = [];
|
|
30819
|
+
for (let index2 = 0;index2 < runIds.length; index2 += this.GAME_METRICS_RUN_IDS_PER_REQUEST) {
|
|
30820
|
+
chunks.push(runIds.slice(index2, index2 + this.GAME_METRICS_RUN_IDS_PER_REQUEST));
|
|
30821
|
+
}
|
|
30822
|
+
return chunks;
|
|
30823
|
+
}
|
|
30824
|
+
static isAbortError(error) {
|
|
30825
|
+
return error instanceof Error && error.name === "AbortError";
|
|
30826
|
+
}
|
|
30661
30827
|
static roundXpToTenths(value) {
|
|
30662
30828
|
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
30663
30829
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
@@ -30818,6 +30984,56 @@ class TimebackAdminService {
|
|
|
30818
30984
|
const remediationItems = events.map((event) => mapCaliperEventToRemediationActivity(event, relevantCourseIds)).filter((item) => Boolean(item));
|
|
30819
30985
|
return [...groupedGameplayItems, ...remediationItems].toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
30820
30986
|
}
|
|
30987
|
+
async getGameMetricComparisonsForActivities(user, options) {
|
|
30988
|
+
const runIds = TimebackAdminService.normalizeRunIds(options.activities.map((activity) => activity.runId).filter((runId) => Boolean(runId)));
|
|
30989
|
+
if (runIds.length === 0) {
|
|
30990
|
+
return new Map;
|
|
30991
|
+
}
|
|
30992
|
+
const activitiesByRunId = new Map(options.activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0).map((activity) => [activity.runId.toLowerCase(), activity]));
|
|
30993
|
+
const comparisons = new Map;
|
|
30994
|
+
await Promise.all(TimebackAdminService.chunkRunIds(runIds).map(async (chunk) => {
|
|
30995
|
+
const activities = [];
|
|
30996
|
+
for (const runId of chunk) {
|
|
30997
|
+
const activity = activitiesByRunId.get(runId);
|
|
30998
|
+
if (activity) {
|
|
30999
|
+
activities.push(activity);
|
|
31000
|
+
}
|
|
31001
|
+
}
|
|
31002
|
+
if (activities.length === 0) {
|
|
31003
|
+
return;
|
|
31004
|
+
}
|
|
31005
|
+
let response;
|
|
31006
|
+
try {
|
|
31007
|
+
response = await this.getGameMetrics(options.gameId, options.studentId, user, {
|
|
31008
|
+
runIds: chunk,
|
|
31009
|
+
timeoutMs: options.timeoutMs
|
|
31010
|
+
});
|
|
31011
|
+
} catch (error) {
|
|
31012
|
+
response = {
|
|
31013
|
+
supported: false,
|
|
31014
|
+
reason: "fetch_failed",
|
|
31015
|
+
details: error instanceof Error ? error.message : String(error)
|
|
31016
|
+
};
|
|
31017
|
+
}
|
|
31018
|
+
for (const [runId, comparison] of buildGameRunMetricComparisons(activities, options.course, response)) {
|
|
31019
|
+
comparisons.set(runId, comparison);
|
|
31020
|
+
}
|
|
31021
|
+
}));
|
|
31022
|
+
return comparisons;
|
|
31023
|
+
}
|
|
31024
|
+
async attachGameMetricSummariesToActivities(user, options) {
|
|
31025
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, options);
|
|
31026
|
+
if (comparisons.size === 0) {
|
|
31027
|
+
return [...options.activities];
|
|
31028
|
+
}
|
|
31029
|
+
return options.activities.map((activity) => {
|
|
31030
|
+
const comparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31031
|
+
return comparison ? {
|
|
31032
|
+
...activity,
|
|
31033
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison)
|
|
31034
|
+
} : activity;
|
|
31035
|
+
});
|
|
31036
|
+
}
|
|
30821
31037
|
async getStudentEnrollmentsByCourseId(client, studentId, courseIds, options) {
|
|
30822
31038
|
const enrollments = new Map;
|
|
30823
31039
|
const allEnrollments = new Map;
|
|
@@ -30977,7 +31193,7 @@ class TimebackAdminService {
|
|
|
30977
31193
|
});
|
|
30978
31194
|
return { gameId, courseId, students: deduped };
|
|
30979
31195
|
}
|
|
30980
|
-
async getGameMetrics(gameId, timebackId, user) {
|
|
31196
|
+
async getGameMetrics(gameId, timebackId, user, options) {
|
|
30981
31197
|
const client = this.requireClient();
|
|
30982
31198
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
30983
31199
|
const [targetUser, integrations, game2, deployment] = await Promise.all([
|
|
@@ -31012,7 +31228,8 @@ class TimebackAdminService {
|
|
|
31012
31228
|
if (!metricsBaseUrl) {
|
|
31013
31229
|
return { supported: false, reason: "no_active_deployment" };
|
|
31014
31230
|
}
|
|
31015
|
-
const
|
|
31231
|
+
const runIds = TimebackAdminService.normalizeRunIds(options?.runIds, TimebackAdminService.GAME_METRICS_RUN_IDS_PER_REQUEST);
|
|
31232
|
+
const metricsUrl = TimebackAdminService.resolveGameMetricsUrl(metricsBaseUrl, runIds);
|
|
31016
31233
|
if (!metricsUrl) {
|
|
31017
31234
|
return {
|
|
31018
31235
|
supported: false,
|
|
@@ -31022,7 +31239,7 @@ class TimebackAdminService {
|
|
|
31022
31239
|
}
|
|
31023
31240
|
const token = await this.deps.mintPlatformServiceToken(gameId, targetUser.id);
|
|
31024
31241
|
const controller = new AbortController;
|
|
31025
|
-
const timeout = setTimeout(() => controller.abort(), TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
31242
|
+
const timeout = setTimeout(() => controller.abort(), options?.timeoutMs ?? TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
31026
31243
|
let response;
|
|
31027
31244
|
try {
|
|
31028
31245
|
response = await fetch(metricsUrl, {
|
|
@@ -31034,10 +31251,19 @@ class TimebackAdminService {
|
|
|
31034
31251
|
signal: controller.signal
|
|
31035
31252
|
});
|
|
31036
31253
|
} catch (error) {
|
|
31254
|
+
const timedOut = TimebackAdminService.isAbortError(error);
|
|
31255
|
+
let details;
|
|
31256
|
+
if (timedOut) {
|
|
31257
|
+
details = "Game metrics request timed out";
|
|
31258
|
+
} else if (error instanceof Error) {
|
|
31259
|
+
details = error.message;
|
|
31260
|
+
} else {
|
|
31261
|
+
details = String(error);
|
|
31262
|
+
}
|
|
31037
31263
|
return {
|
|
31038
31264
|
supported: false,
|
|
31039
|
-
reason: "fetch_failed",
|
|
31040
|
-
details
|
|
31265
|
+
reason: timedOut ? "timeout" : "fetch_failed",
|
|
31266
|
+
details
|
|
31041
31267
|
};
|
|
31042
31268
|
} finally {
|
|
31043
31269
|
clearTimeout(timeout);
|
|
@@ -31141,7 +31367,14 @@ class TimebackAdminService {
|
|
|
31141
31367
|
const allActivities = await this.listRecentActivityForStudent(client, studentId, gameSource, relevantCourseIds, fetchLimit);
|
|
31142
31368
|
const activities = allActivities.slice(safeOffset, safeOffset + safeLimit);
|
|
31143
31369
|
const hasMore = allActivities.length > safeOffset + safeLimit;
|
|
31144
|
-
|
|
31370
|
+
const activitiesWithGameMetrics = await this.attachGameMetricSummariesToActivities(user, {
|
|
31371
|
+
gameId,
|
|
31372
|
+
studentId,
|
|
31373
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31374
|
+
activities,
|
|
31375
|
+
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
31376
|
+
});
|
|
31377
|
+
return { activities: activitiesWithGameMetrics, hasMore };
|
|
31145
31378
|
}
|
|
31146
31379
|
async getActivityDetail(user, options) {
|
|
31147
31380
|
const { gameId, studentId, courseId, activityId, runId } = options;
|
|
@@ -31177,7 +31410,22 @@ class TimebackAdminService {
|
|
|
31177
31410
|
if (!activity) {
|
|
31178
31411
|
throw new NotFoundError("Activity", activityId);
|
|
31179
31412
|
}
|
|
31180
|
-
|
|
31413
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, {
|
|
31414
|
+
gameId,
|
|
31415
|
+
studentId,
|
|
31416
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31417
|
+
activities: [activity]
|
|
31418
|
+
});
|
|
31419
|
+
const gameMetricsComparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31420
|
+
const activityWithGameMetrics = gameMetricsComparison ? {
|
|
31421
|
+
...activity,
|
|
31422
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison)
|
|
31423
|
+
} : activity;
|
|
31424
|
+
return {
|
|
31425
|
+
activity: activityWithGameMetrics,
|
|
31426
|
+
rawEvents: matchedEvents,
|
|
31427
|
+
...gameMetricsComparison ? { gameMetricsComparison } : {}
|
|
31428
|
+
};
|
|
31181
31429
|
}
|
|
31182
31430
|
async grantManualXp(data, user) {
|
|
31183
31431
|
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
@@ -31465,6 +31713,7 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
31465
31713
|
init_errors();
|
|
31466
31714
|
init_timeback_admin_metrics_util();
|
|
31467
31715
|
init_timeback_admin_util();
|
|
31716
|
+
init_timeback_game_metrics_comparison_util();
|
|
31468
31717
|
init_timeback_mastery_completion_util();
|
|
31469
31718
|
init_timeback_util();
|
|
31470
31719
|
logger17 = log.scope("TimebackAdminService");
|
|
@@ -32886,6 +33135,7 @@ var init_timeback_service = __esm(() => {
|
|
|
32886
33135
|
sessionTimingData,
|
|
32887
33136
|
xpEarned,
|
|
32888
33137
|
masteredUnits,
|
|
33138
|
+
masteredUnitsAbsolute,
|
|
32889
33139
|
extensions,
|
|
32890
33140
|
user
|
|
32891
33141
|
}) {
|
|
@@ -32909,6 +33159,7 @@ var init_timeback_service = __esm(() => {
|
|
|
32909
33159
|
durationSeconds: timingData.durationSeconds,
|
|
32910
33160
|
xpEarned,
|
|
32911
33161
|
masteredUnits,
|
|
33162
|
+
masteredUnitsAbsolute,
|
|
32912
33163
|
extensions: extensionsWithResumeId,
|
|
32913
33164
|
activityId: activityData.activityId,
|
|
32914
33165
|
activityName: activityData.activityName,
|
|
@@ -33157,6 +33408,46 @@ var init_timeback_service = __esm(() => {
|
|
|
33157
33408
|
});
|
|
33158
33409
|
return result;
|
|
33159
33410
|
}
|
|
33411
|
+
async getStudentMastery(timebackId, user, options) {
|
|
33412
|
+
const client = this.requireClient();
|
|
33413
|
+
const db2 = this.deps.db;
|
|
33414
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
33415
|
+
const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
|
|
33416
|
+
if (options.grade !== undefined && options.subject) {
|
|
33417
|
+
conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
|
|
33418
|
+
conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
|
|
33419
|
+
}
|
|
33420
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
33421
|
+
where: and(...conditions2)
|
|
33422
|
+
});
|
|
33423
|
+
const courseIds = integrations.map((i2) => i2.courseId);
|
|
33424
|
+
if (courseIds.length === 0) {
|
|
33425
|
+
logger20.debug("No integrations found for game, returning empty mastery", {
|
|
33426
|
+
timebackId,
|
|
33427
|
+
gameId: options.gameId,
|
|
33428
|
+
grade: options.grade,
|
|
33429
|
+
subject: options.subject
|
|
33430
|
+
});
|
|
33431
|
+
return {
|
|
33432
|
+
totalMasteredUnits: 0,
|
|
33433
|
+
totalMasterableUnits: 0,
|
|
33434
|
+
...options.include?.perCourse && { courses: [] }
|
|
33435
|
+
};
|
|
33436
|
+
}
|
|
33437
|
+
const result = await client.getStudentMastery(timebackId, {
|
|
33438
|
+
courseIds,
|
|
33439
|
+
include: options.include
|
|
33440
|
+
});
|
|
33441
|
+
logger20.debug("Retrieved student mastery", {
|
|
33442
|
+
timebackId,
|
|
33443
|
+
gameId: options.gameId,
|
|
33444
|
+
grade: options.grade,
|
|
33445
|
+
subject: options.subject,
|
|
33446
|
+
totalMasteredUnits: result.totalMasteredUnits,
|
|
33447
|
+
courseCount: result.courses?.length
|
|
33448
|
+
});
|
|
33449
|
+
return result;
|
|
33450
|
+
}
|
|
33160
33451
|
};
|
|
33161
33452
|
});
|
|
33162
33453
|
|
|
@@ -35482,15 +35773,18 @@ class MasteryTracker {
|
|
|
35482
35773
|
this.edubridgeNamespace = edubridgeNamespace;
|
|
35483
35774
|
}
|
|
35484
35775
|
async checkProgress(input) {
|
|
35485
|
-
const { studentId, courseId, resourceId, masteredUnits } = input;
|
|
35486
|
-
|
|
35776
|
+
const { studentId, courseId, resourceId, masteredUnits, masteredUnitsAbsolute } = input;
|
|
35777
|
+
const hasIncremental = typeof masteredUnits === "number" && masteredUnits !== 0;
|
|
35778
|
+
const hasAbsolute = typeof masteredUnitsAbsolute === "number";
|
|
35779
|
+
if (!hasIncremental && !hasAbsolute) {
|
|
35487
35780
|
return;
|
|
35488
35781
|
}
|
|
35489
35782
|
const status = await this.calculateStatus({
|
|
35490
35783
|
studentId,
|
|
35491
35784
|
courseId,
|
|
35492
35785
|
resourceId,
|
|
35493
|
-
additionalMasteredUnits: masteredUnits
|
|
35786
|
+
additionalMasteredUnits: hasAbsolute ? 0 : masteredUnits,
|
|
35787
|
+
absoluteMasteredUnits: hasAbsolute ? masteredUnitsAbsolute : undefined
|
|
35494
35788
|
});
|
|
35495
35789
|
if (!status) {
|
|
35496
35790
|
return;
|
|
@@ -35499,7 +35793,8 @@ class MasteryTracker {
|
|
|
35499
35793
|
return {
|
|
35500
35794
|
pctCompleteApp: status.pctCompleteApp,
|
|
35501
35795
|
masteryAchieved: !wasComplete && status.isComplete,
|
|
35502
|
-
masteryRevoked: wasComplete && !status.isComplete
|
|
35796
|
+
masteryRevoked: wasComplete && !status.isComplete,
|
|
35797
|
+
effectiveDelta: status.effectiveDelta
|
|
35503
35798
|
};
|
|
35504
35799
|
}
|
|
35505
35800
|
async getStatus(input) {
|
|
@@ -35521,7 +35816,8 @@ class MasteryTracker {
|
|
|
35521
35816
|
studentId,
|
|
35522
35817
|
courseId,
|
|
35523
35818
|
resourceId,
|
|
35524
|
-
additionalMasteredUnits
|
|
35819
|
+
additionalMasteredUnits,
|
|
35820
|
+
absoluteMasteredUnits
|
|
35525
35821
|
}) {
|
|
35526
35822
|
const masterableUnits = await this.resolveMasterableUnits(resourceId);
|
|
35527
35823
|
if (!masterableUnits || masterableUnits <= 0) {
|
|
@@ -35540,7 +35836,15 @@ class MasteryTracker {
|
|
|
35540
35836
|
return;
|
|
35541
35837
|
}
|
|
35542
35838
|
const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
|
|
35543
|
-
|
|
35839
|
+
let totalMastered;
|
|
35840
|
+
let effectiveDelta;
|
|
35841
|
+
if (absoluteMasteredUnits !== undefined) {
|
|
35842
|
+
totalMastered = Math.max(0, absoluteMasteredUnits);
|
|
35843
|
+
effectiveDelta = totalMastered - historicalMasteredUnits;
|
|
35844
|
+
} else {
|
|
35845
|
+
effectiveDelta = additionalMasteredUnits;
|
|
35846
|
+
totalMastered = Math.max(0, historicalMasteredUnits + effectiveDelta);
|
|
35847
|
+
}
|
|
35544
35848
|
const rawPct = totalMastered / masterableUnits * 100;
|
|
35545
35849
|
const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
|
|
35546
35850
|
return {
|
|
@@ -35548,7 +35852,8 @@ class MasteryTracker {
|
|
|
35548
35852
|
masterableUnits,
|
|
35549
35853
|
pctCompleteApp,
|
|
35550
35854
|
isComplete: totalMastered >= masterableUnits,
|
|
35551
|
-
historicalMasteredUnits
|
|
35855
|
+
historicalMasteredUnits,
|
|
35856
|
+
effectiveDelta
|
|
35552
35857
|
};
|
|
35553
35858
|
}
|
|
35554
35859
|
async createCompletionEntry(studentId, courseId, classId, appName) {
|
|
@@ -35777,7 +36082,7 @@ class ProgressRecorder {
|
|
|
35777
36082
|
validateProgressData(progressData);
|
|
35778
36083
|
const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
|
|
35779
36084
|
const { id: studentId, email: studentEmail } = student;
|
|
35780
|
-
const { score, totalQuestions, correctQuestions, xpEarned,
|
|
36085
|
+
const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
|
|
35781
36086
|
const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
|
|
35782
36087
|
const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
|
|
35783
36088
|
const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
|
|
@@ -35786,8 +36091,10 @@ class ProgressRecorder {
|
|
|
35786
36091
|
studentId,
|
|
35787
36092
|
courseId,
|
|
35788
36093
|
resourceId: ids.resource,
|
|
35789
|
-
masteredUnits: progressData.masteredUnits ?? 0
|
|
36094
|
+
masteredUnits: progressData.masteredUnits ?? 0,
|
|
36095
|
+
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
35790
36096
|
});
|
|
36097
|
+
const effectiveMasteredUnits = masteryProgress ? masteryProgress.effectiveDelta : progressData.masteredUnits ?? 0;
|
|
35791
36098
|
let pctCompleteApp;
|
|
35792
36099
|
let masteryAchieved = false;
|
|
35793
36100
|
let scoreStatus = SCORE_STATUS5.fullyGraded;
|
|
@@ -35815,7 +36122,7 @@ class ProgressRecorder {
|
|
|
35815
36122
|
appName: progressData.appName,
|
|
35816
36123
|
totalQuestions,
|
|
35817
36124
|
correctQuestions,
|
|
35818
|
-
masteredUnits,
|
|
36125
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
35819
36126
|
pctCompleteApp
|
|
35820
36127
|
});
|
|
35821
36128
|
} else {
|
|
@@ -35853,7 +36160,7 @@ class ProgressRecorder {
|
|
|
35853
36160
|
totalQuestions,
|
|
35854
36161
|
correctQuestions,
|
|
35855
36162
|
xpEarned: calculatedXp,
|
|
35856
|
-
masteredUnits,
|
|
36163
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
35857
36164
|
attemptNumber: currentAttemptNumber,
|
|
35858
36165
|
progressData,
|
|
35859
36166
|
extensions,
|
|
@@ -35862,7 +36169,7 @@ class ProgressRecorder {
|
|
|
35862
36169
|
return {
|
|
35863
36170
|
xpAwarded: calculatedXp,
|
|
35864
36171
|
attemptNumber: currentAttemptNumber,
|
|
35865
|
-
masteredUnitsApplied:
|
|
36172
|
+
masteredUnitsApplied: effectiveMasteredUnits,
|
|
35866
36173
|
pctCompleteApp,
|
|
35867
36174
|
scoreStatus,
|
|
35868
36175
|
inProgress
|
|
@@ -36397,6 +36704,65 @@ class TimebackClient {
|
|
|
36397
36704
|
resourceId: ids.resource
|
|
36398
36705
|
});
|
|
36399
36706
|
}
|
|
36707
|
+
async getStudentMastery(studentId, options) {
|
|
36708
|
+
await this._ensureAuthenticated();
|
|
36709
|
+
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
36710
|
+
const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
|
|
36711
|
+
if (filteredEnrollments.length === 0) {
|
|
36712
|
+
return {
|
|
36713
|
+
totalMasteredUnits: 0,
|
|
36714
|
+
totalMasterableUnits: 0,
|
|
36715
|
+
...options?.include?.perCourse && { courses: [] }
|
|
36716
|
+
};
|
|
36717
|
+
}
|
|
36718
|
+
const masteryResults = await Promise.all(filteredEnrollments.map(async (enrollment) => {
|
|
36719
|
+
try {
|
|
36720
|
+
const ids = deriveSourcedIds2(enrollment.course.id);
|
|
36721
|
+
const status = await this.masteryTracker.getStatus({
|
|
36722
|
+
studentId,
|
|
36723
|
+
courseId: enrollment.course.id,
|
|
36724
|
+
resourceId: ids.resource
|
|
36725
|
+
});
|
|
36726
|
+
return { enrollment, status };
|
|
36727
|
+
} catch (error) {
|
|
36728
|
+
log.warn("[TimebackClient] Failed to fetch mastery for enrollment", {
|
|
36729
|
+
enrollmentId: enrollment.id,
|
|
36730
|
+
error
|
|
36731
|
+
});
|
|
36732
|
+
return { enrollment, status: undefined };
|
|
36733
|
+
}
|
|
36734
|
+
}));
|
|
36735
|
+
let totalMasteredUnits = 0;
|
|
36736
|
+
let totalMasterableUnits = 0;
|
|
36737
|
+
const courses = [];
|
|
36738
|
+
for (const { enrollment, status } of masteryResults) {
|
|
36739
|
+
const masteredUnits = status?.masteredUnits ?? 0;
|
|
36740
|
+
const masterableUnits = status?.masterableUnits ?? 0;
|
|
36741
|
+
totalMasteredUnits += masteredUnits;
|
|
36742
|
+
totalMasterableUnits += masterableUnits;
|
|
36743
|
+
if (options?.include?.perCourse) {
|
|
36744
|
+
const gradeStr = enrollment.course.grades?.[0];
|
|
36745
|
+
const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
|
|
36746
|
+
const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
|
|
36747
|
+
const subjectStr = enrollment.course.subjects?.[0];
|
|
36748
|
+
const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
|
|
36749
|
+
courses.push({
|
|
36750
|
+
grade,
|
|
36751
|
+
subject,
|
|
36752
|
+
title: enrollment.course.title,
|
|
36753
|
+
masteredUnits,
|
|
36754
|
+
masterableUnits,
|
|
36755
|
+
pctComplete: status?.pctCompleteApp ?? 0,
|
|
36756
|
+
isComplete: status?.isComplete ?? false
|
|
36757
|
+
});
|
|
36758
|
+
}
|
|
36759
|
+
}
|
|
36760
|
+
return {
|
|
36761
|
+
totalMasteredUnits,
|
|
36762
|
+
totalMasterableUnits,
|
|
36763
|
+
...options?.include?.perCourse && { courses }
|
|
36764
|
+
};
|
|
36765
|
+
}
|
|
36400
36766
|
async getStudentXp(studentId, options) {
|
|
36401
36767
|
await this._ensureAuthenticated();
|
|
36402
36768
|
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
@@ -93719,7 +94085,7 @@ var init_session_controller = __esm(() => {
|
|
|
93719
94085
|
});
|
|
93720
94086
|
|
|
93721
94087
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
93722
|
-
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, 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;
|
|
94088
|
+
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, 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;
|
|
93723
94089
|
var init_timeback_controller = __esm(() => {
|
|
93724
94090
|
init_esm();
|
|
93725
94091
|
init_schemas_index();
|
|
@@ -93877,6 +94243,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
93877
94243
|
sessionTimingData,
|
|
93878
94244
|
xpEarned,
|
|
93879
94245
|
masteredUnits,
|
|
94246
|
+
masteredUnitsAbsolute,
|
|
93880
94247
|
extensions
|
|
93881
94248
|
} = body2;
|
|
93882
94249
|
logger45.debug("Ending activity", { userId: ctx.user.id, gameId });
|
|
@@ -93891,6 +94258,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
93891
94258
|
sessionTimingData,
|
|
93892
94259
|
xpEarned,
|
|
93893
94260
|
masteredUnits,
|
|
94261
|
+
masteredUnitsAbsolute,
|
|
93894
94262
|
extensions,
|
|
93895
94263
|
user: ctx.user
|
|
93896
94264
|
});
|
|
@@ -94000,6 +94368,53 @@ var init_timeback_controller = __esm(() => {
|
|
|
94000
94368
|
include
|
|
94001
94369
|
});
|
|
94002
94370
|
});
|
|
94371
|
+
getStudentMastery = requireDeveloper(async (ctx) => {
|
|
94372
|
+
const timebackId = ctx.params.timebackId;
|
|
94373
|
+
if (!timebackId) {
|
|
94374
|
+
throw ApiError.badRequest("Missing timebackId parameter");
|
|
94375
|
+
}
|
|
94376
|
+
const gameId = ctx.url.searchParams.get("gameId");
|
|
94377
|
+
if (!gameId) {
|
|
94378
|
+
throw ApiError.badRequest("Missing required gameId query parameter");
|
|
94379
|
+
}
|
|
94380
|
+
const gradeParam = ctx.url.searchParams.get("grade");
|
|
94381
|
+
const subjectParam = ctx.url.searchParams.get("subject");
|
|
94382
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
94383
|
+
throw ApiError.badRequest("Both grade and subject must be provided together");
|
|
94384
|
+
}
|
|
94385
|
+
let grade;
|
|
94386
|
+
let subject;
|
|
94387
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
94388
|
+
const parsedGrade = parseInt(gradeParam, 10);
|
|
94389
|
+
if (!isTimebackGrade2(parsedGrade)) {
|
|
94390
|
+
throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
94391
|
+
}
|
|
94392
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
94393
|
+
throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
94394
|
+
}
|
|
94395
|
+
grade = parsedGrade;
|
|
94396
|
+
subject = subjectParam;
|
|
94397
|
+
}
|
|
94398
|
+
const includeParam = ctx.url.searchParams.get("include");
|
|
94399
|
+
const includeOptions = includeParam ? includeParam.split(",").map((opt) => opt.trim().toLowerCase()) : [];
|
|
94400
|
+
const include = {
|
|
94401
|
+
perCourse: includeOptions.includes("percourse")
|
|
94402
|
+
};
|
|
94403
|
+
logger45.debug("Getting student mastery", {
|
|
94404
|
+
requesterId: ctx.user.id,
|
|
94405
|
+
timebackId,
|
|
94406
|
+
gameId,
|
|
94407
|
+
grade,
|
|
94408
|
+
subject,
|
|
94409
|
+
include
|
|
94410
|
+
});
|
|
94411
|
+
return ctx.services.timeback.getStudentMastery(timebackId, ctx.user, {
|
|
94412
|
+
gameId,
|
|
94413
|
+
grade,
|
|
94414
|
+
subject,
|
|
94415
|
+
include
|
|
94416
|
+
});
|
|
94417
|
+
});
|
|
94003
94418
|
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
94004
94419
|
const gameId = ctx.params.gameId;
|
|
94005
94420
|
const courseId = ctx.params.courseId;
|
|
@@ -94035,15 +94450,19 @@ var init_timeback_controller = __esm(() => {
|
|
|
94035
94450
|
getGameMetrics = requireGameManagementAccess(async (ctx) => {
|
|
94036
94451
|
const gameId = ctx.params.gameId;
|
|
94037
94452
|
const timebackId = ctx.params.timebackId;
|
|
94453
|
+
const runIds = [
|
|
94454
|
+
...new Set(ctx.url.searchParams.getAll("runId").map((runId) => runId.trim().toLowerCase()).filter(isValidUUID))
|
|
94455
|
+
];
|
|
94038
94456
|
if (!gameId || !timebackId) {
|
|
94039
94457
|
throw ApiError.badRequest("Missing gameId or timebackId path parameter");
|
|
94040
94458
|
}
|
|
94041
94459
|
logger45.debug("Getting game metrics", {
|
|
94042
94460
|
requesterId: ctx.user.id,
|
|
94043
94461
|
gameId,
|
|
94044
|
-
timebackId
|
|
94462
|
+
timebackId,
|
|
94463
|
+
runIds
|
|
94045
94464
|
});
|
|
94046
|
-
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user);
|
|
94465
|
+
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user, { runIds });
|
|
94047
94466
|
});
|
|
94048
94467
|
getStudentActivity = requireGameManagementAccess(async (ctx) => {
|
|
94049
94468
|
const timebackId = ctx.params.timebackId;
|
|
@@ -94332,6 +94751,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
94332
94751
|
heartbeat,
|
|
94333
94752
|
advanceCourse,
|
|
94334
94753
|
getStudentXp,
|
|
94754
|
+
getStudentMastery,
|
|
94335
94755
|
getRoster,
|
|
94336
94756
|
getStudentOverview,
|
|
94337
94757
|
getGameMetrics,
|
|
@@ -95218,6 +95638,7 @@ var init_timeback6 = __esm(() => {
|
|
|
95218
95638
|
init_controllers();
|
|
95219
95639
|
init_errors();
|
|
95220
95640
|
init_utils11();
|
|
95641
|
+
init_schemas_index();
|
|
95221
95642
|
init_api();
|
|
95222
95643
|
init_error_handler();
|
|
95223
95644
|
init_timeback5();
|
|
@@ -95297,6 +95718,14 @@ var init_timeback6 = __esm(() => {
|
|
|
95297
95718
|
}
|
|
95298
95719
|
if (gradeParam !== null && subjectParam !== null) {
|
|
95299
95720
|
const grade = parseInt(gradeParam, 10);
|
|
95721
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
95722
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
95723
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95724
|
+
}
|
|
95725
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
95726
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
95727
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95728
|
+
}
|
|
95300
95729
|
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
95301
95730
|
}
|
|
95302
95731
|
const mockCourses = enrollments.map((e) => {
|
|
@@ -95321,6 +95750,62 @@ var init_timeback6 = __esm(() => {
|
|
|
95321
95750
|
}
|
|
95322
95751
|
return handle2(timeback2.getStudentXp)(c2);
|
|
95323
95752
|
});
|
|
95753
|
+
timebackRouter.get("/student-mastery/:timebackId", async (c2) => {
|
|
95754
|
+
const user = c2.get("user");
|
|
95755
|
+
if (!user) {
|
|
95756
|
+
const error2 = ApiError.unauthorized("Must be logged in to get student mastery");
|
|
95757
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95758
|
+
}
|
|
95759
|
+
if (shouldMockTimeback()) {
|
|
95760
|
+
const url2 = new URL(c2.req.url);
|
|
95761
|
+
const gradeParam = url2.searchParams.get("grade");
|
|
95762
|
+
const subjectParam = url2.searchParams.get("subject");
|
|
95763
|
+
const includeParam = url2.searchParams.get("include") || "";
|
|
95764
|
+
const includeOptions = includeParam.split(",").map((opt) => opt.trim().toLowerCase());
|
|
95765
|
+
const includePerCourse = includeOptions.includes("percourse");
|
|
95766
|
+
const db2 = c2.get("db");
|
|
95767
|
+
let enrollments = await getMockEnrollments(db2);
|
|
95768
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
95769
|
+
const error2 = ApiError.badRequest("Both grade and subject must be provided together");
|
|
95770
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95771
|
+
}
|
|
95772
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
95773
|
+
const grade = parseInt(gradeParam, 10);
|
|
95774
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
95775
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
95776
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95777
|
+
}
|
|
95778
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
95779
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
95780
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95781
|
+
}
|
|
95782
|
+
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
95783
|
+
}
|
|
95784
|
+
const mockCourses = enrollments.map((e) => {
|
|
95785
|
+
const seed3 = hashCode(`${e.grade}-${e.subject}`);
|
|
95786
|
+
const masterableUnits = 5 + seed3 % 16;
|
|
95787
|
+
const masteredUnits = seed3 % (masterableUnits + 1);
|
|
95788
|
+
const pctComplete = masterableUnits > 0 ? Math.round(masteredUnits / masterableUnits * 1e4) / 100 : 0;
|
|
95789
|
+
return {
|
|
95790
|
+
grade: e.grade,
|
|
95791
|
+
subject: e.subject,
|
|
95792
|
+
title: `${e.subject} ${formatGradeLabel(e.grade)}`,
|
|
95793
|
+
masteredUnits,
|
|
95794
|
+
masterableUnits,
|
|
95795
|
+
pctComplete,
|
|
95796
|
+
isComplete: masteredUnits >= masterableUnits
|
|
95797
|
+
};
|
|
95798
|
+
});
|
|
95799
|
+
const totalMasteredUnits = mockCourses.reduce((sum2, course) => sum2 + course.masteredUnits, 0);
|
|
95800
|
+
const totalMasterableUnits = mockCourses.reduce((sum2, course) => sum2 + course.masterableUnits, 0);
|
|
95801
|
+
return c2.json({
|
|
95802
|
+
totalMasteredUnits,
|
|
95803
|
+
totalMasterableUnits,
|
|
95804
|
+
...includePerCourse && { courses: mockCourses }
|
|
95805
|
+
});
|
|
95806
|
+
}
|
|
95807
|
+
return handle2(timeback2.getStudentMastery)(c2);
|
|
95808
|
+
});
|
|
95324
95809
|
});
|
|
95325
95810
|
|
|
95326
95811
|
// src/routes/integrations/lti.ts
|