@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.
- package/dist/index.js +286 -164
- 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.
|
|
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
|
-
|
|
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)}
|
|
190577
|
+
const url = `${ONEROSTER_ENDPOINTS.assessmentResults}?filter=${encodeURIComponent(filter2)}`;
|
|
190557
190578
|
const response = await client2["request"](url, "GET");
|
|
190558
|
-
|
|
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
|
|
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
|
-
|
|
190904
|
-
|
|
190905
|
-
|
|
190906
|
-
|
|
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
|
-
|
|
190909
|
-
|
|
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
|
-
|
|
190967
|
-
constructor(studentResolver, cacheManager, onerosterNamespace, caliperNamespace,
|
|
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.
|
|
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
|
|
190982
|
-
const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned,
|
|
191166
|
+
const isFirstActiveAttempt = currentAttemptNumber === 1;
|
|
191167
|
+
const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, isFirstActiveAttempt);
|
|
190983
191168
|
let extensions = progressData.extensions;
|
|
190984
|
-
const
|
|
191169
|
+
const masteryProgress = await this.masteryTracker.checkProgress({
|
|
190985
191170
|
studentId,
|
|
190986
191171
|
courseId,
|
|
190987
|
-
|
|
190988
|
-
|
|
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.
|
|
191177
|
+
let scoreStatus = SCORE_STATUS.fullyGraded;
|
|
190993
191178
|
const inProgress = "false";
|
|
190994
|
-
if (
|
|
190995
|
-
masteryAchieved =
|
|
190996
|
-
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
|
|
191185
|
-
if (
|
|
191186
|
-
|
|
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
|
|
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 = (
|
|
194059
|
+
ZodUnion2.create = (types22, params) => {
|
|
193970
194060
|
return new ZodUnion2({
|
|
193971
|
-
options:
|
|
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
|
-
|
|
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
|
|
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
|
|
195477
|
-
|
|
195478
|
-
|
|
195479
|
-
|
|
195480
|
-
|
|
195481
|
-
|
|
195482
|
-
|
|
195483
|
-
|
|
195484
|
-
|
|
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
|
|
195793
|
-
var
|
|
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
|
|
195810
|
-
return typeof value === "string" &&
|
|
195911
|
+
function isTimebackSubject2(value) {
|
|
195912
|
+
return typeof value === "string" && SUBJECT_VALUES2.includes(value);
|
|
195811
195913
|
}
|
|
195812
|
-
function
|
|
195813
|
-
return typeof value === "number" && Number.isInteger(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 (!
|
|
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 (!
|
|
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
|
|
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/:
|
|
210553
|
-
const
|
|
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: {
|
|
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.
|
|
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.
|
|
24
|
+
"playcademy": "0.14.24"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@inquirer/prompts": "^7.8.6",
|