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