@playcademy/sandbox 0.4.2-beta.1 → 0.4.2-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 CHANGED
@@ -249,6 +249,7 @@ var init_timeback2 = __esm(() => {
249
249
  END_ACTIVITY: "/integrations/timeback/end-activity",
250
250
  GET_XP: "/integrations/timeback/xp",
251
251
  GET_MASTERY: "/integrations/timeback/mastery",
252
+ GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
252
253
  HEARTBEAT: "/integrations/timeback/heartbeat",
253
254
  ADVANCE_COURSE: "/integrations/timeback/advance-course",
254
255
  UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
@@ -1077,7 +1078,7 @@ var package_default;
1077
1078
  var init_package = __esm(() => {
1078
1079
  package_default = {
1079
1080
  name: "@playcademy/sandbox",
1080
- version: "0.4.2-beta.1",
1081
+ version: "0.4.2-beta.2",
1081
1082
  description: "Local development server for Playcademy game development",
1082
1083
  type: "module",
1083
1084
  exports: {
@@ -28248,6 +28249,7 @@ var init_constants3 = __esm(() => {
28248
28249
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
28249
28250
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
28250
28251
  GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
28252
+ GET_HIGHEST_GRADE_MASTERED: `/api${TIMEBACK_ROUTES.GET_HIGHEST_GRADE_MASTERED}`,
28251
28253
  HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
28252
28254
  ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`,
28253
28255
  UNENROLL_COURSE: `/api${TIMEBACK_ROUTES.UNENROLL_COURSE}`
@@ -29128,6 +29130,7 @@ var init_utils6 = __esm(() => {
29128
29130
  };
29129
29131
  });
29130
29132
  init_constants7();
29133
+ init_constants7();
29131
29134
  if (process.env.DEBUG === "true") {
29132
29135
  process.env.TERM = "dumb";
29133
29136
  }
@@ -33560,6 +33563,25 @@ var init_timeback_service = __esm(() => {
33560
33563
  });
33561
33564
  return result;
33562
33565
  }
33566
+ async getStudentHighestGradeMastered(timebackId, user, options) {
33567
+ const client = this.requireClient();
33568
+ const db2 = this.deps.db;
33569
+ await this.deps.validateDeveloperAccess(user, options.gameId);
33570
+ const integration = await db2.query.gameTimebackIntegrations.findFirst({
33571
+ where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject))
33572
+ });
33573
+ if (!integration) {
33574
+ throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
33575
+ }
33576
+ const result = await client.getHighestGradeMastered(timebackId, options.subject);
33577
+ logger20.debug("Retrieved student highest grade mastered", {
33578
+ timebackId,
33579
+ gameId: options.gameId,
33580
+ subject: options.subject,
33581
+ highestGradeMastered: result.highestGradeMastered
33582
+ });
33583
+ return result;
33584
+ }
33563
33585
  };
33564
33586
  });
33565
33587
 
@@ -34789,6 +34811,19 @@ async function getTimebackTokenResponse(config2) {
34789
34811
  function getAuthUrl(environment = "production") {
34790
34812
  return TIMEBACK_AUTH_URLS5[environment];
34791
34813
  }
34814
+ function parseEduBridgeGrade(value) {
34815
+ if (value === null || value === undefined || value.trim() === "") {
34816
+ return null;
34817
+ }
34818
+ const parsed = Number(value);
34819
+ return isTimebackGrade3(parsed) ? parsed : null;
34820
+ }
34821
+ function normalizeHighestGradeMastered(response, subject) {
34822
+ return {
34823
+ subject,
34824
+ highestGradeMastered: parseEduBridgeGrade(response.grades.highestGradeOverall)
34825
+ };
34826
+ }
34792
34827
  function handleHttpError(res, errorBody, attempt, retries, url2) {
34793
34828
  const error = new TimebackApiError2(res.status, res.statusText, errorBody);
34794
34829
  if (res.status >= HTTP_STATUS5.CLIENT_ERROR_MIN && res.status < HTTP_STATUS5.CLIENT_ERROR_MAX) {
@@ -35133,7 +35168,8 @@ function createEduBridgeNamespace(client) {
35133
35168
  const analytics = {
35134
35169
  getEnrollmentFacts: async (enrollmentId, options) => client["request"](buildPath(`/edubridge/analytics/enrollment/${enrollmentId}`, {
35135
35170
  timezone: options?.timezone
35136
- }), "GET")
35171
+ }), "GET"),
35172
+ getHighestGradeMastered: async (studentId, subject) => client["request"](`/edubridge/analytics/highestGradeMastered/${encodeURIComponent(studentId)}/${encodeURIComponent(subject)}`, "GET")
35137
35173
  };
35138
35174
  return {
35139
35175
  enrollments,
@@ -36875,6 +36911,11 @@ class TimebackClient {
36875
36911
  ...options?.include?.perCourse && { courses }
36876
36912
  };
36877
36913
  }
36914
+ async getHighestGradeMastered(studentId, subject) {
36915
+ await this._ensureAuthenticated();
36916
+ const response = await this.edubridge.analytics.getHighestGradeMastered(studentId, subject);
36917
+ return normalizeHighestGradeMastered(response, subject);
36918
+ }
36878
36919
  async getStudentXp(studentId, options) {
36879
36920
  await this._ensureAuthenticated();
36880
36921
  const enrollments = await this.edubridge.enrollments.listByUser(studentId);
@@ -94197,7 +94238,7 @@ var init_session_controller = __esm(() => {
94197
94238
  });
94198
94239
 
94199
94240
  // ../api-core/src/controllers/timeback.controller.ts
94200
- var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
94241
+ var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
94201
94242
  var init_timeback_controller = __esm(() => {
94202
94243
  init_esm();
94203
94244
  init_schemas_index();
@@ -94541,6 +94582,33 @@ var init_timeback_controller = __esm(() => {
94541
94582
  include
94542
94583
  });
94543
94584
  });
94585
+ getStudentHighestGradeMastered = requireDeveloper(async (ctx) => {
94586
+ const timebackId = ctx.params.timebackId;
94587
+ if (!timebackId) {
94588
+ throw ApiError.badRequest("Missing timebackId parameter");
94589
+ }
94590
+ const gameId = ctx.url.searchParams.get("gameId");
94591
+ if (!gameId) {
94592
+ throw ApiError.badRequest("Missing required gameId query parameter");
94593
+ }
94594
+ const subjectParam = ctx.url.searchParams.get("subject");
94595
+ if (!subjectParam) {
94596
+ throw ApiError.badRequest("Missing required subject query parameter");
94597
+ }
94598
+ if (!isTimebackSubject2(subjectParam)) {
94599
+ throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
94600
+ }
94601
+ logger45.debug("Getting student highest grade mastered", {
94602
+ requesterId: ctx.user.id,
94603
+ timebackId,
94604
+ gameId,
94605
+ subject: subjectParam
94606
+ });
94607
+ return ctx.services.timeback.getStudentHighestGradeMastered(timebackId, ctx.user, {
94608
+ gameId,
94609
+ subject: subjectParam
94610
+ });
94611
+ });
94544
94612
  getRoster = requireGameManagementAccess(async (ctx) => {
94545
94613
  const gameId = ctx.params.gameId;
94546
94614
  const courseId = ctx.params.courseId;
@@ -94879,6 +94947,7 @@ var init_timeback_controller = __esm(() => {
94879
94947
  unenrollCourse,
94880
94948
  getStudentXp,
94881
94949
  getStudentMastery,
94950
+ getStudentHighestGradeMastered,
94882
94951
  getRoster,
94883
94952
  getStudentOverview,
94884
94953
  getGameMetrics,
@@ -95087,6 +95156,10 @@ async function getMockTimebackUser(db2, gameId) {
95087
95156
  const timebackId = config.timeback.timebackId || "mock-student-00000001";
95088
95157
  return getMockTimebackData(db2, timebackId, gameId);
95089
95158
  }
95159
+ function getMockHighestGradeMastered(enrollments, subject) {
95160
+ const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
95161
+ return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
95162
+ }
95090
95163
  async function buildMockUserResponse(db2, user, gameId) {
95091
95164
  const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
95092
95165
  if (gameId) {
@@ -95934,6 +96007,39 @@ var init_timeback6 = __esm(() => {
95934
96007
  }
95935
96008
  return handle2(timeback2.getStudentMastery)(c2);
95936
96009
  });
96010
+ timebackRouter.get("/student-highest-grade-mastered/:timebackId", async (c2) => {
96011
+ const user = c2.get("user");
96012
+ if (!user) {
96013
+ const error2 = ApiError.unauthorized("Must be logged in to get student highest grade mastered");
96014
+ return c2.json(createErrorResponse(error2), error2.status);
96015
+ }
96016
+ if (shouldMockTimeback()) {
96017
+ const url2 = new URL(c2.req.url);
96018
+ const subject = url2.searchParams.get("subject");
96019
+ const contextGameId = c2.get("gameId");
96020
+ const gameId = url2.searchParams.get("gameId") || (typeof contextGameId === "string" ? contextGameId : undefined);
96021
+ if (!subject) {
96022
+ const error2 = ApiError.badRequest("Missing required subject query parameter");
96023
+ return c2.json(createErrorResponse(error2), error2.status);
96024
+ }
96025
+ if (!gameId) {
96026
+ const error2 = ApiError.badRequest("Missing required gameId query parameter");
96027
+ return c2.json(createErrorResponse(error2), error2.status);
96028
+ }
96029
+ if (!isTimebackSubject2(subject)) {
96030
+ const error2 = ApiError.badRequest(`Invalid subject: ${subject}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
96031
+ return c2.json(createErrorResponse(error2), error2.status);
96032
+ }
96033
+ const db2 = c2.get("db");
96034
+ const enrollments = await getMockEnrollments(db2);
96035
+ const gameScopedEnrollments = filterEnrollmentsByGame(enrollments, gameId);
96036
+ return c2.json({
96037
+ subject,
96038
+ highestGradeMastered: getMockHighestGradeMastered(gameScopedEnrollments, subject)
96039
+ });
96040
+ }
96041
+ return handle2(timeback2.getStudentHighestGradeMastered)(c2);
96042
+ });
95937
96043
  });
95938
96044
 
95939
96045
  // src/routes/integrations/lti.ts
@@ -98311,7 +98417,8 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
98311
98417
  port,
98312
98418
  url: `http://localhost:${port}/api`,
98313
98419
  startedAt: Date.now(),
98314
- projectRoot: process.cwd()
98420
+ projectRoot: process.cwd(),
98421
+ gameId: server.gameId
98315
98422
  });
