@playcademy/vite-plugin 0.2.24-beta.5 → 0.2.24-beta.7

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/index.js CHANGED
@@ -25336,7 +25336,7 @@ var package_default;
25336
25336
  var init_package = __esm(() => {
25337
25337
  package_default = {
25338
25338
  name: "@playcademy/sandbox",
25339
- version: "0.3.17-beta.8",
25339
+ version: "0.3.17-beta.10",
25340
25340
  description: "Local development server for Playcademy game development",
25341
25341
  type: "module",
25342
25342
  exports: {
@@ -29951,6 +29951,7 @@ var init_esm = __esm(() => {
29951
29951
  function createMinimalConfig(overrides) {
29952
29952
  return apiConfigSchema.parse({
29953
29953
  stage: "local",
29954
+ isLocal: false,
29954
29955
  ...overrides
29955
29956
  });
29956
29957
  }
@@ -29978,6 +29979,7 @@ var init_schema = __esm(() => {
29978
29979
  });
29979
29980
  apiConfigSchema = exports_external.object({
29980
29981
  stage: stageSchema,
29982
+ isLocal: exports_external.boolean().default(false),
29981
29983
  baseUrl: exports_external.string().url().optional(),
29982
29984
  gameDomain: exports_external.string().optional(),
29983
29985
  lti: ltiConfigSchema.optional(),
@@ -54196,6 +54198,36 @@ var init_pure = __esm(() => {
54196
54198
  var init_src4 = __esm(() => {
54197
54199
  init_pure();
54198
54200
  });
54201
+ function toAttributionEventTime(date3) {
54202
+ if (!date3) {
54203
+ return;
54204
+ }
54205
+ const match = date3.match(/^(\d{4})-(\d{2})-(\d{2})$/);
54206
+ if (!match) {
54207
+ throw new ValidationError("Date must be in YYYY-MM-DD format");
54208
+ }
54209
+ const [, yearStr, monthStr, dayStr] = match;
54210
+ const year = Number(yearStr);
54211
+ const month = Number(monthStr);
54212
+ const day = Number(dayStr);
54213
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
54214
+ throw new ValidationError("Date must be in YYYY-MM-DD format");
54215
+ }
54216
+ const eventTime = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
54217
+ if (eventTime.getUTCFullYear() !== year || eventTime.getUTCMonth() + 1 !== month || eventTime.getUTCDate() !== day) {
54218
+ throw new ValidationError("Date must be a valid calendar date");
54219
+ }
54220
+ return eventTime.toISOString();
54221
+ }
54222
+ function resolveAdminEventTime(data) {
54223
+ if (data.useCurrentTime) {
54224
+ return new Date().toISOString();
54225
+ }
54226
+ return toAttributionEventTime(data.date);
54227
+ }
54228
+ var init_timeback_admin_util = __esm(() => {
54229
+ init_errors();
54230
+ });
54199
54231
  function isRecord2(value) {
54200
54232
  return typeof value === "object" && value !== null;
54201
54233
  }
@@ -54240,14 +54272,6 @@ function getPlaycademyMetadata(event) {
54240
54272
  const extensions = getMergedCaliperExtensions(event);
54241
54273
  return isRecord2(extensions.playcademy) ? extensions.playcademy : undefined;
54242
54274
  }
54243
- function getAssessmentPlaycademyMetadata(assessment) {
54244
- return isRecord2(assessment.metadata?.playcademy) ? assessment.metadata.playcademy : undefined;
54245
- }
54246
- function isRemediationAssessmentResult(assessment) {
54247
- const playcademy = getAssessmentPlaycademyMetadata(assessment);
54248
- const eventKind = getStringValue(playcademy?.eventKind);
54249
- return eventKind === "remediation-xp" || eventKind === "remediation-time" || eventKind === "remediation-mastery";
54250
- }
54251
54275
  function getActivityId(event, playcademy) {
54252
54276
  const metadataActivityId = getStringValue(playcademy?.activityId);
54253
54277
  if (metadataActivityId) {
@@ -54264,8 +54288,8 @@ function getActivityId(event, playcademy) {
54264
54288
  const trimmed = objectId.replace(/\/$/, "");
54265
54289
  const segments = trimmed.split("/");
54266
54290
  const activityIndex = segments.lastIndexOf("activities");
54267
- if (activityIndex !== -1 && segments.length >= activityIndex + 4) {
54268
- const candidate = segments[activityIndex + 3];
54291
+ if (activityIndex !== -1 && segments.length >= activityIndex + 3) {
54292
+ const candidate = segments[activityIndex + 2];
54269
54293
  return candidate ? decodeURIComponent(candidate) : undefined;
54270
54294
  }
54271
54295
  return;
@@ -54318,38 +54342,96 @@ function mapAssessmentsToXpEvents(userId, assessments) {
54318
54342
  };
54319
54343
  });
54320
54344
  }
54321
- function isMasteryCompletionEntry(assessment) {
54322
- return isRecord2(assessment.metadata) && assessment.metadata.isMasteryCompletion === true;
54345
+ function getDurationSecondsFromExtensions(event) {
54346
+ const extensions = getMergedCaliperExtensions(event);
54347
+ const playcademy = isRecord2(extensions.playcademy) ? extensions.playcademy : undefined;
54348
+ const rawValue = extensions.durationSeconds ?? playcademy?.durationSeconds;
54349
+ const value = typeof rawValue === "number" ? rawValue : Number(rawValue);
54350
+ return Number.isFinite(value) ? value : undefined;
54351
+ }
54352
+ function getCanonicalRunId(session2) {
54353
+ const sessionId = getStringValue(session2?.id);
54354
+ if (!sessionId) {
54355
+ return;
54356
+ }
54357
+ return sessionId.replace(/^urn:uuid:/, "");
54358
+ }
54359
+ function getResumeId(event) {
54360
+ const playcademy = getPlaycademyMetadata(event);
54361
+ return getStringValue(playcademy?.resumeId);
54362
+ }
54363
+ function isCaliperRemediationOrCompletionEvent(event) {
54364
+ const playcademy = getPlaycademyMetadata(event);
54365
+ return REMEDIATION_OR_COMPLETION_EVENT_KINDS.has(getStringValue(playcademy?.eventKind) || "");
54366
+ }
54367
+ function groupCaliperEventsByRun(events) {
54368
+ const groups = new Map;
54369
+ for (const event of events) {
54370
+ const objectId = getStringValue(event.object.id) || "unknown-activity";
54371
+ const groupKey = `${objectId}::${getStringValue(event.session?.id) || event.externalId}`;
54372
+ const existing = groups.get(groupKey);
54373
+ if (existing) {
54374
+ existing.push(event);
54375
+ } else {
54376
+ groups.set(groupKey, [event]);
54377
+ }
54378
+ }
54379
+ return groups;
54323
54380
  }
54324
- function mapAssessmentResultToRecentActivity(assessment, relevantCourseIds, courseIdByLineItemId) {
54325
- if (!assessment.scoreDate || !assessment.assessmentLineItem?.sourcedId) {
54381
+ function mapCaliperEventGroupToActivity(events, relevantCourseIds) {
54382
+ if (events.length === 0) {
54326
54383
  return null;
54327
54384
  }
54328
- if (isRemediationAssessmentResult(assessment)) {
54385
+ const sortedEvents = events.toSorted((a, b) => a.eventTime.localeCompare(b.eventTime));
54386
+ const activityEvent = [...sortedEvents].toReversed().find((event) => event.type === "ActivityEvent");
54387
+ const contextSource = activityEvent || sortedEvents.at(-1);
54388
+ if (!contextSource) {
54329
54389
  return null;
54330
54390
  }
54331
- const courseId = courseIdByLineItemId?.get(assessment.assessmentLineItem.sourcedId) || [...relevantCourseIds].find((course) => assessment.assessmentLineItem.sourcedId.startsWith(`${course}-`));
54332
- if (!courseId) {
54391
+ const ctx = parseCaliperEventContext(contextSource, relevantCourseIds);
54392
+ if (!ctx) {
54333
54393
  return null;
54334
54394
  }
54335
- if (isMasteryCompletionEntry(assessment)) {
54395
+ const score = activityEvent !== undefined ? (() => {
54396
+ const totalQuestions = getGeneratedMetricValue(activityEvent, "totalQuestions");
54397
+ const correctQuestions = getGeneratedMetricValue(activityEvent, "correctQuestions");
54398
+ if (totalQuestions === undefined || correctQuestions === undefined || totalQuestions <= 0) {
54399
+ return;
54400
+ }
54401
+ return correctQuestions / totalQuestions * 100;
54402
+ })() : undefined;
54403
+ const xpEarned = activityEvent !== undefined ? getGeneratedMetricValue(activityEvent, "xpEarned") : undefined;
54404
+ const masteredUnits = activityEvent !== undefined ? getGeneratedMetricValue(activityEvent, "masteredUnits") : undefined;
54405
+ const timeSpentEvents = sortedEvents.filter((event) => event.type === "TimeSpentEvent");
54406
+ let totalActiveTimeSeconds;
54407
+ if (timeSpentEvents.length > 0) {
54408
+ totalActiveTimeSeconds = timeSpentEvents.reduce((sum2, event) => sum2 + (getGeneratedMetricValue(event, "active") ?? 0), 0);
54409
+ } else if (activityEvent !== undefined) {
54410
+ totalActiveTimeSeconds = getDurationSecondsFromExtensions(activityEvent);
54411
+ }
54412
+ const fallbackActivityId = getActivityId(contextSource, getPlaycademyMetadata(contextSource));
54413
+ const occurredAt = getStringValue(activityEvent?.eventTime) || getStringValue(sortedEvents.at(-1)?.eventTime);
54414
+ const runId = getCanonicalRunId(contextSource.session);
54415
+ const resumeIds = new Set(sortedEvents.map((event) => getResumeId(event)).filter((resumeId) => resumeId !== undefined));
54416
+ const sessionCount = resumeIds.size > 0 ? resumeIds.size : 1;
54417
+ const kind = activityEvent !== undefined ? "activity" : "activity-in-progress";
54418
+ if (!occurredAt) {
54336
54419
  return null;
54337
54420
  }
54338
- const metadata2 = isRecord2(assessment.metadata) ? assessment.metadata : undefined;
54339
- const activityName = getStringValue(metadata2?.activityName);
54340
- const xpEarned = typeof metadata2?.xp === "number" && Number.isFinite(metadata2.xp) ? metadata2.xp : undefined;
54341
- const masteredUnits = typeof metadata2?.masteredUnits === "number" && Number.isFinite(metadata2.masteredUnits) ? metadata2.masteredUnits : undefined;
54342
- const durationSeconds = typeof metadata2?.durationSeconds === "number" && Number.isFinite(metadata2.durationSeconds) ? metadata2.durationSeconds : undefined;
54343
54421
  return {
54344
- id: assessment.sourcedId || `${assessment.assessmentLineItem.sourcedId}:${assessment.scoreDate}`,
54345
- kind: "activity",
54346
- occurredAt: assessment.scoreDate,
54347
- courseId,
54348
- title: activityName || "Activity completed",
54349
- ...typeof assessment.score === "number" ? { score: assessment.score } : {},
54422
+ id: activityEvent?.externalId || sortedEvents.at(-1)?.externalId || events[0].externalId,
54423
+ kind,
54424
+ occurredAt,
54425
+ courseId: ctx.courseId,
54426
+ title: getStringValue(activityEvent?.object.activity?.name) || ctx.titleFromEvent || (fallbackActivityId ? kebabToTitleCase(fallbackActivityId) : "Activity completed"),
54427
+ ...ctx.activityId ? { activityId: ctx.activityId } : {},
54428
+ ...ctx.appName ? { appName: ctx.appName } : {},
54429
+ ...score !== undefined ? { score } : {},
54350
54430
  ...xpEarned !== undefined ? { xpDelta: xpEarned } : {},
54351
54431
  ...masteredUnits !== undefined ? { masteredUnitsDelta: masteredUnits } : {},
54352
- ...durationSeconds !== undefined ? { timeDeltaSeconds: durationSeconds } : {}
54432
+ ...totalActiveTimeSeconds !== undefined ? { timeDeltaSeconds: totalActiveTimeSeconds } : {},
54433
+ ...runId ? { runId } : {},
54434
+ ...sessionCount > 0 ? { sessionCount } : {}
54353
54435
  };
54354
54436
  }
54355
54437
  function parseCaliperEventContext(event, relevantCourseIds) {
@@ -54457,8 +54539,16 @@ function mapCaliperEventToRemediationActivity(event, relevantCourseIds) {
54457
54539
  }
54458
54540
  return null;
54459
54541
  }
54542
+ var REMEDIATION_OR_COMPLETION_EVENT_KINDS;
54460
54543
  var init_timeback_util = __esm(() => {
54461
54544
  init_types4();
54545
+ REMEDIATION_OR_COMPLETION_EVENT_KINDS = new Set([
54546
+ "remediation-xp",
54547
+ "remediation-time",
54548
+ "remediation-mastery",
54549
+ "course-completed",
54550
+ "course-resumed"
54551
+ ]);
54462
54552
  });
54463
54553
 
54464
54554
  class TimebackAdminService {
@@ -54467,11 +54557,9 @@ class TimebackAdminService {
54467
54557
  static RECENT_ACTIVITY_LIMIT = 20;
54468
54558
  static MAX_STUDENT_ACTIVITY_LIMIT = 200;
54469
54559
  static MAX_STUDENT_ACTIVITY_OFFSET = 1000;
54560
+ static MAX_RECENT_ACTIVITY_EVENT_FETCH = 4000;
54470
54561
  static ANALYTICS_CONCURRENCY = 8;
54471
54562
  static MASTERABLE_UNITS_CONCURRENCY = 4;
54472
- static RECENT_ACTIVITY_FETCH_CONCURRENCY = 4;
54473
- static ASSESSMENT_LINE_ITEM_PAGE_SIZE = 1000;
54474
- static ASSESSMENT_RESULT_LINE_ITEM_CHUNK_SIZE = 20;
54475
54563
  constructor(deps) {
54476
54564
  this.deps = deps;
54477
54565
  }
@@ -54479,27 +54567,6 @@ class TimebackAdminService {
54479
54567
  const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
54480
54568
  return Object.is(rounded, -0) ? 0 : rounded;
54481
54569
  }
54482
- static toAttributionEventTime(date3) {
54483
- if (!date3) {
54484
- return;
54485
- }
54486
- const match = date3.match(/^(\d{4})-(\d{2})-(\d{2})$/);
54487
- if (!match) {
54488
- throw new ValidationError("Date must be in YYYY-MM-DD format");
54489
- }
54490
- const [, yearStr, monthStr, dayStr] = match;
54491
- const year = Number(yearStr);
54492
- const month = Number(monthStr);
54493
- const day = Number(dayStr);
54494
- if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
54495
- throw new ValidationError("Date must be in YYYY-MM-DD format");
54496
- }
54497
- const eventTime = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
54498
- if (eventTime.getUTCFullYear() !== year || eventTime.getUTCMonth() + 1 !== month || eventTime.getUTCDate() !== day) {
54499
- throw new ValidationError("Date must be a valid calendar date");
54500
- }
54501
- return eventTime.toISOString();
54502
- }
54503
54570
  requireClient() {
54504
54571
  if (!this.deps.timeback) {
54505
54572
  logger16.error("Timeback client not available in context");
@@ -54647,7 +54714,7 @@ class TimebackAdminService {
54647
54714
  throw new ValidationError(`Game "${game.slug}" has an invalid deploymentUrl: ${game.deploymentUrl}`);
54648
54715
  }
54649
54716
  }
54650
- async getGameSensorUrl(gameId) {
54717
+ async getGameActivitySource(gameId) {
54651
54718
  const game = await this.deps.db.query.games.findFirst({
54652
54719
  where: eq(games.id, gameId),
54653
54720
  columns: { slug: true, deploymentUrl: true }
@@ -54655,7 +54722,17 @@ class TimebackAdminService {
54655
54722
  if (!game) {
54656
54723
  throw new NotFoundError("Game", gameId);
54657
54724
  }
54658
- return this.deriveGameSensorUrl(game);
54725
+ return {
54726
+ gameId,
54727
+ sensorUrl: this.deriveGameSensorUrl(game),
54728
+ sourceMode: this.deps.config.isLocal ? "development" : "production"
54729
+ };
54730
+ }
54731
+ static mapRecentActivityItems(events, relevantCourseIds) {
54732
+ const gameplayEvents = events.filter((event) => (event.type === "ActivityEvent" || event.type === "TimeSpentEvent") && !isCaliperRemediationOrCompletionEvent(event));
54733
+ const groupedGameplayItems = [...groupCaliperEventsByRun(gameplayEvents).values()].map((group) => mapCaliperEventGroupToActivity(group, relevantCourseIds)).filter((item) => Boolean(item));
54734
+ const remediationItems = events.map((event) => mapCaliperEventToRemediationActivity(event, relevantCourseIds)).filter((item) => Boolean(item));
54735
+ return [...groupedGameplayItems, ...remediationItems].toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt));
54659
54736
  }
54660
54737
  async getStudentEnrollmentsByCourseId(client, studentId, courseIds) {
54661
54738
  const relevantCourseIds = new Set(courseIds);
@@ -54686,101 +54763,31 @@ class TimebackAdminService {
54686
54763
  });
54687
54764
  return new Map(results);
54688
54765
  }
54689
- async listAssessmentLineItemCourseMap(client, relevantCourseIds) {
54690
- const lineItemEntries = await TimebackAdminService.runWithConcurrency([...relevantCourseIds], TimebackAdminService.RECENT_ACTIVITY_FETCH_CONCURRENCY, async (courseId) => {
54691
- const entries = [];
54692
- let offset = 0;
54693
- try {
54694
- while (true) {
54695
- const items2 = await client.oneroster.assessmentLineItems.list({
54696
- limit: TimebackAdminService.ASSESSMENT_LINE_ITEM_PAGE_SIZE,
54697
- offset,
54698
- filter: `course.sourcedId='${escapeFilterValue(courseId)}'`,
54699
- fields: "sourcedId,course"
54700
- });
54701
- for (const item of items2) {
54702
- if (item.sourcedId) {
54703
- entries.push([
54704
- item.sourcedId,
54705
- item.course?.sourcedId || courseId
54706
- ]);
54707
- }
54708
- }
54709
- if (items2.length < TimebackAdminService.ASSESSMENT_LINE_ITEM_PAGE_SIZE) {
54710
- break;
54711
- }
54712
- offset += TimebackAdminService.ASSESSMENT_LINE_ITEM_PAGE_SIZE;
54713
- }
54714
- } catch (error) {
54715
- logger16.warn("Failed to load assessment line items for course", {
54716
- courseId,
54717
- error: error instanceof Error ? error.message : String(error)
54718
- });
54719
- }
54720
- return entries;
54721
- });
54722
- return new Map(lineItemEntries.flat());
54723
- }
54724
- static buildAssessmentResultsFilter(studentId, lineItemIds) {
54725
- const studentFilter = `student.sourcedId='${escapeFilterValue(studentId)}'`;
54726
- if (lineItemIds.length === 1) {
54727
- return `${studentFilter} AND assessmentLineItem.sourcedId='${escapeFilterValue(lineItemIds[0])}'`;
54728
- }
54729
- return `${studentFilter} AND assessmentLineItem.sourcedId@'${lineItemIds.map(escapeFilterValue).join(",")}'`;
54730
- }
54731
- async listRecentAssessmentResultsForStudent(client, studentId, courseIdByLineItemId, perChunkLimit = TimebackAdminService.RECENT_ACTIVITY_LIMIT) {
54732
- const lineItemIds = [...courseIdByLineItemId.keys()];
54733
- if (lineItemIds.length === 0) {
54734
- return [];
54735
- }
54736
- const resultPages = await TimebackAdminService.runWithConcurrency(TimebackAdminService.chunkItems(lineItemIds, TimebackAdminService.ASSESSMENT_RESULT_LINE_ITEM_CHUNK_SIZE), TimebackAdminService.RECENT_ACTIVITY_FETCH_CONCURRENCY, async (lineItemChunk) => {
54737
- try {
54738
- return await client.oneroster.assessmentResults.list({
54739
- limit: perChunkLimit,
54740
- sort: "scoreDate",
54741
- orderBy: "desc",
54742
- fields: "sourcedId,assessmentLineItem,score,scoreDate,metadata",
54743
- filter: TimebackAdminService.buildAssessmentResultsFilter(studentId, lineItemChunk)
54744
- });
54745
- } catch (error) {
54746
- logger16.warn("Failed to load recent assessment results for student", {
54747
- studentId,
54748
- lineItemCount: lineItemChunk.length,
54749
- error: error instanceof Error ? error.message : String(error)
54750
- });
54751
- return [];
54752
- }
54753
- });
54754
- const uniqueResults = new Map;
54755
- for (const result of resultPages.flat()) {
54756
- const key = result.sourcedId || `${result.assessmentLineItem?.sourcedId || "unknown"}:${result.scoreDate || ""}`;
54757
- uniqueResults.set(key, result);
54758
- }
54759
- return [...uniqueResults.values()].toSorted((a, b) => (b.scoreDate || "").localeCompare(a.scoreDate || "")).slice(0, perChunkLimit);
54760
- }
54761
- async listRecentActivityForStudent(client, studentId, sensorUrl, relevantCourseIds, maxResults = TimebackAdminService.RECENT_ACTIVITY_LIMIT) {
54766
+ async listRecentActivityForStudent(client, studentId, source, relevantCourseIds, maxResults = TimebackAdminService.RECENT_ACTIVITY_LIMIT) {
54762
54767
  if (relevantCourseIds.size === 0) {
54763
54768
  return [];
54764
54769
  }
54765
- const courseIdByLineItemId = await this.listAssessmentLineItemCourseMap(client, relevantCourseIds);
54766
- const assessments = await this.listRecentAssessmentResultsForStudent(client, studentId, courseIdByLineItemId, maxResults);
54767
- const assessmentRecentItems = assessments.map((assessment) => mapAssessmentResultToRecentActivity(assessment, relevantCourseIds, courseIdByLineItemId)).filter((activity) => Boolean(activity));
54768
- let caliperRecentItems = [];
54769
54770
  try {
54770
54771
  const actorId = `${client.getBaseUrl().replace(/\/$/, "")}/ims/oneroster/rostering/v1p2/users/${studentId}`;
54772
+ const eventLimit = Math.min(Math.max(200, maxResults * 20), TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
54771
54773
  const { events } = await client.caliper.events.list({
54772
- limit: Math.max(100, maxResults),
54774
+ limit: eventLimit,
54773
54775
  actorId,
54774
- sensor: sensorUrl
54776
+ ...source.sourceMode === "production" ? { sensor: source.sensorUrl } : {},
54777
+ extensions: {
54778
+ gameId: source.gameId
54779
+ }
54775
54780
  });
54776
- caliperRecentItems = events.map((event) => mapCaliperEventToRemediationActivity(event, relevantCourseIds)).filter((item) => Boolean(item));
54781
+ return TimebackAdminService.mapRecentActivityItems(events, relevantCourseIds).slice(0, maxResults);
54777
54782
  } catch (error) {
54778
54783
  logger16.warn("Failed to load recent Caliper activity", {
54779
54784
  studentId,
54785
+ gameId: source.gameId,
54786
+ sourceMode: source.sourceMode,
54780
54787
  error: error instanceof Error ? error.message : String(error)
54781
54788
  });
54789
+ return [];
54782
54790
  }
54783
- return [...assessmentRecentItems, ...caliperRecentItems].toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt)).slice(0, maxResults);
54784
54791
  }
54785
54792
  async listStudentsForCourse(gameId, courseId, user) {
54786
54793
  const client = this.requireClient();
@@ -54877,11 +54884,11 @@ class TimebackAdminService {
54877
54884
  const safeLimit = Math.max(1, Math.min(limit, TimebackAdminService.MAX_STUDENT_ACTIVITY_LIMIT));
54878
54885
  const safeOffset = Math.max(0, Math.min(offset, TimebackAdminService.MAX_STUDENT_ACTIVITY_OFFSET));
54879
54886
  await this.deps.validateGameManagementAccess(user, gameId);
54880
- const [integration, sensorUrl] = await Promise.all([
54887
+ const [integration, gameSource] = await Promise.all([
54881
54888
  this.deps.db.query.gameTimebackIntegrations.findFirst({
54882
54889
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
54883
54890
  }),
54884
- this.getGameSensorUrl(gameId)
54891
+ this.getGameActivitySource(gameId)
54885
54892
  ]);
54886
54893
  if (!integration) {
54887
54894
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
@@ -54889,7 +54896,7 @@ class TimebackAdminService {
54889
54896
  await this.assertStudentEnrolledInCourse(client, studentId, courseId);
54890
54897
  const relevantCourseIds = new Set([courseId]);
54891
54898
  const fetchLimit = Math.min(safeOffset + safeLimit + 1, TimebackAdminService.MAX_STUDENT_ACTIVITY_OFFSET + TimebackAdminService.MAX_STUDENT_ACTIVITY_LIMIT + 1);
54892
- const allActivities = await this.listRecentActivityForStudent(client, studentId, sensorUrl, relevantCourseIds, fetchLimit);
54899
+ const allActivities = await this.listRecentActivityForStudent(client, studentId, gameSource, relevantCourseIds, fetchLimit);
54893
54900
  const activities = allActivities.slice(safeOffset, safeOffset + safeLimit);
54894
54901
  const hasMore = allActivities.length > safeOffset + safeLimit;
54895
54902
  return { activities, hasMore };
@@ -54901,7 +54908,7 @@ class TimebackAdminService {
54901
54908
  courseId: data.courseId,
54902
54909
  studentId: data.studentId,
54903
54910
  xpEarned: data.xp,
54904
- eventTime: TimebackAdminService.toAttributionEventTime(data.date),
54911
+ eventTime: resolveAdminEventTime(data),
54905
54912
  reason: data.reason,
54906
54913
  actor,
54907
54914
  appName,
@@ -54916,7 +54923,7 @@ class TimebackAdminService {
54916
54923
  courseId: data.courseId,
54917
54924
  studentId: data.studentId,
54918
54925
  activeTimeSeconds: data.seconds,
54919
- eventTime: TimebackAdminService.toAttributionEventTime(data.date),
54926
+ eventTime: resolveAdminEventTime(data),
54920
54927
  reason: data.reason,
54921
54928
  actor,
54922
54929
  appName,
@@ -54931,7 +54938,7 @@ class TimebackAdminService {
54931
54938
  courseId: data.courseId,
54932
54939
  studentId: data.studentId,
54933
54940
  masteredUnits: data.units,
54934
- eventTime: TimebackAdminService.toAttributionEventTime(data.date),
54941
+ eventTime: resolveAdminEventTime(data),
54935
54942
  reason: data.reason,
54936
54943
  actor,
54937
54944
  appName,
@@ -55142,17 +55149,6 @@ class TimebackAdminService {
55142
55149
  }));
55143
55150
  return results;
55144
55151
  }
55145
- static chunkItems(items2, chunkSize) {
55146
- if (items2.length === 0) {
55147
- return [];
55148
- }
55149
- const effectiveChunkSize = Math.max(1, chunkSize);
55150
- const chunks = [];
55151
- for (let index2 = 0;index2 < items2.length; index2 += effectiveChunkSize) {
55152
- chunks.push(items2.slice(index2, index2 + effectiveChunkSize));
55153
- }
55154
- return chunks;
55155
- }
55156
55152
  }
55157
55153
  var logger16;
55158
55154
  var init_timeback_admin_service = __esm(() => {
@@ -55164,6 +55160,7 @@ var init_timeback_admin_service = __esm(() => {
55164
55160
  init_utils6();
55165
55161
  init_src4();
55166
55162
  init_errors();
55163
+ init_timeback_admin_util();
55167
55164
  init_timeback_util();
55168
55165
  logger16 = log.scope("TimebackAdminService");
55169
55166
  });
@@ -55207,6 +55204,18 @@ var init_timeback_service = __esm(() => {
55207
55204
  static clearInFlightHeartbeatWindow(key) {
55208
55205
  this.inFlightHeartbeatWindows.delete(key);
55209
55206
  }
55207
+ static addResumeIdToExtensions(extensions, resumeId) {
55208
+ const base = extensions ?? {};
55209
+ const existingPlaycademy = base.playcademy;
55210
+ const playcademy = typeof existingPlaycademy === "object" && existingPlaycademy !== null && !Array.isArray(existingPlaycademy) ? existingPlaycademy : {};
55211
+ return {
55212
+ ...base,
55213
+ playcademy: {
55214
+ ...playcademy,
55215
+ resumeId
55216
+ }
55217
+ };
55218
+ }
55210
55219
  constructor(deps) {
55211
55220
  this.deps = deps;
55212
55221
  }
@@ -55671,6 +55680,7 @@ var init_timeback_service = __esm(() => {
55671
55680
  gameId,
55672
55681
  studentId,
55673
55682
  runId,
55683
+ resumeId,
55674
55684
  activityData,
55675
55685
  scoreData,
55676
55686
  timingData,
@@ -55682,6 +55692,7 @@ var init_timeback_service = __esm(() => {
55682
55692
  }) {
55683
55693
  const client = this.requireClient();
55684
55694
  const db2 = this.deps.db;
55695
+ const extensionsWithResumeId = TimebackService2.addResumeIdToExtensions(extensions, resumeId);
55685
55696
  await this.deps.validateDeveloperAccess(user, gameId);
55686
55697
  const integration = await db2.query.gameTimebackIntegrations.findFirst({
55687
55698
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
@@ -55691,13 +55702,14 @@ var init_timeback_service = __esm(() => {
55691
55702
  }
55692
55703
  const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
55693
55704
  const result = await client.recordProgress(integration.courseId, studentId, {
55705
+ gameId,
55694
55706
  score: scorePercentage,
55695
55707
  totalQuestions: scoreData.totalQuestions,
55696
55708
  correctQuestions: scoreData.correctQuestions,
55697
55709
  durationSeconds: timingData.durationSeconds,
55698
55710
  xpEarned,
55699
55711
  masteredUnits,
55700
- extensions,
55712
+ extensions: extensionsWithResumeId,
55701
55713
  activityId: activityData.activityId,
55702
55714
  activityName: activityData.activityName,
55703
55715
  subject: activityData.subject,
@@ -55713,6 +55725,7 @@ var init_timeback_service = __esm(() => {
55713
55725
  const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
55714
55726
  if (sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0) {
55715
55727
  await client.recordSessionEnd(integration.courseId, studentId, {
55728
+ gameId,
55716
55729
  activeTimeSeconds: sessionEndActiveSeconds,
55717
55730
  ...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {},
55718
55731
  activityId: activityData.activityId,
@@ -55723,6 +55736,7 @@ var init_timeback_service = __esm(() => {
55723
55736
  courseId: activityData.courseId,
55724
55737
  courseName: activityData.courseName,
55725
55738
  studentEmail: activityData.studentEmail,
55739
+ extensions: extensionsWithResumeId,
55726
55740
  ...runId ? { runId } : {}
55727
55741
  });
55728
55742
  }
@@ -55747,21 +55761,22 @@ var init_timeback_service = __esm(() => {
55747
55761
  gameId,
55748
55762
  studentId,
55749
55763
  runId,
55764
+ resumeId,
55750
55765
  activityData,
55751
55766
  timingData,
55752
- windowSequence,
55767
+ windowStartedAtMs,
55753
55768
  isFinal,
55754
55769
  user
55755
55770
  }) {
55756
55771
  const client = this.requireClient();
55757
55772
  const db2 = this.deps.db;
55758
- const heartbeatWindowKey = `${runId}:${windowSequence}`;
55773
+ const heartbeatWindowKey = `${runId}:${windowStartedAtMs}`;
55759
55774
  if (TimebackService2.isDuplicateHeartbeatWindow(heartbeatWindowKey)) {
55760
55775
  logger17.debug("Skipping duplicate heartbeat window", {
55761
55776
  gameId,
55762
55777
  studentId,
55763
55778
  runId,
55764
- windowSequence,
55779
+ windowStartedAtMs,
55765
55780
  isFinal
55766
55781
  });
55767
55782
  return { status: "ok" };
@@ -55773,7 +55788,7 @@ var init_timeback_service = __esm(() => {
55773
55788
  gameId,
55774
55789
  studentId,
55775
55790
  runId,
55776
- windowSequence,
55791
+ windowStartedAtMs,
55777
55792
  isFinal
55778
55793
  });
55779
55794
  return inFlightHeartbeat;
@@ -55789,6 +55804,7 @@ var init_timeback_service = __esm(() => {
55789
55804
  const inactiveTimeSeconds = timingData.pausedMs / 1000;
55790
55805
  if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
55791
55806
  await client.recordSessionEnd(integration.courseId, studentId, {
55807
+ gameId,
55792
55808
  activeTimeSeconds,
55793
55809
  ...inactiveTimeSeconds > 0 ? { inactiveTimeSeconds } : {},
55794
55810
  activityId: activityData.activityId,
@@ -55799,6 +55815,7 @@ var init_timeback_service = __esm(() => {
55799
55815
  courseId: activityData.courseId,
55800
55816
  courseName: activityData.courseName,
55801
55817
  studentEmail: activityData.studentEmail,
55818
+ extensions: TimebackService2.addResumeIdToExtensions(undefined, resumeId),
55802
55819
  ...runId ? { runId } : {}
55803
55820
  });
55804
55821
  }
@@ -55808,7 +55825,7 @@ var init_timeback_service = __esm(() => {
55808
55825
  courseId: integration.courseId,
55809
55826
  studentId,
55810
55827
  runId,
55811
- windowSequence,
55828
+ windowStartedAtMs,
55812
55829
  activeTimeSeconds,
55813
55830
  isFinal
55814
55831
  });
@@ -55959,6 +55976,7 @@ function createPlatformServices(deps) {
55959
55976
  validateGameManagementAccess
55960
55977
  });
55961
55978
  const timebackAdmin = new TimebackAdminService({
55979
+ config: config2,
55962
55980
  db: db2,
55963
55981
  timeback: timebackClient,
55964
55982
  validateDeveloperAccess,
@@ -58889,6 +58907,16 @@ async function requestCaliper(options) {
58889
58907
  baseUrl: caliperUrl
58890
58908
  });
58891
58909
  }
58910
+ function buildEventExtensions({
58911
+ eventExtensions,
58912
+ gameId
58913
+ }) {
58914
+ const mergedExtensions = {
58915
+ ...eventExtensions,
58916
+ ...gameId ? { gameId } : {}
58917
+ };
58918
+ return Object.keys(mergedExtensions).length > 0 ? mergedExtensions : undefined;
58919
+ }
58892
58920
  function createCaliperNamespace(client) {
58893
58921
  const urls = createOneRosterUrls(client.getBaseUrl());
58894
58922
  const caliper = {
@@ -58933,11 +58961,20 @@ function createCaliperNamespace(client) {
58933
58961
  if (params.actorEmail) {
58934
58962
  query.set("actorEmail", params.actorEmail);
58935
58963
  }
58964
+ if (params.extensions) {
58965
+ for (const [key, value] of Object.entries(params.extensions)) {
58966
+ query.set(`extensions.${key}`, value);
58967
+ }
58968
+ }
58936
58969
  const requestPath = `${CALIPER_ENDPOINTS4.events}?${query.toString()}`;
58937
58970
  return client["requestCaliper"](requestPath, "GET");
58938
58971
  }
58939
58972
  },
58940
58973
  emitActivityEvent: async (data) => {
58974
+ const eventExtensions = buildEventExtensions({
58975
+ eventExtensions: data.eventExtensions,
58976
+ gameId: data.gameId
58977
+ });
58941
58978
  const event = {
58942
58979
  "@context": CALIPER_CONSTANTS4.context,
58943
58980
  id: `urn:uuid:${crypto.randomUUID()}`,
@@ -58997,11 +59034,15 @@ function createCaliperNamespace(client) {
58997
59034
  }
58998
59035
  } : {}
58999
59036
  },
59000
- ...data.eventExtensions ? { extensions: data.eventExtensions } : {}
59037
+ ...eventExtensions ? { extensions: eventExtensions } : {}
59001
59038
  };
59002
59039
  return caliper.emit(event, data.sensorUrl);
59003
59040
  },
59004
59041
  emitTimeSpentEvent: async (data) => {
59042
+ const eventExtensions = buildEventExtensions({
59043
+ eventExtensions: data.eventExtensions,
59044
+ gameId: data.gameId
59045
+ });
59005
59046
  const event = {
59006
59047
  "@context": CALIPER_CONSTANTS4.context,
59007
59048
  id: `urn:uuid:${crypto.randomUUID()}`,
@@ -59042,7 +59083,8 @@ function createCaliperNamespace(client) {
59042
59083
  ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES4.waste, value: data.wasteTimeSeconds }] : []
59043
59084
  ],
59044
59085
  ...data.extensions ? { extensions: data.extensions } : {}
59045
- }
59086
+ },
59087
+ ...eventExtensions ? { extensions: eventExtensions } : {}
59046
59088
  };
59047
59089
  return caliper.emit(event, data.sensorUrl);
59048
59090
  },
@@ -59535,6 +59577,7 @@ class AdminEventRecorder {
59535
59577
  await this.caliper.emitActivityEvent({
59536
59578
  studentId: ctx.student.id,
59537
59579
  studentEmail: ctx.student.email,
59580
+ gameId: data.gameId,
59538
59581
  activityId: ctx.activityId,
59539
59582
  activityName: data.activityName || `Manual XP Assignment - ${ctx.courseContext.subject}`,
59540
59583
  courseId: data.courseId,
@@ -59561,6 +59604,7 @@ class AdminEventRecorder {
59561
59604
  await this.caliper.emitTimeSpentEvent({
59562
59605
  studentId: ctx.student.id,
59563
59606
  studentEmail: ctx.student.email,
59607
+ gameId: data.gameId,
59564
59608
  activityId: ctx.activityId,
59565
59609
  activityName: data.activityName || "Playcademy Admin Time Adjustment",
59566
59610
  courseId: data.courseId,
@@ -59582,6 +59626,7 @@ class AdminEventRecorder {
59582
59626
  await this.caliper.emitActivityEvent({
59583
59627
  studentId: ctx.student.id,
59584
59628
  studentEmail: ctx.student.email,
59629
+ gameId: data.gameId,
59585
59630
  activityId: ctx.activityId,
59586
59631
  activityName: data.activityName || "Playcademy Admin Mastery Adjustment",
59587
59632
  courseId: data.courseId,
@@ -59607,6 +59652,7 @@ class AdminEventRecorder {
59607
59652
  await this.caliper.emitActivityEvent({
59608
59653
  studentId: ctx.student.id,
59609
59654
  studentEmail: ctx.student.email,
59655
+ gameId: data.gameId,
59610
59656
  activityId: ctx.activityId,
59611
59657
  activityName: isResume ? "Course resumed" : "Course marked complete",
59612
59658
  courseId: data.courseId,
@@ -60055,15 +60101,13 @@ class ProgressRecorder {
60055
60101
  studentId,
60056
60102
  attemptNumber: currentAttemptNumber,
60057
60103
  score,
60058
- totalQuestions,
60059
- correctQuestions,
60060
60104
  xp: calculatedXp,
60061
- masteredUnits,
60062
60105
  scoreStatus,
60063
60106
  inProgress,
60064
60107
  appName: progressData.appName,
60065
- activityName,
60066
- durationSeconds: progressData.durationSeconds
60108
+ totalQuestions,
60109
+ correctQuestions,
60110
+ masteredUnits
60067
60111
  });
60068
60112
  } else {
60069
60113
  log.warn("[ProgressRecorder] Score not provided, skipping gradebook entry", {
@@ -60077,6 +60121,7 @@ class ProgressRecorder {
60077
60121
  await this.emitCourseCompletionHistoryEvent({
60078
60122
  studentId,
60079
60123
  studentEmail,
60124
+ gameId: progressData.gameId,
60080
60125
  activityId,
60081
60126
  courseId: ids.course,
60082
60127
  courseName,
@@ -60088,6 +60133,7 @@ class ProgressRecorder {
60088
60133
  await this.emitCaliperEvent({
60089
60134
  studentId,
60090
60135
  studentEmail,
60136
+ gameId: progressData.gameId,
60091
60137
  activityId,
60092
60138
  activityName,
60093
60139
  courseId: ids.course,
@@ -60189,15 +60235,13 @@ class ProgressRecorder {
60189
60235
  studentId,
60190
60236
  attemptNumber,
60191
60237
  score,
60192
- totalQuestions,
60193
- correctQuestions,
60194
60238
  xp,
60195
- masteredUnits,
60196
60239
  scoreStatus,
60197
60240
  inProgress,
60198
60241
  appName,
60199
- activityName,
60200
- durationSeconds
60242
+ totalQuestions,
60243
+ correctQuestions,
60244
+ masteredUnits
60201
60245
  }) {
60202
60246
  const timestamp3 = Date.now().toString(36);
60203
60247
  const resultId = `${lineItemId}:${studentId}:${timestamp3}`;
@@ -60212,21 +60256,18 @@ class ProgressRecorder {
60212
60256
  inProgress,
60213
60257
  metadata: {
60214
60258
  xp,
60215
- totalQuestions,
60216
- correctQuestions,
60217
- accuracy: totalQuestions && correctQuestions ? correctQuestions / totalQuestions * 100 : undefined,
60218
60259
  attemptNumber,
60219
- lastUpdated: new Date().toISOString(),
60220
- masteredUnits,
60221
60260
  appName,
60222
- activityName,
60223
- durationSeconds
60261
+ ...totalQuestions !== undefined ? { totalQuestions } : {},
60262
+ ...correctQuestions !== undefined ? { correctQuestions } : {},
60263
+ ...masteredUnits !== undefined ? { masteredUnits } : {}
60224
60264
  }
60225
60265
  });
60226
60266
  }
60227
60267
  async emitCaliperEvent({
60228
60268
  studentId,
60229
60269
  studentEmail,
60270
+ gameId,
60230
60271
  activityId,
60231
60272
  activityName,
60232
60273
  courseId,
@@ -60243,6 +60284,7 @@ class ProgressRecorder {
60243
60284
  await this.caliperNamespace.emitActivityEvent({
60244
60285
  studentId,
60245
60286
  studentEmail,
60287
+ gameId,
60246
60288
  activityId,
60247
60289
  activityName,
60248
60290
  courseId,
@@ -60265,6 +60307,7 @@ class ProgressRecorder {
60265
60307
  await this.caliperNamespace.emitActivityEvent({
60266
60308
  studentId: data.studentId,
60267
60309
  studentEmail: data.studentEmail,
60310
+ gameId: data.gameId,
60268
60311
  activityId: data.activityId,
60269
60312
  activityName: "Course completed",
60270
60313
  courseId: data.courseId,
@@ -60314,6 +60357,7 @@ class SessionRecorder {
60314
60357
  await this.caliperNamespace.emitTimeSpentEvent({
60315
60358
  studentId,
60316
60359
  studentEmail,
60360
+ gameId: sessionData.gameId,
60317
60361
  activityId,
60318
60362
  activityName,
60319
60363
  courseId: ids.course,
@@ -118902,18 +118946,23 @@ async function seedCoreGames(db2) {
118902
118946
  }
118903
118947
  async function seedCurrentProjectGame(db2, project) {
118904
118948
  const now2 = new Date;
118949
+ const desiredGameId = project.gameId?.trim() || undefined;
118905
118950
  try {
118906
118951
  const existingGame = await db2.query.games.findFirst({
118907
- where: (row, { eq: eq3 }) => eq3(row.slug, project.slug)
118952
+ where: (row, operators) => operators.eq(row.slug, project.slug)
118908
118953
  });
118909
118954
  if (existingGame) {
118910
- if (project.timebackCourses && project.timebackCourses.length > 0) {
118911
- await seedTimebackIntegrations(db2, existingGame.id, project.timebackCourses);
118955
+ if (desiredGameId && existingGame.id !== desiredGameId) {
118956
+ await db2.delete(games).where(eq(games.id, existingGame.id));
118957
+ } else {
118958
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
118959
+ await seedTimebackIntegrations(db2, existingGame.id, project.timebackCourses);
118960
+ }
118961
+ return existingGame;
118912
118962
  }
118913
- return existingGame;
118914
118963
  }
118915
118964
  const gameRecord = {
118916
- id: crypto.randomUUID(),
118965
+ id: desiredGameId ?? crypto.randomUUID(),
118917
118966
  developerId: DEMO_USERS.developer.id,
118918
118967
  slug: project.slug,
118919
118968
  displayName: project.displayName,
@@ -118942,6 +118991,7 @@ async function seedCurrentProjectGame(db2, project) {
118942
118991
  }
118943
118992
  }
118944
118993
  var init_games = __esm(() => {
118994
+ init_drizzle_orm();
118945
118995
  init_src();
118946
118996
  init_tables_index();
118947
118997
  init_constants();
@@ -120340,6 +120390,7 @@ var init_schemas11 = __esm(() => {
120340
120390
  gameId: exports_external.string().uuid(),
120341
120391
  studentId: exports_external.string().min(1),
120342
120392
  runId: exports_external.string().uuid().optional(),
120393
+ resumeId: exports_external.string().uuid(),
120343
120394
  activityData: TimebackActivityDataSchema,
120344
120395
  scoreData: exports_external.object({
120345
120396
  correctQuestions: exports_external.number().int().min(0),
@@ -120360,12 +120411,13 @@ var init_schemas11 = __esm(() => {
120360
120411
  gameId: exports_external.string().uuid(),
120361
120412
  studentId: exports_external.string().min(1),
120362
120413
  runId: exports_external.string().uuid(),
120414
+ resumeId: exports_external.string().uuid(),
120363
120415
  activityData: TimebackActivityDataSchema,
120364
120416
  timingData: exports_external.object({
120365
120417
  activeMs: exports_external.number().nonnegative(),
120366
120418
  pausedMs: exports_external.number().nonnegative()
120367
120419
  }),
120368
- windowSequence: exports_external.number().int().nonnegative(),
120420
+ windowStartedAtMs: exports_external.number().int().nonnegative(),
120369
120421
  isFinal: exports_external.boolean().optional()
120370
120422
  });
120371
120423
  PopulateStudentRequestSchema = exports_external.object({
@@ -120447,15 +120499,18 @@ var init_schemas11 = __esm(() => {
120447
120499
  });
120448
120500
  GrantTimebackXpRequestSchema = AdminTimebackMutationBaseSchema.extend({
120449
120501
  xp: exports_external.number().min(-100, "Amount must be between -100 and 100").max(100, "Amount must be between -100 and 100").refine((value) => value !== 0, { message: "Amount cannot be 0" }),
120450
- date: AdminAttributionDateSchema.optional()
120502
+ date: AdminAttributionDateSchema.optional(),
120503
+ useCurrentTime: exports_external.boolean().optional()
120451
120504
  });
120452
120505
  AdjustTimebackTimeRequestSchema = AdminTimebackMutationBaseSchema.extend({
120453
120506
  seconds: exports_external.number().refine((value) => value !== 0, { message: "Time amount cannot be 0" }),
120454
- date: AdminAttributionDateSchema.optional()
120507
+ date: AdminAttributionDateSchema.optional(),
120508
+ useCurrentTime: exports_external.boolean().optional()
120455
120509
  });
120456
120510
  AdjustTimebackMasteryRequestSchema = AdminTimebackMutationBaseSchema.extend({
120457
120511
  units: exports_external.number().refine((value) => value !== 0, { message: "Units cannot be 0" }),
120458
- date: AdminAttributionDateSchema.optional()
120512
+ date: AdminAttributionDateSchema.optional(),
120513
+ useCurrentTime: exports_external.boolean().optional()
120459
120514
  });
120460
120515
  ToggleCourseCompletionRequestSchema = exports_external.object({
120461
120516
  gameId: exports_external.string().uuid(),
@@ -122836,6 +122891,7 @@ var init_timeback_controller = __esm(() => {
122836
122891
  gameId,
122837
122892
  studentId,
122838
122893
  runId,
122894
+ resumeId,
122839
122895
  activityData,
122840
122896
  scoreData,
122841
122897
  timingData,
@@ -122849,6 +122905,7 @@ var init_timeback_controller = __esm(() => {
122849
122905
  gameId,
122850
122906
  studentId,
122851
122907
  runId,
122908
+ resumeId,
122852
122909
  activityData,
122853
122910
  scoreData,
122854
122911
  timingData,
@@ -122872,12 +122929,22 @@ var init_timeback_controller = __esm(() => {
122872
122929
  }
122873
122930
  throw ApiError.badRequest("Invalid JSON body");
122874
122931
  }
122875
- const { gameId, studentId, runId, activityData, timingData, windowSequence, isFinal } = body2;
122932
+ const {
122933
+ gameId,
122934
+ studentId,
122935
+ runId,
122936
+ resumeId,
122937
+ activityData,
122938
+ timingData,
122939
+ windowStartedAtMs,
122940
+ isFinal
122941
+ } = body2;
122876
122942
  logger63.debug("Recording heartbeat", {
122877
122943
  userId: ctx.user.id,
122878
122944
  gameId,
122879
122945
  runId,
122880
- windowSequence,
122946
+ resumeId,
122947
+ windowStartedAtMs,
122881
122948
  activeMs: timingData.activeMs,
122882
122949
  isFinal
122883
122950
  });
@@ -122885,9 +122952,10 @@ var init_timeback_controller = __esm(() => {
122885
122952
  gameId,
122886
122953
  studentId,
122887
122954
  runId,
122955
+ resumeId,
122888
122956
  activityData,
122889
122957
  timingData,
122890
- windowSequence,
122958
+ windowStartedAtMs,
122891
122959
  isFinal,
122892
122960
  user: ctx.user
122893
122961
  });
@@ -124471,6 +124539,203 @@ function printBanner(viteConfig, options) {
124471
124539
  import fs5 from "node:fs";
124472
124540
  import path3 from "node:path";
124473
124541
  import { loadPlaycademyConfig } from "playcademy/utils";
124542
+
124543
+ // ../utils/src/uuid.ts
124544
+ var UUID_REGEX2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
124545
+ function isValidUUID2(value) {
124546
+ if (!value || typeof value !== "string") {
124547
+ return false;
124548
+ }
124549
+ return UUID_REGEX2.test(value);
124550
+ }
124551
+ // ../utils/src/ansi.ts
124552
+ var colors3 = {
124553
+ black: "\x1B[30m",
124554
+ red: "\x1B[31m",
124555
+ green: "\x1B[32m",
124556
+ yellow: "\x1B[33m",
124557
+ blue: "\x1B[34m",
124558
+ magenta: "\x1B[35m",
124559
+ cyan: "\x1B[36m",
124560
+ white: "\x1B[37m",
124561
+ gray: "\x1B[90m"
124562
+ };
124563
+ var styles3 = {
124564
+ reset: "\x1B[0m",
124565
+ bold: "\x1B[1m",
124566
+ dim: "\x1B[2m",
124567
+ italic: "\x1B[3m",
124568
+ underline: "\x1B[4m"
124569
+ };
124570
+ var cursor2 = {
124571
+ hide: "\x1B[?25l",
124572
+ show: "\x1B[?25h",
124573
+ up: (n3) => `\x1B[${n3}A`,
124574
+ down: (n3) => `\x1B[${n3}B`,
124575
+ forward: (n3) => `\x1B[${n3}C`,
124576
+ back: (n3) => `\x1B[${n3}D`,
124577
+ clearLine: "\x1B[K",
124578
+ clearScreen: "\x1B[2J",
124579
+ home: "\x1B[H"
124580
+ };
124581
+ var isInteractive2 = typeof process !== "undefined" && process.stdout?.isTTY && !process.env.CI && process.env.TERM !== "dumb";
124582
+ function stripAnsi2(text2) {
124583
+ return text2.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
124584
+ }
124585
+
124586
+ // ../utils/src/spinner.ts
124587
+ import { stdout as stdout2 } from "process";
124588
+ var SPINNER_FRAMES2 = [
124589
+ 10251,
124590
+ 10265,
124591
+ 10297,
124592
+ 10296,
124593
+ 10300,
124594
+ 10292,
124595
+ 10278,
124596
+ 10279,
124597
+ 10247,
124598
+ 10255
124599
+ ].map((code) => String.fromCodePoint(code));
124600
+ var CHECK_MARK2 = String.fromCodePoint(10004);
124601
+ var CROSS_MARK2 = String.fromCodePoint(10006);
124602
+ var CANCEL_MARK2 = String.fromCodePoint(9675);
124603
+ var SPINNER_INTERVAL2 = 80;
124604
+
124605
+ class Spinner3 {
124606
+ tasks = new Map;
124607
+ frameIndex = 0;
124608
+ intervalId = null;
124609
+ renderCount = 0;
124610
+ previousLineCount = 0;
124611
+ printedTasks = new Set;
124612
+ indent;
124613
+ constructor(taskIds, texts, options) {
124614
+ this.indent = options?.indent ?? 0;
124615
+ taskIds.forEach((id, index6) => {
124616
+ this.tasks.set(id, {
124617
+ text: texts[index6] || "",
124618
+ status: "pending"
124619
+ });
124620
+ });
124621
+ }
124622
+ start() {
124623
+ if (isInteractive2) {
124624
+ stdout2.write(cursor2.hide);
124625
+ this.render();
124626
+ this.intervalId = setInterval(() => {
124627
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES2.length;
124628
+ this.render();
124629
+ }, SPINNER_INTERVAL2);
124630
+ }
124631
+ }
124632
+ clear() {
124633
+ if (this.intervalId) {
124634
+ clearInterval(this.intervalId);
124635
+ this.intervalId = null;
124636
+ }
124637
+ if (isInteractive2 && this.previousLineCount > 0) {
124638
+ stdout2.write(cursor2.up(this.previousLineCount));
124639
+ for (let i3 = 0;i3 < this.previousLineCount; i3++) {
124640
+ stdout2.write(`\r${cursor2.clearLine}
124641
+ `);
124642
+ }
124643
+ stdout2.write(cursor2.up(this.previousLineCount));
124644
+ stdout2.write(cursor2.show);
124645
+ }
124646
+ this.previousLineCount = 0;
124647
+ }
124648
+ updateTask(taskId, status, finalText) {
124649
+ const task = this.tasks.get(taskId);
124650
+ if (task) {
124651
+ task.status = status;
124652
+ if (finalText) {
124653
+ task.finalText = finalText;
124654
+ }
124655
+ if (!isInteractive2) {
124656
+ this.renderNonInteractive(taskId, task);
124657
+ }
124658
+ }
124659
+ }
124660
+ renderNonInteractive(taskId, task) {
124661
+ const key = `${taskId}-${task.status}`;
124662
+ if (this.printedTasks.has(key)) {
124663
+ return;
124664
+ }
124665
+ this.printedTasks.add(key);
124666
+ const indentStr = " ".repeat(this.indent);
124667
+ let line2 = "";
124668
+ switch (task.status) {
124669
+ case "running": {
124670
+ line2 = `${indentStr}[RUNNING] ${stripAnsi2(task.text)}`;
124671
+ break;
124672
+ }
124673
+ case "success": {
124674
+ line2 = `${indentStr}[SUCCESS] ${stripAnsi2(task.finalText || task.text)}`;
124675
+ break;
124676
+ }
124677
+ case "error": {
124678
+ line2 = `${indentStr}[ERROR] Failed: ${stripAnsi2(task.text)}`;
124679
+ break;
124680
+ }
124681
+ case "cancelled": {
124682
+ line2 = `${indentStr}[CANCELLED] ${stripAnsi2(task.finalText || task.text)}`;
124683
+ break;
124684
+ }
124685
+ }
124686
+ console.log(line2);
124687
+ }
124688
+ render() {
124689
+ if (this.previousLineCount > 0) {
124690
+ stdout2.write(cursor2.up(this.previousLineCount));
124691
+ }
124692
+ const spinner = SPINNER_FRAMES2[this.frameIndex];
124693
+ const indentStr = " ".repeat(this.indent);
124694
+ const visibleTasks = [...this.tasks.values()].filter((task) => task.status !== "pending");
124695
+ for (const task of visibleTasks) {
124696
+ stdout2.write(`\r${cursor2.clearLine}`);
124697
+ let line2 = "";
124698
+ switch (task.status) {
124699
+ case "running": {
124700
+ line2 = `${indentStr}${colors3.blue}${spinner}${styles3.reset} ${task.text}`;
124701
+ break;
124702
+ }
124703
+ case "success": {
124704
+ line2 = `${indentStr}${colors3.green}${CHECK_MARK2}${styles3.reset} ${task.finalText || task.text}`;
124705
+ break;
124706
+ }
124707
+ case "error": {
124708
+ line2 = `${indentStr}${colors3.red}${CROSS_MARK2}${styles3.reset} Failed: ${task.text}`;
124709
+ break;
124710
+ }
124711
+ case "cancelled": {
124712
+ line2 = `${indentStr}${colors3.gray}${CANCEL_MARK2}${styles3.reset} Cancelled: ${task.finalText || task.text}`;
124713
+ break;
124714
+ }
124715
+ }
124716
+ console.log(line2);
124717
+ }
124718
+ this.previousLineCount = visibleTasks.length;
124719
+ this.renderCount++;
124720
+ }
124721
+ stop() {
124722
+ if (this.intervalId) {
124723
+ clearInterval(this.intervalId);
124724
+ this.intervalId = null;
124725
+ }
124726
+ if (isInteractive2) {
124727
+ this.render();
124728
+ stdout2.write(cursor2.show);
124729
+ } else {
124730
+ this.tasks.forEach((task, taskId) => {
124731
+ if (task.status !== "pending") {
124732
+ this.renderNonInteractive(taskId, task);
124733
+ }
124734
+ });
124735
+ }
124736
+ }
124737
+ }
124738
+ // src/lib/sandbox/project-info.ts
124474
124739
  function extractTimebackCourses(config2, timebackOptions) {
124475
124740
  const courses = config2?.integrations?.timeback?.courses;
124476
124741
  if (!courses || courses.length === 0) {
@@ -124504,17 +124769,20 @@ async function extractProjectInfo(viteConfig, timebackOptions) {
124504
124769
  packageJson = JSON.parse(packageJsonContent);
124505
124770
  }
124506
124771
  } catch {}
124507
- const name2 = config2?.name || packageJson.name || "";
124508
- let slug = name2;
124509
- if (slug.includes("/")) {
124510
- slug = slug.split("/")[1] || slug;
124772
+ const name4 = config2?.name || packageJson.name || "";
124773
+ let slug2 = name4;
124774
+ if (slug2.includes("/")) {
124775
+ slug2 = slug2.split("/")[1] || slug2;
124511
124776
  }
124512
- if (!slug) {
124513
- slug = directoryName;
124777
+ if (!slug2) {
124778
+ slug2 = directoryName;
124514
124779
  }
124515
- const displayName = slug.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
124780
+ const displayName = slug2.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
124781
+ const envGameId = process.env.SANDBOX_GAME_ID;
124782
+ const gameId = envGameId && isValidUUID2(envGameId) ? envGameId : undefined;
124516
124783
  return {
124517
- slug,
124784
+ gameId,
124785
+ slug: slug2,
124518
124786
  displayName,
124519
124787
  version: packageJson.version || "dev",
124520
124788
  description: packageJson.description,
@@ -125549,7 +125817,7 @@ var DEBOUNCE_MS = 500;
125549
125817
  var VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.js", "vite.config.mjs"];
125550
125818
  var debounceTimer = null;
125551
125819
  function findExistingFiles(projectRoot, fileNames) {
125552
- return fileNames.map((name2) => path4.join(projectRoot, name2)).filter((file) => fs7.existsSync(file));
125820
+ return fileNames.map((name4) => path4.join(projectRoot, name4)).filter((file) => fs7.existsSync(file));
125553
125821
  }
125554
125822
  function createChangeHandler(server, viteConfig, platformModeOptions, watchedFiles) {
125555
125823
  return async (changedPath) => {
@@ -125617,7 +125885,7 @@ function cyclePlatformRoleHotkey(options) {
125617
125885
 
125618
125886
  // src/server/hotkeys/cycle-timeback-role.ts
125619
125887
  var import_picocolors9 = __toESM(require_picocolors(), 1);
125620
- var { bold: bold4, cyan: cyan4, green: green4, red: red3, yellow: yellow3 } = import_picocolors9.default;
125888
+ var { bold: bold5, cyan: cyan4, green: green4, red: red3, yellow: yellow3 } = import_picocolors9.default;
125621
125889
  function cycleTimebackRole(logger) {
125622
125890
  const currentRole = getTimebackRoleOverride() ?? "student";
125623
125891
  const currentIndex = TIMEBACK_ROLES.indexOf(currentRole);
@@ -125636,14 +125904,14 @@ function cycleTimebackRole(logger) {
125636
125904
  function cycleTimebackRoleHotkey(options) {
125637
125905
  return {
125638
125906
  key: "t",
125639
- description: `${cyan4(bold4("[playcademy]"))} cycle Timeback role`,
125907
+ description: `${cyan4(bold5("[playcademy]"))} cycle Timeback role`,
125640
125908
  action: () => cycleTimebackRole(options.viteConfig.logger)
125641
125909
  };
125642
125910
  }
125643
125911
 
125644
125912
  // src/server/hotkeys/recreate-database.ts
125645
125913
  var import_picocolors10 = __toESM(require_picocolors(), 1);
125646
- var { bold: bold5, cyan: cyan5 } = import_picocolors10.default;
125914
+ var { bold: bold6, cyan: cyan5 } = import_picocolors10.default;
125647
125915
  async function recreateSandboxDatabase(options) {
125648
125916
  await recreateSandbox({
125649
125917
  viteConfig: options.viteConfig,
@@ -125653,7 +125921,7 @@ async function recreateSandboxDatabase(options) {
125653
125921
  function recreateDatabaseHotkey(options) {
125654
125922
  return {
125655
125923
  key: "d",
125656
- description: `${cyan5(bold5("[playcademy]"))} recreate sandbox database`,
125924
+ description: `${cyan5(bold6("[playcademy]"))} recreate sandbox database`,
125657
125925
  action: () => recreateSandboxDatabase(options)
125658
125926
  };
125659
125927
  }
@@ -125663,7 +125931,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
125663
125931
  // package.json
125664
125932
  var package_default2 = {
125665
125933
  name: "@playcademy/vite-plugin",
125666
- version: "0.2.24-beta.5",
125934
+ version: "0.2.24-beta.7",
125667
125935
  type: "module",
125668
125936
  exports: {
125669
125937
  ".": {
@@ -77,6 +77,7 @@ export interface TimebackPluginContext {
77
77
  * Project information extracted from package.json and directory structure
78
78
  */
79
79
  export interface ProjectInfo {
80
+ gameId?: string;
80
81
  slug: string;
81
82
  displayName: string;
82
83
  version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "0.2.24-beta.5",
3
+ "version": "0.2.24-beta.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {