@playcademy/sandbox 0.3.16-beta.3 → 0.3.16-beta.4
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/cli.js +170 -45
- package/dist/server.js +170 -45
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1310,7 +1310,7 @@ var package_default;
|
|
|
1310
1310
|
var init_package = __esm(() => {
|
|
1311
1311
|
package_default = {
|
|
1312
1312
|
name: "@playcademy/sandbox",
|
|
1313
|
-
version: "0.3.16-beta.
|
|
1313
|
+
version: "0.3.16-beta.4",
|
|
1314
1314
|
description: "Local development server for Playcademy game development",
|
|
1315
1315
|
type: "module",
|
|
1316
1316
|
exports: {
|
|
@@ -30207,16 +30207,7 @@ function mapAssessmentResultToRecentActivity(assessment, relevantCourseIds, cour
|
|
|
30207
30207
|
return null;
|
|
30208
30208
|
}
|
|
30209
30209
|
if (isMasteryCompletionEntry(assessment)) {
|
|
30210
|
-
|
|
30211
|
-
const isResume = Boolean(metadata3?.resumedAt);
|
|
30212
|
-
return {
|
|
30213
|
-
id: assessment.sourcedId || `${assessment.assessmentLineItem.sourcedId}:${assessment.scoreDate}`,
|
|
30214
|
-
kind: "admin-completion",
|
|
30215
|
-
occurredAt: assessment.scoreDate,
|
|
30216
|
-
courseId,
|
|
30217
|
-
title: isResume ? "Course resumed" : "Course marked complete",
|
|
30218
|
-
reason: metadata3?.adminAction ? "Admin action" : undefined
|
|
30219
|
-
};
|
|
30210
|
+
return null;
|
|
30220
30211
|
}
|
|
30221
30212
|
const metadata2 = isRecord2(assessment.metadata) ? assessment.metadata : undefined;
|
|
30222
30213
|
const activityName = getStringValue(metadata2?.activityName);
|
|
@@ -30249,6 +30240,7 @@ function parseCaliperEventContext(event, relevantCourseIds) {
|
|
|
30249
30240
|
courseId,
|
|
30250
30241
|
occurredAt,
|
|
30251
30242
|
eventKind: getStringValue(playcademy?.eventKind),
|
|
30243
|
+
source: getStringValue(playcademy?.source),
|
|
30252
30244
|
reason: getStringValue(playcademy?.reason),
|
|
30253
30245
|
titleFromEvent: getStringValue(event.object.activity?.name),
|
|
30254
30246
|
appName: getStringValue(event.object.app?.name),
|
|
@@ -30272,30 +30264,59 @@ function mapTimeSpentRemediation(event, ctx) {
|
|
|
30272
30264
|
};
|
|
30273
30265
|
}
|
|
30274
30266
|
function mapActivityRemediation(event, ctx) {
|
|
30275
|
-
let kind;
|
|
30276
30267
|
if (ctx.eventKind === "remediation-xp") {
|
|
30277
|
-
|
|
30278
|
-
|
|
30279
|
-
|
|
30280
|
-
|
|
30281
|
-
|
|
30268
|
+
return {
|
|
30269
|
+
id: event.externalId,
|
|
30270
|
+
kind: "remediation-xp",
|
|
30271
|
+
occurredAt: ctx.occurredAt,
|
|
30272
|
+
courseId: ctx.courseId,
|
|
30273
|
+
title: "XP Adjustment",
|
|
30274
|
+
activityId: ctx.activityId,
|
|
30275
|
+
appName: ctx.appName,
|
|
30276
|
+
reason: ctx.reason,
|
|
30277
|
+
xpDelta: getGeneratedMetricValue(event, "xpEarned"),
|
|
30278
|
+
masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
|
|
30279
|
+
};
|
|
30282
30280
|
}
|
|
30283
|
-
|
|
30284
|
-
|
|
30285
|
-
|
|
30286
|
-
|
|
30287
|
-
|
|
30288
|
-
|
|
30289
|
-
|
|
30290
|
-
|
|
30291
|
-
|
|
30292
|
-
|
|
30293
|
-
|
|
30294
|
-
|
|
30295
|
-
|
|
30296
|
-
|
|
30297
|
-
|
|
30298
|
-
|
|
30281
|
+
if (ctx.eventKind === "remediation-mastery") {
|
|
30282
|
+
return {
|
|
30283
|
+
id: event.externalId,
|
|
30284
|
+
kind: "remediation-mastery",
|
|
30285
|
+
occurredAt: ctx.occurredAt,
|
|
30286
|
+
courseId: ctx.courseId,
|
|
30287
|
+
title: "Mastery Adjustment",
|
|
30288
|
+
activityId: ctx.activityId,
|
|
30289
|
+
appName: ctx.appName,
|
|
30290
|
+
reason: ctx.reason,
|
|
30291
|
+
xpDelta: getGeneratedMetricValue(event, "xpEarned"),
|
|
30292
|
+
masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
|
|
30293
|
+
};
|
|
30294
|
+
}
|
|
30295
|
+
if (ctx.eventKind === "course-completed") {
|
|
30296
|
+
return {
|
|
30297
|
+
id: event.externalId,
|
|
30298
|
+
kind: "course-completed",
|
|
30299
|
+
occurredAt: ctx.occurredAt,
|
|
30300
|
+
courseId: ctx.courseId,
|
|
30301
|
+
title: ctx.source === "admin" ? "Course marked complete" : "Course completed",
|
|
30302
|
+
activityId: ctx.activityId,
|
|
30303
|
+
appName: ctx.appName,
|
|
30304
|
+
reason: ctx.reason
|
|
30305
|
+
};
|
|
30306
|
+
}
|
|
30307
|
+
if (ctx.eventKind === "course-resumed") {
|
|
30308
|
+
return {
|
|
30309
|
+
id: event.externalId,
|
|
30310
|
+
kind: "course-resumed",
|
|
30311
|
+
occurredAt: ctx.occurredAt,
|
|
30312
|
+
courseId: ctx.courseId,
|
|
30313
|
+
title: "Course resumed",
|
|
30314
|
+
activityId: ctx.activityId,
|
|
30315
|
+
appName: ctx.appName,
|
|
30316
|
+
reason: ctx.reason
|
|
30317
|
+
};
|
|
30318
|
+
}
|
|
30319
|
+
return null;
|
|
30299
30320
|
}
|
|
30300
30321
|
function mapCaliperEventToRemediationActivity(event, relevantCourseIds) {
|
|
30301
30322
|
const ctx = parseCaliperEventContext(event, relevantCourseIds);
|
|
@@ -30317,6 +30338,7 @@ var init_timeback_util = __esm(() => {
|
|
|
30317
30338
|
// ../api-core/src/services/timeback-admin.service.ts
|
|
30318
30339
|
class TimebackAdminService {
|
|
30319
30340
|
deps;
|
|
30341
|
+
static XP_PRECISION_FACTOR = 10;
|
|
30320
30342
|
static RECENT_ACTIVITY_LIMIT = 20;
|
|
30321
30343
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
30322
30344
|
static MAX_STUDENT_ACTIVITY_OFFSET = 1000;
|
|
@@ -30328,6 +30350,10 @@ class TimebackAdminService {
|
|
|
30328
30350
|
constructor(deps) {
|
|
30329
30351
|
this.deps = deps;
|
|
30330
30352
|
}
|
|
30353
|
+
static roundXpToTenths(value) {
|
|
30354
|
+
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
30355
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
30356
|
+
}
|
|
30331
30357
|
requireClient() {
|
|
30332
30358
|
if (!this.deps.timeback) {
|
|
30333
30359
|
logger16.error("Timeback client not available in context");
|
|
@@ -30335,6 +30361,17 @@ class TimebackAdminService {
|
|
|
30335
30361
|
}
|
|
30336
30362
|
return this.deps.timeback;
|
|
30337
30363
|
}
|
|
30364
|
+
async recordCourseCompletionHistory(client, data) {
|
|
30365
|
+
await client.recordAdminCourseCompletionChange(data).catch((error) => {
|
|
30366
|
+
logger16.error("Failed to record admin course completion history event", {
|
|
30367
|
+
gameId: data.gameId,
|
|
30368
|
+
courseId: data.courseId,
|
|
30369
|
+
studentId: data.studentId,
|
|
30370
|
+
action: data.action,
|
|
30371
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30372
|
+
});
|
|
30373
|
+
});
|
|
30374
|
+
}
|
|
30338
30375
|
async resolveAdminMutationContext(gameId, courseId, user, studentId) {
|
|
30339
30376
|
const client = this.requireClient();
|
|
30340
30377
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
@@ -30374,8 +30411,8 @@ class TimebackAdminService {
|
|
|
30374
30411
|
}
|
|
30375
30412
|
const today = formatDateYMD();
|
|
30376
30413
|
const history = [];
|
|
30377
|
-
let
|
|
30378
|
-
let
|
|
30414
|
+
let totalXpRaw = 0;
|
|
30415
|
+
let todayXpRaw = 0;
|
|
30379
30416
|
let activeTimeSeconds = 0;
|
|
30380
30417
|
let masteredUnits = 0;
|
|
30381
30418
|
for (const [date3, subjectFacts] of Object.entries(facts)) {
|
|
@@ -30392,15 +30429,16 @@ class TimebackAdminService {
|
|
|
30392
30429
|
masteredUnitsForDay += masteredUnitsFromFact;
|
|
30393
30430
|
}
|
|
30394
30431
|
}
|
|
30395
|
-
|
|
30432
|
+
const roundedXpForDay = TimebackAdminService.roundXpToTenths(xpForDay);
|
|
30433
|
+
totalXpRaw += xpForDay;
|
|
30396
30434
|
activeTimeSeconds += activeSecondsForDay;
|
|
30397
30435
|
masteredUnits += masteredUnitsForDay;
|
|
30398
30436
|
if (date3 === today) {
|
|
30399
|
-
|
|
30437
|
+
todayXpRaw += xpForDay;
|
|
30400
30438
|
}
|
|
30401
30439
|
history.push({
|
|
30402
30440
|
date: date3,
|
|
30403
|
-
xpEarned:
|
|
30441
|
+
xpEarned: roundedXpForDay,
|
|
30404
30442
|
activeTimeSeconds: activeSecondsForDay,
|
|
30405
30443
|
masteredUnits: masteredUnitsForDay
|
|
30406
30444
|
});
|
|
@@ -30408,8 +30446,8 @@ class TimebackAdminService {
|
|
|
30408
30446
|
history.sort((a, b) => a.date.localeCompare(b.date));
|
|
30409
30447
|
return {
|
|
30410
30448
|
analyticsAvailable: true,
|
|
30411
|
-
totalXp,
|
|
30412
|
-
todayXp,
|
|
30449
|
+
totalXp: TimebackAdminService.roundXpToTenths(totalXpRaw),
|
|
30450
|
+
todayXp: TimebackAdminService.roundXpToTenths(todayXpRaw),
|
|
30413
30451
|
activeTimeSeconds,
|
|
30414
30452
|
masteredUnits,
|
|
30415
30453
|
history
|
|
@@ -30751,6 +30789,7 @@ class TimebackAdminService {
|
|
|
30751
30789
|
}
|
|
30752
30790
|
async toggleCourseCompletion(data, user) {
|
|
30753
30791
|
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
30792
|
+
const historyClient = client;
|
|
30754
30793
|
const ids = deriveSourcedIds(data.courseId);
|
|
30755
30794
|
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
30756
30795
|
const resultId = `${lineItemId}:${data.studentId}:completion`;
|
|
@@ -30801,11 +30840,19 @@ class TimebackAdminService {
|
|
|
30801
30840
|
inProgress: "false",
|
|
30802
30841
|
metadata: {
|
|
30803
30842
|
isMasteryCompletion: true,
|
|
30804
|
-
completedAt: new Date().toISOString(),
|
|
30805
30843
|
adminAction: true,
|
|
30806
30844
|
appName
|
|
30807
30845
|
}
|
|
30808
30846
|
});
|
|
30847
|
+
await this.recordCourseCompletionHistory(historyClient, {
|
|
30848
|
+
gameId: data.gameId,
|
|
30849
|
+
courseId: data.courseId,
|
|
30850
|
+
studentId: data.studentId,
|
|
30851
|
+
action: "complete",
|
|
30852
|
+
actor,
|
|
30853
|
+
appName,
|
|
30854
|
+
sensorUrl
|
|
30855
|
+
});
|
|
30809
30856
|
} else {
|
|
30810
30857
|
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
30811
30858
|
sourcedId: resultId,
|
|
@@ -30818,11 +30865,19 @@ class TimebackAdminService {
|
|
|
30818
30865
|
inProgress: "true",
|
|
30819
30866
|
metadata: {
|
|
30820
30867
|
isMasteryCompletion: true,
|
|
30821
|
-
resumedAt: new Date().toISOString(),
|
|
30822
30868
|
adminAction: true,
|
|
30823
30869
|
appName
|
|
30824
30870
|
}
|
|
30825
30871
|
});
|
|
30872
|
+
await this.recordCourseCompletionHistory(historyClient, {
|
|
30873
|
+
gameId: data.gameId,
|
|
30874
|
+
courseId: data.courseId,
|
|
30875
|
+
studentId: data.studentId,
|
|
30876
|
+
action: "resume",
|
|
30877
|
+
actor,
|
|
30878
|
+
appName,
|
|
30879
|
+
sensorUrl
|
|
30880
|
+
});
|
|
30826
30881
|
}
|
|
30827
30882
|
return { status: "ok" };
|
|
30828
30883
|
}
|
|
@@ -35089,7 +35144,8 @@ function buildAdminEventMetadata({
|
|
|
35089
35144
|
return {
|
|
35090
35145
|
playcademy: {
|
|
35091
35146
|
eventKind,
|
|
35092
|
-
reason
|
|
35147
|
+
reason,
|
|
35148
|
+
source: "admin"
|
|
35093
35149
|
}
|
|
35094
35150
|
};
|
|
35095
35151
|
}
|
|
@@ -35205,6 +35261,30 @@ class AdminEventRecorder {
|
|
|
35205
35261
|
eventExtensions: ctx.metadata
|
|
35206
35262
|
});
|
|
35207
35263
|
}
|
|
35264
|
+
async recordCourseCompletionChange(data) {
|
|
35265
|
+
const isResume = data.action === "resume";
|
|
35266
|
+
const ctx = await this.prepareAdminEvent({
|
|
35267
|
+
...data,
|
|
35268
|
+
defaultActivityId: isResume ? "playcademy-admin-course-resumed" : "playcademy-admin-course-completed",
|
|
35269
|
+
reason: "Admin action",
|
|
35270
|
+
eventKind: isResume ? "course-resumed" : "course-completed"
|
|
35271
|
+
});
|
|
35272
|
+
await this.caliper.emitActivityEvent({
|
|
35273
|
+
studentId: ctx.student.id,
|
|
35274
|
+
studentEmail: ctx.student.email,
|
|
35275
|
+
activityId: ctx.activityId,
|
|
35276
|
+
activityName: isResume ? "Course resumed" : "Course marked complete",
|
|
35277
|
+
courseId: data.courseId,
|
|
35278
|
+
courseName: ctx.courseContext.courseName,
|
|
35279
|
+
subject: ctx.courseContext.subject,
|
|
35280
|
+
appName: ctx.appName,
|
|
35281
|
+
sensorUrl: ctx.sensorUrl,
|
|
35282
|
+
process: false,
|
|
35283
|
+
includeAttempt: false,
|
|
35284
|
+
generatedExtensions: ctx.metadata,
|
|
35285
|
+
eventExtensions: ctx.metadata
|
|
35286
|
+
});
|
|
35287
|
+
}
|
|
35208
35288
|
}
|
|
35209
35289
|
|
|
35210
35290
|
class TimebackCache {
|
|
@@ -35418,7 +35498,7 @@ class MasteryTracker {
|
|
|
35418
35498
|
const totalMastered = historicalMasteredUnits + masteredUnits;
|
|
35419
35499
|
const rawPct = totalMastered / masterableUnits * 100;
|
|
35420
35500
|
const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
|
|
35421
|
-
const masteryAchieved = totalMastered >= masterableUnits;
|
|
35501
|
+
const masteryAchieved = historicalMasteredUnits < masterableUnits && totalMastered >= masterableUnits;
|
|
35422
35502
|
return { pctCompleteApp, masteryAchieved };
|
|
35423
35503
|
}
|
|
35424
35504
|
async createCompletionEntry(studentId, courseId, classId, appName) {
|
|
@@ -35444,7 +35524,6 @@ class MasteryTracker {
|
|
|
35444
35524
|
inProgress: "false",
|
|
35445
35525
|
metadata: {
|
|
35446
35526
|
isMasteryCompletion: true,
|
|
35447
|
-
completedAt: new Date().toISOString(),
|
|
35448
35527
|
appName
|
|
35449
35528
|
}
|
|
35450
35529
|
});
|
|
@@ -35660,6 +35739,16 @@ class ProgressRecorder {
|
|
|
35660
35739
|
}
|
|
35661
35740
|
if (masteryAchieved) {
|
|
35662
35741
|
await this.masteryTracker.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
35742
|
+
await this.emitCourseCompletionHistoryEvent({
|
|
35743
|
+
studentId,
|
|
35744
|
+
studentEmail,
|
|
35745
|
+
activityId,
|
|
35746
|
+
courseId: ids.course,
|
|
35747
|
+
courseName,
|
|
35748
|
+
subject: progressData.subject,
|
|
35749
|
+
appName: progressData.appName,
|
|
35750
|
+
sensorUrl: progressData.sensorUrl
|
|
35751
|
+
});
|
|
35663
35752
|
}
|
|
35664
35753
|
await this.emitCaliperEvent({
|
|
35665
35754
|
studentId,
|
|
@@ -35834,6 +35923,38 @@ class ProgressRecorder {
|
|
|
35834
35923
|
log.error("[ProgressRecorder] Failed to emit activity event", { error });
|
|
35835
35924
|
});
|
|
35836
35925
|
}
|
|
35926
|
+
async emitCourseCompletionHistoryEvent(data) {
|
|
35927
|
+
await this.caliperNamespace.emitActivityEvent({
|
|
35928
|
+
studentId: data.studentId,
|
|
35929
|
+
studentEmail: data.studentEmail,
|
|
35930
|
+
activityId: data.activityId,
|
|
35931
|
+
activityName: "Course completed",
|
|
35932
|
+
courseId: data.courseId,
|
|
35933
|
+
courseName: data.courseName,
|
|
35934
|
+
subject: data.subject,
|
|
35935
|
+
appName: data.appName,
|
|
35936
|
+
sensorUrl: data.sensorUrl,
|
|
35937
|
+
process: false,
|
|
35938
|
+
includeAttempt: false,
|
|
35939
|
+
eventExtensions: {
|
|
35940
|
+
playcademy: {
|
|
35941
|
+
eventKind: "course-completed",
|
|
35942
|
+
source: "gameplay"
|
|
35943
|
+
}
|
|
35944
|
+
},
|
|
35945
|
+
generatedExtensions: {
|
|
35946
|
+
playcademy: {
|
|
35947
|
+
eventKind: "course-completed",
|
|
35948
|
+
source: "gameplay",
|
|
35949
|
+
activityId: data.activityId
|
|
35950
|
+
}
|
|
35951
|
+
}
|
|
35952
|
+
}).catch((error) => {
|
|
35953
|
+
log.error("[ProgressRecorder] Failed to emit course completion history event", {
|
|
35954
|
+
error
|
|
35955
|
+
});
|
|
35956
|
+
});
|
|
35957
|
+
}
|
|
35837
35958
|
}
|
|
35838
35959
|
|
|
35839
35960
|
class SessionRecorder {
|
|
@@ -36228,6 +36349,10 @@ class TimebackClient {
|
|
|
36228
36349
|
await this._ensureAuthenticated();
|
|
36229
36350
|
return this.adminEventRecorder.recordMasteryAdjustment(data);
|
|
36230
36351
|
}
|
|
36352
|
+
async recordAdminCourseCompletionChange(data) {
|
|
36353
|
+
await this._ensureAuthenticated();
|
|
36354
|
+
return this.adminEventRecorder.recordCourseCompletionChange(data);
|
|
36355
|
+
}
|
|
36231
36356
|
clearCaches() {
|
|
36232
36357
|
this.cacheManager.clearAll();
|
|
36233
36358
|
}
|
package/dist/server.js
CHANGED
|
@@ -1309,7 +1309,7 @@ var package_default;
|
|
|
1309
1309
|
var init_package = __esm(() => {
|
|
1310
1310
|
package_default = {
|
|
1311
1311
|
name: "@playcademy/sandbox",
|
|
1312
|
-
version: "0.3.16-beta.
|
|
1312
|
+
version: "0.3.16-beta.4",
|
|
1313
1313
|
description: "Local development server for Playcademy game development",
|
|
1314
1314
|
type: "module",
|
|
1315
1315
|
exports: {
|
|
@@ -30206,16 +30206,7 @@ function mapAssessmentResultToRecentActivity(assessment, relevantCourseIds, cour
|
|
|
30206
30206
|
return null;
|
|
30207
30207
|
}
|
|
30208
30208
|
if (isMasteryCompletionEntry(assessment)) {
|
|
30209
|
-
|
|
30210
|
-
const isResume = Boolean(metadata3?.resumedAt);
|
|
30211
|
-
return {
|
|
30212
|
-
id: assessment.sourcedId || `${assessment.assessmentLineItem.sourcedId}:${assessment.scoreDate}`,
|
|
30213
|
-
kind: "admin-completion",
|
|
30214
|
-
occurredAt: assessment.scoreDate,
|
|
30215
|
-
courseId,
|
|
30216
|
-
title: isResume ? "Course resumed" : "Course marked complete",
|
|
30217
|
-
reason: metadata3?.adminAction ? "Admin action" : undefined
|
|
30218
|
-
};
|
|
30209
|
+
return null;
|
|
30219
30210
|
}
|
|
30220
30211
|
const metadata2 = isRecord2(assessment.metadata) ? assessment.metadata : undefined;
|
|
30221
30212
|
const activityName = getStringValue(metadata2?.activityName);
|
|
@@ -30248,6 +30239,7 @@ function parseCaliperEventContext(event, relevantCourseIds) {
|
|
|
30248
30239
|
courseId,
|
|
30249
30240
|
occurredAt,
|
|
30250
30241
|
eventKind: getStringValue(playcademy?.eventKind),
|
|
30242
|
+
source: getStringValue(playcademy?.source),
|
|
30251
30243
|
reason: getStringValue(playcademy?.reason),
|
|
30252
30244
|
titleFromEvent: getStringValue(event.object.activity?.name),
|
|
30253
30245
|
appName: getStringValue(event.object.app?.name),
|
|
@@ -30271,30 +30263,59 @@ function mapTimeSpentRemediation(event, ctx) {
|
|
|
30271
30263
|
};
|
|
30272
30264
|
}
|
|
30273
30265
|
function mapActivityRemediation(event, ctx) {
|
|
30274
|
-
let kind;
|
|
30275
30266
|
if (ctx.eventKind === "remediation-xp") {
|
|
30276
|
-
|
|
30277
|
-
|
|
30278
|
-
|
|
30279
|
-
|
|
30280
|
-
|
|
30267
|
+
return {
|
|
30268
|
+
id: event.externalId,
|
|
30269
|
+
kind: "remediation-xp",
|
|
30270
|
+
occurredAt: ctx.occurredAt,
|
|
30271
|
+
courseId: ctx.courseId,
|
|
30272
|
+
title: "XP Adjustment",
|
|
30273
|
+
activityId: ctx.activityId,
|
|
30274
|
+
appName: ctx.appName,
|
|
30275
|
+
reason: ctx.reason,
|
|
30276
|
+
xpDelta: getGeneratedMetricValue(event, "xpEarned"),
|
|
30277
|
+
masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
|
|
30278
|
+
};
|
|
30281
30279
|
}
|
|
30282
|
-
|
|
30283
|
-
|
|
30284
|
-
|
|
30285
|
-
|
|
30286
|
-
|
|
30287
|
-
|
|
30288
|
-
|
|
30289
|
-
|
|
30290
|
-
|
|
30291
|
-
|
|
30292
|
-
|
|
30293
|
-
|
|
30294
|
-
|
|
30295
|
-
|
|
30296
|
-
|
|
30297
|
-
|
|
30280
|
+
if (ctx.eventKind === "remediation-mastery") {
|
|
30281
|
+
return {
|
|
30282
|
+
id: event.externalId,
|
|
30283
|
+
kind: "remediation-mastery",
|
|
30284
|
+
occurredAt: ctx.occurredAt,
|
|
30285
|
+
courseId: ctx.courseId,
|
|
30286
|
+
title: "Mastery Adjustment",
|
|
30287
|
+
activityId: ctx.activityId,
|
|
30288
|
+
appName: ctx.appName,
|
|
30289
|
+
reason: ctx.reason,
|
|
30290
|
+
xpDelta: getGeneratedMetricValue(event, "xpEarned"),
|
|
30291
|
+
masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
|
|
30292
|
+
};
|
|
30293
|
+
}
|
|
30294
|
+
if (ctx.eventKind === "course-completed") {
|
|
30295
|
+
return {
|
|
30296
|
+
id: event.externalId,
|
|
30297
|
+
kind: "course-completed",
|
|
30298
|
+
occurredAt: ctx.occurredAt,
|
|
30299
|
+
courseId: ctx.courseId,
|
|
30300
|
+
title: ctx.source === "admin" ? "Course marked complete" : "Course completed",
|
|
30301
|
+
activityId: ctx.activityId,
|
|
30302
|
+
appName: ctx.appName,
|
|
30303
|
+
reason: ctx.reason
|
|
30304
|
+
};
|
|
30305
|
+
}
|
|
30306
|
+
if (ctx.eventKind === "course-resumed") {
|
|
30307
|
+
return {
|
|
30308
|
+
id: event.externalId,
|
|
30309
|
+
kind: "course-resumed",
|
|
30310
|
+
occurredAt: ctx.occurredAt,
|
|
30311
|
+
courseId: ctx.courseId,
|
|
30312
|
+
title: "Course resumed",
|
|
30313
|
+
activityId: ctx.activityId,
|
|
30314
|
+
appName: ctx.appName,
|
|
30315
|
+
reason: ctx.reason
|
|
30316
|
+
};
|
|
30317
|
+
}
|
|
30318
|
+
return null;
|
|
30298
30319
|
}
|
|
30299
30320
|
function mapCaliperEventToRemediationActivity(event, relevantCourseIds) {
|
|
30300
30321
|
const ctx = parseCaliperEventContext(event, relevantCourseIds);
|
|
@@ -30316,6 +30337,7 @@ var init_timeback_util = __esm(() => {
|
|
|
30316
30337
|
// ../api-core/src/services/timeback-admin.service.ts
|
|
30317
30338
|
class TimebackAdminService {
|
|
30318
30339
|
deps;
|
|
30340
|
+
static XP_PRECISION_FACTOR = 10;
|
|
30319
30341
|
static RECENT_ACTIVITY_LIMIT = 20;
|
|
30320
30342
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
30321
30343
|
static MAX_STUDENT_ACTIVITY_OFFSET = 1000;
|
|
@@ -30327,6 +30349,10 @@ class TimebackAdminService {
|
|
|
30327
30349
|
constructor(deps) {
|
|
30328
30350
|
this.deps = deps;
|
|
30329
30351
|
}
|
|
30352
|
+
static roundXpToTenths(value) {
|
|
30353
|
+
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
30354
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
30355
|
+
}
|
|
30330
30356
|
requireClient() {
|
|
30331
30357
|
if (!this.deps.timeback) {
|
|
30332
30358
|
logger16.error("Timeback client not available in context");
|
|
@@ -30334,6 +30360,17 @@ class TimebackAdminService {
|
|
|
30334
30360
|
}
|
|
30335
30361
|
return this.deps.timeback;
|
|
30336
30362
|
}
|
|
30363
|
+
async recordCourseCompletionHistory(client, data) {
|
|
30364
|
+
await client.recordAdminCourseCompletionChange(data).catch((error) => {
|
|
30365
|
+
logger16.error("Failed to record admin course completion history event", {
|
|
30366
|
+
gameId: data.gameId,
|
|
30367
|
+
courseId: data.courseId,
|
|
30368
|
+
studentId: data.studentId,
|
|
30369
|
+
action: data.action,
|
|
30370
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30371
|
+
});
|
|
30372
|
+
});
|
|
30373
|
+
}
|
|
30337
30374
|
async resolveAdminMutationContext(gameId, courseId, user, studentId) {
|
|
30338
30375
|
const client = this.requireClient();
|
|
30339
30376
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
@@ -30373,8 +30410,8 @@ class TimebackAdminService {
|
|
|
30373
30410
|
}
|
|
30374
30411
|
const today = formatDateYMD();
|
|
30375
30412
|
const history = [];
|
|
30376
|
-
let
|
|
30377
|
-
let
|
|
30413
|
+
let totalXpRaw = 0;
|
|
30414
|
+
let todayXpRaw = 0;
|
|
30378
30415
|
let activeTimeSeconds = 0;
|
|
30379
30416
|
let masteredUnits = 0;
|
|
30380
30417
|
for (const [date3, subjectFacts] of Object.entries(facts)) {
|
|
@@ -30391,15 +30428,16 @@ class TimebackAdminService {
|
|
|
30391
30428
|
masteredUnitsForDay += masteredUnitsFromFact;
|
|
30392
30429
|
}
|
|
30393
30430
|
}
|
|
30394
|
-
|
|
30431
|
+
const roundedXpForDay = TimebackAdminService.roundXpToTenths(xpForDay);
|
|
30432
|
+
totalXpRaw += xpForDay;
|
|
30395
30433
|
activeTimeSeconds += activeSecondsForDay;
|
|
30396
30434
|
masteredUnits += masteredUnitsForDay;
|
|
30397
30435
|
if (date3 === today) {
|
|
30398
|
-
|
|
30436
|
+
todayXpRaw += xpForDay;
|
|
30399
30437
|
}
|
|
30400
30438
|
history.push({
|
|
30401
30439
|
date: date3,
|
|
30402
|
-
xpEarned:
|
|
30440
|
+
xpEarned: roundedXpForDay,
|
|
30403
30441
|
activeTimeSeconds: activeSecondsForDay,
|
|
30404
30442
|
masteredUnits: masteredUnitsForDay
|
|
30405
30443
|
});
|
|
@@ -30407,8 +30445,8 @@ class TimebackAdminService {
|
|
|
30407
30445
|
history.sort((a, b) => a.date.localeCompare(b.date));
|
|
30408
30446
|
return {
|
|
30409
30447
|
analyticsAvailable: true,
|
|
30410
|
-
totalXp,
|
|
30411
|
-
todayXp,
|
|
30448
|
+
totalXp: TimebackAdminService.roundXpToTenths(totalXpRaw),
|
|
30449
|
+
todayXp: TimebackAdminService.roundXpToTenths(todayXpRaw),
|
|
30412
30450
|
activeTimeSeconds,
|
|
30413
30451
|
masteredUnits,
|
|
30414
30452
|
history
|
|
@@ -30750,6 +30788,7 @@ class TimebackAdminService {
|
|
|
30750
30788
|
}
|
|
30751
30789
|
async toggleCourseCompletion(data, user) {
|
|
30752
30790
|
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
30791
|
+
const historyClient = client;
|
|
30753
30792
|
const ids = deriveSourcedIds(data.courseId);
|
|
30754
30793
|
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
30755
30794
|
const resultId = `${lineItemId}:${data.studentId}:completion`;
|
|
@@ -30800,11 +30839,19 @@ class TimebackAdminService {
|
|
|
30800
30839
|
inProgress: "false",
|
|
30801
30840
|
metadata: {
|
|
30802
30841
|
isMasteryCompletion: true,
|
|
30803
|
-
completedAt: new Date().toISOString(),
|
|
30804
30842
|
adminAction: true,
|
|
30805
30843
|
appName
|
|
30806
30844
|
}
|
|
30807
30845
|
});
|
|
30846
|
+
await this.recordCourseCompletionHistory(historyClient, {
|
|
30847
|
+
gameId: data.gameId,
|
|
30848
|
+
courseId: data.courseId,
|
|
30849
|
+
studentId: data.studentId,
|
|
30850
|
+
action: "complete",
|
|
30851
|
+
actor,
|
|
30852
|
+
appName,
|
|
30853
|
+
sensorUrl
|
|
30854
|
+
});
|
|
30808
30855
|
} else {
|
|
30809
30856
|
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
30810
30857
|
sourcedId: resultId,
|
|
@@ -30817,11 +30864,19 @@ class TimebackAdminService {
|
|
|
30817
30864
|
inProgress: "true",
|
|
30818
30865
|
metadata: {
|
|
30819
30866
|
isMasteryCompletion: true,
|
|
30820
|
-
resumedAt: new Date().toISOString(),
|
|
30821
30867
|
adminAction: true,
|
|
30822
30868
|
appName
|
|
30823
30869
|
}
|
|
30824
30870
|
});
|
|
30871
|
+
await this.recordCourseCompletionHistory(historyClient, {
|
|
30872
|
+
gameId: data.gameId,
|
|
30873
|
+
courseId: data.courseId,
|
|
30874
|
+
studentId: data.studentId,
|
|
30875
|
+
action: "resume",
|
|
30876
|
+
actor,
|
|
30877
|
+
appName,
|
|
30878
|
+
sensorUrl
|
|
30879
|
+
});
|
|
30825
30880
|
}
|
|
30826
30881
|
return { status: "ok" };
|
|
30827
30882
|
}
|
|
@@ -35088,7 +35143,8 @@ function buildAdminEventMetadata({
|
|
|
35088
35143
|
return {
|
|
35089
35144
|
playcademy: {
|
|
35090
35145
|
eventKind,
|
|
35091
|
-
reason
|
|
35146
|
+
reason,
|
|
35147
|
+
source: "admin"
|
|
35092
35148
|
}
|
|
35093
35149
|
};
|
|
35094
35150
|
}
|
|
@@ -35204,6 +35260,30 @@ class AdminEventRecorder {
|
|
|
35204
35260
|
eventExtensions: ctx.metadata
|
|
35205
35261
|
});
|
|
35206
35262
|
}
|
|
35263
|
+
async recordCourseCompletionChange(data) {
|
|
35264
|
+
const isResume = data.action === "resume";
|
|
35265
|
+
const ctx = await this.prepareAdminEvent({
|
|
35266
|
+
...data,
|
|
35267
|
+
defaultActivityId: isResume ? "playcademy-admin-course-resumed" : "playcademy-admin-course-completed",
|
|
35268
|
+
reason: "Admin action",
|
|
35269
|
+
eventKind: isResume ? "course-resumed" : "course-completed"
|
|
35270
|
+
});
|
|
35271
|
+
await this.caliper.emitActivityEvent({
|
|
35272
|
+
studentId: ctx.student.id,
|
|
35273
|
+
studentEmail: ctx.student.email,
|
|
35274
|
+
activityId: ctx.activityId,
|
|
35275
|
+
activityName: isResume ? "Course resumed" : "Course marked complete",
|
|
35276
|
+
courseId: data.courseId,
|
|
35277
|
+
courseName: ctx.courseContext.courseName,
|
|
35278
|
+
subject: ctx.courseContext.subject,
|
|
35279
|
+
appName: ctx.appName,
|
|
35280
|
+
sensorUrl: ctx.sensorUrl,
|
|
35281
|
+
process: false,
|
|
35282
|
+
includeAttempt: false,
|
|
35283
|
+
generatedExtensions: ctx.metadata,
|
|
35284
|
+
eventExtensions: ctx.metadata
|
|
35285
|
+
});
|
|
35286
|
+
}
|
|
35207
35287
|
}
|
|
35208
35288
|
|
|
35209
35289
|
class TimebackCache {
|
|
@@ -35417,7 +35497,7 @@ class MasteryTracker {
|
|
|
35417
35497
|
const totalMastered = historicalMasteredUnits + masteredUnits;
|
|
35418
35498
|
const rawPct = totalMastered / masterableUnits * 100;
|
|
35419
35499
|
const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
|
|
35420
|
-
const masteryAchieved = totalMastered >= masterableUnits;
|
|
35500
|
+
const masteryAchieved = historicalMasteredUnits < masterableUnits && totalMastered >= masterableUnits;
|
|
35421
35501
|
return { pctCompleteApp, masteryAchieved };
|
|
35422
35502
|
}
|
|
35423
35503
|
async createCompletionEntry(studentId, courseId, classId, appName) {
|
|
@@ -35443,7 +35523,6 @@ class MasteryTracker {
|
|
|
35443
35523
|
inProgress: "false",
|
|
35444
35524
|
metadata: {
|
|
35445
35525
|
isMasteryCompletion: true,
|
|
35446
|
-
completedAt: new Date().toISOString(),
|
|
35447
35526
|
appName
|
|
35448
35527
|
}
|
|
35449
35528
|
});
|
|
@@ -35659,6 +35738,16 @@ class ProgressRecorder {
|
|
|
35659
35738
|
}
|
|
35660
35739
|
if (masteryAchieved) {
|
|
35661
35740
|
await this.masteryTracker.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
35741
|
+
await this.emitCourseCompletionHistoryEvent({
|
|
35742
|
+
studentId,
|
|
35743
|
+
studentEmail,
|
|
35744
|
+
activityId,
|
|
35745
|
+
courseId: ids.course,
|
|
35746
|
+
courseName,
|
|
35747
|
+
subject: progressData.subject,
|
|
35748
|
+
appName: progressData.appName,
|
|
35749
|
+
sensorUrl: progressData.sensorUrl
|
|
35750
|
+
});
|
|
35662
35751
|
}
|
|
35663
35752
|
await this.emitCaliperEvent({
|
|
35664
35753
|
studentId,
|
|
@@ -35833,6 +35922,38 @@ class ProgressRecorder {
|
|
|
35833
35922
|
log.error("[ProgressRecorder] Failed to emit activity event", { error });
|
|
35834
35923
|
});
|
|
35835
35924
|
}
|
|
35925
|
+
async emitCourseCompletionHistoryEvent(data) {
|
|
35926
|
+
await this.caliperNamespace.emitActivityEvent({
|
|
35927
|
+
studentId: data.studentId,
|
|
35928
|
+
studentEmail: data.studentEmail,
|
|
35929
|
+
activityId: data.activityId,
|
|
35930
|
+
activityName: "Course completed",
|
|
35931
|
+
courseId: data.courseId,
|
|
35932
|
+
courseName: data.courseName,
|
|
35933
|
+
subject: data.subject,
|
|
35934
|
+
appName: data.appName,
|
|
35935
|
+
sensorUrl: data.sensorUrl,
|
|
35936
|
+
process: false,
|
|
35937
|
+
includeAttempt: false,
|
|
35938
|
+
eventExtensions: {
|
|
35939
|
+
playcademy: {
|
|
35940
|
+
eventKind: "course-completed",
|
|
35941
|
+
source: "gameplay"
|
|
35942
|
+
}
|
|
35943
|
+
},
|
|
35944
|
+
generatedExtensions: {
|
|
35945
|
+
playcademy: {
|
|
35946
|
+
eventKind: "course-completed",
|
|
35947
|
+
source: "gameplay",
|
|
35948
|
+
activityId: data.activityId
|
|
35949
|
+
}
|
|
35950
|
+
}
|
|
35951
|
+
}).catch((error) => {
|
|
35952
|
+
log.error("[ProgressRecorder] Failed to emit course completion history event", {
|
|
35953
|
+
error
|
|
35954
|
+
});
|
|
35955
|
+
});
|
|
35956
|
+
}
|
|
35836
35957
|
}
|
|
35837
35958
|
|
|
35838
35959
|
class SessionRecorder {
|
|
@@ -36227,6 +36348,10 @@ class TimebackClient {
|
|
|
36227
36348
|
await this._ensureAuthenticated();
|
|
36228
36349
|
return this.adminEventRecorder.recordMasteryAdjustment(data);
|
|
36229
36350
|
}
|
|
36351
|
+
async recordAdminCourseCompletionChange(data) {
|
|
36352
|
+
await this._ensureAuthenticated();
|
|
36353
|
+
return this.adminEventRecorder.recordCourseCompletionChange(data);
|
|
36354
|
+
}
|
|
36230
36355
|
clearCaches() {
|
|
36231
36356
|
this.cacheManager.clearAll();
|
|
36232
36357
|
}
|