98316
98423
  const totalCourses = project?.timebackCourses?.length ?? 0;
98317
98424
  const excludedCount = options.timebackExcludedCourses ? options.timebackExcludedCourses.split(",").filter(Boolean).length : 0;
package/dist/constants.js CHANGED
@@ -84,6 +84,7 @@ var init_timeback = __esm(() => {
84
84
  END_ACTIVITY: "/integrations/timeback/end-activity",
85
85
  GET_XP: "/integrations/timeback/xp",
86
86
  GET_MASTERY: "/integrations/timeback/mastery",
87
+ GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
87
88
  HEARTBEAT: "/integrations/timeback/heartbeat",
88
89
  ADVANCE_COURSE: "/integrations/timeback/advance-course",
89
90
  UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
@@ -30,6 +30,7 @@ export declare function getMockTimebackData(db: DatabaseInstance, timebackId: st
30
30
  * Uses the configured timebackId from sandbox config.
31
31
  */
32
32
  export declare function getMockTimebackUser(db: DatabaseInstance, gameId?: string): Promise<UserTimebackData>;
33
+ export declare function getMockHighestGradeMastered(enrollments: UserEnrollment[], subject: string): number | null;
33
34
  /**
34
35
  * Build a complete user response with mock timeback data.
35
36
  * Used to bypass api-core when in mock mode (avoids real API calls).
package/dist/server.js CHANGED
@@ -248,6 +248,7 @@ var init_timeback2 = __esm(() => {
248
248
  END_ACTIVITY: "/integrations/timeback/end-activity",
249
249
  GET_XP: "/integrations/timeback/xp",
250
250
  GET_MASTERY: "/integrations/timeback/mastery",
251
+ GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
251
252
  HEARTBEAT: "/integrations/timeback/heartbeat",
252
253
  ADVANCE_COURSE: "/integrations/timeback/advance-course",
253
254
  UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
@@ -1076,7 +1077,7 @@ var package_default;
1076
1077
  var init_package = __esm(() => {
1077
1078
  package_default = {
1078
1079
  name: "@playcademy/sandbox",
1079
- version: "0.4.2-beta.1",
1080
+ version: "0.4.2-beta.2",
1080
1081
  description: "Local development server for Playcademy game development",
1081
1082
  type: "module",
1082
1083
  exports: {
@@ -28247,6 +28248,7 @@ var init_constants3 = __esm(() => {
28247
28248
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
28248
28249
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
28249
28250
  GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
28251
+ GET_HIGHEST_GRADE_MASTERED: `/api${TIMEBACK_ROUTES.GET_HIGHEST_GRADE_MASTERED}`,
28250
28252
  HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
28251
28253
  ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`,
28252
28254
  UNENROLL_COURSE: `/api${TIMEBACK_ROUTES.UNENROLL_COURSE}`
@@ -29127,6 +29129,7 @@ var init_utils6 = __esm(() => {
29127
29129
  };
29128
29130
  });
29129
29131
  init_constants7();
29132
+ init_constants7();
29130
29133
  if (process.env.DEBUG === "true") {
29131
29134
  process.env.TERM = "dumb";
29132
29135
  }
@@ -33559,6 +33562,25 @@ var init_timeback_service = __esm(() => {
33559
33562
  });
33560
33563
  return result;
33561
33564
  }
33565
+ async getStudentHighestGradeMastered(timebackId, user, options) {
33566
+ const client = this.requireClient();
33567
+ const db2 = this.deps.db;
33568
+ await this.deps.validateDeveloperAccess(user, options.gameId);
33569
+ const integration = await db2.query.gameTimebackIntegrations.findFirst({
33570
+ where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject))
33571
+ });
33572
+ if (!integration) {
33573
+ throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
33574
+ }
33575
+ const result = await client.getHighestGradeMastered(timebackId, options.subject);
33576
+ logger20.debug("Retrieved student highest grade mastered", {
33577
+ timebackId,
33578
+ gameId: options.gameId,
33579
+ subject: options.subject,
33580
+ highestGradeMastered: result.highestGradeMastered
33581
+ });
33582
+ return result;
33583
+ }
33562
33584
  };
33563
33585
  });
33564
33586
 
@@ -34788,6 +34810,19 @@ async function getTimebackTokenResponse(config2) {
34788
34810
  function getAuthUrl(environment = "production") {
34789
34811
  return TIMEBACK_AUTH_URLS5[environment];
34790
34812
  }
34813
+ function parseEduBridgeGrade(value) {
34814
+ if (value === null || value === undefined || value.trim() === "") {
34815
+ return null;
34816
+ }
34817
+ const parsed = Number(value);
34818
+ return isTimebackGrade3(parsed) ? parsed : null;
34819
+ }
34820
+ function normalizeHighestGradeMastered(response, subject) {
34821
+ return {
34822
+ subject,
34823
+ highestGradeMastered: parseEduBridgeGrade(response.grades.highestGradeOverall)
34824
+ };
34825
+ }
34791
34826
  function handleHttpError(res, errorBody, attempt, retries, url2) {
34792
34827
  const error = new TimebackApiError2(res.status, res.statusText, errorBody);
34793
34828
  if (res.status >= HTTP_STATUS5.CLIENT_ERROR_MIN && res.status < HTTP_STATUS5.CLIENT_ERROR_MAX) {
@@ -35132,7 +35167,8 @@ function createEduBridgeNamespace(client) {
35132
35167
  const analytics = {
35133
35168
  getEnrollmentFacts: async (enrollmentId, options) => client["request"](buildPath(`/edubridge/analytics/enrollment/${enrollmentId}`, {
35134
35169
  timezone: options?.timezone
35135
- }), "GET")
35170
+ }), "GET"),
35171
+ getHighestGradeMastered: async (studentId, subject) => client["request"](`/edubridge/analytics/highestGradeMastered/${encodeURIComponent(studentId)}/${encodeURIComponent(subject)}`, "GET")
35136
35172
  };
35137
35173
  return {
35138
35174
  enrollments,
@@ -36874,6 +36910,11 @@ class TimebackClient {
36874
36910
  ...options?.include?.perCourse && { courses }
36875
36911
  };
36876
36912
  }
36913
+ async getHighestGradeMastered(studentId, subject) {
36914
+ await this._ensureAuthenticated();
36915
+ const response = await this.edubridge.analytics.getHighestGradeMastered(studentId, subject);
36916
+ return normalizeHighestGradeMastered(response, subject);
36917
+ }
36877
36918
  async getStudentXp(studentId, options) {
36878
36919
  await this._ensureAuthenticated();
36879
36920
  const enrollments = await this.edubridge.enrollments.listByUser(studentId);
@@ -94196,7 +94237,7 @@ var init_session_controller = __esm(() => {
94196
94237
  });
94197
94238
 
94198
94239
  // ../api-core/src/controllers/timeback.controller.ts
94199
- var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
94240
+ var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
94200
94241
  var init_timeback_controller = __esm(() => {
94201
94242
  init_esm();
94202
94243
  init_schemas_index();
@@ -94540,6 +94581,33 @@ var init_timeback_controller = __esm(() => {
94540
94581
  include
94541
94582
  });
94542
94583
  });
94584
+ getStudentHighestGradeMastered = requireDeveloper(async (ctx) => {
94585
+ const timebackId = ctx.params.timebackId;
94586
+ if (!timebackId) {
94587
+ throw ApiError.badRequest("Missing timebackId parameter");
94588
+ }
94589
+ const gameId = ctx.url.searchParams.get("gameId");
94590
+ if (!gameId) {
94591
+ throw ApiError.badRequest("Missing required gameId query parameter");
94592
+ }
94593
+ const subjectParam = ctx.url.searchParams.get("subject");
94594
+ if (!subjectParam) {
94595
+ throw ApiError.badRequest("Missing required subject query parameter");
94596
+ }
94597
+ if (!isTimebackSubject2(subjectParam)) {
94598
+ throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
94599
+ }
94600
+ logger45.debug("Getting student highest grade mastered", {
94601
+ requesterId: ctx.user.id,
94602
+ timebackId,
94603
+ gameId,
94604
+ subject: subjectParam
94605
+ });
94606
+ return ctx.services.timeback.getStudentHighestGradeMastered(timebackId, ctx.user, {
94607
+ gameId,
94608
+ subject: subjectParam
94609
+ });
94610
+ });
94543
94611
  getRoster = requireGameManagementAccess(async (ctx) => {
94544
94612
  const gameId = ctx.params.gameId;
94545
94613
  const courseId = ctx.params.courseId;
@@ -94878,6 +94946,7 @@ var init_timeback_controller = __esm(() => {
94878
94946
  unenrollCourse,
94879
94947
  getStudentXp,
94880
94948
  getStudentMastery,
94949
+ getStudentHighestGradeMastered,
94881
94950
  getRoster,
94882
94951
  getStudentOverview,
94883
94952
  getGameMetrics,
@@ -95086,6 +95155,10 @@ async function getMockTimebackUser(db2, gameId) {
95086
95155
  const timebackId = config.timeback.timebackId || "mock-student-00000001";
95087
95156
  return getMockTimebackData(db2, timebackId, gameId);
95088
95157
  }
95158
+ function getMockHighestGradeMastered(enrollments, subject) {
95159
+ const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
95160
+ return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
95161
+ }
95089
95162
  async function buildMockUserResponse(db2, user, gameId) {
95090
95163
  const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
95091
95164
  if (gameId) {
@@ -95933,6 +96006,39 @@ var init_timeback6 = __esm(() => {
95933
96006
  }
95934
96007
  return handle2(timeback2.getStudentMastery)(c2);
95935
96008
  });
96009
+ timebackRouter.get("/student-highest-grade-mastered/:timebackId", async (c2) => {
96010
+ const user = c2.get("user");
96011
+ if (!user) {
96012
+ const error2 = ApiError.unauthorized("Must be logged in to get student highest grade mastered");
96013
+ return c2.json(createErrorResponse(error2), error2.status);
96014
+ }
96015
+ if (shouldMockTimeback()) {
96016
+ const url2 = new URL(c2.req.url);
96017
+ const subject = url2.searchParams.get("subject");
96018
+ const contextGameId = c2.get("gameId");
96019
+ const gameId = url2.searchParams.get("gameId") || (typeof contextGameId === "string" ? contextGameId : undefined);
96020
+ if (!subject) {
96021
+ const error2 = ApiError.badRequest("Missing required subject query parameter");
96022
+ return c2.json(createErrorResponse(error2), error2.status);
96023
+ }
96024
+ if (!gameId) {
96025
+ const error2 = ApiError.badRequest("Missing required gameId query parameter");
96026
+ return c2.json(createErrorResponse(error2), error2.status);
96027
+ }
96028
+ if (!isTimebackSubject2(subject)) {
96029
+ const error2 = ApiError.badRequest(`Invalid subject: ${subject}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
96030
+ return c2.json(createErrorResponse(error2), error2.status);
96031
+ }
96032
+ const db2 = c2.get("db");
96033
+ const enrollments = await getMockEnrollments(db2);
96034
+ const gameScopedEnrollments = filterEnrollmentsByGame(enrollments, gameId);
96035
+ return c2.json({
96036
+ subject,
96037
+ highestGradeMastered: getMockHighestGradeMastered(gameScopedEnrollments, subject)
96038
+ });
96039
+ }
96040
+ return handle2(timeback2.getStudentHighestGradeMastered)(c2);
96041
+ });
95936
96042
  });
95937
96043
 
95938
96044
  // src/routes/integrations/lti.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.4.2-beta.1",
3
+ "version": "0.4.2-beta.2",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {