@playcademy/vite-plugin 0.2.24-beta.6 → 0.2.24-beta.8

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.9",
25339
+ version: "0.3.17-beta.11",
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,8 @@ var init_timeback_service = __esm(() => {
55682
55692
  }) {
55683
55693
  const client = this.requireClient();
55684
55694
  const db2 = this.deps.db;
55695
+ const effectiveResumeId = resumeId ?? runId ?? crypto.randomUUID();
55696
+ const extensionsWithResumeId = TimebackService2.addResumeIdToExtensions(extensions, effectiveResumeId);
55685
55697
  await this.deps.validateDeveloperAccess(user, gameId);
55686
55698
  const integration = await db2.query.gameTimebackIntegrations.findFirst({
55687
55699
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
@@ -55691,13 +55703,14 @@ var init_timeback_service = __esm(() => {
55691
55703
  }
55692
55704
  const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
55693
55705
  const result = await client.recordProgress(integration.courseId, studentId, {
55706
+ gameId,
55694
55707
  score: scorePercentage,
55695
55708
  totalQuestions: scoreData.totalQuestions,
55696
55709
  correctQuestions: scoreData.correctQuestions,
55697
55710
  durationSeconds: timingData.durationSeconds,
55698
55711
  xpEarned,
55699
55712
  masteredUnits,
55700
- extensions,
55713
+ extensions: extensionsWithResumeId,
55701
55714
  activityId: activityData.activityId,
55702
55715
  activityName: activityData.activityName,
55703
55716
  subject: activityData.subject,
@@ -55713,6 +55726,7 @@ var init_timeback_service = __esm(() => {
55713
55726
  const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
55714
55727
  if (sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0) {
55715
55728
  await client.recordSessionEnd(integration.courseId, studentId, {
55729
+ gameId,
55716
55730
  activeTimeSeconds: sessionEndActiveSeconds,
55717
55731
  ...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {},
55718
55732
  activityId: activityData.activityId,
@@ -55723,6 +55737,7 @@ var init_timeback_service = __esm(() => {
55723
55737
  courseId: activityData.courseId,
55724
55738
  courseName: activityData.courseName,
55725
55739
  studentEmail: activityData.studentEmail,
55740
+ extensions: extensionsWithResumeId,
55726
55741
  ...runId ? { runId } : {}
55727
55742
  });
55728
55743
  }
@@ -55747,20 +55762,29 @@ var init_timeback_service = __esm(() => {
55747
55762
  gameId,
55748
55763
  studentId,
55749
55764
  runId,
55765
+ resumeId,
55750
55766
  activityData,
55751
55767
  timingData,
55768
+ windowStartedAtMs,
55752
55769
  windowSequence,
55753
55770
  isFinal,
55754
55771
  user
55755
55772
  }) {
55756
55773
  const client = this.requireClient();
55757
55774
  const db2 = this.deps.db;
55758
- const heartbeatWindowKey = `${runId}:${windowSequence}`;
55775
+ const hasWindowStartedAtMs = windowStartedAtMs !== undefined;
55776
+ const hasWindowSequence = windowSequence !== undefined;
55777
+ if (hasWindowStartedAtMs === hasWindowSequence) {
55778
+ throw new ValidationError("Provide exactly one of windowStartedAtMs or windowSequence");
55779
+ }
55780
+ const heartbeatWindowKey = hasWindowStartedAtMs ? `${runId}:t:${windowStartedAtMs}` : `${runId}:s:${windowSequence}`;
55781
+ const effectiveResumeId = resumeId ?? runId;
55759
55782
  if (TimebackService2.isDuplicateHeartbeatWindow(heartbeatWindowKey)) {
55760
55783
  logger17.debug("Skipping duplicate heartbeat window", {
55761
55784
  gameId,
55762
55785
  studentId,
55763
55786
  runId,
55787
+ windowStartedAtMs,
55764
55788
  windowSequence,
55765
55789
  isFinal
55766
55790
  });
@@ -55773,6 +55797,7 @@ var init_timeback_service = __esm(() => {
55773
55797
  gameId,
55774
55798
  studentId,
55775
55799
  runId,
55800
+ windowStartedAtMs,
55776
55801
  windowSequence,
55777
55802
  isFinal
55778
55803
  });
@@ -55789,6 +55814,7 @@ var init_timeback_service = __esm(() => {
55789
55814
  const inactiveTimeSeconds = timingData.pausedMs / 1000;
55790
55815
  if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
55791
55816
  await client.recordSessionEnd(integration.courseId, studentId, {
55817
+ gameId,
55792
55818
  activeTimeSeconds,
55793
55819
  ...inactiveTimeSeconds > 0 ? { inactiveTimeSeconds } : {},
55794
55820
  activityId: activityData.activityId,
@@ -55799,6 +55825,7 @@ var init_timeback_service = __esm(() => {
55799
55825
  courseId: activityData.courseId,
55800
55826
  courseName: activityData.courseName,
55801
55827
  studentEmail: activityData.studentEmail,
55828
+ extensions: TimebackService2.addResumeIdToExtensions(undefined, effectiveResumeId),
55802
55829
  ...runId ? { runId } : {}
55803
55830
  });
55804
55831
  }
@@ -55808,6 +55835,7 @@ var init_timeback_service = __esm(() => {
55808
55835
  courseId: integration.courseId,
55809
55836
  studentId,
55810
55837
  runId,
55838
+ windowStartedAtMs,
55811
55839
  windowSequence,
55812
55840
  activeTimeSeconds,
55813
55841
  isFinal
@@ -55959,6 +55987,7 @@ function createPlatformServices(deps) {
55959
55987
  validateGameManagementAccess
55960
55988
  });
55961
55989
  const timebackAdmin = new TimebackAdminService({
55990
+ config: config2,
55962
55991
  db: db2,
55963
55992
  timeback: timebackClient,
55964
55993
  validateDeveloperAccess,
@@ -58889,6 +58918,16 @@ async function requestCaliper(options) {
58889
58918
  baseUrl: caliperUrl
58890
58919
  });
58891
58920
  }
58921
+ function buildEventExtensions({
58922
+ eventExtensions,
58923
+ gameId
58924
+ }) {
58925
+ const mergedExtensions = {
58926
+ ...eventExtensions,
58927
+ ...gameId ? { gameId } : {}
58928
+ };
58929
+ return Object.keys(mergedExtensions).length > 0 ? mergedExtensions : undefined;
58930
+ }
58892
58931
  function createCaliperNamespace(client) {
58893
58932
  const urls = createOneRosterUrls(client.getBaseUrl());
58894
58933
  const caliper = {
@@ -58933,11 +58972,20 @@ function createCaliperNamespace(client) {
58933
58972
  if (params.actorEmail) {
58934
58973
  query.set("actorEmail", params.actorEmail);
58935
58974
  }
58975
+ if (params.extensions) {
58976
+ for (const [key, value] of Object.entries(params.extensions)) {
58977
+ query.set(`extensions.${key}`, value);
58978
+ }
58979
+ }
58936
58980
  const requestPath = `${CALIPER_ENDPOINTS4.events}?${query.toString()}`;
58937
58981
  return client["requestCaliper"](requestPath, "GET");
58938
58982
  }
58939
58983
  },
58940
58984
  emitActivityEvent: async (data) => {
58985
+ const eventExtensions = buildEventExtensions({
58986
+ eventExtensions: data.eventExtensions,
58987
+ gameId: data.gameId
58988
+ });
58941
58989
  const event = {
58942
58990
  "@context": CALIPER_CONSTANTS4.context,
58943
58991
  id: `urn:uuid:${crypto.randomUUID()}`,
@@ -58997,11 +59045,15 @@ function createCaliperNamespace(client) {
58997
59045
  }
58998
59046
  } : {}
58999
59047
  },
59000
- ...data.eventExtensions ? { extensions: data.eventExtensions } : {}
59048
+ ...eventExtensions ? { extensions: eventExtensions } : {}
59001
59049
  };
59002
59050
  return caliper.emit(event, data.sensorUrl);
59003
59051
  },
59004
59052
  emitTimeSpentEvent: async (data) => {
59053
+ const eventExtensions = buildEventExtensions({
59054
+ eventExtensions: data.eventExtensions,
59055
+ gameId: data.gameId
59056
+ });
59005
59057
  const event = {
59006
59058
  "@context": CALIPER_CONSTANTS4.context,
59007
59059
  id: `urn:uuid:${crypto.randomUUID()}`,
@@ -59042,7 +59094,8 @@ function createCaliperNamespace(client) {
59042
59094
  ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES4.waste, value: data.wasteTimeSeconds }] : []
59043
59095
  ],
59044
59096
  ...data.extensions ? { extensions: data.extensions } : {}
59045
- }
59097
+ },
59098
+ ...eventExtensions ? { extensions: eventExtensions } : {}
59046
59099
  };
59047
59100
  return caliper.emit(event, data.sensorUrl);
59048
59101
  },
@@ -59535,6 +59588,7 @@ class AdminEventRecorder {
59535
59588
  await this.caliper.emitActivityEvent({
59536
59589
  studentId: ctx.student.id,
59537
59590
  studentEmail: ctx.student.email,
59591
+ gameId: data.gameId,
59538
59592
  activityId: ctx.activityId,
59539
59593
  activityName: data.activityName || `Manual XP Assignment - ${ctx.courseContext.subject}`,
59540
59594
  courseId: data.courseId,
@@ -59561,6 +59615,7 @@ class AdminEventRecorder {
59561
59615
  await this.caliper.emitTimeSpentEvent({
59562
59616
  studentId: ctx.student.id,
59563
59617
  studentEmail: ctx.student.email,
59618
+ gameId: data.gameId,
59564
59619
  activityId: ctx.activityId,
59565
59620
  activityName: data.activityName || "Playcademy Admin Time Adjustment",
59566
59621
  courseId: data.courseId,
@@ -59582,6 +59637,7 @@ class AdminEventRecorder {
59582
59637
  await this.caliper.emitActivityEvent({
59583
59638
  studentId: ctx.student.id,
59584
59639
  studentEmail: ctx.student.email,
59640
+ gameId: data.gameId,
59585
59641
  activityId: ctx.activityId,
59586
59642
  activityName: data.activityName || "Playcademy Admin Mastery Adjustment",
59587
59643
  courseId: data.courseId,
@@ -59607,6 +59663,7 @@ class AdminEventRecorder {
59607
59663
  await this.caliper.emitActivityEvent({
59608
59664
  studentId: ctx.student.id,
59609
59665
  studentEmail: ctx.student.email,
59666
+ gameId: data.gameId,
59610
59667
  activityId: ctx.activityId,
59611
59668
  activityName: isResume ? "Course resumed" : "Course marked complete",
59612
59669
  courseId: data.courseId,
@@ -60055,15 +60112,13 @@ class ProgressRecorder {
60055
60112
  studentId,
60056
60113
  attemptNumber: currentAttemptNumber,
60057
60114
  score,
60058
- totalQuestions,
60059
- correctQuestions,
60060
60115
  xp: calculatedXp,
60061
- masteredUnits,
60062
60116
  scoreStatus,
60063
60117
  inProgress,
60064
60118
  appName: progressData.appName,
60065
- activityName,
60066
- durationSeconds: progressData.durationSeconds
60119
+ totalQuestions,
60120
+ correctQuestions,
60121
+ masteredUnits
60067
60122
  });
60068
60123
  } else {
60069
60124
  log.warn("[ProgressRecorder] Score not provided, skipping gradebook entry", {
@@ -60077,6 +60132,7 @@ class ProgressRecorder {
60077
60132
  await this.emitCourseCompletionHistoryEvent({
60078
60133
  studentId,
60079
60134
  studentEmail,
60135
+ gameId: progressData.gameId,
60080
60136
  activityId,
60081
60137
  courseId: ids.course,
60082
60138
  courseName,
@@ -60088,6 +60144,7 @@ class ProgressRecorder {
60088
60144
  await this.emitCaliperEvent({
60089
60145
  studentId,
60090
60146
  studentEmail,
60147
+ gameId: progressData.gameId,
60091
60148
  activityId,
60092
60149
  activityName,
60093
60150
  courseId: ids.course,
@@ -60189,15 +60246,13 @@ class ProgressRecorder {
60189
60246
  studentId,
60190
60247
  attemptNumber,
60191
60248
  score,
60192
- totalQuestions,
60193
- correctQuestions,
60194
60249
  xp,
60195
- masteredUnits,
60196
60250
  scoreStatus,
60197
60251
  inProgress,
60198
60252
  appName,
60199
- activityName,
60200
- durationSeconds
60253
+ totalQuestions,
60254
+ correctQuestions,
60255
+ masteredUnits
60201
60256
  }) {
60202
60257
  const timestamp3 = Date.now().toString(36);
60203
60258
  const resultId = `${lineItemId}:${studentId}:${timestamp3}`;
@@ -60212,21 +60267,18 @@ class ProgressRecorder {
60212
60267
  inProgress,
60213
60268
  metadata: {
60214
60269
  xp,
60215
- totalQuestions,
60216
- correctQuestions,
60217
- accuracy: totalQuestions && correctQuestions ? correctQuestions / totalQuestions * 100 : undefined,
60218
60270
  attemptNumber,
60219
- lastUpdated: new Date().toISOString(),
60220
- masteredUnits,
60221
60271
  appName,
60222
- activityName,
60223
- durationSeconds
60272
+ ...totalQuestions !== undefined ? { totalQuestions } : {},
60273
+ ...correctQuestions !== undefined ? { correctQuestions } : {},
60274
+ ...masteredUnits !== undefined ? { masteredUnits } : {}
60224
60275
  }
60225
60276
  });
60226
60277
  }
60227
60278
  async emitCaliperEvent({
60228
60279
  studentId,
60229
60280
  studentEmail,
60281
+ gameId,
60230
60282
  activityId,
60231
60283
  activityName,
60232
60284
  courseId,
@@ -60243,6 +60295,7 @@ class ProgressRecorder {
60243
60295
  await this.caliperNamespace.emitActivityEvent({
60244
60296
  studentId,
60245
60297
  studentEmail,
60298
+ gameId,
60246
60299
  activityId,
60247
60300
  activityName,
60248
60301
  courseId,
@@ -60265,6 +60318,7 @@ class ProgressRecorder {
60265
60318
  await this.caliperNamespace.emitActivityEvent({
60266
60319
  studentId: data.studentId,
60267
60320
  studentEmail: data.studentEmail,
60321
+ gameId: data.gameId,
60268
60322
  activityId: data.activityId,
60269
60323
  activityName: "Course completed",
60270
60324
  courseId: data.courseId,
@@ -60314,6 +60368,7 @@ class SessionRecorder {
60314
60368
  await this.caliperNamespace.emitTimeSpentEvent({
60315
60369
  studentId,
60316
60370
  studentEmail,
60371
+ gameId: sessionData.gameId,
60317
60372
  activityId,
60318
60373
  activityName,
60319
60374
  courseId: ids.course,
@@ -118902,18 +118957,23 @@ async function seedCoreGames(db2) {
118902
118957
  }
118903
118958
  async function seedCurrentProjectGame(db2, project) {
118904
118959
  const now2 = new Date;
118960
+ const desiredGameId = project.gameId?.trim() || undefined;
118905
118961
  try {
118906
118962
  const existingGame = await db2.query.games.findFirst({
118907
- where: (row, { eq: eq3 }) => eq3(row.slug, project.slug)
118963
+ where: (row, operators) => operators.eq(row.slug, project.slug)
118908
118964
  });
118909
118965
  if (existingGame) {
118910
- if (project.timebackCourses && project.timebackCourses.length > 0) {
118911
- await seedTimebackIntegrations(db2, existingGame.id, project.timebackCourses);
118966
+ if (desiredGameId && existingGame.id !== desiredGameId) {
118967
+ await db2.delete(games).where(eq(games.id, existingGame.id));
118968
+ } else {
118969
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
118970
+ await seedTimebackIntegrations(db2, existingGame.id, project.timebackCourses);
118971
+ }
118972
+ return existingGame;
118912
118973
  }
118913
- return existingGame;
118914
118974
  }
118915
118975
  const gameRecord = {
118916
- id: crypto.randomUUID(),
118976
+ id: desiredGameId ?? crypto.randomUUID(),
118917
118977
  developerId: DEMO_USERS.developer.id,
118918
118978
  slug: project.slug,
118919
118979
  displayName: project.displayName,
@@ -118942,6 +119002,7 @@ async function seedCurrentProjectGame(db2, project) {
118942
119002
  }
118943
119003
  }
118944
119004
  var init_games = __esm(() => {
119005
+ init_drizzle_orm();
118945
119006
  init_src();
118946
119007
  init_tables_index();
118947
119008
  init_constants();
@@ -120340,6 +120401,7 @@ var init_schemas11 = __esm(() => {
120340
120401
  gameId: exports_external.string().uuid(),
120341
120402
  studentId: exports_external.string().min(1),
120342
120403
  runId: exports_external.string().uuid().optional(),
120404
+ resumeId: exports_external.string().uuid().optional(),
120343
120405
  activityData: TimebackActivityDataSchema,
120344
120406
  scoreData: exports_external.object({
120345
120407
  correctQuestions: exports_external.number().int().min(0),
@@ -120360,13 +120422,18 @@ var init_schemas11 = __esm(() => {
120360
120422
  gameId: exports_external.string().uuid(),
120361
120423
  studentId: exports_external.string().min(1),
120362
120424
  runId: exports_external.string().uuid(),
120425
+ resumeId: exports_external.string().uuid().optional(),
120363
120426
  activityData: TimebackActivityDataSchema,
120364
120427
  timingData: exports_external.object({
120365
120428
  activeMs: exports_external.number().nonnegative(),
120366
120429
  pausedMs: exports_external.number().nonnegative()
120367
120430
  }),
120368
- windowSequence: exports_external.number().int().nonnegative(),
120431
+ windowStartedAtMs: exports_external.number().int().nonnegative().optional(),
120432
+ windowSequence: exports_external.number().int().nonnegative().optional(),
120369
120433
  isFinal: exports_external.boolean().optional()
120434
+ }).refine((value) => value.windowStartedAtMs !== undefined !== (value.windowSequence !== undefined), {
120435
+ message: "Provide exactly one of windowStartedAtMs or windowSequence",
120436
+ path: ["windowStartedAtMs"]
120370
120437
  });
120371
120438
  PopulateStudentRequestSchema = exports_external.object({
120372
120439
  firstName: exports_external.string().min(1).optional(),
@@ -120447,15 +120514,18 @@ var init_schemas11 = __esm(() => {
120447
120514
  });
120448
120515
  GrantTimebackXpRequestSchema = AdminTimebackMutationBaseSchema.extend({
120449
120516
  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()
120517
+ date: AdminAttributionDateSchema.optional(),
120518
+ useCurrentTime: exports_external.boolean().optional()
120451
120519
  });
120452
120520
  AdjustTimebackTimeRequestSchema = AdminTimebackMutationBaseSchema.extend({
120453
120521
  seconds: exports_external.number().refine((value) => value !== 0, { message: "Time amount cannot be 0" }),
120454
- date: AdminAttributionDateSchema.optional()
120522
+ date: AdminAttributionDateSchema.optional(),
120523
+ useCurrentTime: exports_external.boolean().optional()
120455
120524
  });
120456
120525
  AdjustTimebackMasteryRequestSchema = AdminTimebackMutationBaseSchema.extend({
120457
120526
  units: exports_external.number().refine((value) => value !== 0, { message: "Units cannot be 0" }),
120458
- date: AdminAttributionDateSchema.optional()
120527
+ date: AdminAttributionDateSchema.optional(),
120528
+ useCurrentTime: exports_external.boolean().optional()
120459
120529
  });
120460
120530
  ToggleCourseCompletionRequestSchema = exports_external.object({
120461
120531
  gameId: exports_external.string().uuid(),
@@ -122836,6 +122906,7 @@ var init_timeback_controller = __esm(() => {
122836
122906
  gameId,
122837
122907
  studentId,
122838
122908
  runId,
122909
+ resumeId,
122839
122910
  activityData,
122840
122911
  scoreData,
122841
122912
  timingData,
@@ -122849,6 +122920,7 @@ var init_timeback_controller = __esm(() => {
122849
122920
  gameId,
122850
122921
  studentId,
122851
122922
  runId,
122923
+ resumeId,
122852
122924
  activityData,
122853
122925
  scoreData,
122854
122926
  timingData,
@@ -122872,11 +122944,23 @@ var init_timeback_controller = __esm(() => {
122872
122944
  }
122873
122945
  throw ApiError.badRequest("Invalid JSON body");
122874
122946
  }
122875
- const { gameId, studentId, runId, activityData, timingData, windowSequence, isFinal } = body2;
122947
+ const {
122948
+ gameId,
122949
+ studentId,
122950
+ runId,
122951
+ resumeId,
122952
+ activityData,
122953
+ timingData,
122954
+ windowStartedAtMs,
122955
+ windowSequence,
122956
+ isFinal
122957
+ } = body2;
122876
122958
  logger63.debug("Recording heartbeat", {
122877
122959
  userId: ctx.user.id,
122878
122960
  gameId,
122879
122961
  runId,
122962
+ resumeId,
122963
+ windowStartedAtMs,
122880
122964
  windowSequence,
122881
122965
  activeMs: timingData.activeMs,
122882
122966
  isFinal
@@ -122885,8 +122969,10 @@ var init_timeback_controller = __esm(() => {
122885
122969
  gameId,
122886
122970
  studentId,
122887
122971
  runId,
122972
+ resumeId,
122888
122973
  activityData,
122889
122974
  timingData,
122975
+ windowStartedAtMs,
122890
122976
  windowSequence,
122891
122977
  isFinal,
122892
122978
  user: ctx.user
@@ -124471,6 +124557,203 @@ function printBanner(viteConfig, options) {
124471
124557
  import fs5 from "node:fs";
124472
124558
  import path3 from "node:path";
124473
124559
  import { loadPlaycademyConfig } from "playcademy/utils";
124560
+
124561
+ // ../utils/src/uuid.ts
124562
+ 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}$/;
124563
+ function isValidUUID2(value) {
124564
+ if (!value || typeof value !== "string") {
124565
+ return false;
124566
+ }
124567
+ return UUID_REGEX2.test(value);
124568
+ }
124569
+ // ../utils/src/ansi.ts
124570
+ var colors3 = {
124571
+ black: "\x1B[30m",
124572
+ red: "\x1B[31m",
124573
+ green: "\x1B[32m",
124574
+ yellow: "\x1B[33m",
124575
+ blue: "\x1B[34m",
124576
+ magenta: "\x1B[35m",
124577
+ cyan: "\x1B[36m",
124578
+ white: "\x1B[37m",
124579
+ gray: "\x1B[90m"
124580
+ };
124581
+ var styles3 = {
124582
+ reset: "\x1B[0m",
124583
+ bold: "\x1B[1m",
124584
+ dim: "\x1B[2m",
124585
+ italic: "\x1B[3m",
124586
+ underline: "\x1B[4m"
124587
+ };
124588
+ var cursor2 = {
124589
+ hide: "\x1B[?25l",
124590
+ show: "\x1B[?25h",
124591
+ up: (n3) => `\x1B[${n3}A`,
124592
+ down: (n3) => `\x1B[${n3}B`,
124593
+ forward: (n3) => `\x1B[${n3}C`,
124594
+ back: (n3) => `\x1B[${n3}D`,
124595
+ clearLine: "\x1B[K",
124596
+ clearScreen: "\x1B[2J",
124597
+ home: "\x1B[H"
124598
+ };
124599
+ var isInteractive2 = typeof process !== "undefined" && process.stdout?.isTTY && !process.env.CI && process.env.TERM !== "dumb";
124600
+ function stripAnsi2(text2) {
124601
+ return text2.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
124602
+ }
124603
+
124604
+ // ../utils/src/spinner.ts
124605
+ import { stdout as stdout2 } from "process";
124606
+ var SPINNER_FRAMES2 = [
124607
+ 10251,
124608
+ 10265,
124609
+ 10297,
124610
+ 10296,
124611
+ 10300,
124612
+ 10292,
124613
+ 10278,
124614
+ 10279,
124615
+ 10247,
124616
+ 10255
124617
+ ].map((code) => String.fromCodePoint(code));
124618
+ var CHECK_MARK2 = String.fromCodePoint(10004);
124619
+ var CROSS_MARK2 = String.fromCodePoint(10006);
124620
+ var CANCEL_MARK2 = String.fromCodePoint(9675);
124621
+ var SPINNER_INTERVAL2 = 80;
124622
+
124623
+ class Spinner3 {
124624
+ tasks = new Map;
124625
+ frameIndex = 0;
124626
+ intervalId = null;
124627
+ renderCount = 0;
124628
+ previousLineCount = 0;
124629
+ printedTasks = new Set;
124630
+ indent;
124631
+ constructor(taskIds, texts, options) {
124632
+ this.indent = options?.indent ?? 0;
124633
+ taskIds.forEach((id, index6) => {
124634
+ this.tasks.set(id, {
124635
+ text: texts[index6] || "",
124636
+ status: "pending"
124637
+ });
124638
+ });
124639
+ }
124640
+ start() {
124641
+ if (isInteractive2) {
124642
+ stdout2.write(cursor2.hide);
124643
+ this.render();
124644
+ this.intervalId = setInterval(() => {
124645
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES2.length;
124646
+ this.render();
124647
+ }, SPINNER_INTERVAL2);
124648
+ }
124649
+ }
124650
+ clear() {
124651
+ if (this.intervalId) {
124652
+ clearInterval(this.intervalId);
124653
+ this.intervalId = null;
124654
+ }
124655
+ if (isInteractive2 && this.previousLineCount > 0) {
124656
+ stdout2.write(cursor2.up(this.previousLineCount));
124657
+ for (let i3 = 0;i3 < this.previousLineCount; i3++) {
124658
+ stdout2.write(`\r${cursor2.clearLine}
124659
+ `);
124660
+ }
124661
+ stdout2.write(cursor2.up(this.previousLineCount));
124662
+ stdout2.write(cursor2.show);
124663
+ }
124664
+ this.previousLineCount = 0;
124665
+ }
124666
+ updateTask(taskId, status, finalText) {
124667
+ const task = this.tasks.get(taskId);
124668
+ if (task) {
124669
+ task.status = status;
124670
+ if (finalText) {
124671
+ task.finalText = finalText;
124672
+ }
124673
+ if (!isInteractive2) {
124674
+ this.renderNonInteractive(taskId, task);
124675
+ }
124676
+ }
124677
+ }
124678
+ renderNonInteractive(taskId, task) {
124679
+ const key = `${taskId}-${task.status}`;
124680
+ if (this.printedTasks.has(key)) {
124681
+ return;
124682
+ }
124683
+ this.printedTasks.add(key);
124684
+ const indentStr = " ".repeat(this.indent);
124685
+ let line2 = "";
124686
+ switch (task.status) {
124687
+ case "running": {
124688
+ line2 = `${indentStr}[RUNNING] ${stripAnsi2(task.text)}`;
124689
+ break;
124690
+ }
124691
+ case "success": {
124692
+ line2 = `${indentStr}[SUCCESS] ${stripAnsi2(task.finalText || task.text)}`;
124693
+ break;
124694
+ }
124695
+ case "error": {
124696
+ line2 = `${indentStr}[ERROR] Failed: ${stripAnsi2(task.text)}`;
124697
+ break;
124698
+ }
124699
+ case "cancelled": {
124700
+ line2 = `${indentStr}[CANCELLED] ${stripAnsi2(task.finalText || task.text)}`;
124701
+ break;
124702
+ }
124703
+ }
124704
+ console.log(line2);
124705
+ }
124706
+ render() {
124707
+ if (this.previousLineCount > 0) {
124708
+ stdout2.write(cursor2.up(this.previousLineCount));
124709
+ }
124710
+ const spinner = SPINNER_FRAMES2[this.frameIndex];
124711
+ const indentStr = " ".repeat(this.indent);
124712
+ const visibleTasks = [...this.tasks.values()].filter((task) => task.status !== "pending");
124713
+ for (const task of visibleTasks) {
124714
+ stdout2.write(`\r${cursor2.clearLine}`);
124715
+ let line2 = "";
124716
+ switch (task.status) {
124717
+ case "running": {
124718
+ line2 = `${indentStr}${colors3.blue}${spinner}${styles3.reset} ${task.text}`;
124719
+ break;
124720
+ }
124721
+ case "success": {
124722
+ line2 = `${indentStr}${colors3.green}${CHECK_MARK2}${styles3.reset} ${task.finalText || task.text}`;
124723
+ break;
124724
+ }
124725
+ case "error": {
124726
+ line2 = `${indentStr}${colors3.red}${CROSS_MARK2}${styles3.reset} Failed: ${task.text}`;
124727
+ break;
124728
+ }
124729
+ case "cancelled": {
124730
+ line2 = `${indentStr}${colors3.gray}${CANCEL_MARK2}${styles3.reset} Cancelled: ${task.finalText || task.text}`;
124731
+ break;
124732
+ }
124733
+ }
124734
+ console.log(line2);
124735
+ }
124736
+ this.previousLineCount = visibleTasks.length;
124737
+ this.renderCount++;
124738
+ }
124739
+ stop() {
124740
+ if (this.intervalId) {
124741
+ clearInterval(this.intervalId);
124742
+ this.intervalId = null;
124743
+ }
124744
+ if (isInteractive2) {
124745
+ this.render();
124746
+ stdout2.write(cursor2.show);
124747
+ } else {
124748
+ this.tasks.forEach((task, taskId) => {
124749
+ if (task.status !== "pending") {
124750
+ this.renderNonInteractive(taskId, task);
124751
+ }
124752
+ });
124753
+ }
124754
+ }
124755
+ }
124756
+ // src/lib/sandbox/project-info.ts
124474
124757
  function extractTimebackCourses(config2, timebackOptions) {
124475
124758
  const courses = config2?.integrations?.timeback?.courses;
124476
124759
  if (!courses || courses.length === 0) {
@@ -124504,17 +124787,20 @@ async function extractProjectInfo(viteConfig, timebackOptions) {
124504
124787
  packageJson = JSON.parse(packageJsonContent);
124505
124788
  }
124506
124789
  } catch {}
124507
- const name2 = config2?.name || packageJson.name || "";
124508
- let slug = name2;
124509
- if (slug.includes("/")) {
124510
- slug = slug.split("/")[1] || slug;
124790
+ const name4 = config2?.name || packageJson.name || "";
124791
+ let slug2 = name4;
124792
+ if (slug2.includes("/")) {
124793
+ slug2 = slug2.split("/")[1] || slug2;
124511
124794
  }
124512
- if (!slug) {
124513
- slug = directoryName;
124795
+ if (!slug2) {
124796
+ slug2 = directoryName;
124514
124797
  }
124515
- const displayName = slug.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
124798
+ const displayName = slug2.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
124799
+ const envGameId = process.env.SANDBOX_GAME_ID;
124800
+ const gameId = envGameId && isValidUUID2(envGameId) ? envGameId : undefined;
124516
124801
  return {
124517
- slug,
124802
+ gameId,
124803
+ slug: slug2,
124518
124804
  displayName,
124519
124805
  version: packageJson.version || "dev",
124520
124806
  description: packageJson.description,
@@ -125549,7 +125835,7 @@ var DEBOUNCE_MS = 500;
125549
125835
  var VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.js", "vite.config.mjs"];
125550
125836
  var debounceTimer = null;
125551
125837
  function findExistingFiles(projectRoot, fileNames) {
125552
- return fileNames.map((name2) => path4.join(projectRoot, name2)).filter((file) => fs7.existsSync(file));
125838
+ return fileNames.map((name4) => path4.join(projectRoot, name4)).filter((file) => fs7.existsSync(file));
125553
125839
  }
125554
125840
  function createChangeHandler(server, viteConfig, platformModeOptions, watchedFiles) {
125555
125841
  return async (changedPath) => {
@@ -125617,7 +125903,7 @@ function cyclePlatformRoleHotkey(options) {
125617
125903
 
125618
125904
  // src/server/hotkeys/cycle-timeback-role.ts
125619
125905
  var import_picocolors9 = __toESM(require_picocolors(), 1);
125620
- var { bold: bold4, cyan: cyan4, green: green4, red: red3, yellow: yellow3 } = import_picocolors9.default;
125906
+ var { bold: bold5, cyan: cyan4, green: green4, red: red3, yellow: yellow3 } = import_picocolors9.default;
125621
125907
  function cycleTimebackRole(logger) {
125622
125908
  const currentRole = getTimebackRoleOverride() ?? "student";
125623
125909
  const currentIndex = TIMEBACK_ROLES.indexOf(currentRole);
@@ -125636,14 +125922,14 @@ function cycleTimebackRole(logger) {
125636
125922
  function cycleTimebackRoleHotkey(options) {
125637
125923
  return {
125638
125924
  key: "t",
125639
- description: `${cyan4(bold4("[playcademy]"))} cycle Timeback role`,
125925
+ description: `${cyan4(bold5("[playcademy]"))} cycle Timeback role`,
125640
125926
  action: () => cycleTimebackRole(options.viteConfig.logger)
125641
125927
  };
125642
125928
  }
125643
125929
 
125644
125930
  // src/server/hotkeys/recreate-database.ts
125645
125931
  var import_picocolors10 = __toESM(require_picocolors(), 1);
125646
- var { bold: bold5, cyan: cyan5 } = import_picocolors10.default;
125932
+ var { bold: bold6, cyan: cyan5 } = import_picocolors10.default;
125647
125933
  async function recreateSandboxDatabase(options) {
125648
125934
  await recreateSandbox({
125649
125935
  viteConfig: options.viteConfig,
@@ -125653,7 +125939,7 @@ async function recreateSandboxDatabase(options) {
125653
125939
  function recreateDatabaseHotkey(options) {
125654
125940
  return {
125655
125941
  key: "d",
125656
- description: `${cyan5(bold5("[playcademy]"))} recreate sandbox database`,
125942
+ description: `${cyan5(bold6("[playcademy]"))} recreate sandbox database`,
125657
125943
  action: () => recreateSandboxDatabase(options)
125658
125944
  };
125659
125945
  }
@@ -125663,7 +125949,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
125663
125949
  // package.json
125664
125950
  var package_default2 = {
125665
125951
  name: "@playcademy/vite-plugin",
125666
- version: "0.2.24-beta.6",
125952
+ version: "0.2.24-beta.8",
125667
125953
  type: "module",
125668
125954
  exports: {
125669
125955
  ".": {
@@ -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.6",
3
+ "version": "0.2.24-beta.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {