@playcademy/vite-plugin 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +286 -164
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -41199,7 +41199,7 @@ var import_picocolors7 = __toESM(require_picocolors(), 1);
41199
41199
  // package.json
41200
41200
  var package_default = {
41201
41201
  name: "@playcademy/vite-plugin",
41202
- version: "0.1.34",
41202
+ version: "0.1.36",
41203
41203
  type: "module",
41204
41204
  exports: {
41205
41205
  ".": {
@@ -189515,7 +189515,9 @@ var init_constants = __esm3(() => {
189515
189515
  studentTTL: 600000,
189516
189516
  studentMaxSize: 500,
189517
189517
  assessmentTTL: 1800000,
189518
- assessmentMaxSize: 200
189518
+ assessmentMaxSize: 200,
189519
+ enrollmentTTL: 5000,
189520
+ enrollmentMaxSize: 100
189519
189521
  };
189520
189522
  CONFIG_DEFAULTS = {
189521
189523
  fileNames: ["timeback.config.js", "timeback.config.json"]
@@ -189733,6 +189735,25 @@ class ResourceNotFoundError extends TimebackError {
189733
189735
  Object.setPrototypeOf(this, ResourceNotFoundError.prototype);
189734
189736
  }
189735
189737
  }
189738
+ init_constants();
189739
+ var isObject2 = (value) => typeof value === "object" && value !== null;
189740
+ var SUBJECT_VALUES = TIMEBACK_SUBJECTS;
189741
+ var GRADE_VALUES = TIMEBACK_GRADE_LEVELS;
189742
+ function isPlaycademyResourceMetadata(value) {
189743
+ if (!isObject2(value)) {
189744
+ return false;
189745
+ }
189746
+ if (!("mastery" in value) || value.mastery === undefined) {
189747
+ return true;
189748
+ }
189749
+ return isObject2(value.mastery);
189750
+ }
189751
+ function isTimebackSubject(value) {
189752
+ return typeof value === "string" && SUBJECT_VALUES.includes(value);
189753
+ }
189754
+ function isTimebackGrade(value) {
189755
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
189756
+ }
189736
189757
  var isBrowser2 = () => {
189737
189758
  const g52 = globalThis;
189738
189759
  return typeof g52.window !== "undefined" && typeof g52.document !== "undefined";
@@ -190550,14 +190571,30 @@ function createOneRosterNamespace(client2) {
190550
190571
  update: async (sourcedId, data) => {
190551
190572
  return client2["request"](`${ONEROSTER_ENDPOINTS.assessmentResults}/${sourcedId}`, "PUT", data);
190552
190573
  },
190553
- getLatestForStudent: async (studentId, lineItemId) => {
190574
+ getAttemptStats: async (studentId, lineItemId) => {
190554
190575
  try {
190555
190576
  const filter2 = `student.sourcedId='${studentId}' AND assessmentLineItem.sourcedId='${lineItemId}'`;
190556
- const url = `${ONEROSTER_ENDPOINTS.assessmentResults}?filter=${encodeURIComponent(filter2)}&sort=scoreDate&orderBy=desc&limit=1`;
190577
+ const url = `${ONEROSTER_ENDPOINTS.assessmentResults}?filter=${encodeURIComponent(filter2)}`;
190557
190578
  const response = await client2["request"](url, "GET");
190558
- return response.assessmentResults[0] || null;
190579
+ const results = response.assessmentResults || [];
190580
+ if (results.length === 0)
190581
+ return null;
190582
+ let maxAttemptResult = results[0];
190583
+ let maxAttemptNumber = maxAttemptResult.metadata?.attemptNumber || 0;
190584
+ let activeAttemptCount = 0;
190585
+ for (const result of results) {
190586
+ const attemptNumber = result.metadata?.attemptNumber || 0;
190587
+ if (attemptNumber > maxAttemptNumber) {
190588
+ maxAttemptNumber = attemptNumber;
190589
+ maxAttemptResult = result;
190590
+ }
190591
+ if (result.status === "active") {
190592
+ activeAttemptCount++;
190593
+ }
190594
+ }
190595
+ return { maxAttemptNumber, activeAttemptCount, maxAttemptResult };
190559
190596
  } catch (error2) {
190560
- logTimebackError("query latest assessment result", error2, {
190597
+ logTimebackError("query attempt stats", error2, {
190561
190598
  studentId,
190562
190599
  lineItemId
190563
190600
  });
@@ -190841,6 +190878,7 @@ class TimebackCacheManager {
190841
190878
  studentCache;
190842
190879
  assessmentLineItemCache;
190843
190880
  resourceMasteryCache;
190881
+ enrollmentCache;
190844
190882
  constructor() {
190845
190883
  this.studentCache = new TimebackCache({
190846
190884
  defaultTTL: CACHE_DEFAULTS.studentTTL,
@@ -190857,6 +190895,11 @@ class TimebackCacheManager {
190857
190895
  maxSize: CACHE_DEFAULTS.assessmentMaxSize,
190858
190896
  name: "ResourceMasteryCache"
190859
190897
  });
190898
+ this.enrollmentCache = new TimebackCache({
190899
+ defaultTTL: CACHE_DEFAULTS.enrollmentTTL,
190900
+ maxSize: CACHE_DEFAULTS.enrollmentMaxSize,
190901
+ name: "EnrollmentCache"
190902
+ });
190860
190903
  }
190861
190904
  getStudent(key) {
190862
190905
  return this.studentCache.get(key);
@@ -190876,40 +190919,182 @@ class TimebackCacheManager {
190876
190919
  setResourceMasterableUnits(key, value) {
190877
190920
  this.resourceMasteryCache.set(key, value);
190878
190921
  }
190922
+ getEnrollments(studentId) {
190923
+ return this.enrollmentCache.get(studentId);
190924
+ }
190925
+ setEnrollments(studentId, enrollments) {
190926
+ this.enrollmentCache.set(studentId, enrollments);
190927
+ }
190879
190928
  clearAll() {
190880
190929
  this.studentCache.clear();
190881
190930
  this.assessmentLineItemCache.clear();
190931
+ this.resourceMasteryCache.clear();
190932
+ this.enrollmentCache.clear();
190882
190933
  log32.info("[TimebackCacheManager] All caches cleared");
190883
190934
  }
190884
190935
  getStats() {
190885
190936
  return {
190886
190937
  studentCache: this.studentCache.stats(),
190887
190938
  assessmentLineItemCache: this.assessmentLineItemCache.stats(),
190888
- resourceMasteryCache: this.resourceMasteryCache.stats()
190939
+ resourceMasteryCache: this.resourceMasteryCache.stats(),
190940
+ enrollmentCache: this.enrollmentCache.stats()
190889
190941
  };
190890
190942
  }
190891
190943
  cleanup() {
190892
190944
  this.studentCache.cleanup();
190893
190945
  this.assessmentLineItemCache.cleanup();
190894
190946
  this.resourceMasteryCache.cleanup();
190947
+ this.enrollmentCache.cleanup();
190895
190948
  log32.debug("[TimebackCacheManager] Cache cleanup completed");
190896
190949
  }
190897
190950
  }
190898
- function kebabToTitleCase(kebabStr) {
190899
- return kebabStr.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
190900
- }
190901
- init_constants();
190902
190951
  init_constants();
190903
- var isObject2 = (value) => typeof value === "object" && value !== null;
190904
- function isPlaycademyResourceMetadata(value) {
190905
- if (!isObject2(value)) {
190906
- return false;
190952
+
190953
+ class MasteryTracker {
190954
+ cacheManager;
190955
+ onerosterNamespace;
190956
+ edubridgeNamespace;
190957
+ constructor(cacheManager, onerosterNamespace, edubridgeNamespace) {
190958
+ this.cacheManager = cacheManager;
190959
+ this.onerosterNamespace = onerosterNamespace;
190960
+ this.edubridgeNamespace = edubridgeNamespace;
190907
190961
  }
190908
- if (!("mastery" in value) || value.mastery === undefined) {
190909
- return true;
190962
+ async checkProgress(input) {
190963
+ const { studentId, courseId, resourceId, masteredUnits } = input;
190964
+ if (typeof masteredUnits !== "number" || masteredUnits <= 0) {
190965
+ return;
190966
+ }
190967
+ const masterableUnits = await this.resolveMasterableUnits(resourceId);
190968
+ if (!masterableUnits || masterableUnits <= 0) {
190969
+ log32.warn("[MasteryTracker] No masterableUnits configured for course", {
190970
+ courseId,
190971
+ resourceId
190972
+ });
190973
+ return;
190974
+ }
190975
+ const facts = await this.fetchEnrollmentAnalyticsFacts(studentId, courseId);
190976
+ if (!facts) {
190977
+ log32.warn("[MasteryTracker] Unable to retrieve analytics for mastery-based completion", {
190978
+ studentId,
190979
+ courseId
190980
+ });
190981
+ return;
190982
+ }
190983
+ const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
190984
+ const totalMastered = historicalMasteredUnits + masteredUnits;
190985
+ const rawPct = totalMastered / masterableUnits * 100;
190986
+ const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
190987
+ const masteryAchieved = totalMastered >= masterableUnits;
190988
+ return { pctCompleteApp, masteryAchieved };
190989
+ }
190990
+ async createCompletionEntry(studentId, courseId, classId, appName) {
190991
+ const ids = deriveSourcedIds(courseId);
190992
+ const lineItemId = `${ids.course}-mastery-completion-assessment`;
190993
+ const resultId = `${lineItemId}:${studentId}:completion`;
190994
+ try {
190995
+ await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
190996
+ sourcedId: lineItemId,
190997
+ title: "Mastery Completion",
190998
+ status: ONEROSTER_STATUS.active,
190999
+ ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } },
191000
+ ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
191001
+ });
191002
+ await this.onerosterNamespace.assessmentResults.upsert(resultId, {
191003
+ sourcedId: resultId,
191004
+ status: ONEROSTER_STATUS.active,
191005
+ assessmentLineItem: { sourcedId: lineItemId },
191006
+ student: { sourcedId: studentId },
191007
+ score: 100,
191008
+ scoreDate: new Date().toISOString(),
191009
+ scoreStatus: SCORE_STATUS.fullyGraded,
191010
+ inProgress: "false",
191011
+ metadata: {
191012
+ isMasteryCompletion: true,
191013
+ completedAt: new Date().toISOString(),
191014
+ appName
191015
+ }
191016
+ });
191017
+ log32.info("[MasteryTracker] Created mastery completion entry", {
191018
+ studentId,
191019
+ lineItemId,
191020
+ resultId
191021
+ });
191022
+ } catch (error2) {
191023
+ log32.error("[MasteryTracker] Failed to create mastery completion entry", {
191024
+ studentId,
191025
+ lineItemId,
191026
+ error: error2
191027
+ });
191028
+ }
191029
+ }
191030
+ async resolveMasterableUnits(resourceId) {
191031
+ if (!resourceId) {
191032
+ return;
191033
+ }
191034
+ const cached = this.cacheManager.getResourceMasterableUnits(resourceId);
191035
+ if (cached !== undefined) {
191036
+ return cached === null ? undefined : cached;
191037
+ }
191038
+ try {
191039
+ const resource = await this.onerosterNamespace.resources.get(resourceId);
191040
+ const playcademyMetadata = resource.metadata?.playcademy;
191041
+ if (!playcademyMetadata) {
191042
+ return;
191043
+ }
191044
+ const masterableUnits = isPlaycademyResourceMetadata(playcademyMetadata) ? playcademyMetadata.mastery?.masterableUnits : undefined;
191045
+ this.cacheManager.setResourceMasterableUnits(resourceId, masterableUnits ?? null);
191046
+ return masterableUnits;
191047
+ } catch (error2) {
191048
+ log32.error("[MasteryTracker] Failed to fetch resource metadata for mastery config", {
191049
+ resourceId,
191050
+ error: error2
191051
+ });
191052
+ this.cacheManager.setResourceMasterableUnits(resourceId, null);
191053
+ return;
191054
+ }
191055
+ }
191056
+ async fetchEnrollmentAnalyticsFacts(studentId, courseId) {
191057
+ try {
191058
+ const enrollments = await this.edubridgeNamespace.enrollments.listByUser(studentId);
191059
+ const enrollment = enrollments.find((e2) => e2.course.id === courseId);
191060
+ if (!enrollment) {
191061
+ log32.warn("[MasteryTracker] Enrollment not found for student/course", {
191062
+ studentId,
191063
+ courseId
191064
+ });
191065
+ return;
191066
+ }
191067
+ const analytics = await this.edubridgeNamespace.analytics.getEnrollmentFacts(enrollment.id);
191068
+ return analytics.facts;
191069
+ } catch (error2) {
191070
+ log32.error("[MasteryTracker] Failed to load enrollment analytics facts", {
191071
+ studentId,
191072
+ courseId,
191073
+ error: error2
191074
+ });
191075
+ return;
191076
+ }
191077
+ }
191078
+ sumAnalyticsMetric(facts, metric) {
191079
+ if (!facts) {
191080
+ return 0;
191081
+ }
191082
+ let total = 0;
191083
+ Object.values(facts).forEach((dateFacts) => {
191084
+ Object.values(dateFacts).forEach((subjectFacts) => {
191085
+ const metrics = subjectFacts.activityMetrics;
191086
+ if (metrics && typeof metrics[metric] === "number") {
191087
+ total += metrics[metric];
191088
+ }
191089
+ });
191090
+ });
191091
+ return total;
190910
191092
  }
190911
- return isObject2(value.mastery);
190912
191093
  }
191094
+ function kebabToTitleCase(kebabStr) {
191095
+ return kebabStr.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
191096
+ }
191097
+ init_constants();
190913
191098
  function validateProgressData(progressData) {
190914
191099
  if (!progressData.subject) {
190915
191100
  throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
@@ -190963,13 +191148,13 @@ class ProgressRecorder {
190963
191148
  cacheManager;
190964
191149
  onerosterNamespace;
190965
191150
  caliperNamespace;
190966
- edubridgeNamespace;
190967
- constructor(studentResolver, cacheManager, onerosterNamespace, caliperNamespace, edubridgeNamespace) {
191151
+ masteryTracker;
191152
+ constructor(studentResolver, cacheManager, onerosterNamespace, caliperNamespace, masteryTracker) {
190968
191153
  this.studentResolver = studentResolver;
190969
191154
  this.cacheManager = cacheManager;
190970
191155
  this.onerosterNamespace = onerosterNamespace;
190971
191156
  this.caliperNamespace = caliperNamespace;
190972
- this.edubridgeNamespace = edubridgeNamespace;
191157
+ this.masteryTracker = masteryTracker;
190973
191158
  }
190974
191159
  async record(courseId, studentIdentifier, progressData) {
190975
191160
  validateProgressData(progressData);
@@ -190978,22 +191163,22 @@ class ProgressRecorder {
190978
191163
  const { score, totalQuestions, correctQuestions, xpEarned, masteredUnits, attemptNumber } = progressData;
190979
191164
  const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
190980
191165
  const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
190981
- const isFirstAttempt = currentAttemptNumber === 1;
190982
- const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, isFirstAttempt);
191166
+ const isFirstActiveAttempt = currentAttemptNumber === 1;
191167
+ const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, isFirstActiveAttempt);
190983
191168
  let extensions = progressData.extensions;
190984
- const completionProgress = await this.buildCompletionProgress({
191169
+ const masteryProgress = await this.masteryTracker.checkProgress({
190985
191170
  studentId,
190986
191171
  courseId,
190987
- progressData,
190988
- resourceId: ids.resource
191172
+ resourceId: ids.resource,
191173
+ masteredUnits: progressData.masteredUnits ?? 0
190989
191174
  });
190990
191175
  let pctCompleteApp;
190991
191176
  let masteryAchieved = false;
190992
- let scoreStatus = SCORE_STATUS.partiallyGraded;
191177
+ let scoreStatus = SCORE_STATUS.fullyGraded;
190993
191178
  const inProgress = "false";
190994
- if (completionProgress) {
190995
- masteryAchieved = completionProgress.masteryAchieved;
190996
- pctCompleteApp = completionProgress.pctCompleteApp;
191179
+ if (masteryProgress) {
191180
+ masteryAchieved = masteryProgress.masteryAchieved;
191181
+ pctCompleteApp = masteryProgress.pctCompleteApp;
190997
191182
  extensions = {
190998
191183
  ...extensions || {},
190999
191184
  ...pctCompleteApp !== undefined ? { pctCompleteApp } : {}
@@ -191011,6 +191196,9 @@ class ProgressRecorder {
191011
191196
  attemptNumber: currentAttemptNumber
191012
191197
  });
191013
191198
  }
191199
+ if (masteryAchieved) {
191200
+ await this.masteryTracker.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
191201
+ }
191014
191202
  await this.emitCaliperEvent(studentId, studentEmail, activityId, activityName, ids.course, courseName, totalQuestions, correctQuestions, calculatedXp, masteredUnits, currentAttemptNumber, progressData, extensions);
191015
191203
  return {
191016
191204
  xpAwarded: calculatedXp,
@@ -191039,8 +191227,9 @@ class ProgressRecorder {
191039
191227
  return actualLineItemId;
191040
191228
  }
191041
191229
  async resolveAttemptNumber(providedAttemptNumber, score, studentId, lineItemId) {
191042
- if (providedAttemptNumber)
191230
+ if (providedAttemptNumber) {
191043
191231
  return providedAttemptNumber;
191232
+ }
191044
191233
  if (score !== undefined) {
191045
191234
  return this.determineAttemptNumber(studentId, lineItemId);
191046
191235
  }
@@ -191064,106 +191253,13 @@ class ProgressRecorder {
191064
191253
  }
191065
191254
  return 0;
191066
191255
  }
191067
- async buildCompletionProgress({
191068
- studentId,
191069
- courseId,
191070
- progressData,
191071
- resourceId
191072
- }) {
191073
- if (typeof progressData.masteredUnits !== "number" || progressData.masteredUnits <= 0) {
191074
- return;
191075
- }
191076
- const masterableUnits = await this.resolveMasterableUnits(resourceId);
191077
- if (!masterableUnits || masterableUnits <= 0) {
191078
- log32.warn("[ProgressRecorder] No masterableUnits configured for course", {
191079
- courseId,
191080
- resourceId
191081
- });
191082
- return;
191083
- }
191084
- const facts = await this.fetchEnrollmentAnalyticsFacts(studentId, courseId);
191085
- if (!facts) {
191086
- log32.warn("[ProgressRecorder] Unable to retrieve analytics for mastery-based completion", { studentId, courseId });
191087
- return;
191088
- }
191089
- const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
191090
- const totalMastered = historicalMasteredUnits + progressData.masteredUnits;
191091
- const rawPct = totalMastered / masterableUnits * 100;
191092
- const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
191093
- const masteryAchieved = totalMastered >= masterableUnits;
191094
- return { pctCompleteApp, masteryAchieved };
191095
- }
191096
- async fetchEnrollmentAnalyticsFacts(studentId, courseId) {
191097
- try {
191098
- const enrollments = await this.edubridgeNamespace.enrollments.listByUser(studentId);
191099
- const enrollment = enrollments.find((e2) => e2.course.id === courseId);
191100
- if (!enrollment) {
191101
- log32.warn("[ProgressRecorder] Enrollment not found for student/course", {
191102
- studentId,
191103
- courseId
191104
- });
191105
- return;
191106
- }
191107
- const analytics = await this.edubridgeNamespace.analytics.getEnrollmentFacts(enrollment.id);
191108
- return analytics.facts;
191109
- } catch (error2) {
191110
- log32.error("[ProgressRecorder] Failed to load enrollment analytics facts", {
191111
- studentId,
191112
- courseId,
191113
- error: error2
191114
- });
191115
- return;
191116
- }
191117
- }
191118
- sumAnalyticsMetric(facts, metric) {
191119
- if (!facts) {
191120
- return 0;
191121
- }
191122
- let total = 0;
191123
- Object.values(facts).forEach((dateFacts) => {
191124
- Object.values(dateFacts).forEach((subjectFacts) => {
191125
- const metrics = subjectFacts.activityMetrics;
191126
- if (metrics && typeof metrics[metric] === "number") {
191127
- total += metrics[metric];
191128
- }
191129
- });
191130
- });
191131
- return total;
191132
- }
191133
- async resolveMasterableUnits(resourceId) {
191134
- if (!resourceId) {
191135
- return;
191136
- }
191137
- const cached = this.cacheManager.getResourceMasterableUnits(resourceId);
191138
- if (cached !== undefined) {
191139
- return cached === null ? undefined : cached;
191140
- }
191141
- try {
191142
- const resource = await this.onerosterNamespace.resources.get(resourceId);
191143
- const playcademyMetadata = resource.metadata?.playcademy;
191144
- if (!playcademyMetadata) {
191145
- return;
191146
- }
191147
- const masterableUnits = isPlaycademyResourceMetadata(playcademyMetadata) ? playcademyMetadata.mastery?.masterableUnits : undefined;
191148
- this.cacheManager.setResourceMasterableUnits(resourceId, masterableUnits ?? null);
191149
- return masterableUnits;
191150
- } catch (error2) {
191151
- log32.error("[ProgressRecorder] Failed to fetch resource metadata for mastery config", {
191152
- resourceId,
191153
- error: error2
191154
- });
191155
- this.cacheManager.setResourceMasterableUnits(resourceId, null);
191156
- return;
191157
- }
191158
- }
191159
191256
  async getOrCreateLineItem(lineItemId, activityName, classId, ids) {
191160
191257
  try {
191161
191258
  const lineItem = await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
191162
191259
  sourcedId: lineItemId,
191163
191260
  title: activityName,
191164
191261
  status: ONEROSTER_STATUS.active,
191165
- ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } },
191166
- ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : ids.component ? { component: { sourcedId: ids.component } } : {}
191262
+ ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } }
191167
191263
  });
191168
191264
  if (!lineItem.sourcedId) {
191169
191265
  throw new TimebackError(`Assessment line item created but has no sourcedId. This should not happen and indicates an upstream API issue.`);
@@ -191181,21 +191277,15 @@ class ProgressRecorder {
191181
191277
  }
191182
191278
  }
191183
191279
  async determineAttemptNumber(studentId, lineItemId) {
191184
- const latestAttempt = await this.onerosterNamespace.assessmentResults.getLatestForStudent(studentId, lineItemId);
191185
- if (latestAttempt) {
191186
- const previousAttemptNumber = latestAttempt.metadata?.attemptNumber || 0;
191187
- const newAttemptNumber = previousAttemptNumber + 1;
191188
- log32.debug("[ProgressRecorder] Found previous attempt, incrementing", {
191189
- previousAttemptNumber,
191190
- newAttemptNumber,
191191
- previousScore: latestAttempt.score
191192
- });
191193
- return newAttemptNumber;
191280
+ const stats = await this.onerosterNamespace.assessmentResults.getAttemptStats(studentId, lineItemId);
191281
+ if (stats) {
191282
+ return stats.activeAttemptCount + 1;
191194
191283
  }
191195
191284
  return 1;
191196
191285
  }
191197
191286
  async createGradebookEntry(lineItemId, studentId, attemptNumber, score, totalQuestions, correctQuestions, xp, masteredUnits, scoreStatus, inProgress, appName) {
191198
- const resultId = `${lineItemId}:${studentId}:attempt-${attemptNumber}`;
191287
+ const timestamp4 = Date.now().toString(36);
191288
+ const resultId = `${lineItemId}:${studentId}:${timestamp4}`;
191199
191289
  await this.onerosterNamespace.assessmentResults.upsert(resultId, {
191200
191290
  sourcedId: resultId,
191201
191291
  status: ONEROSTER_STATUS.active,
@@ -193966,9 +194056,9 @@ class ZodUnion2 extends ZodType2 {
193966
194056
  return this._def.options;
193967
194057
  }
193968
194058
  }
193969
- ZodUnion2.create = (types4, params) => {
194059
+ ZodUnion2.create = (types22, params) => {
193970
194060
  return new ZodUnion2({
193971
- options: types4,
194061
+ options: types22,
193972
194062
  typeName: ZodFirstPartyTypeKind2.ZodUnion,
193973
194063
  ...processCreateParams2(params)
193974
194064
  });
@@ -195351,7 +195441,8 @@ class TimebackClient {
195351
195441
  this.edubridge = createEduBridgeNamespace(this);
195352
195442
  this.cacheManager = new TimebackCacheManager;
195353
195443
  this.studentResolver = new StudentResolver(this.cacheManager, this.oneroster);
195354
- this.progressRecorder = new ProgressRecorder(this.studentResolver, this.cacheManager, this.oneroster, this.caliper, this.edubridge);
195444
+ const masteryTracker = new MasteryTracker(this.cacheManager, this.oneroster, this.edubridge);
195445
+ this.progressRecorder = new ProgressRecorder(this.studentResolver, this.cacheManager, this.oneroster, this.caliper, masteryTracker);
195355
195446
  this.sessionRecorder = new SessionRecorder(this.studentResolver, this.caliper);
195356
195447
  if (this.credentials) {
195357
195448
  this._ensureAuthenticated().catch((error2) => {
@@ -195471,18 +195562,27 @@ class TimebackClient {
195471
195562
  await this._ensureAuthenticated();
195472
195563
  return this.sessionRecorder.record(courseId, studentIdentifier, sessionData);
195473
195564
  }
195474
- async getEnrollments(studentId, options) {
195565
+ async getEnrollments(studentId) {
195566
+ const cached = this.cacheManager.getEnrollments(studentId);
195567
+ if (cached) {
195568
+ return cached;
195569
+ }
195475
195570
  await this._ensureAuthenticated();
195476
- const classes = await this.oneroster.classes.listByStudent(studentId, options);
195477
- return classes.filter((cls) => cls.sourcedId && cls.status && cls.course?.sourcedId).map((cls) => ({
195478
- sourcedId: cls.sourcedId,
195479
- title: cls.title,
195480
- classCode: cls.classCode ?? null,
195481
- courseId: cls.course.sourcedId,
195482
- status: cls.status,
195483
- grades: cls.grades ?? null,
195484
- subjects: cls.subjects ?? null
195485
- }));
195571
+ const edubridgeEnrollments = await this.edubridge.enrollments.listByUser(studentId);
195572
+ const enrollments = edubridgeEnrollments.map((enrollment) => {
195573
+ const grades = enrollment.course.grades ? enrollment.course.grades.map((g52) => parseInt(g52, 10)).filter(isTimebackGrade) : null;
195574
+ const subjects = enrollment.course.subjects ? enrollment.course.subjects.filter(isTimebackSubject) : null;
195575
+ return {
195576
+ sourcedId: enrollment.id,
195577
+ title: enrollment.course.title,
195578
+ courseId: enrollment.course.id,
195579
+ status: "active",
195580
+ grades,
195581
+ subjects
195582
+ };
195583
+ });
195584
+ this.cacheManager.setEnrollments(studentId, enrollments);
195585
+ return enrollments;
195486
195586
  }
195487
195587
  clearCaches() {
195488
195588
  this.cacheManager.clearAll();
@@ -195721,7 +195821,9 @@ var init_constants2 = __esm4(() => {
195721
195821
  studentTTL: 600000,
195722
195822
  studentMaxSize: 500,
195723
195823
  assessmentTTL: 1800000,
195724
- assessmentMaxSize: 200
195824
+ assessmentMaxSize: 200,
195825
+ enrollmentTTL: 5000,
195826
+ enrollmentMaxSize: 100
195725
195827
  };
195726
195828
  CONFIG_DEFAULTS2 = {
195727
195829
  fileNames: ["timeback.config.js", "timeback.config.json"]
@@ -195789,8 +195891,8 @@ var init_constants2 = __esm4(() => {
195789
195891
  });
195790
195892
  init_constants2();
195791
195893
  var isObject3 = (value) => typeof value === "object" && value !== null;
195792
- var SUBJECT_VALUES = TIMEBACK_SUBJECTS2;
195793
- var GRADE_VALUES = TIMEBACK_GRADE_LEVELS2;
195894
+ var SUBJECT_VALUES2 = TIMEBACK_SUBJECTS2;
195895
+ var GRADE_VALUES2 = TIMEBACK_GRADE_LEVELS2;
195794
195896
  function isCourseMetadata(value) {
195795
195897
  return isObject3(value);
195796
195898
  }
@@ -195806,11 +195908,11 @@ function isPlaycademyResourceMetadata2(value) {
195806
195908
  }
195807
195909
  return isObject3(value.mastery);
195808
195910
  }
195809
- function isTimebackSubject(value) {
195810
- return typeof value === "string" && SUBJECT_VALUES.includes(value);
195911
+ function isTimebackSubject2(value) {
195912
+ return typeof value === "string" && SUBJECT_VALUES2.includes(value);
195811
195913
  }
195812
- function isTimebackGrade(value) {
195813
- return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
195914
+ function isTimebackGrade2(value) {
195915
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES2.includes(value);
195814
195916
  }
195815
195917
  var UUID_REGEX = /^[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}$/;
195816
195918
  function isValidUUID(value) {
@@ -209934,11 +210036,11 @@ async function setupTimebackIntegration(ctx) {
209934
210036
  totalXp: derivedTotalXp,
209935
210037
  masterableUnits: derivedMasterableUnits
209936
210038
  } = courseConfig;
209937
- if (!isTimebackSubject(subject)) {
210039
+ if (!isTimebackSubject2(subject)) {
209938
210040
  log2.error("[API] Invalid subject in TimeBack request", { subject });
209939
210041
  throw ApiError.badRequest(`Invalid subject "${subject}" provided for course "${title}".`);
209940
210042
  }
209941
- if (!isTimebackGrade(grade)) {
210043
+ if (!isTimebackGrade2(grade)) {
209942
210044
  log2.error("[API] Invalid grade in TimeBack request", { grade });
209943
210045
  throw ApiError.badRequest(`Invalid grade "${grade}" provided for course "${title}".`);
209944
210046
  }
@@ -210334,14 +210436,34 @@ async function getStudentEnrollments(ctx) {
210334
210436
  if (!timebackId) {
210335
210437
  throw ApiError.badRequest("Missing timebackId parameter");
210336
210438
  }
210439
+ const db = getDatabase();
210440
+ const isLocal = process.env.PUBLIC_IS_LOCAL === "true";
210441
+ if (isLocal) {
210442
+ log2.debug("[API] Local mode: returning all integrations as mock enrollments", {
210443
+ userId: user.id,
210444
+ timebackId
210445
+ });
210446
+ const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
210447
+ const enrollments2 = allIntegrations.map((integration) => ({
210448
+ gameId: integration.gameId,
210449
+ grade: integration.grade,
210450
+ subject: integration.subject,
210451
+ courseId: integration.courseId
210452
+ }));
210453
+ log2.info("[API] Retrieved mock enrollments (local mode)", {
210454
+ userId: user.id,
210455
+ timebackId,
210456
+ enrollmentCount: enrollments2.length
210457
+ });
210458
+ return { enrollments: enrollments2 };
210459
+ }
210337
210460
  log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
210338
210461
  const client2 = await getTimebackClient();
210339
- const classes = await client2.getEnrollments(timebackId, { limit: 100 });
210462
+ const classes = await client2.getEnrollments(timebackId);
210340
210463
  const courseIds = classes.map((cls) => cls.courseId).filter((id) => Boolean(id));
210341
210464
  if (courseIds.length === 0) {
210342
210465
  return { enrollments: [] };
210343
210466
  }
210344
- const db = getDatabase();
210345
210467
  const integrations = await db.query.gameTimebackIntegrations.findMany({
210346
210468
  where: inArray(gameTimebackIntegrations.courseId, courseIds)
210347
210469
  });
@@ -210549,11 +210671,11 @@ timebackRouter.post("/end-activity", async (c3) => {
210549
210671
  return c3.json(createUnknownErrorResponse(error2), 500);
210550
210672
  }
210551
210673
  });
210552
- timebackRouter.get("/enrollments/:studentId", async (c3) => {
210553
- const studentId = c3.req.param("studentId");
210674
+ timebackRouter.get("/enrollments/:timebackId", async (c3) => {
210675
+ const timebackId = c3.req.param("timebackId");
210554
210676
  const ctx = {
210555
210677
  user: c3.get("user"),
210556
- params: { studentId },
210678
+ params: { timebackId },
210557
210679
  url: new URL(c3.req.url),
210558
210680
  request: c3.req.raw
210559
210681
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -21,7 +21,7 @@
21
21
  "dependencies": {
22
22
  "archiver": "^7.0.1",
23
23
  "picocolors": "^1.1.1",
24
- "playcademy": "0.14.22"
24
+ "playcademy": "0.14.24"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@inquirer/prompts": "^7.8.6",