@playcademy/vite-plugin 0.3.2 → 0.3.3-beta.2
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 +548 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23746,7 +23746,7 @@ import path from "node:path";
|
|
|
23746
23746
|
// package.json
|
|
23747
23747
|
var package_default = {
|
|
23748
23748
|
name: "@playcademy/vite-plugin",
|
|
23749
|
-
version: "0.3.2",
|
|
23749
|
+
version: "0.3.3-beta.2",
|
|
23750
23750
|
type: "module",
|
|
23751
23751
|
exports: {
|
|
23752
23752
|
".": {
|
|
@@ -24339,10 +24339,13 @@ var TIMEBACK_COURSE_DEFAULTS;
|
|
|
24339
24339
|
var TIMEBACK_RESOURCE_DEFAULTS;
|
|
24340
24340
|
var TIMEBACK_COMPONENT_DEFAULTS;
|
|
24341
24341
|
var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
|
|
24342
|
+
var TIMEBACK_GAME_METRIC_DECIMAL_PLACES;
|
|
24343
|
+
var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
|
|
24342
24344
|
var init_timeback2 = __esm(() => {
|
|
24343
24345
|
TIMEBACK_ROUTES = {
|
|
24344
24346
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
24345
24347
|
GET_XP: "/integrations/timeback/xp",
|
|
24348
|
+
GET_MASTERY: "/integrations/timeback/mastery",
|
|
24346
24349
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
24347
24350
|
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
24348
24351
|
};
|
|
@@ -24382,6 +24385,17 @@ var init_timeback2 = __esm(() => {
|
|
|
24382
24385
|
sortOrder: 1,
|
|
24383
24386
|
lessonType: "quiz"
|
|
24384
24387
|
};
|
|
24388
|
+
TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
|
|
24389
|
+
xp: 1,
|
|
24390
|
+
mastery: 0,
|
|
24391
|
+
score: 2
|
|
24392
|
+
};
|
|
24393
|
+
TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
|
|
24394
|
+
xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
|
|
24395
|
+
mastery: 0,
|
|
24396
|
+
time: 60,
|
|
24397
|
+
score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
|
|
24398
|
+
};
|
|
24385
24399
|
});
|
|
24386
24400
|
var WORKER_NAMING;
|
|
24387
24401
|
var SECRETS_PREFIX = "secrets_";
|
|
@@ -25176,7 +25190,7 @@ var package_default2;
|
|
|
25176
25190
|
var init_package = __esm(() => {
|
|
25177
25191
|
package_default2 = {
|
|
25178
25192
|
name: "@playcademy/sandbox",
|
|
25179
|
-
version: "0.4.
|
|
25193
|
+
version: "0.4.1-beta.2",
|
|
25180
25194
|
description: "Local development server for Playcademy game development",
|
|
25181
25195
|
type: "module",
|
|
25182
25196
|
exports: {
|
|
@@ -52124,6 +52138,7 @@ var init_constants3 = __esm(() => {
|
|
|
52124
52138
|
TIMEBACK: {
|
|
52125
52139
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
52126
52140
|
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
52141
|
+
GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
|
|
52127
52142
|
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
|
|
52128
52143
|
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
|
|
52129
52144
|
}
|
|
@@ -53871,7 +53886,7 @@ var CourseGoalsSchema;
|
|
|
53871
53886
|
var UpdateGameTimebackIntegrationRequestSchema;
|
|
53872
53887
|
var TimebackActivityDataSchema;
|
|
53873
53888
|
var EndActivityRequestSchema;
|
|
53874
|
-
var
|
|
53889
|
+
var GameRunMetricsSchema;
|
|
53875
53890
|
var GameCourseMetricsSchema;
|
|
53876
53891
|
var GameMetricsResponseSchema;
|
|
53877
53892
|
var AdvanceCourseRequestSchema;
|
|
@@ -53964,24 +53979,28 @@ var init_schemas4 = __esm(() => {
|
|
|
53964
53979
|
}).optional(),
|
|
53965
53980
|
xpEarned: exports_external.number().optional(),
|
|
53966
53981
|
masteredUnits: exports_external.number().optional(),
|
|
53982
|
+
masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
|
|
53967
53983
|
extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
53984
|
+
}).refine((data) => !(data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined), {
|
|
53985
|
+
message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
|
|
53986
|
+
path: ["masteredUnitsAbsolute"]
|
|
53968
53987
|
});
|
|
53969
|
-
|
|
53988
|
+
GameRunMetricsSchema = exports_external.object({
|
|
53989
|
+
runId: exports_external.string().uuid(),
|
|
53970
53990
|
activityId: exports_external.string().min(1),
|
|
53971
53991
|
activityName: exports_external.string().optional(),
|
|
53972
|
-
totalXp: exports_external.number().nonnegative(),
|
|
53973
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
53974
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
53975
|
-
|
|
53976
|
-
lastCompletedAt: exports_external.string().datetime().optional()
|
|
53992
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
53993
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
53994
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
53995
|
+
score: exports_external.number().min(0).max(100).optional()
|
|
53977
53996
|
});
|
|
53978
53997
|
GameCourseMetricsSchema = exports_external.object({
|
|
53979
53998
|
grade: TimebackGradeSchema,
|
|
53980
53999
|
subject: TimebackSubjectSchema,
|
|
53981
|
-
totalXp: exports_external.number().nonnegative(),
|
|
53982
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
53983
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
53984
|
-
activities: exports_external.array(
|
|
54000
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
54001
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
54002
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
54003
|
+
activities: exports_external.array(GameRunMetricsSchema).optional()
|
|
53985
54004
|
});
|
|
53986
54005
|
GameMetricsResponseSchema = exports_external.object({
|
|
53987
54006
|
studentId: exports_external.string().min(1),
|
|
@@ -54251,6 +54270,122 @@ function compareEnrollmentsByRecency(a, b) {
|
|
|
54251
54270
|
var init_timeback_admin_util = __esm(() => {
|
|
54252
54271
|
init_errors();
|
|
54253
54272
|
});
|
|
54273
|
+
function createMetricRow(definition) {
|
|
54274
|
+
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
54275
|
+
if (timebackValue === undefined && gameValue === undefined) {
|
|
54276
|
+
return null;
|
|
54277
|
+
}
|
|
54278
|
+
if (gameValue === undefined) {
|
|
54279
|
+
return {
|
|
54280
|
+
metric,
|
|
54281
|
+
kind,
|
|
54282
|
+
status: "not_reported_by_game",
|
|
54283
|
+
...timebackValue !== undefined ? { timebackValue } : {}
|
|
54284
|
+
};
|
|
54285
|
+
}
|
|
54286
|
+
if (timebackValue === undefined) {
|
|
54287
|
+
return {
|
|
54288
|
+
metric,
|
|
54289
|
+
kind,
|
|
54290
|
+
status: "not_recorded_by_timeback",
|
|
54291
|
+
gameValue
|
|
54292
|
+
};
|
|
54293
|
+
}
|
|
54294
|
+
const delta = gameValue - timebackValue;
|
|
54295
|
+
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
54296
|
+
return {
|
|
54297
|
+
metric,
|
|
54298
|
+
kind,
|
|
54299
|
+
status: isDiscrepant ? "discrepant" : "matched",
|
|
54300
|
+
timebackValue,
|
|
54301
|
+
gameValue,
|
|
54302
|
+
delta
|
|
54303
|
+
};
|
|
54304
|
+
}
|
|
54305
|
+
function createRunComparison(activity, gameRun) {
|
|
54306
|
+
const runId = activity.runId ?? "";
|
|
54307
|
+
if (!gameRun) {
|
|
54308
|
+
return {
|
|
54309
|
+
runId,
|
|
54310
|
+
status: "not_reported",
|
|
54311
|
+
discrepancyCount: 0,
|
|
54312
|
+
rows: []
|
|
54313
|
+
};
|
|
54314
|
+
}
|
|
54315
|
+
const rows = [
|
|
54316
|
+
createMetricRow({
|
|
54317
|
+
metric: "xp",
|
|
54318
|
+
kind: "number",
|
|
54319
|
+
timebackValue: activity.xpDelta,
|
|
54320
|
+
gameValue: gameRun.totalXp,
|
|
54321
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
54322
|
+
}),
|
|
54323
|
+
createMetricRow({
|
|
54324
|
+
metric: "mastery",
|
|
54325
|
+
kind: "number",
|
|
54326
|
+
timebackValue: activity.masteredUnitsDelta,
|
|
54327
|
+
gameValue: gameRun.masteredUnits,
|
|
54328
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
54329
|
+
}),
|
|
54330
|
+
createMetricRow({
|
|
54331
|
+
metric: "time",
|
|
54332
|
+
kind: "time",
|
|
54333
|
+
timebackValue: activity.timeDeltaSeconds,
|
|
54334
|
+
gameValue: gameRun.activeTimeSeconds,
|
|
54335
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
54336
|
+
}),
|
|
54337
|
+
createMetricRow({
|
|
54338
|
+
metric: "score",
|
|
54339
|
+
kind: "percent",
|
|
54340
|
+
timebackValue: activity.score,
|
|
54341
|
+
gameValue: gameRun.score,
|
|
54342
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
54343
|
+
})
|
|
54344
|
+
].filter((row) => row !== null);
|
|
54345
|
+
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
54346
|
+
return {
|
|
54347
|
+
runId,
|
|
54348
|
+
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
54349
|
+
discrepancyCount,
|
|
54350
|
+
rows
|
|
54351
|
+
};
|
|
54352
|
+
}
|
|
54353
|
+
function summarizeGameRunMetricsComparison(comparison) {
|
|
54354
|
+
return {
|
|
54355
|
+
runId: comparison.runId,
|
|
54356
|
+
status: comparison.status,
|
|
54357
|
+
discrepancyCount: comparison.discrepancyCount,
|
|
54358
|
+
...comparison.reason ? { reason: comparison.reason } : {}
|
|
54359
|
+
};
|
|
54360
|
+
}
|
|
54361
|
+
function buildGameRunMetricComparisons(activities, course, response) {
|
|
54362
|
+
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
54363
|
+
const comparisons = new Map;
|
|
54364
|
+
if (activitiesWithRunIds.length === 0) {
|
|
54365
|
+
return comparisons;
|
|
54366
|
+
}
|
|
54367
|
+
if (!response.supported) {
|
|
54368
|
+
for (const activity of activitiesWithRunIds) {
|
|
54369
|
+
comparisons.set(activity.runId, {
|
|
54370
|
+
runId: activity.runId,
|
|
54371
|
+
status: "unavailable",
|
|
54372
|
+
discrepancyCount: 0,
|
|
54373
|
+
reason: response.reason,
|
|
54374
|
+
rows: []
|
|
54375
|
+
});
|
|
54376
|
+
}
|
|
54377
|
+
return comparisons;
|
|
54378
|
+
}
|
|
54379
|
+
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
54380
|
+
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
54381
|
+
for (const activity of activitiesWithRunIds) {
|
|
54382
|
+
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
54383
|
+
}
|
|
54384
|
+
return comparisons;
|
|
54385
|
+
}
|
|
54386
|
+
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
54387
|
+
init_src();
|
|
54388
|
+
});
|
|
54254
54389
|
async function upsertMasteryCompletionEntry(params) {
|
|
54255
54390
|
const { client, courseId, studentId, appName, action } = params;
|
|
54256
54391
|
const ids = deriveSourcedIds(courseId);
|
|
@@ -54325,7 +54460,7 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
54325
54460
|
subject: integration.subject,
|
|
54326
54461
|
courseId: integration.courseId,
|
|
54327
54462
|
orgId: courseToSchool.get(integration.courseId),
|
|
54328
|
-
...enrollment ? {
|
|
54463
|
+
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
54329
54464
|
};
|
|
54330
54465
|
});
|
|
54331
54466
|
}
|
|
@@ -54637,6 +54772,8 @@ class TimebackAdminService {
|
|
|
54637
54772
|
static ANALYTICS_CONCURRENCY = 8;
|
|
54638
54773
|
static MASTERABLE_UNITS_CONCURRENCY = 4;
|
|
54639
54774
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
54775
|
+
static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
|
|
54776
|
+
static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
|
|
54640
54777
|
constructor(deps) {
|
|
54641
54778
|
this.deps = deps;
|
|
54642
54779
|
}
|
|
@@ -54646,13 +54783,42 @@ class TimebackAdminService {
|
|
|
54646
54783
|
}
|
|
54647
54784
|
return this.deps.config.localGameUrls[slug2] ?? deployedUrl;
|
|
54648
54785
|
}
|
|
54649
|
-
static resolveGameMetricsUrl(baseUrl) {
|
|
54786
|
+
static resolveGameMetricsUrl(baseUrl, runIds) {
|
|
54650
54787
|
try {
|
|
54651
|
-
|
|
54788
|
+
const url2 = new URL("/__playcademy/metrics", baseUrl);
|
|
54789
|
+
for (const runId of runIds ?? []) {
|
|
54790
|
+
url2.searchParams.append("runId", runId);
|
|
54791
|
+
}
|
|
54792
|
+
return url2;
|
|
54652
54793
|
} catch {
|
|
54653
54794
|
return null;
|
|
54654
54795
|
}
|
|
54655
54796
|
}
|
|
54797
|
+
static normalizeRunIds(runIds, limit = Number.POSITIVE_INFINITY) {
|
|
54798
|
+
const normalized = [];
|
|
54799
|
+
const seen = new Set;
|
|
54800
|
+
for (const runId of runIds ?? []) {
|
|
54801
|
+
const value = runId.trim().toLowerCase();
|
|
54802
|
+
if (isValidUUID(value) && !seen.has(value)) {
|
|
54803
|
+
seen.add(value);
|
|
54804
|
+
normalized.push(value);
|
|
54805
|
+
if (normalized.length >= limit) {
|
|
54806
|
+
break;
|
|
54807
|
+
}
|
|
54808
|
+
}
|
|
54809
|
+
}
|
|
54810
|
+
return normalized;
|
|
54811
|
+
}
|
|
54812
|
+
static chunkRunIds(runIds) {
|
|
54813
|
+
const chunks = [];
|
|
54814
|
+
for (let index2 = 0;index2 < runIds.length; index2 += this.GAME_METRICS_RUN_IDS_PER_REQUEST) {
|
|
54815
|
+
chunks.push(runIds.slice(index2, index2 + this.GAME_METRICS_RUN_IDS_PER_REQUEST));
|
|
54816
|
+
}
|
|
54817
|
+
return chunks;
|
|
54818
|
+
}
|
|
54819
|
+
static isAbortError(error) {
|
|
54820
|
+
return error instanceof Error && error.name === "AbortError";
|
|
54821
|
+
}
|
|
54656
54822
|
static roundXpToTenths(value) {
|
|
54657
54823
|
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
54658
54824
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
@@ -54813,6 +54979,56 @@ class TimebackAdminService {
|
|
|
54813
54979
|
const remediationItems = events.map((event) => mapCaliperEventToRemediationActivity(event, relevantCourseIds)).filter((item) => Boolean(item));
|
|
54814
54980
|
return [...groupedGameplayItems, ...remediationItems].toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
54815
54981
|
}
|
|
54982
|
+
async getGameMetricComparisonsForActivities(user, options) {
|
|
54983
|
+
const runIds = TimebackAdminService.normalizeRunIds(options.activities.map((activity) => activity.runId).filter((runId) => Boolean(runId)));
|
|
54984
|
+
if (runIds.length === 0) {
|
|
54985
|
+
return new Map;
|
|
54986
|
+
}
|
|
54987
|
+
const activitiesByRunId = new Map(options.activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0).map((activity) => [activity.runId.toLowerCase(), activity]));
|
|
54988
|
+
const comparisons = new Map;
|
|
54989
|
+
await Promise.all(TimebackAdminService.chunkRunIds(runIds).map(async (chunk) => {
|
|
54990
|
+
const activities = [];
|
|
54991
|
+
for (const runId of chunk) {
|
|
54992
|
+
const activity = activitiesByRunId.get(runId);
|
|
54993
|
+
if (activity) {
|
|
54994
|
+
activities.push(activity);
|
|
54995
|
+
}
|
|
54996
|
+
}
|
|
54997
|
+
if (activities.length === 0) {
|
|
54998
|
+
return;
|
|
54999
|
+
}
|
|
55000
|
+
let response;
|
|
55001
|
+
try {
|
|
55002
|
+
response = await this.getGameMetrics(options.gameId, options.studentId, user, {
|
|
55003
|
+
runIds: chunk,
|
|
55004
|
+
timeoutMs: options.timeoutMs
|
|
55005
|
+
});
|
|
55006
|
+
} catch (error) {
|
|
55007
|
+
response = {
|
|
55008
|
+
supported: false,
|
|
55009
|
+
reason: "fetch_failed",
|
|
55010
|
+
details: error instanceof Error ? error.message : String(error)
|
|
55011
|
+
};
|
|
55012
|
+
}
|
|
55013
|
+
for (const [runId, comparison] of buildGameRunMetricComparisons(activities, options.course, response)) {
|
|
55014
|
+
comparisons.set(runId, comparison);
|
|
55015
|
+
}
|
|
55016
|
+
}));
|
|
55017
|
+
return comparisons;
|
|
55018
|
+
}
|
|
55019
|
+
async attachGameMetricSummariesToActivities(user, options) {
|
|
55020
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, options);
|
|
55021
|
+
if (comparisons.size === 0) {
|
|
55022
|
+
return [...options.activities];
|
|
55023
|
+
}
|
|
55024
|
+
return options.activities.map((activity) => {
|
|
55025
|
+
const comparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
55026
|
+
return comparison ? {
|
|
55027
|
+
...activity,
|
|
55028
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison)
|
|
55029
|
+
} : activity;
|
|
55030
|
+
});
|
|
55031
|
+
}
|
|
54816
55032
|
async getStudentEnrollmentsByCourseId(client, studentId, courseIds, options) {
|
|
54817
55033
|
const enrollments = new Map;
|
|
54818
55034
|
const allEnrollments = new Map;
|
|
@@ -54972,7 +55188,7 @@ class TimebackAdminService {
|
|
|
54972
55188
|
});
|
|
54973
55189
|
return { gameId, courseId, students: deduped };
|
|
54974
55190
|
}
|
|
54975
|
-
async getGameMetrics(gameId, timebackId, user) {
|
|
55191
|
+
async getGameMetrics(gameId, timebackId, user, options) {
|
|
54976
55192
|
const client = this.requireClient();
|
|
54977
55193
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
54978
55194
|
const [targetUser, integrations, game2, deployment] = await Promise.all([
|
|
@@ -55007,7 +55223,8 @@ class TimebackAdminService {
|
|
|
55007
55223
|
if (!metricsBaseUrl) {
|
|
55008
55224
|
return { supported: false, reason: "no_active_deployment" };
|
|
55009
55225
|
}
|
|
55010
|
-
const
|
|
55226
|
+
const runIds = TimebackAdminService.normalizeRunIds(options?.runIds, TimebackAdminService.GAME_METRICS_RUN_IDS_PER_REQUEST);
|
|
55227
|
+
const metricsUrl = TimebackAdminService.resolveGameMetricsUrl(metricsBaseUrl, runIds);
|
|
55011
55228
|
if (!metricsUrl) {
|
|
55012
55229
|
return {
|
|
55013
55230
|
supported: false,
|
|
@@ -55017,7 +55234,7 @@ class TimebackAdminService {
|
|
|
55017
55234
|
}
|
|
55018
55235
|
const token = await this.deps.mintPlatformServiceToken(gameId, targetUser.id);
|
|
55019
55236
|
const controller = new AbortController;
|
|
55020
|
-
const timeout = setTimeout(() => controller.abort(), TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
55237
|
+
const timeout = setTimeout(() => controller.abort(), options?.timeoutMs ?? TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
55021
55238
|
let response;
|
|
55022
55239
|
try {
|
|
55023
55240
|
response = await fetch(metricsUrl, {
|
|
@@ -55029,10 +55246,19 @@ class TimebackAdminService {
|
|
|
55029
55246
|
signal: controller.signal
|
|
55030
55247
|
});
|
|
55031
55248
|
} catch (error) {
|
|
55249
|
+
const timedOut = TimebackAdminService.isAbortError(error);
|
|
55250
|
+
let details;
|
|
55251
|
+
if (timedOut) {
|
|
55252
|
+
details = "Game metrics request timed out";
|
|
55253
|
+
} else if (error instanceof Error) {
|
|
55254
|
+
details = error.message;
|
|
55255
|
+
} else {
|
|
55256
|
+
details = String(error);
|
|
55257
|
+
}
|
|
55032
55258
|
return {
|
|
55033
55259
|
supported: false,
|
|
55034
|
-
reason: "fetch_failed",
|
|
55035
|
-
details
|
|
55260
|
+
reason: timedOut ? "timeout" : "fetch_failed",
|
|
55261
|
+
details
|
|
55036
55262
|
};
|
|
55037
55263
|
} finally {
|
|
55038
55264
|
clearTimeout(timeout);
|
|
@@ -55136,7 +55362,14 @@ class TimebackAdminService {
|
|
|
55136
55362
|
const allActivities = await this.listRecentActivityForStudent(client, studentId, gameSource, relevantCourseIds, fetchLimit);
|
|
55137
55363
|
const activities = allActivities.slice(safeOffset, safeOffset + safeLimit);
|
|
55138
55364
|
const hasMore = allActivities.length > safeOffset + safeLimit;
|
|
55139
|
-
|
|
55365
|
+
const activitiesWithGameMetrics = await this.attachGameMetricSummariesToActivities(user, {
|
|
55366
|
+
gameId,
|
|
55367
|
+
studentId,
|
|
55368
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
55369
|
+
activities,
|
|
55370
|
+
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
55371
|
+
});
|
|
55372
|
+
return { activities: activitiesWithGameMetrics, hasMore };
|
|
55140
55373
|
}
|
|
55141
55374
|
async getActivityDetail(user, options) {
|
|
55142
55375
|
const { gameId, studentId, courseId, activityId, runId } = options;
|
|
@@ -55172,7 +55405,22 @@ class TimebackAdminService {
|
|
|
55172
55405
|
if (!activity) {
|
|
55173
55406
|
throw new NotFoundError("Activity", activityId);
|
|
55174
55407
|
}
|
|
55175
|
-
|
|
55408
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, {
|
|
55409
|
+
gameId,
|
|
55410
|
+
studentId,
|
|
55411
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
55412
|
+
activities: [activity]
|
|
55413
|
+
});
|
|
55414
|
+
const gameMetricsComparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
55415
|
+
const activityWithGameMetrics = gameMetricsComparison ? {
|
|
55416
|
+
...activity,
|
|
55417
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison)
|
|
55418
|
+
} : activity;
|
|
55419
|
+
return {
|
|
55420
|
+
activity: activityWithGameMetrics,
|
|
55421
|
+
rawEvents: matchedEvents,
|
|
55422
|
+
...gameMetricsComparison ? { gameMetricsComparison } : {}
|
|
55423
|
+
};
|
|
55176
55424
|
}
|
|
55177
55425
|
async grantManualXp(data, user) {
|
|
55178
55426
|
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
@@ -55460,6 +55708,7 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
55460
55708
|
init_errors();
|
|
55461
55709
|
init_timeback_admin_metrics_util();
|
|
55462
55710
|
init_timeback_admin_util();
|
|
55711
|
+
init_timeback_game_metrics_comparison_util();
|
|
55463
55712
|
init_timeback_mastery_completion_util();
|
|
55464
55713
|
init_timeback_util();
|
|
55465
55714
|
logger17 = log.scope("TimebackAdminService");
|
|
@@ -56904,6 +57153,7 @@ var init_timeback_service = __esm(() => {
|
|
|
56904
57153
|
sessionTimingData,
|
|
56905
57154
|
xpEarned,
|
|
56906
57155
|
masteredUnits,
|
|
57156
|
+
masteredUnitsAbsolute,
|
|
56907
57157
|
extensions,
|
|
56908
57158
|
user
|
|
56909
57159
|
}) {
|
|
@@ -56927,6 +57177,7 @@ var init_timeback_service = __esm(() => {
|
|
|
56927
57177
|
durationSeconds: timingData.durationSeconds,
|
|
56928
57178
|
xpEarned,
|
|
56929
57179
|
masteredUnits,
|
|
57180
|
+
masteredUnitsAbsolute,
|
|
56930
57181
|
extensions: extensionsWithResumeId,
|
|
56931
57182
|
activityId: activityData.activityId,
|
|
56932
57183
|
activityName: activityData.activityName,
|
|
@@ -57175,6 +57426,46 @@ var init_timeback_service = __esm(() => {
|
|
|
57175
57426
|
});
|
|
57176
57427
|
return result;
|
|
57177
57428
|
}
|
|
57429
|
+
async getStudentMastery(timebackId, user, options) {
|
|
57430
|
+
const client = this.requireClient();
|
|
57431
|
+
const db2 = this.deps.db;
|
|
57432
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
57433
|
+
const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
|
|
57434
|
+
if (options.grade !== undefined && options.subject) {
|
|
57435
|
+
conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
|
|
57436
|
+
conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
|
|
57437
|
+
}
|
|
57438
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
57439
|
+
where: and(...conditions2)
|
|
57440
|
+
});
|
|
57441
|
+
const courseIds = integrations.map((i2) => i2.courseId);
|
|
57442
|
+
if (courseIds.length === 0) {
|
|
57443
|
+
logger20.debug("No integrations found for game, returning empty mastery", {
|
|
57444
|
+
timebackId,
|
|
57445
|
+
gameId: options.gameId,
|
|
57446
|
+
grade: options.grade,
|
|
57447
|
+
subject: options.subject
|
|
57448
|
+
});
|
|
57449
|
+
return {
|
|
57450
|
+
totalMasteredUnits: 0,
|
|
57451
|
+
totalMasterableUnits: 0,
|
|
57452
|
+
...options.include?.perCourse && { courses: [] }
|
|
57453
|
+
};
|
|
57454
|
+
}
|
|
57455
|
+
const result = await client.getStudentMastery(timebackId, {
|
|
57456
|
+
courseIds,
|
|
57457
|
+
include: options.include
|
|
57458
|
+
});
|
|
57459
|
+
logger20.debug("Retrieved student mastery", {
|
|
57460
|
+
timebackId,
|
|
57461
|
+
gameId: options.gameId,
|
|
57462
|
+
grade: options.grade,
|
|
57463
|
+
subject: options.subject,
|
|
57464
|
+
totalMasteredUnits: result.totalMasteredUnits,
|
|
57465
|
+
courseCount: result.courses?.length
|
|
57466
|
+
});
|
|
57467
|
+
return result;
|
|
57468
|
+
}
|
|
57178
57469
|
};
|
|
57179
57470
|
});
|
|
57180
57471
|
|
|
@@ -59481,15 +59772,18 @@ class MasteryTracker {
|
|
|
59481
59772
|
this.edubridgeNamespace = edubridgeNamespace;
|
|
59482
59773
|
}
|
|
59483
59774
|
async checkProgress(input) {
|
|
59484
|
-
const { studentId, courseId, resourceId, masteredUnits } = input;
|
|
59485
|
-
|
|
59775
|
+
const { studentId, courseId, resourceId, masteredUnits, masteredUnitsAbsolute } = input;
|
|
59776
|
+
const hasIncremental = typeof masteredUnits === "number" && masteredUnits !== 0;
|
|
59777
|
+
const hasAbsolute = typeof masteredUnitsAbsolute === "number";
|
|
59778
|
+
if (!hasIncremental && !hasAbsolute) {
|
|
59486
59779
|
return;
|
|
59487
59780
|
}
|
|
59488
59781
|
const status = await this.calculateStatus({
|
|
59489
59782
|
studentId,
|
|
59490
59783
|
courseId,
|
|
59491
59784
|
resourceId,
|
|
59492
|
-
additionalMasteredUnits: masteredUnits
|
|
59785
|
+
additionalMasteredUnits: hasAbsolute ? 0 : masteredUnits,
|
|
59786
|
+
absoluteMasteredUnits: hasAbsolute ? masteredUnitsAbsolute : undefined
|
|
59493
59787
|
});
|
|
59494
59788
|
if (!status) {
|
|
59495
59789
|
return;
|
|
@@ -59498,7 +59792,8 @@ class MasteryTracker {
|
|
|
59498
59792
|
return {
|
|
59499
59793
|
pctCompleteApp: status.pctCompleteApp,
|
|
59500
59794
|
masteryAchieved: !wasComplete && status.isComplete,
|
|
59501
|
-
masteryRevoked: wasComplete && !status.isComplete
|
|
59795
|
+
masteryRevoked: wasComplete && !status.isComplete,
|
|
59796
|
+
effectiveDelta: status.effectiveDelta
|
|
59502
59797
|
};
|
|
59503
59798
|
}
|
|
59504
59799
|
async getStatus(input) {
|
|
@@ -59520,7 +59815,8 @@ class MasteryTracker {
|
|
|
59520
59815
|
studentId,
|
|
59521
59816
|
courseId,
|
|
59522
59817
|
resourceId,
|
|
59523
|
-
additionalMasteredUnits
|
|
59818
|
+
additionalMasteredUnits,
|
|
59819
|
+
absoluteMasteredUnits
|
|
59524
59820
|
}) {
|
|
59525
59821
|
const masterableUnits = await this.resolveMasterableUnits(resourceId);
|
|
59526
59822
|
if (!masterableUnits || masterableUnits <= 0) {
|
|
@@ -59539,7 +59835,15 @@ class MasteryTracker {
|
|
|
59539
59835
|
return;
|
|
59540
59836
|
}
|
|
59541
59837
|
const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
|
|
59542
|
-
|
|
59838
|
+
let totalMastered;
|
|
59839
|
+
let effectiveDelta;
|
|
59840
|
+
if (absoluteMasteredUnits !== undefined) {
|
|
59841
|
+
totalMastered = Math.max(0, absoluteMasteredUnits);
|
|
59842
|
+
effectiveDelta = totalMastered - historicalMasteredUnits;
|
|
59843
|
+
} else {
|
|
59844
|
+
effectiveDelta = additionalMasteredUnits;
|
|
59845
|
+
totalMastered = Math.max(0, historicalMasteredUnits + effectiveDelta);
|
|
59846
|
+
}
|
|
59543
59847
|
const rawPct = totalMastered / masterableUnits * 100;
|
|
59544
59848
|
const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
|
|
59545
59849
|
return {
|
|
@@ -59547,7 +59851,8 @@ class MasteryTracker {
|
|
|
59547
59851
|
masterableUnits,
|
|
59548
59852
|
pctCompleteApp,
|
|
59549
59853
|
isComplete: totalMastered >= masterableUnits,
|
|
59550
|
-
historicalMasteredUnits
|
|
59854
|
+
historicalMasteredUnits,
|
|
59855
|
+
effectiveDelta
|
|
59551
59856
|
};
|
|
59552
59857
|
}
|
|
59553
59858
|
async createCompletionEntry(studentId, courseId, classId, appName) {
|
|
@@ -59776,7 +60081,7 @@ class ProgressRecorder {
|
|
|
59776
60081
|
validateProgressData(progressData);
|
|
59777
60082
|
const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
|
|
59778
60083
|
const { id: studentId, email: studentEmail } = student;
|
|
59779
|
-
const { score, totalQuestions, correctQuestions, xpEarned,
|
|
60084
|
+
const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
|
|
59780
60085
|
const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
|
|
59781
60086
|
const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
|
|
59782
60087
|
const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
|
|
@@ -59785,8 +60090,10 @@ class ProgressRecorder {
|
|
|
59785
60090
|
studentId,
|
|
59786
60091
|
courseId,
|
|
59787
60092
|
resourceId: ids.resource,
|
|
59788
|
-
masteredUnits: progressData.masteredUnits ?? 0
|
|
60093
|
+
masteredUnits: progressData.masteredUnits ?? 0,
|
|
60094
|
+
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
59789
60095
|
});
|
|
60096
|
+
const effectiveMasteredUnits = masteryProgress ? masteryProgress.effectiveDelta : progressData.masteredUnits ?? 0;
|
|
59790
60097
|
let pctCompleteApp;
|
|
59791
60098
|
let masteryAchieved = false;
|
|
59792
60099
|
let scoreStatus = SCORE_STATUS5.fullyGraded;
|
|
@@ -59814,7 +60121,7 @@ class ProgressRecorder {
|
|
|
59814
60121
|
appName: progressData.appName,
|
|
59815
60122
|
totalQuestions,
|
|
59816
60123
|
correctQuestions,
|
|
59817
|
-
masteredUnits,
|
|
60124
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
59818
60125
|
pctCompleteApp
|
|
59819
60126
|
});
|
|
59820
60127
|
} else {
|
|
@@ -59852,7 +60159,7 @@ class ProgressRecorder {
|
|
|
59852
60159
|
totalQuestions,
|
|
59853
60160
|
correctQuestions,
|
|
59854
60161
|
xpEarned: calculatedXp,
|
|
59855
|
-
masteredUnits,
|
|
60162
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
59856
60163
|
attemptNumber: currentAttemptNumber,
|
|
59857
60164
|
progressData,
|
|
59858
60165
|
extensions,
|
|
@@ -59861,7 +60168,7 @@ class ProgressRecorder {
|
|
|
59861
60168
|
return {
|
|
59862
60169
|
xpAwarded: calculatedXp,
|
|
59863
60170
|
attemptNumber: currentAttemptNumber,
|
|
59864
|
-
masteredUnitsApplied:
|
|
60171
|
+
masteredUnitsApplied: effectiveMasteredUnits,
|
|
59865
60172
|
pctCompleteApp,
|
|
59866
60173
|
scoreStatus,
|
|
59867
60174
|
inProgress
|
|
@@ -60396,6 +60703,65 @@ class TimebackClient {
|
|
|
60396
60703
|
resourceId: ids.resource
|
|
60397
60704
|
});
|
|
60398
60705
|
}
|
|
60706
|
+
async getStudentMastery(studentId, options) {
|
|
60707
|
+
await this._ensureAuthenticated();
|
|
60708
|
+
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
60709
|
+
const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
|
|
60710
|
+
if (filteredEnrollments.length === 0) {
|
|
60711
|
+
return {
|
|
60712
|
+
totalMasteredUnits: 0,
|
|
60713
|
+
totalMasterableUnits: 0,
|
|
60714
|
+
...options?.include?.perCourse && { courses: [] }
|
|
60715
|
+
};
|
|
60716
|
+
}
|
|
60717
|
+
const masteryResults = await Promise.all(filteredEnrollments.map(async (enrollment) => {
|
|
60718
|
+
try {
|
|
60719
|
+
const ids = deriveSourcedIds2(enrollment.course.id);
|
|
60720
|
+
const status = await this.masteryTracker.getStatus({
|
|
60721
|
+
studentId,
|
|
60722
|
+
courseId: enrollment.course.id,
|
|
60723
|
+
resourceId: ids.resource
|
|
60724
|
+
});
|
|
60725
|
+
return { enrollment, status };
|
|
60726
|
+
} catch (error) {
|
|
60727
|
+
log.warn("[TimebackClient] Failed to fetch mastery for enrollment", {
|
|
60728
|
+
enrollmentId: enrollment.id,
|
|
60729
|
+
error
|
|
60730
|
+
});
|
|
60731
|
+
return { enrollment, status: undefined };
|
|
60732
|
+
}
|
|
60733
|
+
}));
|
|
60734
|
+
let totalMasteredUnits = 0;
|
|
60735
|
+
let totalMasterableUnits = 0;
|
|
60736
|
+
const courses = [];
|
|
60737
|
+
for (const { enrollment, status } of masteryResults) {
|
|
60738
|
+
const masteredUnits = status?.masteredUnits ?? 0;
|
|
60739
|
+
const masterableUnits = status?.masterableUnits ?? 0;
|
|
60740
|
+
totalMasteredUnits += masteredUnits;
|
|
60741
|
+
totalMasterableUnits += masterableUnits;
|
|
60742
|
+
if (options?.include?.perCourse) {
|
|
60743
|
+
const gradeStr = enrollment.course.grades?.[0];
|
|
60744
|
+
const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
|
|
60745
|
+
const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
|
|
60746
|
+
const subjectStr = enrollment.course.subjects?.[0];
|
|
60747
|
+
const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
|
|
60748
|
+
courses.push({
|
|
60749
|
+
grade,
|
|
60750
|
+
subject,
|
|
60751
|
+
title: enrollment.course.title,
|
|
60752
|
+
masteredUnits,
|
|
60753
|
+
masterableUnits,
|
|
60754
|
+
pctComplete: status?.pctCompleteApp ?? 0,
|
|
60755
|
+
isComplete: status?.isComplete ?? false
|
|
60756
|
+
});
|
|
60757
|
+
}
|
|
60758
|
+
}
|
|
60759
|
+
return {
|
|
60760
|
+
totalMasteredUnits,
|
|
60761
|
+
totalMasterableUnits,
|
|
60762
|
+
...options?.include?.perCourse && { courses }
|
|
60763
|
+
};
|
|
60764
|
+
}
|
|
60399
60765
|
async getStudentXp(studentId, options) {
|
|
60400
60766
|
await this._ensureAuthenticated();
|
|
60401
60767
|
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
@@ -120401,6 +120767,7 @@ var endActivity;
|
|
|
120401
120767
|
var heartbeat;
|
|
120402
120768
|
var advanceCourse;
|
|
120403
120769
|
var getStudentXp;
|
|
120770
|
+
var getStudentMastery;
|
|
120404
120771
|
var getRoster;
|
|
120405
120772
|
var getStudentOverview;
|
|
120406
120773
|
var getGameMetrics;
|
|
@@ -120585,6 +120952,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
120585
120952
|
sessionTimingData,
|
|
120586
120953
|
xpEarned,
|
|
120587
120954
|
masteredUnits,
|
|
120955
|
+
masteredUnitsAbsolute,
|
|
120588
120956
|
extensions
|
|
120589
120957
|
} = body2;
|
|
120590
120958
|
logger45.debug("Ending activity", { userId: ctx.user.id, gameId });
|
|
@@ -120599,6 +120967,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
120599
120967
|
sessionTimingData,
|
|
120600
120968
|
xpEarned,
|
|
120601
120969
|
masteredUnits,
|
|
120970
|
+
masteredUnitsAbsolute,
|
|
120602
120971
|
extensions,
|
|
120603
120972
|
user: ctx.user
|
|
120604
120973
|
});
|
|
@@ -120708,6 +121077,53 @@ var init_timeback_controller = __esm(() => {
|
|
|
120708
121077
|
include
|
|
120709
121078
|
});
|
|
120710
121079
|
});
|
|
121080
|
+
getStudentMastery = requireDeveloper(async (ctx) => {
|
|
121081
|
+
const timebackId = ctx.params.timebackId;
|
|
121082
|
+
if (!timebackId) {
|
|
121083
|
+
throw ApiError.badRequest("Missing timebackId parameter");
|
|
121084
|
+
}
|
|
121085
|
+
const gameId = ctx.url.searchParams.get("gameId");
|
|
121086
|
+
if (!gameId) {
|
|
121087
|
+
throw ApiError.badRequest("Missing required gameId query parameter");
|
|
121088
|
+
}
|
|
121089
|
+
const gradeParam = ctx.url.searchParams.get("grade");
|
|
121090
|
+
const subjectParam = ctx.url.searchParams.get("subject");
|
|
121091
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
121092
|
+
throw ApiError.badRequest("Both grade and subject must be provided together");
|
|
121093
|
+
}
|
|
121094
|
+
let grade;
|
|
121095
|
+
let subject;
|
|
121096
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
121097
|
+
const parsedGrade = parseInt(gradeParam, 10);
|
|
121098
|
+
if (!isTimebackGrade2(parsedGrade)) {
|
|
121099
|
+
throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
121100
|
+
}
|
|
121101
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
121102
|
+
throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
121103
|
+
}
|
|
121104
|
+
grade = parsedGrade;
|
|
121105
|
+
subject = subjectParam;
|
|
121106
|
+
}
|
|
121107
|
+
const includeParam = ctx.url.searchParams.get("include");
|
|
121108
|
+
const includeOptions = includeParam ? includeParam.split(",").map((opt) => opt.trim().toLowerCase()) : [];
|
|
121109
|
+
const include = {
|
|
121110
|
+
perCourse: includeOptions.includes("percourse")
|
|
121111
|
+
};
|
|
121112
|
+
logger45.debug("Getting student mastery", {
|
|
121113
|
+
requesterId: ctx.user.id,
|
|
121114
|
+
timebackId,
|
|
121115
|
+
gameId,
|
|
121116
|
+
grade,
|
|
121117
|
+
subject,
|
|
121118
|
+
include
|
|
121119
|
+
});
|
|
121120
|
+
return ctx.services.timeback.getStudentMastery(timebackId, ctx.user, {
|
|
121121
|
+
gameId,
|
|
121122
|
+
grade,
|
|
121123
|
+
subject,
|
|
121124
|
+
include
|
|
121125
|
+
});
|
|
121126
|
+
});
|
|
120711
121127
|
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
120712
121128
|
const gameId = ctx.params.gameId;
|
|
120713
121129
|
const courseId = ctx.params.courseId;
|
|
@@ -120743,15 +121159,19 @@ var init_timeback_controller = __esm(() => {
|
|
|
120743
121159
|
getGameMetrics = requireGameManagementAccess(async (ctx) => {
|
|
120744
121160
|
const gameId = ctx.params.gameId;
|
|
120745
121161
|
const timebackId = ctx.params.timebackId;
|
|
121162
|
+
const runIds = [
|
|
121163
|
+
...new Set(ctx.url.searchParams.getAll("runId").map((runId) => runId.trim().toLowerCase()).filter(isValidUUID))
|
|
121164
|
+
];
|
|
120746
121165
|
if (!gameId || !timebackId) {
|
|
120747
121166
|
throw ApiError.badRequest("Missing gameId or timebackId path parameter");
|
|
120748
121167
|
}
|
|
120749
121168
|
logger45.debug("Getting game metrics", {
|
|
120750
121169
|
requesterId: ctx.user.id,
|
|
120751
121170
|
gameId,
|
|
120752
|
-
timebackId
|
|
121171
|
+
timebackId,
|
|
121172
|
+
runIds
|
|
120753
121173
|
});
|
|
120754
|
-
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user);
|
|
121174
|
+
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user, { runIds });
|
|
120755
121175
|
});
|
|
120756
121176
|
getStudentActivity = requireGameManagementAccess(async (ctx) => {
|
|
120757
121177
|
const timebackId = ctx.params.timebackId;
|
|
@@ -121040,6 +121460,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
121040
121460
|
heartbeat,
|
|
121041
121461
|
advanceCourse,
|
|
121042
121462
|
getStudentXp,
|
|
121463
|
+
getStudentMastery,
|
|
121043
121464
|
getRoster,
|
|
121044
121465
|
getStudentOverview,
|
|
121045
121466
|
getGameMetrics,
|
|
@@ -121890,6 +122311,7 @@ var init_timeback6 = __esm(() => {
|
|
|
121890
122311
|
init_controllers();
|
|
121891
122312
|
init_errors();
|
|
121892
122313
|
init_utils11();
|
|
122314
|
+
init_schemas_index();
|
|
121893
122315
|
init_api();
|
|
121894
122316
|
init_error_handler();
|
|
121895
122317
|
init_timeback5();
|
|
@@ -121969,6 +122391,14 @@ var init_timeback6 = __esm(() => {
|
|
|
121969
122391
|
}
|
|
121970
122392
|
if (gradeParam !== null && subjectParam !== null) {
|
|
121971
122393
|
const grade = parseInt(gradeParam, 10);
|
|
122394
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
122395
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
122396
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
122397
|
+
}
|
|
122398
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
122399
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
122400
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
122401
|
+
}
|
|
121972
122402
|
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
121973
122403
|
}
|
|
121974
122404
|
const mockCourses = enrollments.map((e) => {
|
|
@@ -121993,6 +122423,62 @@ var init_timeback6 = __esm(() => {
|
|
|
121993
122423
|
}
|
|
121994
122424
|
return handle2(timeback2.getStudentXp)(c2);
|
|
121995
122425
|
});
|
|
122426
|
+
timebackRouter.get("/student-mastery/:timebackId", async (c2) => {
|
|
122427
|
+
const user = c2.get("user");
|
|
122428
|
+
if (!user) {
|
|
122429
|
+
const error2 = ApiError.unauthorized("Must be logged in to get student mastery");
|
|
122430
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
122431
|
+
}
|
|
122432
|
+
if (shouldMockTimeback()) {
|
|
122433
|
+
const url2 = new URL(c2.req.url);
|
|
122434
|
+
const gradeParam = url2.searchParams.get("grade");
|
|
122435
|
+
const subjectParam = url2.searchParams.get("subject");
|
|
122436
|
+
const includeParam = url2.searchParams.get("include") || "";
|
|
122437
|
+
const includeOptions = includeParam.split(",").map((opt) => opt.trim().toLowerCase());
|
|
122438
|
+
const includePerCourse = includeOptions.includes("percourse");
|
|
122439
|
+
const db2 = c2.get("db");
|
|
122440
|
+
let enrollments = await getMockEnrollments(db2);
|
|
122441
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
122442
|
+
const error2 = ApiError.badRequest("Both grade and subject must be provided together");
|
|
122443
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
122444
|
+
}
|
|
122445
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
122446
|
+
const grade = parseInt(gradeParam, 10);
|
|
122447
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
122448
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
122449
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
122450
|
+
}
|
|
122451
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
122452
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
122453
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
122454
|
+
}
|
|
122455
|
+
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
122456
|
+
}
|
|
122457
|
+
const mockCourses = enrollments.map((e) => {
|
|
122458
|
+
const seed3 = hashCode(`${e.grade}-${e.subject}`);
|
|
122459
|
+
const masterableUnits = 5 + seed3 % 16;
|
|
122460
|
+
const masteredUnits = seed3 % (masterableUnits + 1);
|
|
122461
|
+
const pctComplete = masterableUnits > 0 ? Math.round(masteredUnits / masterableUnits * 1e4) / 100 : 0;
|
|
122462
|
+
return {
|
|
122463
|
+
grade: e.grade,
|
|
122464
|
+
subject: e.subject,
|
|
122465
|
+
title: `${e.subject} ${formatGradeLabel(e.grade)}`,
|
|
122466
|
+
masteredUnits,
|
|
122467
|
+
masterableUnits,
|
|
122468
|
+
pctComplete,
|
|
122469
|
+
isComplete: masteredUnits >= masterableUnits
|
|
122470
|
+
};
|
|
122471
|
+
});
|
|
122472
|
+
const totalMasteredUnits = mockCourses.reduce((sum2, course) => sum2 + course.masteredUnits, 0);
|
|
122473
|
+
const totalMasterableUnits = mockCourses.reduce((sum2, course) => sum2 + course.masterableUnits, 0);
|
|
122474
|
+
return c2.json({
|
|
122475
|
+
totalMasteredUnits,
|
|
122476
|
+
totalMasterableUnits,
|
|
122477
|
+
...includePerCourse && { courses: mockCourses }
|
|
122478
|
+
});
|
|
122479
|
+
}
|
|
122480
|
+
return handle2(timeback2.getStudentMastery)(c2);
|
|
122481
|
+
});
|
|
121996
122482
|
});
|
|
121997
122483
|
function verifyMockToken(idToken) {
|
|
121998
122484
|
if (!idToken.startsWith("mock:")) {
|
|
@@ -122528,6 +123014,17 @@ var POSTHOG_CONFIG = {
|
|
|
122528
123014
|
var TIMEBACK_ORG_SOURCED_ID2 = "PLAYCADEMY";
|
|
122529
123015
|
var TIMEBACK_ORG_NAME2 = "Playcademy Studios";
|
|
122530
123016
|
var TIMEBACK_ORG_TYPE2 = "department";
|
|
123017
|
+
var TIMEBACK_GAME_METRIC_DECIMAL_PLACES2 = {
|
|
123018
|
+
xp: 1,
|
|
123019
|
+
mastery: 0,
|
|
123020
|
+
score: 2
|
|
123021
|
+
};
|
|
123022
|
+
var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE2 = {
|
|
123023
|
+
xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES2.xp,
|
|
123024
|
+
mastery: 0,
|
|
123025
|
+
time: 60,
|
|
123026
|
+
score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES2.score
|
|
123027
|
+
};
|
|
122531
123028
|
// src/lib/sandbox/timeback.ts
|
|
122532
123029
|
function detectTimebackOptions() {
|
|
122533
123030
|
if (process.env.TIMEBACK_LOCAL === "true") {
|
|
@@ -122744,10 +123241,13 @@ var TIMEBACK_COURSE_DEFAULTS2;
|
|
|
122744
123241
|
var TIMEBACK_RESOURCE_DEFAULTS2;
|
|
122745
123242
|
var TIMEBACK_COMPONENT_DEFAULTS2;
|
|
122746
123243
|
var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2;
|
|
123244
|
+
var TIMEBACK_GAME_METRIC_DECIMAL_PLACES3;
|
|
123245
|
+
var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE3;
|
|
122747
123246
|
var init_timeback7 = __esm8(() => {
|
|
122748
123247
|
TIMEBACK_ROUTES2 = {
|
|
122749
123248
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
122750
123249
|
GET_XP: "/integrations/timeback/xp",
|
|
123250
|
+
GET_MASTERY: "/integrations/timeback/mastery",
|
|
122751
123251
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
122752
123252
|
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
122753
123253
|
};
|
|
@@ -122787,6 +123287,17 @@ var init_timeback7 = __esm8(() => {
|
|
|
122787
123287
|
sortOrder: 1,
|
|
122788
123288
|
lessonType: "quiz"
|
|
122789
123289
|
};
|
|
123290
|
+
TIMEBACK_GAME_METRIC_DECIMAL_PLACES3 = {
|
|
123291
|
+
xp: 1,
|
|
123292
|
+
mastery: 0,
|
|
123293
|
+
score: 2
|
|
123294
|
+
};
|
|
123295
|
+
TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE3 = {
|
|
123296
|
+
xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES3.xp,
|
|
123297
|
+
mastery: 0,
|
|
123298
|
+
time: 60,
|
|
123299
|
+
score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES3.score
|
|
123300
|
+
};
|
|
122790
123301
|
});
|
|
122791
123302
|
var WORKER_NAMING2;
|
|
122792
123303
|
var init_cloudflare2 = __esm8(() => {
|