@playcademy/sandbox 0.4.0 → 0.4.1-beta.1

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 CHANGED
@@ -248,6 +248,7 @@ 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
  };
@@ -1064,7 +1065,7 @@ var package_default;
1064
1065
  var init_package = __esm(() => {
1065
1066
  package_default = {
1066
1067
  name: "@playcademy/sandbox",
1067
- version: "0.4.0",
1068
+ version: "0.4.1-beta.1",
1068
1069
  description: "Local development server for Playcademy game development",
1069
1070
  type: "module",
1070
1071
  exports: {
@@ -28234,6 +28235,7 @@ var init_constants3 = __esm(() => {
28234
28235
  TIMEBACK: {
28235
28236
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
28236
28237
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
28238
+ GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
28237
28239
  HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
28238
28240
  ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
28239
28241
  }
@@ -29958,7 +29960,11 @@ var init_schemas4 = __esm(() => {
29958
29960
  }).optional(),
29959
29961
  xpEarned: exports_external.number().optional(),
29960
29962
  masteredUnits: exports_external.number().optional(),
29963
+ masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
29961
29964
  extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
29965
+ }).refine((data) => !(data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined), {
29966
+ message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
29967
+ path: ["masteredUnitsAbsolute"]
29962
29968
  });
29963
29969
  GameActivityMetricsSchema = exports_external.object({
29964
29970
  activityId: exports_external.string().min(1),
@@ -30329,7 +30335,7 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
30329
30335
  subject: integration.subject,
30330
30336
  courseId: integration.courseId,
30331
30337
  orgId: courseToSchool.get(integration.courseId),
30332
- ...enrollment ? { enrollmentIds: { active: enrollment.sourcedId } } : {}
30338
+ ...enrollment ? { id: enrollment.sourcedId } : {}
30333
30339
  };
30334
30340
  });
30335
30341
  }
@@ -32886,6 +32892,7 @@ var init_timeback_service = __esm(() => {
32886
32892
  sessionTimingData,
32887
32893
  xpEarned,
32888
32894
  masteredUnits,
32895
+ masteredUnitsAbsolute,
32889
32896
  extensions,
32890
32897
  user
32891
32898
  }) {
@@ -32909,6 +32916,7 @@ var init_timeback_service = __esm(() => {
32909
32916
  durationSeconds: timingData.durationSeconds,
32910
32917
  xpEarned,
32911
32918
  masteredUnits,
32919
+ masteredUnitsAbsolute,
32912
32920
  extensions: extensionsWithResumeId,
32913
32921
  activityId: activityData.activityId,
32914
32922
  activityName: activityData.activityName,
@@ -33157,6 +33165,46 @@ var init_timeback_service = __esm(() => {
33157
33165
  });
33158
33166
  return result;
33159
33167
  }
33168
+ async getStudentMastery(timebackId, user, options) {
33169
+ const client = this.requireClient();
33170
+ const db2 = this.deps.db;
33171
+ await this.deps.validateDeveloperAccess(user, options.gameId);
33172
+ const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
33173
+ if (options.grade !== undefined && options.subject) {
33174
+ conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
33175
+ conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
33176
+ }
33177
+ const integrations = await db2.query.gameTimebackIntegrations.findMany({
33178
+ where: and(...conditions2)
33179
+ });
33180
+ const courseIds = integrations.map((i2) => i2.courseId);
33181
+ if (courseIds.length === 0) {
33182
+ logger20.debug("No integrations found for game, returning empty mastery", {
33183
+ timebackId,
33184
+ gameId: options.gameId,
33185
+ grade: options.grade,
33186
+ subject: options.subject
33187
+ });
33188
+ return {
33189
+ totalMasteredUnits: 0,
33190
+ totalMasterableUnits: 0,
33191
+ ...options.include?.perCourse && { courses: [] }
33192
+ };
33193
+ }
33194
+ const result = await client.getStudentMastery(timebackId, {
33195
+ courseIds,
33196
+ include: options.include
33197
+ });
33198
+ logger20.debug("Retrieved student mastery", {
33199
+ timebackId,
33200
+ gameId: options.gameId,
33201
+ grade: options.grade,
33202
+ subject: options.subject,
33203
+ totalMasteredUnits: result.totalMasteredUnits,
33204
+ courseCount: result.courses?.length
33205
+ });
33206
+ return result;
33207
+ }
33160
33208
  };
33161
33209
  });
33162
33210
 
@@ -35482,15 +35530,18 @@ class MasteryTracker {
35482
35530
  this.edubridgeNamespace = edubridgeNamespace;
35483
35531
  }
35484
35532
  async checkProgress(input) {
35485
- const { studentId, courseId, resourceId, masteredUnits } = input;
35486
- if (typeof masteredUnits !== "number" || masteredUnits === 0) {
35533
+ const { studentId, courseId, resourceId, masteredUnits, masteredUnitsAbsolute } = input;
35534
+ const hasIncremental = typeof masteredUnits === "number" && masteredUnits !== 0;
35535
+ const hasAbsolute = typeof masteredUnitsAbsolute === "number";
35536
+ if (!hasIncremental && !hasAbsolute) {
35487
35537
  return;
35488
35538
  }
35489
35539
  const status = await this.calculateStatus({
35490
35540
  studentId,
35491
35541
  courseId,
35492
35542
  resourceId,
35493
- additionalMasteredUnits: masteredUnits
35543
+ additionalMasteredUnits: hasAbsolute ? 0 : masteredUnits,
35544
+ absoluteMasteredUnits: hasAbsolute ? masteredUnitsAbsolute : undefined
35494
35545
  });
35495
35546
  if (!status) {
35496
35547
  return;
@@ -35499,7 +35550,8 @@ class MasteryTracker {
35499
35550
  return {
35500
35551
  pctCompleteApp: status.pctCompleteApp,
35501
35552
  masteryAchieved: !wasComplete && status.isComplete,
35502
- masteryRevoked: wasComplete && !status.isComplete
35553
+ masteryRevoked: wasComplete && !status.isComplete,
35554
+ effectiveDelta: status.effectiveDelta
35503
35555
  };
35504
35556
  }
35505
35557
  async getStatus(input) {
@@ -35521,7 +35573,8 @@ class MasteryTracker {
35521
35573
  studentId,
35522
35574
  courseId,
35523
35575
  resourceId,
35524
- additionalMasteredUnits
35576
+ additionalMasteredUnits,
35577
+ absoluteMasteredUnits
35525
35578
  }) {
35526
35579
  const masterableUnits = await this.resolveMasterableUnits(resourceId);
35527
35580
  if (!masterableUnits || masterableUnits <= 0) {
@@ -35540,7 +35593,15 @@ class MasteryTracker {
35540
35593
  return;
35541
35594
  }
35542
35595
  const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
35543
- const totalMastered = Math.max(0, historicalMasteredUnits + additionalMasteredUnits);
35596
+ let totalMastered;
35597
+ let effectiveDelta;
35598
+ if (absoluteMasteredUnits !== undefined) {
35599
+ totalMastered = Math.max(0, absoluteMasteredUnits);
35600
+ effectiveDelta = totalMastered - historicalMasteredUnits;
35601
+ } else {
35602
+ effectiveDelta = additionalMasteredUnits;
35603
+ totalMastered = Math.max(0, historicalMasteredUnits + effectiveDelta);
35604
+ }
35544
35605
  const rawPct = totalMastered / masterableUnits * 100;
35545
35606
  const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
35546
35607
  return {
@@ -35548,7 +35609,8 @@ class MasteryTracker {
35548
35609
  masterableUnits,
35549
35610
  pctCompleteApp,
35550
35611
  isComplete: totalMastered >= masterableUnits,
35551
- historicalMasteredUnits
35612
+ historicalMasteredUnits,
35613
+ effectiveDelta
35552
35614
  };
35553
35615
  }
35554
35616
  async createCompletionEntry(studentId, courseId, classId, appName) {
@@ -35777,7 +35839,7 @@ class ProgressRecorder {
35777
35839
  validateProgressData(progressData);
35778
35840
  const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
35779
35841
  const { id: studentId, email: studentEmail } = student;
35780
- const { score, totalQuestions, correctQuestions, xpEarned, masteredUnits, attemptNumber } = progressData;
35842
+ const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
35781
35843
  const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
35782
35844
  const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
35783
35845
  const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
@@ -35786,8 +35848,10 @@ class ProgressRecorder {
35786
35848
  studentId,
35787
35849
  courseId,
35788
35850
  resourceId: ids.resource,
35789
- masteredUnits: progressData.masteredUnits ?? 0
35851
+ masteredUnits: progressData.masteredUnits ?? 0,
35852
+ masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
35790
35853
  });
35854
+ const effectiveMasteredUnits = masteryProgress ? masteryProgress.effectiveDelta : progressData.masteredUnits ?? 0;
35791
35855
  let pctCompleteApp;
35792
35856
  let masteryAchieved = false;
35793
35857
  let scoreStatus = SCORE_STATUS5.fullyGraded;
@@ -35815,7 +35879,7 @@ class ProgressRecorder {
35815
35879
  appName: progressData.appName,
35816
35880
  totalQuestions,
35817
35881
  correctQuestions,
35818
- masteredUnits,
35882
+ masteredUnits: effectiveMasteredUnits || undefined,
35819
35883
  pctCompleteApp
35820
35884
  });
35821
35885
  } else {
@@ -35853,7 +35917,7 @@ class ProgressRecorder {
35853
35917
  totalQuestions,
35854
35918
  correctQuestions,
35855
35919
  xpEarned: calculatedXp,
35856
- masteredUnits,
35920
+ masteredUnits: effectiveMasteredUnits || undefined,
35857
35921
  attemptNumber: currentAttemptNumber,
35858
35922
  progressData,
35859
35923
  extensions,
@@ -35862,7 +35926,7 @@ class ProgressRecorder {
35862
35926
  return {
35863
35927
  xpAwarded: calculatedXp,
35864
35928
  attemptNumber: currentAttemptNumber,
35865
- masteredUnitsApplied: progressData.masteredUnits ?? 0,
35929
+ masteredUnitsApplied: effectiveMasteredUnits,
35866
35930
  pctCompleteApp,
35867
35931
  scoreStatus,
35868
35932
  inProgress
@@ -36397,6 +36461,65 @@ class TimebackClient {
36397
36461
  resourceId: ids.resource
36398
36462
  });
36399
36463
  }
36464
+ async getStudentMastery(studentId, options) {
36465
+ await this._ensureAuthenticated();
36466
+ const enrollments = await this.edubridge.enrollments.listByUser(studentId);
36467
+ const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
36468
+ if (filteredEnrollments.length === 0) {
36469
+ return {
36470
+ totalMasteredUnits: 0,
36471
+ totalMasterableUnits: 0,
36472
+ ...options?.include?.perCourse && { courses: [] }
36473
+ };
36474
+ }
36475
+ const masteryResults = await Promise.all(filteredEnrollments.map(async (enrollment) => {
36476
+ try {
36477
+ const ids = deriveSourcedIds2(enrollment.course.id);
36478
+ const status = await this.masteryTracker.getStatus({
36479
+ studentId,
36480
+ courseId: enrollment.course.id,
36481
+ resourceId: ids.resource
36482
+ });
36483
+ return { enrollment, status };
36484
+ } catch (error) {
36485
+ log.warn("[TimebackClient] Failed to fetch mastery for enrollment", {
36486
+ enrollmentId: enrollment.id,
36487
+ error
36488
+ });
36489
+ return { enrollment, status: undefined };
36490
+ }
36491
+ }));
36492
+ let totalMasteredUnits = 0;
36493
+ let totalMasterableUnits = 0;
36494
+ const courses = [];
36495
+ for (const { enrollment, status } of masteryResults) {
36496
+ const masteredUnits = status?.masteredUnits ?? 0;
36497
+ const masterableUnits = status?.masterableUnits ?? 0;
36498
+ totalMasteredUnits += masteredUnits;
36499
+ totalMasterableUnits += masterableUnits;
36500
+ if (options?.include?.perCourse) {
36501
+ const gradeStr = enrollment.course.grades?.[0];
36502
+ const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
36503
+ const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
36504
+ const subjectStr = enrollment.course.subjects?.[0];
36505
+ const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
36506
+ courses.push({
36507
+ grade,
36508
+ subject,
36509
+ title: enrollment.course.title,
36510
+ masteredUnits,
36511
+ masterableUnits,
36512
+ pctComplete: status?.pctCompleteApp ?? 0,
36513
+ isComplete: status?.isComplete ?? false
36514
+ });
36515
+ }
36516
+ }
36517
+ return {
36518
+ totalMasteredUnits,
36519
+ totalMasterableUnits,
36520
+ ...options?.include?.perCourse && { courses }
36521
+ };
36522
+ }
36400
36523
  async getStudentXp(studentId, options) {
36401
36524
  await this._ensureAuthenticated();
36402
36525
  const enrollments = await this.edubridge.enrollments.listByUser(studentId);
@@ -93719,7 +93842,7 @@ var init_session_controller = __esm(() => {
93719
93842
  });
93720
93843
 
93721
93844
  // ../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;
93845
+ 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
93846
  var init_timeback_controller = __esm(() => {
93724
93847
  init_esm();
93725
93848
  init_schemas_index();
@@ -93877,6 +94000,7 @@ var init_timeback_controller = __esm(() => {
93877
94000
  sessionTimingData,
93878
94001
  xpEarned,
93879
94002
  masteredUnits,
94003
+ masteredUnitsAbsolute,
93880
94004
  extensions
93881
94005
  } = body2;
93882
94006
  logger45.debug("Ending activity", { userId: ctx.user.id, gameId });
@@ -93891,6 +94015,7 @@ var init_timeback_controller = __esm(() => {
93891
94015
  sessionTimingData,
93892
94016
  xpEarned,
93893
94017
  masteredUnits,
94018
+ masteredUnitsAbsolute,
93894
94019
  extensions,
93895
94020
  user: ctx.user
93896
94021
  });
@@ -94000,6 +94125,53 @@ var init_timeback_controller = __esm(() => {
94000
94125
  include
94001
94126
  });
94002
94127
  });
94128
+ getStudentMastery = requireDeveloper(async (ctx) => {
94129
+ const timebackId = ctx.params.timebackId;
94130
+ if (!timebackId) {
94131
+ throw ApiError.badRequest("Missing timebackId parameter");
94132
+ }
94133
+ const gameId = ctx.url.searchParams.get("gameId");
94134
+ if (!gameId) {
94135
+ throw ApiError.badRequest("Missing required gameId query parameter");
94136
+ }
94137
+ const gradeParam = ctx.url.searchParams.get("grade");
94138
+ const subjectParam = ctx.url.searchParams.get("subject");
94139
+ if (gradeParam !== null !== (subjectParam !== null)) {
94140
+ throw ApiError.badRequest("Both grade and subject must be provided together");
94141
+ }
94142
+ let grade;
94143
+ let subject;
94144
+ if (gradeParam !== null && subjectParam !== null) {
94145
+ const parsedGrade = parseInt(gradeParam, 10);
94146
+ if (!isTimebackGrade2(parsedGrade)) {
94147
+ throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
94148
+ }
94149
+ if (!isTimebackSubject2(subjectParam)) {
94150
+ throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
94151
+ }
94152
+ grade = parsedGrade;
94153
+ subject = subjectParam;
94154
+ }
94155
+ const includeParam = ctx.url.searchParams.get("include");
94156
+ const includeOptions = includeParam ? includeParam.split(",").map((opt) => opt.trim().toLowerCase()) : [];
94157
+ const include = {
94158
+ perCourse: includeOptions.includes("percourse")
94159
+ };
94160
+ logger45.debug("Getting student mastery", {
94161
+ requesterId: ctx.user.id,
94162
+ timebackId,
94163
+ gameId,
94164
+ grade,
94165
+ subject,
94166
+ include
94167
+ });
94168
+ return ctx.services.timeback.getStudentMastery(timebackId, ctx.user, {
94169
+ gameId,
94170
+ grade,
94171
+ subject,
94172
+ include
94173
+ });
94174
+ });
94003
94175
  getRoster = requireGameManagementAccess(async (ctx) => {
94004
94176
  const gameId = ctx.params.gameId;
94005
94177
  const courseId = ctx.params.courseId;
@@ -94332,6 +94504,7 @@ var init_timeback_controller = __esm(() => {
94332
94504
  heartbeat,
94333
94505
  advanceCourse,
94334
94506
  getStudentXp,
94507
+ getStudentMastery,
94335
94508
  getRoster,
94336
94509
  getStudentOverview,
94337
94510
  getGameMetrics,
@@ -95218,6 +95391,7 @@ var init_timeback6 = __esm(() => {
95218
95391
  init_controllers();
95219
95392
  init_errors();
95220
95393
  init_utils11();
95394
+ init_schemas_index();
95221
95395
  init_api();
95222
95396
  init_error_handler();
95223
95397
  init_timeback5();
@@ -95297,6 +95471,14 @@ var init_timeback6 = __esm(() => {
95297
95471
  }
95298
95472
  if (gradeParam !== null && subjectParam !== null) {
95299
95473
  const grade = parseInt(gradeParam, 10);
95474
+ if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
95475
+ const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
95476
+ return c2.json(createErrorResponse(error2), error2.status);
95477
+ }
95478
+ if (!isTimebackSubject2(subjectParam)) {
95479
+ const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
95480
+ return c2.json(createErrorResponse(error2), error2.status);
95481
+ }
95300
95482
  enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
95301
95483
  }
95302
95484
  const mockCourses = enrollments.map((e) => {
@@ -95321,6 +95503,62 @@ var init_timeback6 = __esm(() => {
95321
95503
  }
95322
95504
  return handle2(timeback2.getStudentXp)(c2);
95323
95505
  });
95506
+ timebackRouter.get("/student-mastery/:timebackId", async (c2) => {
95507
+ const user = c2.get("user");
95508
+ if (!user) {
95509
+ const error2 = ApiError.unauthorized("Must be logged in to get student mastery");
95510
+ return c2.json(createErrorResponse(error2), error2.status);
95511
+ }
95512
+ if (shouldMockTimeback()) {
95513
+ const url2 = new URL(c2.req.url);
95514
+ const gradeParam = url2.searchParams.get("grade");
95515
+ const subjectParam = url2.searchParams.get("subject");
95516
+ const includeParam = url2.searchParams.get("include") || "";
95517
+ const includeOptions = includeParam.split(",").map((opt) => opt.trim().toLowerCase());
95518
+ const includePerCourse = includeOptions.includes("percourse");
95519
+ const db2 = c2.get("db");
95520
+ let enrollments = await getMockEnrollments(db2);
95521
+ if (gradeParam !== null !== (subjectParam !== null)) {
95522
+ const error2 = ApiError.badRequest("Both grade and subject must be provided together");
95523
+ return c2.json(createErrorResponse(error2), error2.status);
95524
+ }
95525
+ if (gradeParam !== null && subjectParam !== null) {
95526
+ const grade = parseInt(gradeParam, 10);
95527
+ if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
95528
+ const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
95529
+ return c2.json(createErrorResponse(error2), error2.status);
95530
+ }
95531
+ if (!isTimebackSubject2(subjectParam)) {
95532
+ const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
95533
+ return c2.json(createErrorResponse(error2), error2.status);
95534
+ }
95535
+ enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
95536
+ }
95537
+ const mockCourses = enrollments.map((e) => {
95538
+ const seed3 = hashCode(`${e.grade}-${e.subject}`);
95539
+ const masterableUnits = 5 + seed3 % 16;
95540
+ const masteredUnits = seed3 % (masterableUnits + 1);
95541
+ const pctComplete = masterableUnits > 0 ? Math.round(masteredUnits / masterableUnits * 1e4) / 100 : 0;
95542
+ return {
95543
+ grade: e.grade,
95544
+ subject: e.subject,
95545
+ title: `${e.subject} ${formatGradeLabel(e.grade)}`,
95546
+ masteredUnits,
95547
+ masterableUnits,
95548
+ pctComplete,
95549
+ isComplete: masteredUnits >= masterableUnits
95550
+ };
95551
+ });
95552
+ const totalMasteredUnits = mockCourses.reduce((sum2, course) => sum2 + course.masteredUnits, 0);
95553
+ const totalMasterableUnits = mockCourses.reduce((sum2, course) => sum2 + course.masterableUnits, 0);
95554
+ return c2.json({
95555
+ totalMasteredUnits,
95556
+ totalMasterableUnits,
95557
+ ...includePerCourse && { courses: mockCourses }
95558
+ });
95559
+ }
95560
+ return handle2(timeback2.getStudentMastery)(c2);
95561
+ });
95324
95562
  });
95325
95563
 
95326
95564
  // src/routes/integrations/lti.ts
package/dist/constants.js CHANGED
@@ -83,6 +83,7 @@ var init_timeback = __esm(() => {
83
83
  TIMEBACK_ROUTES = {
84
84
  END_ACTIVITY: "/integrations/timeback/end-activity",
85
85
  GET_XP: "/integrations/timeback/xp",
86
+ GET_MASTERY: "/integrations/timeback/mastery",
86
87
  HEARTBEAT: "/integrations/timeback/heartbeat",
87
88
  ADVANCE_COURSE: "/integrations/timeback/advance-course"
88
89
  };
package/dist/server.js CHANGED
@@ -247,6 +247,7 @@ 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
  };
@@ -1063,7 +1064,7 @@ var package_default;
1063
1064
  var init_package = __esm(() => {
1064
1065
  package_default = {
1065
1066
  name: "@playcademy/sandbox",
1066
- version: "0.4.0",
1067
+ version: "0.4.1-beta.1",
1067
1068
  description: "Local development server for Playcademy game development",
1068
1069
  type: "module",
1069
1070
  exports: {
@@ -28233,6 +28234,7 @@ var init_constants3 = __esm(() => {
28233
28234
  TIMEBACK: {
28234
28235
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
28235
28236
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
28237
+ GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
28236
28238
  HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
28237
28239
  ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
28238
28240
  }
@@ -29957,7 +29959,11 @@ var init_schemas4 = __esm(() => {
29957
29959
  }).optional(),
29958
29960
  xpEarned: exports_external.number().optional(),
29959
29961
  masteredUnits: exports_external.number().optional(),
29962
+ masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
29960
29963
  extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
29964
+ }).refine((data) => !(data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined), {
29965
+ message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
29966
+ path: ["masteredUnitsAbsolute"]
29961
29967
  });
29962
29968
  GameActivityMetricsSchema = exports_external.object({
29963
29969
  activityId: exports_external.string().min(1),
@@ -30328,7 +30334,7 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
30328
30334
  subject: integration.subject,
30329
30335
  courseId: integration.courseId,
30330
30336
  orgId: courseToSchool.get(integration.courseId),
30331
- ...enrollment ? { enrollmentIds: { active: enrollment.sourcedId } } : {}
30337
+ ...enrollment ? { id: enrollment.sourcedId } : {}
30332
30338
  };
30333
30339
  });
30334
30340
  }
@@ -32885,6 +32891,7 @@ var init_timeback_service = __esm(() => {
32885
32891
  sessionTimingData,
32886
32892
  xpEarned,
32887
32893
  masteredUnits,
32894
+ masteredUnitsAbsolute,
32888
32895
  extensions,
32889
32896
  user
32890
32897
  }) {
@@ -32908,6 +32915,7 @@ var init_timeback_service = __esm(() => {
32908
32915
  durationSeconds: timingData.durationSeconds,
32909
32916
  xpEarned,
32910
32917
  masteredUnits,
32918
+ masteredUnitsAbsolute,
32911
32919
  extensions: extensionsWithResumeId,
32912
32920
  activityId: activityData.activityId,
32913
32921
  activityName: activityData.activityName,
@@ -33156,6 +33164,46 @@ var init_timeback_service = __esm(() => {
33156
33164
  });
33157
33165
  return result;
33158
33166
  }
33167
+ async getStudentMastery(timebackId, user, options) {
33168
+ const client = this.requireClient();
33169
+ const db2 = this.deps.db;
33170
+ await this.deps.validateDeveloperAccess(user, options.gameId);
33171
+ const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
33172
+ if (options.grade !== undefined && options.subject) {
33173
+ conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
33174
+ conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
33175
+ }
33176
+ const integrations = await db2.query.gameTimebackIntegrations.findMany({
33177
+ where: and(...conditions2)
33178
+ });
33179
+ const courseIds = integrations.map((i2) => i2.courseId);
33180
+ if (courseIds.length === 0) {
33181
+ logger20.debug("No integrations found for game, returning empty mastery", {
33182
+ timebackId,
33183
+ gameId: options.gameId,
33184
+ grade: options.grade,
33185
+ subject: options.subject
33186
+ });
33187
+ return {
33188
+ totalMasteredUnits: 0,
33189
+ totalMasterableUnits: 0,
33190
+ ...options.include?.perCourse && { courses: [] }
33191
+ };
33192
+ }
33193
+ const result = await client.getStudentMastery(timebackId, {
33194
+ courseIds,
33195
+ include: options.include
33196
+ });
33197
+ logger20.debug("Retrieved student mastery", {
33198
+ timebackId,
33199
+ gameId: options.gameId,
33200
+ grade: options.grade,
33201
+ subject: options.subject,
33202
+ totalMasteredUnits: result.totalMasteredUnits,
33203
+ courseCount: result.courses?.length
33204
+ });
33205
+ return result;
33206
+ }
33159
33207
  };
33160
33208
  });
33161
33209
 
@@ -35481,15 +35529,18 @@ class MasteryTracker {
35481
35529
  this.edubridgeNamespace = edubridgeNamespace;
35482
35530
  }
35483
35531
  async checkProgress(input) {
35484
- const { studentId, courseId, resourceId, masteredUnits } = input;
35485
- if (typeof masteredUnits !== "number" || masteredUnits === 0) {
35532
+ const { studentId, courseId, resourceId, masteredUnits, masteredUnitsAbsolute } = input;
35533
+ const hasIncremental = typeof masteredUnits === "number" && masteredUnits !== 0;
35534
+ const hasAbsolute = typeof masteredUnitsAbsolute === "number";
35535
+ if (!hasIncremental && !hasAbsolute) {
35486
35536
  return;
35487
35537
  }
35488
35538
  const status = await this.calculateStatus({
35489
35539
  studentId,
35490
35540
  courseId,
35491
35541
  resourceId,
35492
- additionalMasteredUnits: masteredUnits
35542
+ additionalMasteredUnits: hasAbsolute ? 0 : masteredUnits,
35543
+ absoluteMasteredUnits: hasAbsolute ? masteredUnitsAbsolute : undefined
35493
35544
  });
35494
35545
  if (!status) {
35495
35546
  return;
@@ -35498,7 +35549,8 @@ class MasteryTracker {
35498
35549
  return {
35499
35550
  pctCompleteApp: status.pctCompleteApp,
35500
35551
  masteryAchieved: !wasComplete && status.isComplete,
35501
- masteryRevoked: wasComplete && !status.isComplete
35552
+ masteryRevoked: wasComplete && !status.isComplete,
35553
+ effectiveDelta: status.effectiveDelta
35502
35554
  };
35503
35555
  }
35504
35556
  async getStatus(input) {
@@ -35520,7 +35572,8 @@ class MasteryTracker {
35520
35572
  studentId,
35521
35573
  courseId,
35522
35574
  resourceId,
35523
- additionalMasteredUnits
35575
+ additionalMasteredUnits,
35576
+ absoluteMasteredUnits
35524
35577
  }) {
35525
35578
  const masterableUnits = await this.resolveMasterableUnits(resourceId);
35526
35579
  if (!masterableUnits || masterableUnits <= 0) {
@@ -35539,7 +35592,15 @@ class MasteryTracker {
35539
35592
  return;
35540
35593
  }
35541
35594
  const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
35542
- const totalMastered = Math.max(0, historicalMasteredUnits + additionalMasteredUnits);
35595
+ let totalMastered;
35596
+ let effectiveDelta;
35597
+ if (absoluteMasteredUnits !== undefined) {
35598
+ totalMastered = Math.max(0, absoluteMasteredUnits);
35599
+ effectiveDelta = totalMastered - historicalMasteredUnits;
35600
+ } else {
35601
+ effectiveDelta = additionalMasteredUnits;
35602
+ totalMastered = Math.max(0, historicalMasteredUnits + effectiveDelta);
35603
+ }
35543
35604
  const rawPct = totalMastered / masterableUnits * 100;
35544
35605
  const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
35545
35606
  return {
@@ -35547,7 +35608,8 @@ class MasteryTracker {
35547
35608
  masterableUnits,
35548
35609
  pctCompleteApp,
35549
35610
  isComplete: totalMastered >= masterableUnits,
35550
- historicalMasteredUnits
35611
+ historicalMasteredUnits,
35612
+ effectiveDelta
35551
35613
  };
35552
35614
  }
35553
35615
  async createCompletionEntry(studentId, courseId, classId, appName) {
@@ -35776,7 +35838,7 @@ class ProgressRecorder {
35776
35838
  validateProgressData(progressData);
35777
35839
  const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
35778
35840
  const { id: studentId, email: studentEmail } = student;
35779
- const { score, totalQuestions, correctQuestions, xpEarned, masteredUnits, attemptNumber } = progressData;
35841
+ const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
35780
35842
  const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
35781
35843
  const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
35782
35844
  const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
@@ -35785,8 +35847,10 @@ class ProgressRecorder {
35785
35847
  studentId,
35786
35848
  courseId,
35787
35849
  resourceId: ids.resource,
35788
- masteredUnits: progressData.masteredUnits ?? 0
35850
+ masteredUnits: progressData.masteredUnits ?? 0,
35851
+ masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
35789
35852
  });
35853
+ const effectiveMasteredUnits = masteryProgress ? masteryProgress.effectiveDelta : progressData.masteredUnits ?? 0;
35790
35854
  let pctCompleteApp;
35791
35855
  let masteryAchieved = false;
35792
35856
  let scoreStatus = SCORE_STATUS5.fullyGraded;
@@ -35814,7 +35878,7 @@ class ProgressRecorder {
35814
35878
  appName: progressData.appName,
35815
35879
  totalQuestions,
35816
35880
  correctQuestions,
35817
- masteredUnits,
35881
+ masteredUnits: effectiveMasteredUnits || undefined,
35818
35882
  pctCompleteApp
35819
35883
  });
35820
35884
  } else {
@@ -35852,7 +35916,7 @@ class ProgressRecorder {
35852
35916
  totalQuestions,
35853
35917
  correctQuestions,
35854
35918
  xpEarned: calculatedXp,
35855
- masteredUnits,
35919
+ masteredUnits: effectiveMasteredUnits || undefined,
35856
35920
  attemptNumber: currentAttemptNumber,
35857
35921
  progressData,
35858
35922
  extensions,
@@ -35861,7 +35925,7 @@ class ProgressRecorder {
35861
35925
  return {
35862
35926
  xpAwarded: calculatedXp,
35863
35927
  attemptNumber: currentAttemptNumber,
35864
- masteredUnitsApplied: progressData.masteredUnits ?? 0,
35928
+ masteredUnitsApplied: effectiveMasteredUnits,
35865
35929
  pctCompleteApp,
35866
35930
  scoreStatus,
35867
35931
  inProgress
@@ -36396,6 +36460,65 @@ class TimebackClient {
36396
36460
  resourceId: ids.resource
36397
36461
  });
36398
36462
  }
36463
+ async getStudentMastery(studentId, options) {
36464
+ await this._ensureAuthenticated();
36465
+ const enrollments = await this.edubridge.enrollments.listByUser(studentId);
36466
+ const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
36467
+ if (filteredEnrollments.length === 0) {
36468
+ return {
36469
+ totalMasteredUnits: 0,
36470
+ totalMasterableUnits: 0,
36471
+ ...options?.include?.perCourse && { courses: [] }
36472
+ };
36473
+ }
36474
+ const masteryResults = await Promise.all(filteredEnrollments.map(async (enrollment) => {
36475
+ try {
36476
+ const ids = deriveSourcedIds2(enrollment.course.id);
36477
+ const status = await this.masteryTracker.getStatus({
36478
+ studentId,
36479
+ courseId: enrollment.course.id,
36480
+ resourceId: ids.resource
36481
+ });
36482
+ return { enrollment, status };
36483
+ } catch (error) {
36484
+ log.warn("[TimebackClient] Failed to fetch mastery for enrollment", {
36485
+ enrollmentId: enrollment.id,
36486
+ error
36487
+ });
36488
+ return { enrollment, status: undefined };
36489
+ }
36490
+ }));
36491
+ let totalMasteredUnits = 0;
36492
+ let totalMasterableUnits = 0;
36493
+ const courses = [];
36494
+ for (const { enrollment, status } of masteryResults) {
36495
+ const masteredUnits = status?.masteredUnits ?? 0;
36496
+ const masterableUnits = status?.masterableUnits ?? 0;
36497
+ totalMasteredUnits += masteredUnits;
36498
+ totalMasterableUnits += masterableUnits;
36499
+ if (options?.include?.perCourse) {
36500
+ const gradeStr = enrollment.course.grades?.[0];
36501
+ const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
36502
+ const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
36503
+ const subjectStr = enrollment.course.subjects?.[0];
36504
+ const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
36505
+ courses.push({
36506
+ grade,
36507
+ subject,
36508
+ title: enrollment.course.title,
36509
+ masteredUnits,
36510
+ masterableUnits,
36511
+ pctComplete: status?.pctCompleteApp ?? 0,
36512
+ isComplete: status?.isComplete ?? false
36513
+ });
36514
+ }
36515
+ }
36516
+ return {
36517
+ totalMasteredUnits,
36518
+ totalMasterableUnits,
36519
+ ...options?.include?.perCourse && { courses }
36520
+ };
36521
+ }
36399
36522
  async getStudentXp(studentId, options) {
36400
36523
  await this._ensureAuthenticated();
36401
36524
  const enrollments = await this.edubridge.enrollments.listByUser(studentId);
@@ -93718,7 +93841,7 @@ var init_session_controller = __esm(() => {
93718
93841
  });
93719
93842
 
93720
93843
  // ../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;
93844
+ 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
93845
  var init_timeback_controller = __esm(() => {
93723
93846
  init_esm();
93724
93847
  init_schemas_index();
@@ -93876,6 +93999,7 @@ var init_timeback_controller = __esm(() => {
93876
93999
  sessionTimingData,
93877
94000
  xpEarned,
93878
94001
  masteredUnits,
94002
+ masteredUnitsAbsolute,
93879
94003
  extensions
93880
94004
  } = body2;
93881
94005
  logger45.debug("Ending activity", { userId: ctx.user.id, gameId });
@@ -93890,6 +94014,7 @@ var init_timeback_controller = __esm(() => {
93890
94014
  sessionTimingData,
93891
94015
  xpEarned,
93892
94016
  masteredUnits,
94017
+ masteredUnitsAbsolute,
93893
94018
  extensions,
93894
94019
  user: ctx.user
93895
94020
  });
@@ -93999,6 +94124,53 @@ var init_timeback_controller = __esm(() => {
93999
94124
  include
94000
94125
  });
94001
94126
  });
94127
+ getStudentMastery = requireDeveloper(async (ctx) => {
94128
+ const timebackId = ctx.params.timebackId;
94129
+ if (!timebackId) {
94130
+ throw ApiError.badRequest("Missing timebackId parameter");
94131
+ }
94132
+ const gameId = ctx.url.searchParams.get("gameId");
94133
+ if (!gameId) {
94134
+ throw ApiError.badRequest("Missing required gameId query parameter");
94135
+ }
94136
+ const gradeParam = ctx.url.searchParams.get("grade");
94137
+ const subjectParam = ctx.url.searchParams.get("subject");
94138
+ if (gradeParam !== null !== (subjectParam !== null)) {
94139
+ throw ApiError.badRequest("Both grade and subject must be provided together");
94140
+ }
94141
+ let grade;
94142
+ let subject;
94143
+ if (gradeParam !== null && subjectParam !== null) {
94144
+ const parsedGrade = parseInt(gradeParam, 10);
94145
+ if (!isTimebackGrade2(parsedGrade)) {
94146
+ throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
94147
+ }
94148
+ if (!isTimebackSubject2(subjectParam)) {
94149
+ throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
94150
+ }
94151
+ grade = parsedGrade;
94152
+ subject = subjectParam;
94153
+ }
94154
+ const includeParam = ctx.url.searchParams.get("include");
94155
+ const includeOptions = includeParam ? includeParam.split(",").map((opt) => opt.trim().toLowerCase()) : [];
94156
+ const include = {
94157
+ perCourse: includeOptions.includes("percourse")
94158
+ };
94159
+ logger45.debug("Getting student mastery", {
94160
+ requesterId: ctx.user.id,
94161
+ timebackId,
94162
+ gameId,
94163
+ grade,
94164
+ subject,
94165
+ include
94166
+ });
94167
+ return ctx.services.timeback.getStudentMastery(timebackId, ctx.user, {
94168
+ gameId,
94169
+ grade,
94170
+ subject,
94171
+ include
94172
+ });
94173
+ });
94002
94174
  getRoster = requireGameManagementAccess(async (ctx) => {
94003
94175
  const gameId = ctx.params.gameId;
94004
94176
  const courseId = ctx.params.courseId;
@@ -94331,6 +94503,7 @@ var init_timeback_controller = __esm(() => {
94331
94503
  heartbeat,
94332
94504
  advanceCourse,
94333
94505
  getStudentXp,
94506
+ getStudentMastery,
94334
94507
  getRoster,
94335
94508
  getStudentOverview,
94336
94509
  getGameMetrics,
@@ -95217,6 +95390,7 @@ var init_timeback6 = __esm(() => {
95217
95390
  init_controllers();
95218
95391
  init_errors();
95219
95392
  init_utils11();
95393
+ init_schemas_index();
95220
95394
  init_api();
95221
95395
  init_error_handler();
95222
95396
  init_timeback5();
@@ -95296,6 +95470,14 @@ var init_timeback6 = __esm(() => {
95296
95470
  }
95297
95471
  if (gradeParam !== null && subjectParam !== null) {
95298
95472
  const grade = parseInt(gradeParam, 10);
95473
+ if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
95474
+ const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
95475
+ return c2.json(createErrorResponse(error2), error2.status);
95476
+ }
95477
+ if (!isTimebackSubject2(subjectParam)) {
95478
+ const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
95479
+ return c2.json(createErrorResponse(error2), error2.status);
95480
+ }
95299
95481
  enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
95300
95482
  }
95301
95483
  const mockCourses = enrollments.map((e) => {
@@ -95320,6 +95502,62 @@ var init_timeback6 = __esm(() => {
95320
95502
  }
95321
95503
  return handle2(timeback2.getStudentXp)(c2);
95322
95504
  });
95505
+ timebackRouter.get("/student-mastery/:timebackId", async (c2) => {
95506
+ const user = c2.get("user");
95507
+ if (!user) {
95508
+ const error2 = ApiError.unauthorized("Must be logged in to get student mastery");
95509
+ return c2.json(createErrorResponse(error2), error2.status);
95510
+ }
95511
+ if (shouldMockTimeback()) {
95512
+ const url2 = new URL(c2.req.url);
95513
+ const gradeParam = url2.searchParams.get("grade");
95514
+ const subjectParam = url2.searchParams.get("subject");
95515
+ const includeParam = url2.searchParams.get("include") || "";
95516
+ const includeOptions = includeParam.split(",").map((opt) => opt.trim().toLowerCase());
95517
+ const includePerCourse = includeOptions.includes("percourse");
95518
+ const db2 = c2.get("db");
95519
+ let enrollments = await getMockEnrollments(db2);
95520
+ if (gradeParam !== null !== (subjectParam !== null)) {
95521
+ const error2 = ApiError.badRequest("Both grade and subject must be provided together");
95522
+ return c2.json(createErrorResponse(error2), error2.status);
95523
+ }
95524
+ if (gradeParam !== null && subjectParam !== null) {
95525
+ const grade = parseInt(gradeParam, 10);
95526
+ if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
95527
+ const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
95528
+ return c2.json(createErrorResponse(error2), error2.status);
95529
+ }
95530
+ if (!isTimebackSubject2(subjectParam)) {
95531
+ const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
95532
+ return c2.json(createErrorResponse(error2), error2.status);
95533
+ }
95534
+ enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
95535
+ }
95536
+ const mockCourses = enrollments.map((e) => {
95537
+ const seed3 = hashCode(`${e.grade}-${e.subject}`);
95538
+ const masterableUnits = 5 + seed3 % 16;
95539
+ const masteredUnits = seed3 % (masterableUnits + 1);
95540
+ const pctComplete = masterableUnits > 0 ? Math.round(masteredUnits / masterableUnits * 1e4) / 100 : 0;
95541
+ return {
95542
+ grade: e.grade,
95543
+ subject: e.subject,
95544
+ title: `${e.subject} ${formatGradeLabel(e.grade)}`,
95545
+ masteredUnits,
95546
+ masterableUnits,
95547
+ pctComplete,
95548
+ isComplete: masteredUnits >= masterableUnits
95549
+ };
95550
+ });
95551
+ const totalMasteredUnits = mockCourses.reduce((sum2, course) => sum2 + course.masteredUnits, 0);
95552
+ const totalMasterableUnits = mockCourses.reduce((sum2, course) => sum2 + course.masterableUnits, 0);
95553
+ return c2.json({
95554
+ totalMasteredUnits,
95555
+ totalMasterableUnits,
95556
+ ...includePerCourse && { courses: mockCourses }
95557
+ });
95558
+ }
95559
+ return handle2(timeback2.getStudentMastery)(c2);
95560
+ });
95323
95561
  });
95324
95562
 
95325
95563
  // src/routes/integrations/lti.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-beta.1",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {