@playcademy/sandbox 0.3.17-beta.21 → 0.3.17-beta.23

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.
Files changed (3) hide show
  1. package/dist/cli.js +77 -11
  2. package/dist/server.js +77 -11
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1330,7 +1330,7 @@ var package_default;
1330
1330
  var init_package = __esm(() => {
1331
1331
  package_default = {
1332
1332
  name: "@playcademy/sandbox",
1333
- version: "0.3.17-beta.21",
1333
+ version: "0.3.17-beta.23",
1334
1334
  description: "Local development server for Playcademy game development",
1335
1335
  type: "module",
1336
1336
  exports: {
@@ -30922,21 +30922,25 @@ class TimebackAdminService {
30922
30922
  });
30923
30923
  return new Map(results);
30924
30924
  }
30925
+ async fetchCaliperEventsForStudent(client, studentId, source, limit) {
30926
+ const actorId = `${client.getBaseUrl().replace(/\/$/, "")}/ims/oneroster/rostering/v1p2/users/${studentId}`;
30927
+ const { events } = await client.caliper.events.list({
30928
+ limit,
30929
+ actorId,
30930
+ ...source.sourceMode === "production" ? { sensor: source.sensorUrl } : {},
30931
+ extensions: {
30932
+ gameId: source.gameId
30933
+ }
30934
+ });
30935
+ return events;
30936
+ }
30925
30937
  async listRecentActivityForStudent(client, studentId, source, relevantCourseIds, maxResults = TimebackAdminService.RECENT_ACTIVITY_LIMIT) {
30926
30938
  if (relevantCourseIds.size === 0) {
30927
30939
  return [];
30928
30940
  }
30929
30941
  try {
30930
- const actorId = `${client.getBaseUrl().replace(/\/$/, "")}/ims/oneroster/rostering/v1p2/users/${studentId}`;
30931
30942
  const eventLimit = Math.min(Math.max(200, maxResults * 20), TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
30932
- const { events } = await client.caliper.events.list({
30933
- limit: eventLimit,
30934
- actorId,
30935
- ...source.sourceMode === "production" ? { sensor: source.sensorUrl } : {},
30936
- extensions: {
30937
- gameId: source.gameId
30938
- }
30939
- });
30943
+ const events = await this.fetchCaliperEventsForStudent(client, studentId, source, eventLimit);
30940
30944
  return TimebackAdminService.mapRecentActivityItems(events, relevantCourseIds).slice(0, maxResults);
30941
30945
  } catch (error) {
30942
30946
  logger16.warn("Failed to load recent Caliper activity", {
@@ -31060,6 +31064,42 @@ class TimebackAdminService {
31060
31064
  const hasMore = allActivities.length > safeOffset + safeLimit;
31061
31065
  return { activities, hasMore };
31062
31066
  }
31067
+ async getActivityDetail(user, options) {
31068
+ const { gameId, studentId, courseId, activityId, runId } = options;
31069
+ const client = this.requireClient();
31070
+ await this.deps.validateGameManagementAccess(user, gameId);
31071
+ const [integration, gameSource] = await Promise.all([
31072
+ this.deps.db.query.gameTimebackIntegrations.findFirst({
31073
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
31074
+ }),
31075
+ this.getGameActivitySource(gameId)
31076
+ ]);
31077
+ if (!integration) {
31078
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
31079
+ }
31080
+ await this.assertStudentEnrolledInCourse(client, studentId, courseId);
31081
+ const events = await this.fetchCaliperEventsForStudent(client, studentId, gameSource, TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
31082
+ const relevantCourseIds = new Set([courseId]);
31083
+ let matchedEvents;
31084
+ let activity;
31085
+ if (runId) {
31086
+ const gameplayEvents = events.filter((event) => (event.type === "ActivityEvent" || event.type === "TimeSpentEvent") && !isCaliperRemediationOrCompletionEvent(event));
31087
+ const groups = groupCaliperEventsByRun(gameplayEvents);
31088
+ matchedEvents = [...groups.values()].find((group) => group.some((event) => getCanonicalRunId(event.session) === runId && event.externalId === activityId)) ?? [];
31089
+ activity = mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
31090
+ } else {
31091
+ matchedEvents = events.filter((event) => event.externalId === activityId);
31092
+ if (matchedEvents.length > 0) {
31093
+ activity = mapCaliperEventToRemediationActivity(matchedEvents[0], relevantCourseIds) ?? mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
31094
+ } else {
31095
+ activity = null;
31096
+ }
31097
+ }
31098
+ if (!activity) {
31099
+ throw new NotFoundError("Activity", activityId);
31100
+ }
31101
+ return { activity, rawEvents: matchedEvents };
31102
+ }
31063
31103
  async grantManualXp(data, user) {
31064
31104
  const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
31065
31105
  await client.recordAdminXpAdjustment({
@@ -96522,7 +96562,7 @@ var init_sprite_controller = __esm(() => {
96522
96562
  });
96523
96563
 
96524
96564
  // ../api-core/src/controllers/timeback.controller.ts
96525
- var logger64, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, grantXp, adjustTime, adjustMastery, toggleCompletion, searchStudents, enrollStudent, unenrollStudent, timeback2;
96565
+ var logger64, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, toggleCompletion, searchStudents, enrollStudent, unenrollStudent, timeback2;
96526
96566
  var init_timeback_controller = __esm(() => {
96527
96567
  init_esm();
96528
96568
  init_schemas_index();
@@ -96858,6 +96898,31 @@ var init_timeback_controller = __esm(() => {
96858
96898
  offset
96859
96899
  });
96860
96900
  });
96901
+ getActivityDetail = requireGameManagementAccess(async (ctx) => {
96902
+ const timebackId = ctx.params.timebackId;
96903
+ const courseId = ctx.params.courseId;
96904
+ const activityId = ctx.params.activityId;
96905
+ const gameId = ctx.url.searchParams.get("gameId") || undefined;
96906
+ const runId = ctx.url.searchParams.get("runId") || undefined;
96907
+ if (!timebackId || !courseId || !activityId || !gameId) {
96908
+ throw ApiError.badRequest("Missing timebackId, courseId, or activityId path parameter, or gameId query parameter");
96909
+ }
96910
+ logger64.debug("Getting activity detail", {
96911
+ requesterId: ctx.user.id,
96912
+ timebackId,
96913
+ courseId,
96914
+ activityId,
96915
+ gameId,
96916
+ runId
96917
+ });
96918
+ return ctx.services.timebackAdmin.getActivityDetail(ctx.user, {
96919
+ gameId,
96920
+ studentId: timebackId,
96921
+ courseId,
96922
+ activityId,
96923
+ runId
96924
+ });
96925
+ });
96861
96926
  grantXp = requireDeveloper(async (ctx) => {
96862
96927
  const body2 = await parseRequestBody(ctx.request, GrantTimebackXpRequestSchema);
96863
96928
  logger64.debug("Granting manual XP", {
@@ -96960,6 +97025,7 @@ var init_timeback_controller = __esm(() => {
96960
97025
  getRoster,
96961
97026
  getStudentOverview,
96962
97027
  getStudentActivity,
97028
+ getActivityDetail,
96963
97029
  grantXp,
96964
97030
  adjustTime,
96965
97031
  adjustMastery,
package/dist/server.js CHANGED
@@ -1329,7 +1329,7 @@ var package_default;
1329
1329
  var init_package = __esm(() => {
1330
1330
  package_default = {
1331
1331
  name: "@playcademy/sandbox",
1332
- version: "0.3.17-beta.21",
1332
+ version: "0.3.17-beta.23",
1333
1333
  description: "Local development server for Playcademy game development",
1334
1334
  type: "module",
1335
1335
  exports: {
@@ -30921,21 +30921,25 @@ class TimebackAdminService {
30921
30921
  });
30922
30922
  return new Map(results);
30923
30923
  }
30924
+ async fetchCaliperEventsForStudent(client, studentId, source, limit) {
30925
+ const actorId = `${client.getBaseUrl().replace(/\/$/, "")}/ims/oneroster/rostering/v1p2/users/${studentId}`;
30926
+ const { events } = await client.caliper.events.list({
30927
+ limit,
30928
+ actorId,
30929
+ ...source.sourceMode === "production" ? { sensor: source.sensorUrl } : {},
30930
+ extensions: {
30931
+ gameId: source.gameId
30932
+ }
30933
+ });
30934
+ return events;
30935
+ }
30924
30936
  async listRecentActivityForStudent(client, studentId, source, relevantCourseIds, maxResults = TimebackAdminService.RECENT_ACTIVITY_LIMIT) {
30925
30937
  if (relevantCourseIds.size === 0) {
30926
30938
  return [];
30927
30939
  }
30928
30940
  try {
30929
- const actorId = `${client.getBaseUrl().replace(/\/$/, "")}/ims/oneroster/rostering/v1p2/users/${studentId}`;
30930
30941
  const eventLimit = Math.min(Math.max(200, maxResults * 20), TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
30931
- const { events } = await client.caliper.events.list({
30932
- limit: eventLimit,
30933
- actorId,
30934
- ...source.sourceMode === "production" ? { sensor: source.sensorUrl } : {},
30935
- extensions: {
30936
- gameId: source.gameId
30937
- }
30938
- });
30942
+ const events = await this.fetchCaliperEventsForStudent(client, studentId, source, eventLimit);
30939
30943
  return TimebackAdminService.mapRecentActivityItems(events, relevantCourseIds).slice(0, maxResults);
30940
30944
  } catch (error) {
30941
30945
  logger16.warn("Failed to load recent Caliper activity", {
@@ -31059,6 +31063,42 @@ class TimebackAdminService {
31059
31063
  const hasMore = allActivities.length > safeOffset + safeLimit;
31060
31064
  return { activities, hasMore };
31061
31065
  }
31066
+ async getActivityDetail(user, options) {
31067
+ const { gameId, studentId, courseId, activityId, runId } = options;
31068
+ const client = this.requireClient();
31069
+ await this.deps.validateGameManagementAccess(user, gameId);
31070
+ const [integration, gameSource] = await Promise.all([
31071
+ this.deps.db.query.gameTimebackIntegrations.findFirst({
31072
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
31073
+ }),
31074
+ this.getGameActivitySource(gameId)
31075
+ ]);
31076
+ if (!integration) {
31077
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
31078
+ }
31079
+ await this.assertStudentEnrolledInCourse(client, studentId, courseId);
31080
+ const events = await this.fetchCaliperEventsForStudent(client, studentId, gameSource, TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
31081
+ const relevantCourseIds = new Set([courseId]);
31082
+ let matchedEvents;
31083
+ let activity;
31084
+ if (runId) {
31085
+ const gameplayEvents = events.filter((event) => (event.type === "ActivityEvent" || event.type === "TimeSpentEvent") && !isCaliperRemediationOrCompletionEvent(event));
31086
+ const groups = groupCaliperEventsByRun(gameplayEvents);
31087
+ matchedEvents = [...groups.values()].find((group) => group.some((event) => getCanonicalRunId(event.session) === runId && event.externalId === activityId)) ?? [];
31088
+ activity = mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
31089
+ } else {
31090
+ matchedEvents = events.filter((event) => event.externalId === activityId);
31091
+ if (matchedEvents.length > 0) {
31092
+ activity = mapCaliperEventToRemediationActivity(matchedEvents[0], relevantCourseIds) ?? mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
31093
+ } else {
31094
+ activity = null;
31095
+ }
31096
+ }
31097
+ if (!activity) {
31098
+ throw new NotFoundError("Activity", activityId);
31099
+ }
31100
+ return { activity, rawEvents: matchedEvents };
31101
+ }
31062
31102
  async grantManualXp(data, user) {
31063
31103
  const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
31064
31104
  await client.recordAdminXpAdjustment({
@@ -96521,7 +96561,7 @@ var init_sprite_controller = __esm(() => {
96521
96561
  });
96522
96562
 
96523
96563
  // ../api-core/src/controllers/timeback.controller.ts
96524
- var logger64, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, grantXp, adjustTime, adjustMastery, toggleCompletion, searchStudents, enrollStudent, unenrollStudent, timeback2;
96564
+ var logger64, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, toggleCompletion, searchStudents, enrollStudent, unenrollStudent, timeback2;
96525
96565
  var init_timeback_controller = __esm(() => {
96526
96566
  init_esm();
96527
96567
  init_schemas_index();
@@ -96857,6 +96897,31 @@ var init_timeback_controller = __esm(() => {
96857
96897
  offset
96858
96898
  });
96859
96899
  });
96900
+ getActivityDetail = requireGameManagementAccess(async (ctx) => {
96901
+ const timebackId = ctx.params.timebackId;
96902
+ const courseId = ctx.params.courseId;
96903
+ const activityId = ctx.params.activityId;
96904
+ const gameId = ctx.url.searchParams.get("gameId") || undefined;
96905
+ const runId = ctx.url.searchParams.get("runId") || undefined;
96906
+ if (!timebackId || !courseId || !activityId || !gameId) {
96907
+ throw ApiError.badRequest("Missing timebackId, courseId, or activityId path parameter, or gameId query parameter");
96908
+ }
96909
+ logger64.debug("Getting activity detail", {
96910
+ requesterId: ctx.user.id,
96911
+ timebackId,
96912
+ courseId,
96913
+ activityId,
96914
+ gameId,
96915
+ runId
96916
+ });
96917
+ return ctx.services.timebackAdmin.getActivityDetail(ctx.user, {
96918
+ gameId,
96919
+ studentId: timebackId,
96920
+ courseId,
96921
+ activityId,
96922
+ runId
96923
+ });
96924
+ });
96860
96925
  grantXp = requireDeveloper(async (ctx) => {
96861
96926
  const body2 = await parseRequestBody(ctx.request, GrantTimebackXpRequestSchema);
96862
96927
  logger64.debug("Granting manual XP", {
@@ -96959,6 +97024,7 @@ var init_timeback_controller = __esm(() => {
96959
97024
  getRoster,
96960
97025
  getStudentOverview,
96961
97026
  getStudentActivity,
97027
+ getActivityDetail,
96962
97028
  grantXp,
96963
97029
  adjustTime,
96964
97030
  adjustMastery,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.3.17-beta.21",
3
+ "version": "0.3.17-beta.23",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {