@playcademy/vite-plugin 0.2.22-beta.3 → 0.2.22-beta.5
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 +321 -47
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25334,7 +25334,7 @@ var package_default;
|
|
|
25334
25334
|
var init_package = __esm(() => {
|
|
25335
25335
|
package_default = {
|
|
25336
25336
|
name: "@playcademy/sandbox",
|
|
25337
|
-
version: "0.3.16-beta.
|
|
25337
|
+
version: "0.3.16-beta.5",
|
|
25338
25338
|
description: "Local development server for Playcademy game development",
|
|
25339
25339
|
type: "module",
|
|
25340
25340
|
exports: {
|
|
@@ -50501,9 +50501,37 @@ var init_developer_service = __esm(() => {
|
|
|
50501
50501
|
|
|
50502
50502
|
class GameService {
|
|
50503
50503
|
deps;
|
|
50504
|
+
static MANIFEST_FETCH_TIMEOUT_MS = 5000;
|
|
50505
|
+
static MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
|
|
50504
50506
|
constructor(deps) {
|
|
50505
50507
|
this.deps = deps;
|
|
50506
50508
|
}
|
|
50509
|
+
static getManifestHost(manifestUrl) {
|
|
50510
|
+
try {
|
|
50511
|
+
return new URL(manifestUrl).host;
|
|
50512
|
+
} catch {
|
|
50513
|
+
return manifestUrl;
|
|
50514
|
+
}
|
|
50515
|
+
}
|
|
50516
|
+
static getFetchErrorMessage(error) {
|
|
50517
|
+
let raw;
|
|
50518
|
+
if (error instanceof Error) {
|
|
50519
|
+
raw = error.message;
|
|
50520
|
+
} else if (typeof error === "string") {
|
|
50521
|
+
raw = error;
|
|
50522
|
+
}
|
|
50523
|
+
if (!raw) {
|
|
50524
|
+
return;
|
|
50525
|
+
}
|
|
50526
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
50527
|
+
if (!normalized) {
|
|
50528
|
+
return;
|
|
50529
|
+
}
|
|
50530
|
+
return normalized.slice(0, GameService.MAX_FETCH_ERROR_MESSAGE_LENGTH);
|
|
50531
|
+
}
|
|
50532
|
+
static isRetryableStatus(status) {
|
|
50533
|
+
return status === 429 || status >= 500;
|
|
50534
|
+
}
|
|
50507
50535
|
async list(caller) {
|
|
50508
50536
|
const db2 = this.deps.db;
|
|
50509
50537
|
const isAdmin = caller?.role === "admin";
|
|
@@ -50565,6 +50593,109 @@ class GameService {
|
|
|
50565
50593
|
this.enforceVisibility(game, caller, slug);
|
|
50566
50594
|
return game;
|
|
50567
50595
|
}
|
|
50596
|
+
async getManifest(gameId, caller) {
|
|
50597
|
+
const game = await this.getById(gameId, caller);
|
|
50598
|
+
if (game.gameType !== "hosted" || !game.deploymentUrl) {
|
|
50599
|
+
throw new BadRequestError("Game does not have a deployment manifest");
|
|
50600
|
+
}
|
|
50601
|
+
const deploymentUrl = game.deploymentUrl;
|
|
50602
|
+
const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
|
|
50603
|
+
const manifestHost = GameService.getManifestHost(manifestUrl);
|
|
50604
|
+
const startedAt = Date.now();
|
|
50605
|
+
const controller = new AbortController;
|
|
50606
|
+
const timeout = setTimeout(() => controller.abort(), GameService.MANIFEST_FETCH_TIMEOUT_MS);
|
|
50607
|
+
function buildDetails(fetchOutcome, manifestErrorKind, extra = {}) {
|
|
50608
|
+
return {
|
|
50609
|
+
manifestUrl,
|
|
50610
|
+
manifestHost,
|
|
50611
|
+
deploymentUrl,
|
|
50612
|
+
fetchOutcome,
|
|
50613
|
+
retryCount: 0,
|
|
50614
|
+
durationMs: Date.now() - startedAt,
|
|
50615
|
+
manifestErrorKind,
|
|
50616
|
+
...extra
|
|
50617
|
+
};
|
|
50618
|
+
}
|
|
50619
|
+
let response;
|
|
50620
|
+
try {
|
|
50621
|
+
response = await fetch(manifestUrl, {
|
|
50622
|
+
method: "GET",
|
|
50623
|
+
headers: {
|
|
50624
|
+
Accept: "application/json"
|
|
50625
|
+
},
|
|
50626
|
+
signal: controller.signal
|
|
50627
|
+
});
|
|
50628
|
+
} catch (error) {
|
|
50629
|
+
clearTimeout(timeout);
|
|
50630
|
+
const fetchErrorMessage = GameService.getFetchErrorMessage(error);
|
|
50631
|
+
const details = buildDetails("network_error", "temporary", fetchErrorMessage ? { fetchErrorMessage } : {});
|
|
50632
|
+
logger5.error("Failed to fetch game manifest", {
|
|
50633
|
+
gameId,
|
|
50634
|
+
manifestUrl,
|
|
50635
|
+
error,
|
|
50636
|
+
details
|
|
50637
|
+
});
|
|
50638
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
50639
|
+
throw new TimeoutError("Timed out loading game manifest", details);
|
|
50640
|
+
}
|
|
50641
|
+
throw new ServiceUnavailableError("Failed to load game manifest", details);
|
|
50642
|
+
} finally {
|
|
50643
|
+
clearTimeout(timeout);
|
|
50644
|
+
}
|
|
50645
|
+
if (!response.ok) {
|
|
50646
|
+
const resolvedManifestUrl = response.url || manifestUrl;
|
|
50647
|
+
const resolvedManifestHost = GameService.getManifestHost(resolvedManifestUrl);
|
|
50648
|
+
const manifestErrorKind = GameService.isRetryableStatus(response.status) ? "temporary" : "permanent";
|
|
50649
|
+
const details = buildDetails("bad_status", manifestErrorKind, {
|
|
50650
|
+
manifestUrl: resolvedManifestUrl,
|
|
50651
|
+
manifestHost: resolvedManifestHost,
|
|
50652
|
+
status: response.status,
|
|
50653
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
50654
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
50655
|
+
redirected: response.redirected,
|
|
50656
|
+
...response.redirected ? {
|
|
50657
|
+
originalManifestUrl: manifestUrl,
|
|
50658
|
+
originalManifestHost: manifestHost
|
|
50659
|
+
} : {}
|
|
50660
|
+
});
|
|
50661
|
+
const message = `Failed to fetch manifest: ${response.status} ${response.statusText}`;
|
|
50662
|
+
logger5.error("Game manifest returned non-ok response", {
|
|
50663
|
+
gameId,
|
|
50664
|
+
manifestUrl,
|
|
50665
|
+
status: response.status,
|
|
50666
|
+
details
|
|
50667
|
+
});
|
|
50668
|
+
if (manifestErrorKind === "temporary") {
|
|
50669
|
+
throw new ServiceUnavailableError(message, details);
|
|
50670
|
+
}
|
|
50671
|
+
throw new BadRequestError(message, details);
|
|
50672
|
+
}
|
|
50673
|
+
try {
|
|
50674
|
+
return await response.json();
|
|
50675
|
+
} catch (error) {
|
|
50676
|
+
const resolvedManifestUrl = response.url || manifestUrl;
|
|
50677
|
+
const resolvedManifestHost = GameService.getManifestHost(resolvedManifestUrl);
|
|
50678
|
+
const details = buildDetails("invalid_body", "permanent", {
|
|
50679
|
+
manifestUrl: resolvedManifestUrl,
|
|
50680
|
+
manifestHost: resolvedManifestHost,
|
|
50681
|
+
status: response.status,
|
|
50682
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
50683
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
50684
|
+
redirected: response.redirected,
|
|
50685
|
+
...response.redirected ? {
|
|
50686
|
+
originalManifestUrl: manifestUrl,
|
|
50687
|
+
originalManifestHost: manifestHost
|
|
50688
|
+
} : {}
|
|
50689
|
+
});
|
|
50690
|
+
logger5.error("Failed to parse game manifest", {
|
|
50691
|
+
gameId,
|
|
50692
|
+
manifestUrl,
|
|
50693
|
+
error,
|
|
50694
|
+
details
|
|
50695
|
+
});
|
|
50696
|
+
throw new BadRequestError("Failed to parse game manifest", details);
|
|
50697
|
+
}
|
|
50698
|
+
}
|
|
50568
50699
|
enforceVisibility(game, caller, lookupIdentifier) {
|
|
50569
50700
|
if (game.visibility !== "internal") {
|
|
50570
50701
|
return;
|
|
@@ -54095,16 +54226,7 @@ function mapAssessmentResultToRecentActivity(assessment, relevantCourseIds, cour
|
|
|
54095
54226
|
return null;
|
|
54096
54227
|
}
|
|
54097
54228
|
if (isMasteryCompletionEntry(assessment)) {
|
|
54098
|
-
|
|
54099
|
-
const isResume = Boolean(metadata3?.resumedAt);
|
|
54100
|
-
return {
|
|
54101
|
-
id: assessment.sourcedId || `${assessment.assessmentLineItem.sourcedId}:${assessment.scoreDate}`,
|
|
54102
|
-
kind: "admin-completion",
|
|
54103
|
-
occurredAt: assessment.scoreDate,
|
|
54104
|
-
courseId,
|
|
54105
|
-
title: isResume ? "Course resumed" : "Course marked complete",
|
|
54106
|
-
reason: metadata3?.adminAction ? "Admin action" : undefined
|
|
54107
|
-
};
|
|
54229
|
+
return null;
|
|
54108
54230
|
}
|
|
54109
54231
|
const metadata2 = isRecord2(assessment.metadata) ? assessment.metadata : undefined;
|
|
54110
54232
|
const activityName = getStringValue(metadata2?.activityName);
|
|
@@ -54137,6 +54259,7 @@ function parseCaliperEventContext(event, relevantCourseIds) {
|
|
|
54137
54259
|
courseId,
|
|
54138
54260
|
occurredAt,
|
|
54139
54261
|
eventKind: getStringValue(playcademy?.eventKind),
|
|
54262
|
+
source: getStringValue(playcademy?.source),
|
|
54140
54263
|
reason: getStringValue(playcademy?.reason),
|
|
54141
54264
|
titleFromEvent: getStringValue(event.object.activity?.name),
|
|
54142
54265
|
appName: getStringValue(event.object.app?.name),
|
|
@@ -54160,30 +54283,59 @@ function mapTimeSpentRemediation(event, ctx) {
|
|
|
54160
54283
|
};
|
|
54161
54284
|
}
|
|
54162
54285
|
function mapActivityRemediation(event, ctx) {
|
|
54163
|
-
let kind;
|
|
54164
54286
|
if (ctx.eventKind === "remediation-xp") {
|
|
54165
|
-
|
|
54166
|
-
|
|
54167
|
-
|
|
54168
|
-
|
|
54169
|
-
|
|
54287
|
+
return {
|
|
54288
|
+
id: event.externalId,
|
|
54289
|
+
kind: "remediation-xp",
|
|
54290
|
+
occurredAt: ctx.occurredAt,
|
|
54291
|
+
courseId: ctx.courseId,
|
|
54292
|
+
title: "XP Adjustment",
|
|
54293
|
+
activityId: ctx.activityId,
|
|
54294
|
+
appName: ctx.appName,
|
|
54295
|
+
reason: ctx.reason,
|
|
54296
|
+
xpDelta: getGeneratedMetricValue(event, "xpEarned"),
|
|
54297
|
+
masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
|
|
54298
|
+
};
|
|
54170
54299
|
}
|
|
54171
|
-
|
|
54172
|
-
|
|
54173
|
-
|
|
54174
|
-
|
|
54175
|
-
|
|
54176
|
-
|
|
54177
|
-
|
|
54178
|
-
|
|
54179
|
-
|
|
54180
|
-
|
|
54181
|
-
|
|
54182
|
-
|
|
54183
|
-
|
|
54184
|
-
|
|
54185
|
-
|
|
54186
|
-
|
|
54300
|
+
if (ctx.eventKind === "remediation-mastery") {
|
|
54301
|
+
return {
|
|
54302
|
+
id: event.externalId,
|
|
54303
|
+
kind: "remediation-mastery",
|
|
54304
|
+
occurredAt: ctx.occurredAt,
|
|
54305
|
+
courseId: ctx.courseId,
|
|
54306
|
+
title: "Mastery Adjustment",
|
|
54307
|
+
activityId: ctx.activityId,
|
|
54308
|
+
appName: ctx.appName,
|
|
54309
|
+
reason: ctx.reason,
|
|
54310
|
+
xpDelta: getGeneratedMetricValue(event, "xpEarned"),
|
|
54311
|
+
masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
|
|
54312
|
+
};
|
|
54313
|
+
}
|
|
54314
|
+
if (ctx.eventKind === "course-completed") {
|
|
54315
|
+
return {
|
|
54316
|
+
id: event.externalId,
|
|
54317
|
+
kind: "course-completed",
|
|
54318
|
+
occurredAt: ctx.occurredAt,
|
|
54319
|
+
courseId: ctx.courseId,
|
|
54320
|
+
title: ctx.source === "admin" ? "Course marked complete" : "Course completed",
|
|
54321
|
+
activityId: ctx.activityId,
|
|
54322
|
+
appName: ctx.appName,
|
|
54323
|
+
reason: ctx.reason
|
|
54324
|
+
};
|
|
54325
|
+
}
|
|
54326
|
+
if (ctx.eventKind === "course-resumed") {
|
|
54327
|
+
return {
|
|
54328
|
+
id: event.externalId,
|
|
54329
|
+
kind: "course-resumed",
|
|
54330
|
+
occurredAt: ctx.occurredAt,
|
|
54331
|
+
courseId: ctx.courseId,
|
|
54332
|
+
title: "Course resumed",
|
|
54333
|
+
activityId: ctx.activityId,
|
|
54334
|
+
appName: ctx.appName,
|
|
54335
|
+
reason: ctx.reason
|
|
54336
|
+
};
|
|
54337
|
+
}
|
|
54338
|
+
return null;
|
|
54187
54339
|
}
|
|
54188
54340
|
function mapCaliperEventToRemediationActivity(event, relevantCourseIds) {
|
|
54189
54341
|
const ctx = parseCaliperEventContext(event, relevantCourseIds);
|
|
@@ -54204,6 +54356,7 @@ var init_timeback_util = __esm(() => {
|
|
|
54204
54356
|
|
|
54205
54357
|
class TimebackAdminService {
|
|
54206
54358
|
deps;
|
|
54359
|
+
static XP_PRECISION_FACTOR = 10;
|
|
54207
54360
|
static RECENT_ACTIVITY_LIMIT = 20;
|
|
54208
54361
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
54209
54362
|
static MAX_STUDENT_ACTIVITY_OFFSET = 1000;
|
|
@@ -54215,6 +54368,10 @@ class TimebackAdminService {
|
|
|
54215
54368
|
constructor(deps) {
|
|
54216
54369
|
this.deps = deps;
|
|
54217
54370
|
}
|
|
54371
|
+
static roundXpToTenths(value) {
|
|
54372
|
+
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
54373
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
54374
|
+
}
|
|
54218
54375
|
requireClient() {
|
|
54219
54376
|
if (!this.deps.timeback) {
|
|
54220
54377
|
logger16.error("Timeback client not available in context");
|
|
@@ -54222,6 +54379,17 @@ class TimebackAdminService {
|
|
|
54222
54379
|
}
|
|
54223
54380
|
return this.deps.timeback;
|
|
54224
54381
|
}
|
|
54382
|
+
async recordCourseCompletionHistory(client, data) {
|
|
54383
|
+
await client.recordAdminCourseCompletionChange(data).catch((error) => {
|
|
54384
|
+
logger16.error("Failed to record admin course completion history event", {
|
|
54385
|
+
gameId: data.gameId,
|
|
54386
|
+
courseId: data.courseId,
|
|
54387
|
+
studentId: data.studentId,
|
|
54388
|
+
action: data.action,
|
|
54389
|
+
error: error instanceof Error ? error.message : String(error)
|
|
54390
|
+
});
|
|
54391
|
+
});
|
|
54392
|
+
}
|
|
54225
54393
|
async resolveAdminMutationContext(gameId, courseId, user, studentId) {
|
|
54226
54394
|
const client = this.requireClient();
|
|
54227
54395
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
@@ -54261,8 +54429,8 @@ class TimebackAdminService {
|
|
|
54261
54429
|
}
|
|
54262
54430
|
const today = formatDateYMD();
|
|
54263
54431
|
const history = [];
|
|
54264
|
-
let
|
|
54265
|
-
let
|
|
54432
|
+
let totalXpRaw = 0;
|
|
54433
|
+
let todayXpRaw = 0;
|
|
54266
54434
|
let activeTimeSeconds = 0;
|
|
54267
54435
|
let masteredUnits = 0;
|
|
54268
54436
|
for (const [date3, subjectFacts] of Object.entries(facts)) {
|
|
@@ -54279,15 +54447,16 @@ class TimebackAdminService {
|
|
|
54279
54447
|
masteredUnitsForDay += masteredUnitsFromFact;
|
|
54280
54448
|
}
|
|
54281
54449
|
}
|
|
54282
|
-
|
|
54450
|
+
const roundedXpForDay = TimebackAdminService.roundXpToTenths(xpForDay);
|
|
54451
|
+
totalXpRaw += xpForDay;
|
|
54283
54452
|
activeTimeSeconds += activeSecondsForDay;
|
|
54284
54453
|
masteredUnits += masteredUnitsForDay;
|
|
54285
54454
|
if (date3 === today) {
|
|
54286
|
-
|
|
54455
|
+
todayXpRaw += xpForDay;
|
|
54287
54456
|
}
|
|
54288
54457
|
history.push({
|
|
54289
54458
|
date: date3,
|
|
54290
|
-
xpEarned:
|
|
54459
|
+
xpEarned: roundedXpForDay,
|
|
54291
54460
|
activeTimeSeconds: activeSecondsForDay,
|
|
54292
54461
|
masteredUnits: masteredUnitsForDay
|
|
54293
54462
|
});
|
|
@@ -54295,8 +54464,8 @@ class TimebackAdminService {
|
|
|
54295
54464
|
history.sort((a, b) => a.date.localeCompare(b.date));
|
|
54296
54465
|
return {
|
|
54297
54466
|
analyticsAvailable: true,
|
|
54298
|
-
totalXp,
|
|
54299
|
-
todayXp,
|
|
54467
|
+
totalXp: TimebackAdminService.roundXpToTenths(totalXpRaw),
|
|
54468
|
+
todayXp: TimebackAdminService.roundXpToTenths(todayXpRaw),
|
|
54300
54469
|
activeTimeSeconds,
|
|
54301
54470
|
masteredUnits,
|
|
54302
54471
|
history
|
|
@@ -54638,6 +54807,7 @@ class TimebackAdminService {
|
|
|
54638
54807
|
}
|
|
54639
54808
|
async toggleCourseCompletion(data, user) {
|
|
54640
54809
|
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
54810
|
+
const historyClient = client;
|
|
54641
54811
|
const ids = deriveSourcedIds(data.courseId);
|
|
54642
54812
|
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
54643
54813
|
const resultId = `${lineItemId}:${data.studentId}:completion`;
|
|
@@ -54688,11 +54858,19 @@ class TimebackAdminService {
|
|
|
54688
54858
|
inProgress: "false",
|
|
54689
54859
|
metadata: {
|
|
54690
54860
|
isMasteryCompletion: true,
|
|
54691
|
-
completedAt: new Date().toISOString(),
|
|
54692
54861
|
adminAction: true,
|
|
54693
54862
|
appName
|
|
54694
54863
|
}
|
|
54695
54864
|
});
|
|
54865
|
+
await this.recordCourseCompletionHistory(historyClient, {
|
|
54866
|
+
gameId: data.gameId,
|
|
54867
|
+
courseId: data.courseId,
|
|
54868
|
+
studentId: data.studentId,
|
|
54869
|
+
action: "complete",
|
|
54870
|
+
actor,
|
|
54871
|
+
appName,
|
|
54872
|
+
sensorUrl
|
|
54873
|
+
});
|
|
54696
54874
|
} else {
|
|
54697
54875
|
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
54698
54876
|
sourcedId: resultId,
|
|
@@ -54705,11 +54883,19 @@ class TimebackAdminService {
|
|
|
54705
54883
|
inProgress: "true",
|
|
54706
54884
|
metadata: {
|
|
54707
54885
|
isMasteryCompletion: true,
|
|
54708
|
-
resumedAt: new Date().toISOString(),
|
|
54709
54886
|
adminAction: true,
|
|
54710
54887
|
appName
|
|
54711
54888
|
}
|
|
54712
54889
|
});
|
|
54890
|
+
await this.recordCourseCompletionHistory(historyClient, {
|
|
54891
|
+
gameId: data.gameId,
|
|
54892
|
+
courseId: data.courseId,
|
|
54893
|
+
studentId: data.studentId,
|
|
54894
|
+
action: "resume",
|
|
54895
|
+
actor,
|
|
54896
|
+
appName,
|
|
54897
|
+
sensorUrl
|
|
54898
|
+
});
|
|
54713
54899
|
}
|
|
54714
54900
|
return { status: "ok" };
|
|
54715
54901
|
}
|
|
@@ -58934,7 +59120,8 @@ function buildAdminEventMetadata({
|
|
|
58934
59120
|
return {
|
|
58935
59121
|
playcademy: {
|
|
58936
59122
|
eventKind,
|
|
58937
|
-
reason
|
|
59123
|
+
reason,
|
|
59124
|
+
source: "admin"
|
|
58938
59125
|
}
|
|
58939
59126
|
};
|
|
58940
59127
|
}
|
|
@@ -59050,6 +59237,30 @@ class AdminEventRecorder {
|
|
|
59050
59237
|
eventExtensions: ctx.metadata
|
|
59051
59238
|
});
|
|
59052
59239
|
}
|
|
59240
|
+
async recordCourseCompletionChange(data) {
|
|
59241
|
+
const isResume = data.action === "resume";
|
|
59242
|
+
const ctx = await this.prepareAdminEvent({
|
|
59243
|
+
...data,
|
|
59244
|
+
defaultActivityId: isResume ? "playcademy-admin-course-resumed" : "playcademy-admin-course-completed",
|
|
59245
|
+
reason: "Admin action",
|
|
59246
|
+
eventKind: isResume ? "course-resumed" : "course-completed"
|
|
59247
|
+
});
|
|
59248
|
+
await this.caliper.emitActivityEvent({
|
|
59249
|
+
studentId: ctx.student.id,
|
|
59250
|
+
studentEmail: ctx.student.email,
|
|
59251
|
+
activityId: ctx.activityId,
|
|
59252
|
+
activityName: isResume ? "Course resumed" : "Course marked complete",
|
|
59253
|
+
courseId: data.courseId,
|
|
59254
|
+
courseName: ctx.courseContext.courseName,
|
|
59255
|
+
subject: ctx.courseContext.subject,
|
|
59256
|
+
appName: ctx.appName,
|
|
59257
|
+
sensorUrl: ctx.sensorUrl,
|
|
59258
|
+
process: false,
|
|
59259
|
+
includeAttempt: false,
|
|
59260
|
+
generatedExtensions: ctx.metadata,
|
|
59261
|
+
eventExtensions: ctx.metadata
|
|
59262
|
+
});
|
|
59263
|
+
}
|
|
59053
59264
|
}
|
|
59054
59265
|
|
|
59055
59266
|
class TimebackCache {
|
|
@@ -59263,7 +59474,7 @@ class MasteryTracker {
|
|
|
59263
59474
|
const totalMastered = historicalMasteredUnits + masteredUnits;
|
|
59264
59475
|
const rawPct = totalMastered / masterableUnits * 100;
|
|
59265
59476
|
const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
|
|
59266
|
-
const masteryAchieved = totalMastered >= masterableUnits;
|
|
59477
|
+
const masteryAchieved = historicalMasteredUnits < masterableUnits && totalMastered >= masterableUnits;
|
|
59267
59478
|
return { pctCompleteApp, masteryAchieved };
|
|
59268
59479
|
}
|
|
59269
59480
|
async createCompletionEntry(studentId, courseId, classId, appName) {
|
|
@@ -59289,7 +59500,6 @@ class MasteryTracker {
|
|
|
59289
59500
|
inProgress: "false",
|
|
59290
59501
|
metadata: {
|
|
59291
59502
|
isMasteryCompletion: true,
|
|
59292
|
-
completedAt: new Date().toISOString(),
|
|
59293
59503
|
appName
|
|
59294
59504
|
}
|
|
59295
59505
|
});
|
|
@@ -59505,6 +59715,16 @@ class ProgressRecorder {
|
|
|
59505
59715
|
}
|
|
59506
59716
|
if (masteryAchieved) {
|
|
59507
59717
|
await this.masteryTracker.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
59718
|
+
await this.emitCourseCompletionHistoryEvent({
|
|
59719
|
+
studentId,
|
|
59720
|
+
studentEmail,
|
|
59721
|
+
activityId,
|
|
59722
|
+
courseId: ids.course,
|
|
59723
|
+
courseName,
|
|
59724
|
+
subject: progressData.subject,
|
|
59725
|
+
appName: progressData.appName,
|
|
59726
|
+
sensorUrl: progressData.sensorUrl
|
|
59727
|
+
});
|
|
59508
59728
|
}
|
|
59509
59729
|
await this.emitCaliperEvent({
|
|
59510
59730
|
studentId,
|
|
@@ -59679,6 +59899,38 @@ class ProgressRecorder {
|
|
|
59679
59899
|
log.error("[ProgressRecorder] Failed to emit activity event", { error });
|
|
59680
59900
|
});
|
|
59681
59901
|
}
|
|
59902
|
+
async emitCourseCompletionHistoryEvent(data) {
|
|
59903
|
+
await this.caliperNamespace.emitActivityEvent({
|
|
59904
|
+
studentId: data.studentId,
|
|
59905
|
+
studentEmail: data.studentEmail,
|
|
59906
|
+
activityId: data.activityId,
|
|
59907
|
+
activityName: "Course completed",
|
|
59908
|
+
courseId: data.courseId,
|
|
59909
|
+
courseName: data.courseName,
|
|
59910
|
+
subject: data.subject,
|
|
59911
|
+
appName: data.appName,
|
|
59912
|
+
sensorUrl: data.sensorUrl,
|
|
59913
|
+
process: false,
|
|
59914
|
+
includeAttempt: false,
|
|
59915
|
+
eventExtensions: {
|
|
59916
|
+
playcademy: {
|
|
59917
|
+
eventKind: "course-completed",
|
|
59918
|
+
source: "gameplay"
|
|
59919
|
+
}
|
|
59920
|
+
},
|
|
59921
|
+
generatedExtensions: {
|
|
59922
|
+
playcademy: {
|
|
59923
|
+
eventKind: "course-completed",
|
|
59924
|
+
source: "gameplay",
|
|
59925
|
+
activityId: data.activityId
|
|
59926
|
+
}
|
|
59927
|
+
}
|
|
59928
|
+
}).catch((error) => {
|
|
59929
|
+
log.error("[ProgressRecorder] Failed to emit course completion history event", {
|
|
59930
|
+
error
|
|
59931
|
+
});
|
|
59932
|
+
});
|
|
59933
|
+
}
|
|
59682
59934
|
}
|
|
59683
59935
|
|
|
59684
59936
|
class SessionRecorder {
|
|
@@ -60073,6 +60325,10 @@ class TimebackClient {
|
|
|
60073
60325
|
await this._ensureAuthenticated();
|
|
60074
60326
|
return this.adminEventRecorder.recordMasteryAdjustment(data);
|
|
60075
60327
|
}
|
|
60328
|
+
async recordAdminCourseCompletionChange(data) {
|
|
60329
|
+
await this._ensureAuthenticated();
|
|
60330
|
+
return this.adminEventRecorder.recordCourseCompletionChange(data);
|
|
60331
|
+
}
|
|
60076
60332
|
clearCaches() {
|
|
60077
60333
|
this.cacheManager.clearAll();
|
|
60078
60334
|
}
|
|
@@ -120597,6 +120853,7 @@ var listManageable;
|
|
|
120597
120853
|
var getSubjects;
|
|
120598
120854
|
var getById2;
|
|
120599
120855
|
var getBySlug;
|
|
120856
|
+
var getManifest;
|
|
120600
120857
|
var upsertBySlug;
|
|
120601
120858
|
var remove3;
|
|
120602
120859
|
var games2;
|
|
@@ -120639,6 +120896,21 @@ var init_game_controller = __esm(() => {
|
|
|
120639
120896
|
logger46.debug("Getting game by slug", { userId: ctx.user.id, slug: slug2, launchId: ctx.launchId });
|
|
120640
120897
|
return ctx.services.game.getBySlug(slug2, ctx.user);
|
|
120641
120898
|
});
|
|
120899
|
+
getManifest = requireAuth(async (ctx) => {
|
|
120900
|
+
const gameId = ctx.params.gameId;
|
|
120901
|
+
if (!gameId) {
|
|
120902
|
+
throw ApiError.badRequest("Missing game ID");
|
|
120903
|
+
}
|
|
120904
|
+
if (!isValidUUID(gameId)) {
|
|
120905
|
+
throw ApiError.unprocessableEntity("gameId must be a valid UUID format");
|
|
120906
|
+
}
|
|
120907
|
+
logger46.debug("Getting game manifest by ID", {
|
|
120908
|
+
userId: ctx.user.id,
|
|
120909
|
+
gameId,
|
|
120910
|
+
launchId: ctx.launchId
|
|
120911
|
+
});
|
|
120912
|
+
return ctx.services.game.getManifest(gameId, ctx.user);
|
|
120913
|
+
});
|
|
120642
120914
|
upsertBySlug = requireAuth(async (ctx) => {
|
|
120643
120915
|
const slug2 = ctx.params.slug;
|
|
120644
120916
|
if (!slug2) {
|
|
@@ -120675,6 +120947,7 @@ var init_game_controller = __esm(() => {
|
|
|
120675
120947
|
listManageable,
|
|
120676
120948
|
getSubjects,
|
|
120677
120949
|
getById: getById2,
|
|
120950
|
+
getManifest,
|
|
120678
120951
|
getBySlug,
|
|
120679
120952
|
upsertBySlug,
|
|
120680
120953
|
remove: remove3
|
|
@@ -122549,7 +122822,8 @@ var init_crud = __esm(() => {
|
|
|
122549
122822
|
init_api();
|
|
122550
122823
|
gameCrudRouter = new Hono2;
|
|
122551
122824
|
gameCrudRouter.get("/", handle2(games2.list));
|
|
122552
|
-
gameCrudRouter.get("/:gameId{[0-9a-
|
|
122825
|
+
gameCrudRouter.get("/:gameId{[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}}/manifest", handle2(games2.getManifest));
|
|
122826
|
+
gameCrudRouter.get("/:gameId{[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}}", handle2(games2.getById));
|
|
122553
122827
|
gameCrudRouter.get("/:slug", handle2(games2.getBySlug));
|
|
122554
122828
|
gameCrudRouter.put("/:slug", handle2(games2.upsertBySlug));
|
|
122555
122829
|
gameCrudRouter.delete("/:gameId", handle2(games2.remove, { status: 204 }));
|
|
@@ -124862,7 +125136,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
|
124862
125136
|
// package.json
|
|
124863
125137
|
var package_default2 = {
|
|
124864
125138
|
name: "@playcademy/vite-plugin",
|
|
124865
|
-
version: "0.2.22-beta.
|
|
125139
|
+
version: "0.2.22-beta.5",
|
|
124866
125140
|
type: "module",
|
|
124867
125141
|
exports: {
|
|
124868
125142
|
".": {
|