@playcademy/vite-plugin 0.2.24-beta.3 → 0.2.24-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 +948 -571
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24406,7 +24406,8 @@ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
|
|
|
24406
24406
|
var init_timeback2 = __esm(() => {
|
|
24407
24407
|
TIMEBACK_ROUTES = {
|
|
24408
24408
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
24409
|
-
GET_XP: "/integrations/timeback/xp"
|
|
24409
|
+
GET_XP: "/integrations/timeback/xp",
|
|
24410
|
+
HEARTBEAT: "/integrations/timeback/heartbeat"
|
|
24410
24411
|
};
|
|
24411
24412
|
TIMEBACK_COURSE_DEFAULTS = {
|
|
24412
24413
|
gradingScheme: "STANDARD",
|
|
@@ -25335,7 +25336,7 @@ var package_default;
|
|
|
25335
25336
|
var init_package = __esm(() => {
|
|
25336
25337
|
package_default = {
|
|
25337
25338
|
name: "@playcademy/sandbox",
|
|
25338
|
-
version: "0.3.17-beta.
|
|
25339
|
+
version: "0.3.17-beta.8",
|
|
25339
25340
|
description: "Local development server for Playcademy game development",
|
|
25340
25341
|
type: "module",
|
|
25341
25342
|
exports: {
|
|
@@ -35611,7 +35612,7 @@ var init_table6 = __esm(() => {
|
|
|
35611
35612
|
init_drizzle_orm();
|
|
35612
35613
|
init_pg_core();
|
|
35613
35614
|
init_table5();
|
|
35614
|
-
userRoleEnum = pgEnum("user_role", ["admin", "player", "developer"]);
|
|
35615
|
+
userRoleEnum = pgEnum("user_role", ["admin", "player", "developer", "teacher"]);
|
|
35615
35616
|
developerStatusEnum = pgEnum("developer_status", ["none", "pending", "approved"]);
|
|
35616
35617
|
users = pgTable("user", {
|
|
35617
35618
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
@@ -50583,10 +50584,13 @@ var init_game_service = __esm(() => {
|
|
|
50583
50584
|
});
|
|
50584
50585
|
}
|
|
50585
50586
|
async listManageable(user) {
|
|
50586
|
-
|
|
50587
|
+
const seesAllGames = user.role === "admin" || user.role === "teacher";
|
|
50588
|
+
if (!seesAllGames) {
|
|
50589
|
+
this.validateDeveloperStatus(user);
|
|
50590
|
+
}
|
|
50587
50591
|
const db2 = this.deps.db;
|
|
50588
50592
|
return db2.query.games.findMany({
|
|
50589
|
-
where:
|
|
50593
|
+
where: seesAllGames ? undefined : eq(games.developerId, user.id),
|
|
50590
50594
|
orderBy: [desc(games.createdAt)]
|
|
50591
50595
|
});
|
|
50592
50596
|
}
|
|
@@ -50990,6 +50994,19 @@ var init_game_service = __esm(() => {
|
|
|
50990
50994
|
throw new NotFoundError("Game", gameId);
|
|
50991
50995
|
}
|
|
50992
50996
|
}
|
|
50997
|
+
async validateGameManagementAccess(user, gameId) {
|
|
50998
|
+
if (user.role === "admin" || user.role === "teacher") {
|
|
50999
|
+
const gameExists = await this.deps.db.query.games.findFirst({
|
|
51000
|
+
where: eq(games.id, gameId),
|
|
51001
|
+
columns: { id: true }
|
|
51002
|
+
});
|
|
51003
|
+
if (!gameExists) {
|
|
51004
|
+
throw new NotFoundError("Game", gameId);
|
|
51005
|
+
}
|
|
51006
|
+
return;
|
|
51007
|
+
}
|
|
51008
|
+
return this.validateDeveloperAccess(user, gameId);
|
|
51009
|
+
}
|
|
50993
51010
|
async validateDeveloperAccessBySlug(user, slug) {
|
|
50994
51011
|
this.validateDeveloperStatus(user);
|
|
50995
51012
|
const db2 = this.deps.db;
|
|
@@ -51058,6 +51075,7 @@ function createGameServices(deps) {
|
|
|
51058
51075
|
validators: {
|
|
51059
51076
|
validateDeveloperAccessBySlug: (user, slug) => game.validateDeveloperAccessBySlug(user, slug),
|
|
51060
51077
|
validateDeveloperAccess: (user, gameId) => game.validateDeveloperAccess(user, gameId),
|
|
51078
|
+
validateGameManagementAccess: (user, gameId) => game.validateGameManagementAccess(user, gameId),
|
|
51061
51079
|
validateOwnership: (user, gameId) => game.validateOwnership(user, gameId)
|
|
51062
51080
|
}
|
|
51063
51081
|
};
|
|
@@ -52683,7 +52701,8 @@ var init_constants3 = __esm(() => {
|
|
|
52683
52701
|
HEALTH: "/api/health",
|
|
52684
52702
|
TIMEBACK: {
|
|
52685
52703
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
52686
|
-
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}
|
|
52704
|
+
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
52705
|
+
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`
|
|
52687
52706
|
}
|
|
52688
52707
|
};
|
|
52689
52708
|
});
|
|
@@ -54499,9 +54518,13 @@ class TimebackAdminService {
|
|
|
54499
54518
|
});
|
|
54500
54519
|
});
|
|
54501
54520
|
}
|
|
54502
|
-
async resolveAdminMutationContext(gameId, courseId, user, studentId) {
|
|
54521
|
+
async resolveAdminMutationContext(gameId, courseId, user, studentId, accessLevel = "developer") {
|
|
54503
54522
|
const client = this.requireClient();
|
|
54504
|
-
|
|
54523
|
+
if (accessLevel === "dashboard") {
|
|
54524
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
54525
|
+
} else {
|
|
54526
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
54527
|
+
}
|
|
54505
54528
|
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
54506
54529
|
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
54507
54530
|
});
|
|
@@ -54761,7 +54784,7 @@ class TimebackAdminService {
|
|
|
54761
54784
|
}
|
|
54762
54785
|
async listStudentsForCourse(gameId, courseId, user) {
|
|
54763
54786
|
const client = this.requireClient();
|
|
54764
|
-
await this.deps.
|
|
54787
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
54765
54788
|
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
54766
54789
|
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
54767
54790
|
});
|
|
@@ -54799,7 +54822,7 @@ class TimebackAdminService {
|
|
|
54799
54822
|
}
|
|
54800
54823
|
async getStudentOverview(gameId, studentId, user, courseId) {
|
|
54801
54824
|
const client = this.requireClient();
|
|
54802
|
-
await this.deps.
|
|
54825
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
54803
54826
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
54804
54827
|
where: courseId ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId)) : eq(gameTimebackIntegrations.gameId, gameId)
|
|
54805
54828
|
});
|
|
@@ -54853,7 +54876,7 @@ class TimebackAdminService {
|
|
|
54853
54876
|
const client = this.requireClient();
|
|
54854
54877
|
const safeLimit = Math.max(1, Math.min(limit, TimebackAdminService.MAX_STUDENT_ACTIVITY_LIMIT));
|
|
54855
54878
|
const safeOffset = Math.max(0, Math.min(offset, TimebackAdminService.MAX_STUDENT_ACTIVITY_OFFSET));
|
|
54856
|
-
await this.deps.
|
|
54879
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
54857
54880
|
const [integration, sensorUrl] = await Promise.all([
|
|
54858
54881
|
this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
54859
54882
|
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
@@ -54917,7 +54940,7 @@ class TimebackAdminService {
|
|
|
54917
54940
|
return { status: "ok" };
|
|
54918
54941
|
}
|
|
54919
54942
|
async toggleCourseCompletion(data, user) {
|
|
54920
|
-
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
54943
|
+
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId, "dashboard");
|
|
54921
54944
|
const historyClient = client;
|
|
54922
54945
|
const ids = deriveSourcedIds(data.courseId);
|
|
54923
54946
|
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
@@ -55010,6 +55033,77 @@ class TimebackAdminService {
|
|
|
55010
55033
|
}
|
|
55011
55034
|
return { status: "ok" };
|
|
55012
55035
|
}
|
|
55036
|
+
async searchStudentsForEnrollment(gameId, courseId, query, user) {
|
|
55037
|
+
const client = this.requireClient();
|
|
55038
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
55039
|
+
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
55040
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
55041
|
+
});
|
|
55042
|
+
if (!integration) {
|
|
55043
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
55044
|
+
}
|
|
55045
|
+
const trimmedQuery = query.trim();
|
|
55046
|
+
if (trimmedQuery.length < 2) {
|
|
55047
|
+
return { students: [] };
|
|
55048
|
+
}
|
|
55049
|
+
const filterParts = [
|
|
55050
|
+
`givenName~'${escapeFilterValue(trimmedQuery)}'`,
|
|
55051
|
+
`familyName~'${escapeFilterValue(trimmedQuery)}'`,
|
|
55052
|
+
`email~'${escapeFilterValue(trimmedQuery)}'`
|
|
55053
|
+
];
|
|
55054
|
+
const filter = filterParts.join(" OR ");
|
|
55055
|
+
const params = new URLSearchParams({ filter, limit: "25" });
|
|
55056
|
+
const endpoint = `/ims/oneroster/rostering/v1p2/users?${params}`;
|
|
55057
|
+
let allUsers = [];
|
|
55058
|
+
try {
|
|
55059
|
+
const response = await client["request"](endpoint, "GET");
|
|
55060
|
+
allUsers = response.users || [];
|
|
55061
|
+
} catch (error) {
|
|
55062
|
+
logger16.warn("Failed to search OneRoster users", {
|
|
55063
|
+
query: trimmedQuery,
|
|
55064
|
+
error: error instanceof Error ? error.message : String(error)
|
|
55065
|
+
});
|
|
55066
|
+
return { students: [] };
|
|
55067
|
+
}
|
|
55068
|
+
const roster = await client.oneroster.enrollments.listByCourse(courseId, {
|
|
55069
|
+
role: "student",
|
|
55070
|
+
includeUsers: false
|
|
55071
|
+
});
|
|
55072
|
+
const enrolledStudentIds = new Set(roster.map((entry) => entry.enrollment.user.sourcedId));
|
|
55073
|
+
const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => ({
|
|
55074
|
+
studentId: entry.sourcedId,
|
|
55075
|
+
name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || entry.sourcedId,
|
|
55076
|
+
email: entry.email || null,
|
|
55077
|
+
alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId)
|
|
55078
|
+
}));
|
|
55079
|
+
return { students };
|
|
55080
|
+
}
|
|
55081
|
+
async enrollStudent(data, user) {
|
|
55082
|
+
const client = this.requireClient();
|
|
55083
|
+
await this.deps.validateGameManagementAccess(user, data.gameId);
|
|
55084
|
+
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
55085
|
+
where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
|
|
55086
|
+
});
|
|
55087
|
+
if (!integration) {
|
|
55088
|
+
throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
|
|
55089
|
+
}
|
|
55090
|
+
await client.edubridge.enrollments.enroll(data.studentId, data.courseId, {
|
|
55091
|
+
role: "student"
|
|
55092
|
+
});
|
|
55093
|
+
return { status: "ok" };
|
|
55094
|
+
}
|
|
55095
|
+
async unenrollStudent(data, user) {
|
|
55096
|
+
const client = this.requireClient();
|
|
55097
|
+
await this.deps.validateGameManagementAccess(user, data.gameId);
|
|
55098
|
+
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
55099
|
+
where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
|
|
55100
|
+
});
|
|
55101
|
+
if (!integration) {
|
|
55102
|
+
throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
|
|
55103
|
+
}
|
|
55104
|
+
await client.edubridge.enrollments.unenroll(data.studentId, data.courseId);
|
|
55105
|
+
return { status: "ok" };
|
|
55106
|
+
}
|
|
55013
55107
|
async getCompletionStatus(client, courseId, studentId) {
|
|
55014
55108
|
const ids = deriveSourcedIds(courseId);
|
|
55015
55109
|
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
@@ -55073,590 +55167,704 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
55073
55167
|
init_timeback_util();
|
|
55074
55168
|
logger16 = log.scope("TimebackAdminService");
|
|
55075
55169
|
});
|
|
55076
|
-
|
|
55077
|
-
|
|
55078
|
-
|
|
55079
|
-
|
|
55080
|
-
|
|
55081
|
-
|
|
55082
|
-
|
|
55083
|
-
|
|
55084
|
-
|
|
55085
|
-
|
|
55170
|
+
var logger17;
|
|
55171
|
+
var TimebackService;
|
|
55172
|
+
var init_timeback_service = __esm(() => {
|
|
55173
|
+
init_drizzle_orm();
|
|
55174
|
+
init_src();
|
|
55175
|
+
init_tables_index();
|
|
55176
|
+
init_src2();
|
|
55177
|
+
init_types4();
|
|
55178
|
+
init_src4();
|
|
55179
|
+
init_errors();
|
|
55180
|
+
init_timeback_util();
|
|
55181
|
+
logger17 = log.scope("TimebackService");
|
|
55182
|
+
TimebackService = class TimebackService2 {
|
|
55183
|
+
static HEARTBEAT_DEDUPE_TTL_MS = 300000;
|
|
55184
|
+
static processedHeartbeatWindows = new Map;
|
|
55185
|
+
static inFlightHeartbeatWindows = new Map;
|
|
55186
|
+
deps;
|
|
55187
|
+
static cleanHeartbeatDedupeCache(now2 = Date.now()) {
|
|
55188
|
+
for (const [key, timestamp3] of this.processedHeartbeatWindows) {
|
|
55189
|
+
if (now2 - timestamp3 > this.HEARTBEAT_DEDUPE_TTL_MS) {
|
|
55190
|
+
this.processedHeartbeatWindows.delete(key);
|
|
55191
|
+
}
|
|
55192
|
+
}
|
|
55086
55193
|
}
|
|
55087
|
-
|
|
55088
|
-
|
|
55089
|
-
|
|
55090
|
-
const db2 = this.deps.db;
|
|
55091
|
-
const tz = timezone2 || PLATFORM_TIMEZONE;
|
|
55092
|
-
const base = date3 ? new Date(date3) : new Date;
|
|
55093
|
-
if (isNaN(base.getTime())) {
|
|
55094
|
-
throw new ValidationError("Invalid date format. Use ISO 8601 format.");
|
|
55194
|
+
static isDuplicateHeartbeatWindow(key) {
|
|
55195
|
+
this.cleanHeartbeatDedupeCache();
|
|
55196
|
+
return this.processedHeartbeatWindows.has(key);
|
|
55095
55197
|
}
|
|
55096
|
-
|
|
55097
|
-
|
|
55098
|
-
} catch {
|
|
55099
|
-
throw new ValidationError(`Invalid timezone: ${tz}`);
|
|
55198
|
+
static getInFlightHeartbeatWindow(key) {
|
|
55199
|
+
return this.inFlightHeartbeatWindows.get(key);
|
|
55100
55200
|
}
|
|
55101
|
-
|
|
55102
|
-
|
|
55103
|
-
const result2 = await db2.select({ xp: timebackDailyXp.xp, date: timebackDailyXp.date }).from(timebackDailyXp).where(and(eq(timebackDailyXp.userId, userId), eq(timebackDailyXp.date, todayMidnight))).limit(1);
|
|
55104
|
-
if (result2.length === 0) {
|
|
55105
|
-
return { xp: 0, date: todayMidnight.toISOString() };
|
|
55106
|
-
}
|
|
55107
|
-
return { xp: result2[0].xp, date: result2[0].date.toISOString() };
|
|
55201
|
+
static markHeartbeatWindowProcessed(key) {
|
|
55202
|
+
this.processedHeartbeatWindows.set(key, Date.now());
|
|
55108
55203
|
}
|
|
55109
|
-
|
|
55110
|
-
|
|
55111
|
-
return { xp: Number(result[0]?.totalXp) || 0, date: startOfDay.toISOString() };
|
|
55112
|
-
}
|
|
55113
|
-
async getTotalXp(userId) {
|
|
55114
|
-
const db2 = this.deps.db;
|
|
55115
|
-
const result = await db2.select({ totalXp: sum(timebackDailyXp.xp) }).from(timebackDailyXp).where(eq(timebackDailyXp.userId, userId));
|
|
55116
|
-
return { totalXp: Number(result[0]?.totalXp) || 0 };
|
|
55117
|
-
}
|
|
55118
|
-
async updateTodayXp(userId, data) {
|
|
55119
|
-
const db2 = this.deps.db;
|
|
55120
|
-
const { xp, userTimestamp } = data;
|
|
55121
|
-
let targetDate;
|
|
55122
|
-
if (userTimestamp) {
|
|
55123
|
-
targetDate = new Date(userTimestamp);
|
|
55124
|
-
if (isNaN(targetDate.getTime())) {
|
|
55125
|
-
throw new ValidationError("Invalid userTimestamp format. Use ISO 8601 format.");
|
|
55126
|
-
}
|
|
55127
|
-
targetDate.setHours(0, 0, 0, 0);
|
|
55128
|
-
} else {
|
|
55129
|
-
targetDate = new Date;
|
|
55130
|
-
targetDate.setUTCHours(0, 0, 0, 0);
|
|
55204
|
+
static markHeartbeatWindowInFlight(key, promise) {
|
|
55205
|
+
this.inFlightHeartbeatWindows.set(key, promise);
|
|
55131
55206
|
}
|
|
55132
|
-
|
|
55133
|
-
|
|
55134
|
-
set: { xp: sql`excluded.xp`, updatedAt: new Date }
|
|
55135
|
-
}).returning({ xp: timebackDailyXp.xp, date: timebackDailyXp.date });
|
|
55136
|
-
if (!result) {
|
|
55137
|
-
logger17.error("Daily XP upsert returned no rows", { userId, date: targetDate });
|
|
55138
|
-
throw new InternalError("Failed to update daily XP record");
|
|
55207
|
+
static clearInFlightHeartbeatWindow(key) {
|
|
55208
|
+
this.inFlightHeartbeatWindows.delete(key);
|
|
55139
55209
|
}
|
|
55140
|
-
|
|
55141
|
-
|
|
55142
|
-
async getXpHistory(userId, startDate, endDate) {
|
|
55143
|
-
const db2 = this.deps.db;
|
|
55144
|
-
const whereConditions = [eq(timebackDailyXp.userId, userId)];
|
|
55145
|
-
if (startDate) {
|
|
55146
|
-
const start2 = new Date(startDate);
|
|
55147
|
-
start2.setUTCHours(0, 0, 0, 0);
|
|
55148
|
-
whereConditions.push(gte(timebackDailyXp.date, start2));
|
|
55210
|
+
constructor(deps) {
|
|
55211
|
+
this.deps = deps;
|
|
55149
55212
|
}
|
|
55150
|
-
|
|
55151
|
-
|
|
55152
|
-
|
|
55153
|
-
|
|
55213
|
+
requireClient() {
|
|
55214
|
+
if (!this.deps.timeback) {
|
|
55215
|
+
logger17.error("Timeback client not available in context");
|
|
55216
|
+
throw new ValidationError("Timeback integration not available in this environment");
|
|
55217
|
+
}
|
|
55218
|
+
return this.deps.timeback;
|
|
55154
55219
|
}
|
|
55155
|
-
|
|
55156
|
-
|
|
55157
|
-
|
|
55158
|
-
|
|
55159
|
-
|
|
55160
|
-
|
|
55161
|
-
|
|
55162
|
-
|
|
55163
|
-
|
|
55164
|
-
|
|
55165
|
-
|
|
55166
|
-
|
|
55167
|
-
|
|
55168
|
-
|
|
55169
|
-
|
|
55220
|
+
async getTodayXp(userId, date3, timezone2) {
|
|
55221
|
+
const db2 = this.deps.db;
|
|
55222
|
+
const tz = timezone2 || PLATFORM_TIMEZONE;
|
|
55223
|
+
const base = date3 ? new Date(date3) : new Date;
|
|
55224
|
+
if (isNaN(base.getTime())) {
|
|
55225
|
+
throw new ValidationError("Invalid date format. Use ISO 8601 format.");
|
|
55226
|
+
}
|
|
55227
|
+
try {
|
|
55228
|
+
new Intl.DateTimeFormat(undefined, { timeZone: tz });
|
|
55229
|
+
} catch {
|
|
55230
|
+
throw new ValidationError(`Invalid timezone: ${tz}`);
|
|
55231
|
+
}
|
|
55232
|
+
if (tz === PLATFORM_TIMEZONE) {
|
|
55233
|
+
const todayMidnight = getUtcInstantForMidnight(base, tz);
|
|
55234
|
+
const result2 = await db2.select({ xp: timebackDailyXp.xp, date: timebackDailyXp.date }).from(timebackDailyXp).where(and(eq(timebackDailyXp.userId, userId), eq(timebackDailyXp.date, todayMidnight))).limit(1);
|
|
55235
|
+
if (result2.length === 0) {
|
|
55236
|
+
return { xp: 0, date: todayMidnight.toISOString() };
|
|
55237
|
+
}
|
|
55238
|
+
return { xp: result2[0].xp, date: result2[0].date.toISOString() };
|
|
55239
|
+
}
|
|
55240
|
+
const { startOfDay, endOfDay } = getDayBoundariesInTimezone(base, tz);
|
|
55241
|
+
const result = await db2.select({ totalXp: sum(timebackXpEvents.xpDelta) }).from(timebackXpEvents).where(and(eq(timebackXpEvents.userId, userId), gte(timebackXpEvents.occurredAt, startOfDay), lte(timebackXpEvents.occurredAt, new Date(endOfDay.getTime() - 1))));
|
|
55242
|
+
return { xp: Number(result[0]?.totalXp) || 0, date: startOfDay.toISOString() };
|
|
55170
55243
|
}
|
|
55171
|
-
|
|
55172
|
-
|
|
55173
|
-
|
|
55174
|
-
|
|
55175
|
-
|
|
55176
|
-
|
|
55177
|
-
|
|
55178
|
-
|
|
55179
|
-
|
|
55180
|
-
|
|
55181
|
-
|
|
55182
|
-
|
|
55183
|
-
|
|
55244
|
+
async getTotalXp(userId) {
|
|
55245
|
+
const db2 = this.deps.db;
|
|
55246
|
+
const result = await db2.select({ totalXp: sum(timebackDailyXp.xp) }).from(timebackDailyXp).where(eq(timebackDailyXp.userId, userId));
|
|
55247
|
+
return { totalXp: Number(result[0]?.totalXp) || 0 };
|
|
55248
|
+
}
|
|
55249
|
+
async updateTodayXp(userId, data) {
|
|
55250
|
+
const db2 = this.deps.db;
|
|
55251
|
+
const { xp, userTimestamp } = data;
|
|
55252
|
+
let targetDate;
|
|
55253
|
+
if (userTimestamp) {
|
|
55254
|
+
targetDate = new Date(userTimestamp);
|
|
55255
|
+
if (isNaN(targetDate.getTime())) {
|
|
55256
|
+
throw new ValidationError("Invalid userTimestamp format. Use ISO 8601 format.");
|
|
55257
|
+
}
|
|
55258
|
+
targetDate.setHours(0, 0, 0, 0);
|
|
55259
|
+
} else {
|
|
55260
|
+
targetDate = new Date;
|
|
55261
|
+
targetDate.setUTCHours(0, 0, 0, 0);
|
|
55184
55262
|
}
|
|
55185
|
-
const
|
|
55186
|
-
|
|
55187
|
-
|
|
55188
|
-
|
|
55189
|
-
|
|
55190
|
-
|
|
55191
|
-
|
|
55192
|
-
email: user.email,
|
|
55193
|
-
roles: [
|
|
55194
|
-
{
|
|
55195
|
-
roleType: "primary",
|
|
55196
|
-
role: "student",
|
|
55197
|
-
org: { sourcedId: TIMEBACK_ORG_SOURCED_ID }
|
|
55198
|
-
}
|
|
55199
|
-
]
|
|
55200
|
-
});
|
|
55201
|
-
if (!response.sourcedIdPairs?.allocatedSourcedId) {
|
|
55202
|
-
return { status: "error", message: "Timeback did not return allocatedSourcedId" };
|
|
55263
|
+
const [result] = await db2.insert(timebackDailyXp).values({ userId, date: targetDate, xp }).onConflictDoUpdate({
|
|
55264
|
+
target: [timebackDailyXp.userId, timebackDailyXp.date],
|
|
55265
|
+
set: { xp: sql`excluded.xp`, updatedAt: new Date }
|
|
55266
|
+
}).returning({ xp: timebackDailyXp.xp, date: timebackDailyXp.date });
|
|
55267
|
+
if (!result) {
|
|
55268
|
+
logger17.error("Daily XP upsert returned no rows", { userId, date: targetDate });
|
|
55269
|
+
throw new InternalError("Failed to update daily XP record");
|
|
55203
55270
|
}
|
|
55204
|
-
|
|
55205
|
-
name3 = `${providedNames.firstName} ${providedNames.lastName}`;
|
|
55206
|
-
logger17.info("Created student in OneRoster", { userId: user.id, timebackId });
|
|
55271
|
+
return { xp: result.xp, date: result.date.toISOString() };
|
|
55207
55272
|
}
|
|
55208
|
-
|
|
55209
|
-
|
|
55210
|
-
|
|
55211
|
-
|
|
55212
|
-
|
|
55213
|
-
|
|
55214
|
-
|
|
55215
|
-
|
|
55273
|
+
async getXpHistory(userId, startDate, endDate) {
|
|
55274
|
+
const db2 = this.deps.db;
|
|
55275
|
+
const whereConditions = [eq(timebackDailyXp.userId, userId)];
|
|
55276
|
+
if (startDate) {
|
|
55277
|
+
const start2 = new Date(startDate);
|
|
55278
|
+
start2.setUTCHours(0, 0, 0, 0);
|
|
55279
|
+
whereConditions.push(gte(timebackDailyXp.date, start2));
|
|
55280
|
+
}
|
|
55281
|
+
if (endDate) {
|
|
55282
|
+
const end = new Date(endDate);
|
|
55283
|
+
end.setUTCHours(23, 59, 59, 999);
|
|
55284
|
+
whereConditions.push(lte(timebackDailyXp.date, end));
|
|
55285
|
+
}
|
|
55286
|
+
const result = await db2.select({ date: timebackDailyXp.date, xp: timebackDailyXp.xp }).from(timebackDailyXp).where(and(...whereConditions)).orderBy(timebackDailyXp.date);
|
|
55287
|
+
return {
|
|
55288
|
+
history: result.map((row) => ({ date: row.date.toISOString(), xp: row.xp }))
|
|
55289
|
+
};
|
|
55290
|
+
}
|
|
55291
|
+
async populateStudent(user, providedNames) {
|
|
55292
|
+
const client = this.requireClient();
|
|
55293
|
+
const db2 = this.deps.db;
|
|
55294
|
+
const dbUser = await db2.query.users.findFirst({
|
|
55295
|
+
where: eq(users.id, user.id),
|
|
55296
|
+
columns: { id: true, timebackId: true }
|
|
55297
|
+
});
|
|
55298
|
+
if (dbUser?.timebackId) {
|
|
55299
|
+
logger17.info("Student already onboarded", { userId: user.id });
|
|
55300
|
+
return { status: "already_populated" };
|
|
55301
|
+
}
|
|
55302
|
+
let timebackId;
|
|
55303
|
+
let name3;
|
|
55304
|
+
try {
|
|
55305
|
+
const existingUser = await client.oneroster.users.findByEmail(user.email);
|
|
55306
|
+
timebackId = existingUser.sourcedId;
|
|
55307
|
+
name3 = `${existingUser.givenName} ${existingUser.familyName}`;
|
|
55308
|
+
logger17.info("Found existing student in OneRoster", {
|
|
55309
|
+
userId: user.id,
|
|
55310
|
+
timebackId
|
|
55311
|
+
});
|
|
55312
|
+
} catch {
|
|
55313
|
+
if (!providedNames?.firstName || !providedNames?.lastName) {
|
|
55314
|
+
return { status: "no_record" };
|
|
55216
55315
|
}
|
|
55217
|
-
const
|
|
55218
|
-
|
|
55219
|
-
|
|
55220
|
-
|
|
55221
|
-
|
|
55222
|
-
|
|
55223
|
-
|
|
55316
|
+
const sourcedId = crypto.randomUUID();
|
|
55317
|
+
const response = await client.oneroster.users.create({
|
|
55318
|
+
sourcedId,
|
|
55319
|
+
status: "active",
|
|
55320
|
+
enabledUser: true,
|
|
55321
|
+
givenName: providedNames.firstName,
|
|
55322
|
+
familyName: providedNames.lastName,
|
|
55323
|
+
email: user.email,
|
|
55324
|
+
roles: [
|
|
55325
|
+
{
|
|
55326
|
+
roleType: "primary",
|
|
55327
|
+
role: "student",
|
|
55328
|
+
org: { sourcedId: TIMEBACK_ORG_SOURCED_ID }
|
|
55329
|
+
}
|
|
55330
|
+
]
|
|
55331
|
+
});
|
|
55332
|
+
if (!response.sourcedIdPairs?.allocatedSourcedId) {
|
|
55333
|
+
return { status: "error", message: "Timeback did not return allocatedSourcedId" };
|
|
55334
|
+
}
|
|
55335
|
+
timebackId = response.sourcedIdPairs.allocatedSourcedId;
|
|
55336
|
+
name3 = `${providedNames.firstName} ${providedNames.lastName}`;
|
|
55337
|
+
logger17.info("Created student in OneRoster", { userId: user.id, timebackId });
|
|
55338
|
+
}
|
|
55339
|
+
const assessments = await this.fetchAssessments(timebackId);
|
|
55340
|
+
await db2.transaction(async (tx) => {
|
|
55341
|
+
if (assessments.length > 0) {
|
|
55342
|
+
const events = mapAssessmentsToXpEvents(user.id, assessments);
|
|
55343
|
+
for (const event of events) {
|
|
55344
|
+
try {
|
|
55345
|
+
await tx.insert(timebackXpEvents).values(event);
|
|
55346
|
+
} catch {}
|
|
55347
|
+
}
|
|
55348
|
+
const dailyMap = new Map;
|
|
55349
|
+
for (const a of assessments) {
|
|
55350
|
+
const xp = a.metadata?.xp;
|
|
55351
|
+
if (typeof xp === "number" && a.scoreDate) {
|
|
55352
|
+
const day = getUtcInstantForMidnight(new Date(a.scoreDate), PLATFORM_TIMEZONE);
|
|
55353
|
+
const key = day.toISOString();
|
|
55354
|
+
dailyMap.set(key, (dailyMap.get(key) || 0) + xp);
|
|
55355
|
+
}
|
|
55356
|
+
}
|
|
55357
|
+
if (dailyMap.size > 0) {
|
|
55358
|
+
const dailyRecords = [...dailyMap.entries()].map(([iso, xp]) => ({
|
|
55359
|
+
userId: user.id,
|
|
55360
|
+
date: new Date(iso),
|
|
55361
|
+
xp
|
|
55362
|
+
}));
|
|
55363
|
+
await tx.insert(timebackDailyXp).values(dailyRecords).onConflictDoUpdate({
|
|
55364
|
+
target: [timebackDailyXp.userId, timebackDailyXp.date],
|
|
55365
|
+
set: { xp: sql`excluded.xp`, updatedAt: new Date }
|
|
55366
|
+
});
|
|
55224
55367
|
}
|
|
55225
55368
|
}
|
|
55226
|
-
|
|
55227
|
-
|
|
55369
|
+
const [updated] = await tx.update(users).set({ timebackId, name: name3 }).where(eq(users.id, user.id)).returning({ id: users.id });
|
|
55370
|
+
if (!updated) {
|
|
55371
|
+
logger17.error("User Timeback ID update returned no rows", {
|
|
55228
55372
|
userId: user.id,
|
|
55229
|
-
|
|
55230
|
-
xp
|
|
55231
|
-
}));
|
|
55232
|
-
await tx.insert(timebackDailyXp).values(dailyRecords).onConflictDoUpdate({
|
|
55233
|
-
target: [timebackDailyXp.userId, timebackDailyXp.date],
|
|
55234
|
-
set: { xp: sql`excluded.xp`, updatedAt: new Date }
|
|
55373
|
+
timebackId
|
|
55235
55374
|
});
|
|
55375
|
+
throw new InternalError("Failed to update user with Timeback ID");
|
|
55236
55376
|
}
|
|
55237
|
-
}
|
|
55238
|
-
const [updated] = await tx.update(users).set({ timebackId, name: name3 }).where(eq(users.id, user.id)).returning({ id: users.id });
|
|
55239
|
-
if (!updated) {
|
|
55240
|
-
logger17.error("User Timeback ID update returned no rows", {
|
|
55241
|
-
userId: user.id,
|
|
55242
|
-
timebackId
|
|
55243
|
-
});
|
|
55244
|
-
throw new InternalError("Failed to update user with Timeback ID");
|
|
55245
|
-
}
|
|
55246
|
-
});
|
|
55247
|
-
return { status: "ok" };
|
|
55248
|
-
}
|
|
55249
|
-
async fetchAssessments(studentSourcedId) {
|
|
55250
|
-
const client = this.requireClient();
|
|
55251
|
-
const allAssessments = [];
|
|
55252
|
-
const limit = 3000;
|
|
55253
|
-
const fields = "sourcedId,assessmentLineItem,student,score,textScore,scoreDate,scoreStatus,scorePercentile,comment,metadata,inProgress,incomplete,late,missing";
|
|
55254
|
-
let offset = 0;
|
|
55255
|
-
try {
|
|
55256
|
-
while (true) {
|
|
55257
|
-
const results = await client.oneroster.assessmentResults.listByStudent(studentSourcedId, { limit, offset, fields });
|
|
55258
|
-
allAssessments.push(...results);
|
|
55259
|
-
if (results.length < limit) {
|
|
55260
|
-
break;
|
|
55261
|
-
}
|
|
55262
|
-
offset += limit;
|
|
55263
|
-
}
|
|
55264
|
-
logger17.debug("Fetched assessments", {
|
|
55265
|
-
studentSourcedId,
|
|
55266
|
-
totalCount: allAssessments.length
|
|
55267
55377
|
});
|
|
55268
|
-
return
|
|
55269
|
-
} catch (error) {
|
|
55270
|
-
logger17.warn("Failed to fetch assessments", { studentSourcedId, error });
|
|
55271
|
-
return [];
|
|
55378
|
+
return { status: "ok" };
|
|
55272
55379
|
}
|
|
55273
|
-
|
|
55274
|
-
|
|
55275
|
-
|
|
55276
|
-
|
|
55277
|
-
|
|
55278
|
-
|
|
55279
|
-
|
|
55280
|
-
|
|
55281
|
-
|
|
55282
|
-
|
|
55283
|
-
|
|
55284
|
-
|
|
55285
|
-
|
|
55286
|
-
|
|
55287
|
-
|
|
55288
|
-
|
|
55289
|
-
|
|
55290
|
-
|
|
55291
|
-
}
|
|
55292
|
-
async getUserDataByTimebackId(timebackId) {
|
|
55293
|
-
const [profile, enrollments] = await Promise.all([
|
|
55294
|
-
this.fetchStudentProfile(timebackId),
|
|
55295
|
-
this.fetchEnrollments(timebackId)
|
|
55296
|
-
]);
|
|
55297
|
-
return {
|
|
55298
|
-
id: timebackId,
|
|
55299
|
-
role: profile.role,
|
|
55300
|
-
enrollments,
|
|
55301
|
-
organizations: profile.organizations
|
|
55302
|
-
};
|
|
55303
|
-
}
|
|
55304
|
-
async fetchStudentProfile(timebackId) {
|
|
55305
|
-
const client = this.requireClient();
|
|
55306
|
-
try {
|
|
55307
|
-
const user = await client.oneroster.users.get(timebackId);
|
|
55308
|
-
const primaryRole = user.roles.find((r) => r.roleType === "primary");
|
|
55309
|
-
const role = primaryRole?.role ?? user.roles[0]?.role ?? "student";
|
|
55310
|
-
const orgMap = new Map;
|
|
55311
|
-
if (user.primaryOrg) {
|
|
55312
|
-
orgMap.set(user.primaryOrg.sourcedId, {
|
|
55313
|
-
id: user.primaryOrg.sourcedId,
|
|
55314
|
-
name: user.primaryOrg.name ?? null,
|
|
55315
|
-
type: user.primaryOrg.type || "school",
|
|
55316
|
-
isPrimary: true
|
|
55380
|
+
async fetchAssessments(studentSourcedId) {
|
|
55381
|
+
const client = this.requireClient();
|
|
55382
|
+
const allAssessments = [];
|
|
55383
|
+
const limit = 3000;
|
|
55384
|
+
const fields = "sourcedId,assessmentLineItem,student,score,textScore,scoreDate,scoreStatus,scorePercentile,comment,metadata,inProgress,incomplete,late,missing";
|
|
55385
|
+
let offset = 0;
|
|
55386
|
+
try {
|
|
55387
|
+
while (true) {
|
|
55388
|
+
const results = await client.oneroster.assessmentResults.listByStudent(studentSourcedId, { limit, offset, fields });
|
|
55389
|
+
allAssessments.push(...results);
|
|
55390
|
+
if (results.length < limit) {
|
|
55391
|
+
break;
|
|
55392
|
+
}
|
|
55393
|
+
offset += limit;
|
|
55394
|
+
}
|
|
55395
|
+
logger17.debug("Fetched assessments", {
|
|
55396
|
+
studentSourcedId,
|
|
55397
|
+
totalCount: allAssessments.length
|
|
55317
55398
|
});
|
|
55399
|
+
return allAssessments;
|
|
55400
|
+
} catch (error) {
|
|
55401
|
+
logger17.warn("Failed to fetch assessments", { studentSourcedId, error });
|
|
55402
|
+
return [];
|
|
55318
55403
|
}
|
|
55319
|
-
|
|
55320
|
-
|
|
55321
|
-
|
|
55322
|
-
|
|
55323
|
-
|
|
55324
|
-
|
|
55325
|
-
|
|
55404
|
+
}
|
|
55405
|
+
async getUserData(userId, gameId) {
|
|
55406
|
+
const db2 = this.deps.db;
|
|
55407
|
+
const userData = await db2.query.users.findFirst({ where: eq(users.id, userId) });
|
|
55408
|
+
if (!userData) {
|
|
55409
|
+
throw new NotFoundError("User", userId);
|
|
55410
|
+
}
|
|
55411
|
+
if (!userData.timebackId) {
|
|
55412
|
+
throw new NotFoundError("Timeback account not found for user");
|
|
55413
|
+
}
|
|
55414
|
+
const [profile, allEnrollments] = await Promise.all([
|
|
55415
|
+
this.fetchStudentProfile(userData.timebackId),
|
|
55416
|
+
this.fetchEnrollments(userData.timebackId)
|
|
55417
|
+
]);
|
|
55418
|
+
const enrollments = gameId ? allEnrollments.filter((e) => e.gameId === gameId) : allEnrollments;
|
|
55419
|
+
const enrollmentOrgIds = new Set(enrollments.map((e) => e.orgId).filter(Boolean));
|
|
55420
|
+
const organizations = gameId && enrollmentOrgIds.size > 0 ? profile.organizations.filter((o) => enrollmentOrgIds.has(o.id)) : profile.organizations;
|
|
55421
|
+
return { id: userData.timebackId, role: profile.role, enrollments, organizations };
|
|
55422
|
+
}
|
|
55423
|
+
async getUserDataByTimebackId(timebackId) {
|
|
55424
|
+
const [profile, enrollments] = await Promise.all([
|
|
55425
|
+
this.fetchStudentProfile(timebackId),
|
|
55426
|
+
this.fetchEnrollments(timebackId)
|
|
55427
|
+
]);
|
|
55428
|
+
return {
|
|
55429
|
+
id: timebackId,
|
|
55430
|
+
role: profile.role,
|
|
55431
|
+
enrollments,
|
|
55432
|
+
organizations: profile.organizations
|
|
55433
|
+
};
|
|
55434
|
+
}
|
|
55435
|
+
async fetchStudentProfile(timebackId) {
|
|
55436
|
+
const client = this.requireClient();
|
|
55437
|
+
try {
|
|
55438
|
+
const user = await client.oneroster.users.get(timebackId);
|
|
55439
|
+
const primaryRole = user.roles.find((r) => r.roleType === "primary");
|
|
55440
|
+
const role = primaryRole?.role ?? user.roles[0]?.role ?? "student";
|
|
55441
|
+
const orgMap = new Map;
|
|
55442
|
+
if (user.primaryOrg) {
|
|
55443
|
+
orgMap.set(user.primaryOrg.sourcedId, {
|
|
55444
|
+
id: user.primaryOrg.sourcedId,
|
|
55445
|
+
name: user.primaryOrg.name ?? null,
|
|
55446
|
+
type: user.primaryOrg.type || "school",
|
|
55447
|
+
isPrimary: true
|
|
55326
55448
|
});
|
|
55327
55449
|
}
|
|
55450
|
+
for (const r of user.roles) {
|
|
55451
|
+
if (r.org && !orgMap.has(r.org.sourcedId)) {
|
|
55452
|
+
orgMap.set(r.org.sourcedId, {
|
|
55453
|
+
id: r.org.sourcedId,
|
|
55454
|
+
name: null,
|
|
55455
|
+
type: "school",
|
|
55456
|
+
isPrimary: false
|
|
55457
|
+
});
|
|
55458
|
+
}
|
|
55459
|
+
}
|
|
55460
|
+
return { role, organizations: [...orgMap.values()] };
|
|
55461
|
+
} catch {
|
|
55462
|
+
return { role: "student", organizations: [] };
|
|
55328
55463
|
}
|
|
55329
|
-
return { role, organizations: [...orgMap.values()] };
|
|
55330
|
-
} catch {
|
|
55331
|
-
return { role: "student", organizations: [] };
|
|
55332
55464
|
}
|
|
55333
|
-
|
|
55334
|
-
|
|
55335
|
-
|
|
55336
|
-
|
|
55337
|
-
|
|
55338
|
-
|
|
55339
|
-
|
|
55340
|
-
|
|
55465
|
+
async fetchEnrollments(timebackId) {
|
|
55466
|
+
const client = this.requireClient();
|
|
55467
|
+
const db2 = this.deps.db;
|
|
55468
|
+
try {
|
|
55469
|
+
const enrollments = await client.getEnrollments(timebackId);
|
|
55470
|
+
const courseIds = enrollments.map((e) => e.courseId).filter((id) => Boolean(id));
|
|
55471
|
+
if (courseIds.length === 0) {
|
|
55472
|
+
return [];
|
|
55473
|
+
}
|
|
55474
|
+
const courseToSchool = new Map(enrollments.filter((e) => e.school?.id).map((e) => [e.courseId, e.school.id]));
|
|
55475
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55476
|
+
where: inArray(gameTimebackIntegrations.courseId, courseIds)
|
|
55477
|
+
});
|
|
55478
|
+
return integrations.map((i2) => ({
|
|
55479
|
+
gameId: i2.gameId,
|
|
55480
|
+
grade: i2.grade,
|
|
55481
|
+
subject: i2.subject,
|
|
55482
|
+
courseId: i2.courseId,
|
|
55483
|
+
orgId: courseToSchool.get(i2.courseId)
|
|
55484
|
+
}));
|
|
55485
|
+
} catch {
|
|
55341
55486
|
return [];
|
|
55342
55487
|
}
|
|
55343
|
-
const courseToSchool = new Map(enrollments.filter((e) => e.school?.id).map((e) => [e.courseId, e.school.id]));
|
|
55344
|
-
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55345
|
-
where: inArray(gameTimebackIntegrations.courseId, courseIds)
|
|
55346
|
-
});
|
|
55347
|
-
return integrations.map((i2) => ({
|
|
55348
|
-
gameId: i2.gameId,
|
|
55349
|
-
grade: i2.grade,
|
|
55350
|
-
subject: i2.subject,
|
|
55351
|
-
courseId: i2.courseId,
|
|
55352
|
-
orgId: courseToSchool.get(i2.courseId)
|
|
55353
|
-
}));
|
|
55354
|
-
} catch {
|
|
55355
|
-
return [];
|
|
55356
55488
|
}
|
|
55357
|
-
|
|
55358
|
-
|
|
55359
|
-
|
|
55360
|
-
|
|
55361
|
-
|
|
55362
|
-
|
|
55363
|
-
|
|
55364
|
-
|
|
55365
|
-
|
|
55366
|
-
|
|
55367
|
-
|
|
55368
|
-
|
|
55369
|
-
|
|
55370
|
-
|
|
55371
|
-
|
|
55372
|
-
const {
|
|
55373
|
-
subject: subjectInput,
|
|
55374
|
-
grade,
|
|
55375
|
-
title,
|
|
55376
|
-
courseCode,
|
|
55377
|
-
level,
|
|
55378
|
-
metadata: metadata2,
|
|
55379
|
-
totalXp: derivedTotalXp,
|
|
55380
|
-
masterableUnits: derivedMasterableUnits
|
|
55381
|
-
} = courseConfig;
|
|
55382
|
-
if (!isTimebackSubject(subjectInput)) {
|
|
55383
|
-
logger17.warn("Invalid Timeback subject in course config", {
|
|
55489
|
+
async setupIntegration(gameId, request, user) {
|
|
55490
|
+
const client = this.requireClient();
|
|
55491
|
+
const db2 = this.deps.db;
|
|
55492
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55493
|
+
const { courses, baseConfig, verbose } = request;
|
|
55494
|
+
const existing = await db2.query.gameTimebackIntegrations.findMany({
|
|
55495
|
+
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
55496
|
+
});
|
|
55497
|
+
const integrations = [];
|
|
55498
|
+
const verboseData = [];
|
|
55499
|
+
for (const courseConfig of courses) {
|
|
55500
|
+
let applySuffix = function(text3) {
|
|
55501
|
+
return suffix ? `${text3} ${suffix}` : text3;
|
|
55502
|
+
};
|
|
55503
|
+
const {
|
|
55384
55504
|
subject: subjectInput,
|
|
55385
|
-
courseCode,
|
|
55386
|
-
title
|
|
55387
|
-
});
|
|
55388
|
-
throw new ValidationError(`Invalid subject "${subjectInput}"`);
|
|
55389
|
-
}
|
|
55390
|
-
if (!isTimebackGrade(grade)) {
|
|
55391
|
-
logger17.warn("Invalid Timeback grade in course config", {
|
|
55392
55505
|
grade,
|
|
55393
|
-
courseCode,
|
|
55394
|
-
title
|
|
55395
|
-
});
|
|
55396
|
-
throw new ValidationError(`Invalid grade "${grade}"`);
|
|
55397
|
-
}
|
|
55398
|
-
const subject = subjectInput;
|
|
55399
|
-
const courseMetadata = isCourseMetadata(metadata2) ? metadata2 : undefined;
|
|
55400
|
-
const totalXp = derivedTotalXp ?? courseMetadata?.metrics?.totalXp;
|
|
55401
|
-
const masterableUnits = derivedMasterableUnits ?? (isPlaycademyResourceMetadata(courseMetadata?.playcademy) ? courseMetadata?.playcademy?.mastery?.masterableUnits : undefined);
|
|
55402
|
-
if (typeof totalXp !== "number") {
|
|
55403
|
-
logger17.warn("Course missing totalXp in Timeback config", {
|
|
55404
|
-
courseCode,
|
|
55405
|
-
title
|
|
55406
|
-
});
|
|
55407
|
-
throw new ValidationError(`Course "${title}" is missing totalXp`);
|
|
55408
|
-
}
|
|
55409
|
-
const suffix = baseConfig.component.titleSuffix || "";
|
|
55410
|
-
const fullConfig = {
|
|
55411
|
-
organization: baseConfig.organization,
|
|
55412
|
-
course: {
|
|
55413
55506
|
title,
|
|
55414
|
-
subjects: [subject],
|
|
55415
|
-
grades: [grade],
|
|
55416
55507
|
courseCode,
|
|
55417
55508
|
level,
|
|
55418
|
-
|
|
55419
|
-
|
|
55420
|
-
|
|
55421
|
-
|
|
55422
|
-
|
|
55423
|
-
|
|
55424
|
-
|
|
55425
|
-
|
|
55426
|
-
|
|
55427
|
-
|
|
55428
|
-
|
|
55429
|
-
baseMetadata: baseConfig.resource.metadata,
|
|
55430
|
-
subject,
|
|
55431
|
-
grade,
|
|
55432
|
-
totalXp,
|
|
55433
|
-
masterableUnits
|
|
55434
|
-
})
|
|
55435
|
-
},
|
|
55436
|
-
componentResource: {
|
|
55437
|
-
...baseConfig.componentResource,
|
|
55438
|
-
title: applySuffix(baseConfig.componentResource.title || "")
|
|
55439
|
-
}
|
|
55440
|
-
};
|
|
55441
|
-
const existingIntegration = existing.find((i2) => i2.grade === grade && i2.subject === subject);
|
|
55442
|
-
if (existingIntegration) {
|
|
55443
|
-
await client.update(existingIntegration.courseId, fullConfig);
|
|
55444
|
-
const [updated] = await db2.update(gameTimebackIntegrations).set({ totalXp, updatedAt: new Date }).where(eq(gameTimebackIntegrations.id, existingIntegration.id)).returning();
|
|
55445
|
-
if (updated) {
|
|
55446
|
-
integrations.push(this.toGameTimebackIntegration(updated));
|
|
55509
|
+
metadata: metadata2,
|
|
55510
|
+
totalXp: derivedTotalXp,
|
|
55511
|
+
masterableUnits: derivedMasterableUnits
|
|
55512
|
+
} = courseConfig;
|
|
55513
|
+
if (!isTimebackSubject(subjectInput)) {
|
|
55514
|
+
logger17.warn("Invalid Timeback subject in course config", {
|
|
55515
|
+
subject: subjectInput,
|
|
55516
|
+
courseCode,
|
|
55517
|
+
title
|
|
55518
|
+
});
|
|
55519
|
+
throw new ValidationError(`Invalid subject "${subjectInput}"`);
|
|
55447
55520
|
}
|
|
55448
|
-
|
|
55449
|
-
|
|
55450
|
-
|
|
55451
|
-
|
|
55452
|
-
|
|
55453
|
-
|
|
55454
|
-
|
|
55455
|
-
|
|
55521
|
+
if (!isTimebackGrade(grade)) {
|
|
55522
|
+
logger17.warn("Invalid Timeback grade in course config", {
|
|
55523
|
+
grade,
|
|
55524
|
+
courseCode,
|
|
55525
|
+
title
|
|
55526
|
+
});
|
|
55527
|
+
throw new ValidationError(`Invalid grade "${grade}"`);
|
|
55528
|
+
}
|
|
55529
|
+
const subject = subjectInput;
|
|
55530
|
+
const courseMetadata = isCourseMetadata(metadata2) ? metadata2 : undefined;
|
|
55531
|
+
const totalXp = derivedTotalXp ?? courseMetadata?.metrics?.totalXp;
|
|
55532
|
+
const masterableUnits = derivedMasterableUnits ?? (isPlaycademyResourceMetadata(courseMetadata?.playcademy) ? courseMetadata?.playcademy?.mastery?.masterableUnits : undefined);
|
|
55533
|
+
if (typeof totalXp !== "number") {
|
|
55534
|
+
logger17.warn("Course missing totalXp in Timeback config", {
|
|
55535
|
+
courseCode,
|
|
55536
|
+
title
|
|
55537
|
+
});
|
|
55538
|
+
throw new ValidationError(`Course "${title}" is missing totalXp`);
|
|
55539
|
+
}
|
|
55540
|
+
const suffix = baseConfig.component.titleSuffix || "";
|
|
55541
|
+
const fullConfig = {
|
|
55542
|
+
organization: baseConfig.organization,
|
|
55543
|
+
course: {
|
|
55544
|
+
title,
|
|
55545
|
+
subjects: [subject],
|
|
55546
|
+
grades: [grade],
|
|
55547
|
+
courseCode,
|
|
55548
|
+
level,
|
|
55549
|
+
gradingScheme: "STANDARD",
|
|
55550
|
+
metadata: metadata2
|
|
55551
|
+
},
|
|
55552
|
+
component: {
|
|
55553
|
+
...baseConfig.component,
|
|
55554
|
+
title: applySuffix(baseConfig.component.title || `${title} Activities`)
|
|
55555
|
+
},
|
|
55556
|
+
resource: {
|
|
55557
|
+
...baseConfig.resource,
|
|
55558
|
+
title: applySuffix(baseConfig.resource.title || `${title} Game`),
|
|
55559
|
+
metadata: buildResourceMetadata({
|
|
55560
|
+
baseMetadata: baseConfig.resource.metadata,
|
|
55561
|
+
subject,
|
|
55562
|
+
grade,
|
|
55563
|
+
totalXp,
|
|
55564
|
+
masterableUnits
|
|
55565
|
+
})
|
|
55566
|
+
},
|
|
55567
|
+
componentResource: {
|
|
55568
|
+
...baseConfig.componentResource,
|
|
55569
|
+
title: applySuffix(baseConfig.componentResource.title || "")
|
|
55570
|
+
}
|
|
55571
|
+
};
|
|
55572
|
+
const existingIntegration = existing.find((i2) => i2.grade === grade && i2.subject === subject);
|
|
55573
|
+
if (existingIntegration) {
|
|
55574
|
+
await client.update(existingIntegration.courseId, fullConfig);
|
|
55575
|
+
const [updated] = await db2.update(gameTimebackIntegrations).set({ totalXp, updatedAt: new Date }).where(eq(gameTimebackIntegrations.id, existingIntegration.id)).returning();
|
|
55576
|
+
if (updated) {
|
|
55577
|
+
integrations.push(this.toGameTimebackIntegration(updated));
|
|
55578
|
+
}
|
|
55579
|
+
} else {
|
|
55580
|
+
const result = await client.setup(fullConfig, { verbose });
|
|
55581
|
+
const [integration] = await db2.insert(gameTimebackIntegrations).values({ gameId, courseId: result.courseId, grade, subject, totalXp }).returning();
|
|
55582
|
+
if (integration) {
|
|
55583
|
+
const dto = this.toGameTimebackIntegration(integration);
|
|
55584
|
+
integrations.push(dto);
|
|
55585
|
+
if (verbose && result.verboseData) {
|
|
55586
|
+
verboseData.push({ integration: dto, config: result.verboseData });
|
|
55587
|
+
}
|
|
55456
55588
|
}
|
|
55457
55589
|
}
|
|
55458
55590
|
}
|
|
55591
|
+
return { integrations, ...verbose && verboseData.length > 0 && { verbose: verboseData } };
|
|
55459
55592
|
}
|
|
55460
|
-
|
|
55461
|
-
|
|
55462
|
-
|
|
55463
|
-
|
|
55464
|
-
|
|
55465
|
-
|
|
55466
|
-
});
|
|
55467
|
-
return rows.map((row) => this.toGameTimebackIntegration(row));
|
|
55468
|
-
}
|
|
55469
|
-
async verifyIntegration(gameId, user) {
|
|
55470
|
-
const client = this.requireClient();
|
|
55471
|
-
const db2 = this.deps.db;
|
|
55472
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55473
|
-
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55474
|
-
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
55475
|
-
});
|
|
55476
|
-
if (integrations.length === 0) {
|
|
55477
|
-
throw new NotFoundError("Timeback integration", gameId);
|
|
55593
|
+
async getIntegrations(gameId, user) {
|
|
55594
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
55595
|
+
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
55596
|
+
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
55597
|
+
});
|
|
55598
|
+
return rows.map((row) => this.toGameTimebackIntegration(row));
|
|
55478
55599
|
}
|
|
55479
|
-
|
|
55480
|
-
|
|
55481
|
-
const
|
|
55482
|
-
|
|
55483
|
-
const
|
|
55484
|
-
|
|
55485
|
-
|
|
55486
|
-
|
|
55487
|
-
integration
|
|
55488
|
-
|
|
55489
|
-
|
|
55490
|
-
|
|
55491
|
-
resources
|
|
55492
|
-
|
|
55493
|
-
|
|
55494
|
-
|
|
55495
|
-
|
|
55496
|
-
|
|
55497
|
-
|
|
55498
|
-
|
|
55499
|
-
|
|
55500
|
-
|
|
55501
|
-
|
|
55502
|
-
|
|
55503
|
-
|
|
55504
|
-
|
|
55505
|
-
|
|
55506
|
-
|
|
55507
|
-
|
|
55600
|
+
async verifyIntegration(gameId, user) {
|
|
55601
|
+
const client = this.requireClient();
|
|
55602
|
+
const db2 = this.deps.db;
|
|
55603
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55604
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55605
|
+
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
55606
|
+
});
|
|
55607
|
+
if (integrations.length === 0) {
|
|
55608
|
+
throw new NotFoundError("Timeback integration", gameId);
|
|
55609
|
+
}
|
|
55610
|
+
const now2 = new Date;
|
|
55611
|
+
const results = await Promise.all(integrations.map(async (integration) => {
|
|
55612
|
+
const resources = await client.verify(integration.courseId);
|
|
55613
|
+
const resourceValues = Object.values(resources);
|
|
55614
|
+
const allFound = resourceValues.every((r) => r.found);
|
|
55615
|
+
const errors3 = Object.entries(resources).filter(([_2, r]) => !r.found).map(([name3]) => `${name3} not found`);
|
|
55616
|
+
const status = allFound ? "success" : "error";
|
|
55617
|
+
return {
|
|
55618
|
+
integration: this.toGameTimebackIntegration({
|
|
55619
|
+
...integration,
|
|
55620
|
+
lastVerifiedAt: now2
|
|
55621
|
+
}),
|
|
55622
|
+
resources,
|
|
55623
|
+
status,
|
|
55624
|
+
...errors3.length > 0 && { errors: errors3 }
|
|
55625
|
+
};
|
|
55626
|
+
}));
|
|
55627
|
+
await db2.update(gameTimebackIntegrations).set({ lastVerifiedAt: now2 }).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
55628
|
+
const overallStatus = results.every((r) => r.status === "success") ? "success" : "error";
|
|
55629
|
+
return { status: overallStatus, results };
|
|
55630
|
+
}
|
|
55631
|
+
async getConfig(gameId, user) {
|
|
55632
|
+
const client = this.requireClient();
|
|
55633
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55634
|
+
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
55635
|
+
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
55636
|
+
});
|
|
55637
|
+
if (!integration) {
|
|
55638
|
+
throw new NotFoundError("Timeback integration", gameId);
|
|
55639
|
+
}
|
|
55640
|
+
return client.getConfig(integration.courseId);
|
|
55508
55641
|
}
|
|
55509
|
-
|
|
55510
|
-
|
|
55511
|
-
|
|
55512
|
-
|
|
55513
|
-
|
|
55514
|
-
|
|
55515
|
-
|
|
55516
|
-
|
|
55517
|
-
|
|
55518
|
-
|
|
55519
|
-
|
|
55642
|
+
async deleteIntegrations(gameId, user) {
|
|
55643
|
+
const client = this.requireClient();
|
|
55644
|
+
const db2 = this.deps.db;
|
|
55645
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55646
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55647
|
+
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
55648
|
+
});
|
|
55649
|
+
if (integrations.length === 0) {
|
|
55650
|
+
throw new NotFoundError("Timeback integration", gameId);
|
|
55651
|
+
}
|
|
55652
|
+
for (const integration of integrations) {
|
|
55653
|
+
await client.cleanup(integration.courseId);
|
|
55654
|
+
}
|
|
55655
|
+
await db2.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
55520
55656
|
}
|
|
55521
|
-
|
|
55522
|
-
|
|
55657
|
+
toGameTimebackIntegration(integration) {
|
|
55658
|
+
return {
|
|
55659
|
+
id: integration.id,
|
|
55660
|
+
gameId: integration.gameId,
|
|
55661
|
+
courseId: integration.courseId,
|
|
55662
|
+
grade: integration.grade,
|
|
55663
|
+
subject: integration.subject,
|
|
55664
|
+
totalXp: integration.totalXp ?? null,
|
|
55665
|
+
createdAt: integration.createdAt,
|
|
55666
|
+
updatedAt: integration.updatedAt,
|
|
55667
|
+
lastVerifiedAt: integration.lastVerifiedAt ?? null
|
|
55668
|
+
};
|
|
55523
55669
|
}
|
|
55524
|
-
|
|
55525
|
-
|
|
55526
|
-
|
|
55527
|
-
|
|
55528
|
-
|
|
55529
|
-
|
|
55530
|
-
|
|
55531
|
-
|
|
55532
|
-
subject: integration.subject,
|
|
55533
|
-
totalXp: integration.totalXp ?? null,
|
|
55534
|
-
createdAt: integration.createdAt,
|
|
55535
|
-
updatedAt: integration.updatedAt,
|
|
55536
|
-
lastVerifiedAt: integration.lastVerifiedAt ?? null
|
|
55537
|
-
};
|
|
55538
|
-
}
|
|
55539
|
-
async endActivity({
|
|
55540
|
-
gameId,
|
|
55541
|
-
studentId,
|
|
55542
|
-
activityData,
|
|
55543
|
-
scoreData,
|
|
55544
|
-
timingData,
|
|
55545
|
-
xpEarned,
|
|
55546
|
-
masteredUnits,
|
|
55547
|
-
extensions,
|
|
55548
|
-
user
|
|
55549
|
-
}) {
|
|
55550
|
-
const client = this.requireClient();
|
|
55551
|
-
const db2 = this.deps.db;
|
|
55552
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55553
|
-
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
55554
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
|
|
55555
|
-
});
|
|
55556
|
-
if (!integration) {
|
|
55557
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
55558
|
-
}
|
|
55559
|
-
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
55560
|
-
const result = await client.recordProgress(integration.courseId, studentId, {
|
|
55561
|
-
score: scorePercentage,
|
|
55562
|
-
totalQuestions: scoreData.totalQuestions,
|
|
55563
|
-
correctQuestions: scoreData.correctQuestions,
|
|
55564
|
-
durationSeconds: timingData.durationSeconds,
|
|
55670
|
+
async endActivity({
|
|
55671
|
+
gameId,
|
|
55672
|
+
studentId,
|
|
55673
|
+
runId,
|
|
55674
|
+
activityData,
|
|
55675
|
+
scoreData,
|
|
55676
|
+
timingData,
|
|
55677
|
+
sessionTimingData,
|
|
55565
55678
|
xpEarned,
|
|
55566
55679
|
masteredUnits,
|
|
55567
55680
|
extensions,
|
|
55568
|
-
|
|
55569
|
-
|
|
55570
|
-
|
|
55571
|
-
|
|
55572
|
-
|
|
55573
|
-
|
|
55574
|
-
|
|
55575
|
-
|
|
55576
|
-
|
|
55577
|
-
|
|
55578
|
-
|
|
55579
|
-
|
|
55580
|
-
|
|
55581
|
-
|
|
55582
|
-
|
|
55583
|
-
|
|
55584
|
-
|
|
55585
|
-
|
|
55586
|
-
|
|
55587
|
-
|
|
55588
|
-
|
|
55589
|
-
|
|
55681
|
+
user
|
|
55682
|
+
}) {
|
|
55683
|
+
const client = this.requireClient();
|
|
55684
|
+
const db2 = this.deps.db;
|
|
55685
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55686
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
55687
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
|
|
55688
|
+
});
|
|
55689
|
+
if (!integration) {
|
|
55690
|
+
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
55691
|
+
}
|
|
55692
|
+
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
55693
|
+
const result = await client.recordProgress(integration.courseId, studentId, {
|
|
55694
|
+
score: scorePercentage,
|
|
55695
|
+
totalQuestions: scoreData.totalQuestions,
|
|
55696
|
+
correctQuestions: scoreData.correctQuestions,
|
|
55697
|
+
durationSeconds: timingData.durationSeconds,
|
|
55698
|
+
xpEarned,
|
|
55699
|
+
masteredUnits,
|
|
55700
|
+
extensions,
|
|
55701
|
+
activityId: activityData.activityId,
|
|
55702
|
+
activityName: activityData.activityName,
|
|
55703
|
+
subject: activityData.subject,
|
|
55704
|
+
appName: activityData.appName,
|
|
55705
|
+
sensorUrl: activityData.sensorUrl,
|
|
55706
|
+
courseId: activityData.courseId,
|
|
55707
|
+
courseName: activityData.courseName,
|
|
55708
|
+
studentEmail: activityData.studentEmail,
|
|
55709
|
+
courseTotalXp: integration.totalXp,
|
|
55710
|
+
...runId ? { runId } : {}
|
|
55711
|
+
});
|
|
55712
|
+
const sessionEndActiveSeconds = sessionTimingData?.activeSeconds ?? timingData.durationSeconds;
|
|
55713
|
+
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
55714
|
+
if (sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0) {
|
|
55715
|
+
await client.recordSessionEnd(integration.courseId, studentId, {
|
|
55716
|
+
activeTimeSeconds: sessionEndActiveSeconds,
|
|
55717
|
+
...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {},
|
|
55718
|
+
activityId: activityData.activityId,
|
|
55719
|
+
activityName: activityData.activityName,
|
|
55720
|
+
subject: activityData.subject,
|
|
55721
|
+
appName: activityData.appName,
|
|
55722
|
+
sensorUrl: activityData.sensorUrl,
|
|
55723
|
+
courseId: activityData.courseId,
|
|
55724
|
+
courseName: activityData.courseName,
|
|
55725
|
+
studentEmail: activityData.studentEmail,
|
|
55726
|
+
...runId ? { runId } : {}
|
|
55727
|
+
});
|
|
55728
|
+
}
|
|
55729
|
+
logger17.info("Recorded activity completion", {
|
|
55730
|
+
gameId,
|
|
55731
|
+
courseId: integration.courseId,
|
|
55732
|
+
studentId,
|
|
55733
|
+
runId,
|
|
55734
|
+
score: scorePercentage
|
|
55735
|
+
});
|
|
55736
|
+
return {
|
|
55737
|
+
status: "ok",
|
|
55738
|
+
courseId: integration.courseId,
|
|
55739
|
+
xpAwarded: result.xpAwarded,
|
|
55740
|
+
masteredUnits: result.masteredUnitsApplied,
|
|
55741
|
+
pctCompleteApp: result.pctCompleteApp,
|
|
55742
|
+
scoreStatus: result.scoreStatus,
|
|
55743
|
+
inProgress: result.inProgress
|
|
55744
|
+
};
|
|
55745
|
+
}
|
|
55746
|
+
async recordHeartbeat({
|
|
55590
55747
|
gameId,
|
|
55591
|
-
courseId: integration.courseId,
|
|
55592
55748
|
studentId,
|
|
55593
|
-
|
|
55594
|
-
|
|
55595
|
-
|
|
55596
|
-
|
|
55597
|
-
|
|
55598
|
-
|
|
55599
|
-
|
|
55600
|
-
|
|
55601
|
-
|
|
55602
|
-
|
|
55603
|
-
|
|
55604
|
-
|
|
55605
|
-
|
|
55606
|
-
|
|
55607
|
-
|
|
55608
|
-
|
|
55609
|
-
|
|
55610
|
-
|
|
55611
|
-
|
|
55612
|
-
if (options.grade !== undefined && options.subject) {
|
|
55613
|
-
conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
|
|
55614
|
-
conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
|
|
55749
|
+
runId,
|
|
55750
|
+
activityData,
|
|
55751
|
+
timingData,
|
|
55752
|
+
windowSequence,
|
|
55753
|
+
isFinal,
|
|
55754
|
+
user
|
|
55755
|
+
}) {
|
|
55756
|
+
const client = this.requireClient();
|
|
55757
|
+
const db2 = this.deps.db;
|
|
55758
|
+
const heartbeatWindowKey = `${runId}:${windowSequence}`;
|
|
55759
|
+
if (TimebackService2.isDuplicateHeartbeatWindow(heartbeatWindowKey)) {
|
|
55760
|
+
logger17.debug("Skipping duplicate heartbeat window", {
|
|
55761
|
+
gameId,
|
|
55762
|
+
studentId,
|
|
55763
|
+
runId,
|
|
55764
|
+
windowSequence,
|
|
55765
|
+
isFinal
|
|
55766
|
+
});
|
|
55767
|
+
return { status: "ok" };
|
|
55615
55768
|
}
|
|
55616
|
-
|
|
55617
|
-
|
|
55618
|
-
|
|
55619
|
-
|
|
55620
|
-
|
|
55621
|
-
|
|
55622
|
-
|
|
55623
|
-
|
|
55624
|
-
|
|
55625
|
-
subject: options.subject
|
|
55769
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55770
|
+
const inFlightHeartbeat = TimebackService2.getInFlightHeartbeatWindow(heartbeatWindowKey);
|
|
55771
|
+
if (inFlightHeartbeat) {
|
|
55772
|
+
logger17.debug("Joining in-flight heartbeat window", {
|
|
55773
|
+
gameId,
|
|
55774
|
+
studentId,
|
|
55775
|
+
runId,
|
|
55776
|
+
windowSequence,
|
|
55777
|
+
isFinal
|
|
55626
55778
|
});
|
|
55627
|
-
return
|
|
55628
|
-
|
|
55629
|
-
|
|
55630
|
-
|
|
55631
|
-
|
|
55779
|
+
return inFlightHeartbeat;
|
|
55780
|
+
}
|
|
55781
|
+
const pendingHeartbeat = (async () => {
|
|
55782
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
55783
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
|
|
55784
|
+
});
|
|
55785
|
+
if (!integration) {
|
|
55786
|
+
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
55787
|
+
}
|
|
55788
|
+
const activeTimeSeconds = timingData.activeMs / 1000;
|
|
55789
|
+
const inactiveTimeSeconds = timingData.pausedMs / 1000;
|
|
55790
|
+
if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
|
|
55791
|
+
await client.recordSessionEnd(integration.courseId, studentId, {
|
|
55792
|
+
activeTimeSeconds,
|
|
55793
|
+
...inactiveTimeSeconds > 0 ? { inactiveTimeSeconds } : {},
|
|
55794
|
+
activityId: activityData.activityId,
|
|
55795
|
+
activityName: activityData.activityName,
|
|
55796
|
+
subject: activityData.subject,
|
|
55797
|
+
appName: activityData.appName,
|
|
55798
|
+
sensorUrl: activityData.sensorUrl,
|
|
55799
|
+
courseId: activityData.courseId,
|
|
55800
|
+
courseName: activityData.courseName,
|
|
55801
|
+
studentEmail: activityData.studentEmail,
|
|
55802
|
+
...runId ? { runId } : {}
|
|
55803
|
+
});
|
|
55804
|
+
}
|
|
55805
|
+
TimebackService2.markHeartbeatWindowProcessed(heartbeatWindowKey);
|
|
55806
|
+
logger17.debug("Recorded heartbeat", {
|
|
55807
|
+
gameId,
|
|
55808
|
+
courseId: integration.courseId,
|
|
55809
|
+
studentId,
|
|
55810
|
+
runId,
|
|
55811
|
+
windowSequence,
|
|
55812
|
+
activeTimeSeconds,
|
|
55813
|
+
isFinal
|
|
55814
|
+
});
|
|
55815
|
+
return { status: "ok" };
|
|
55816
|
+
})();
|
|
55817
|
+
TimebackService2.markHeartbeatWindowInFlight(heartbeatWindowKey, pendingHeartbeat);
|
|
55818
|
+
try {
|
|
55819
|
+
return await pendingHeartbeat;
|
|
55820
|
+
} finally {
|
|
55821
|
+
TimebackService2.clearInFlightHeartbeatWindow(heartbeatWindowKey);
|
|
55632
55822
|
}
|
|
55633
55823
|
}
|
|
55634
|
-
|
|
55635
|
-
|
|
55636
|
-
|
|
55637
|
-
|
|
55638
|
-
|
|
55639
|
-
|
|
55640
|
-
|
|
55641
|
-
|
|
55642
|
-
|
|
55643
|
-
|
|
55644
|
-
|
|
55645
|
-
|
|
55646
|
-
|
|
55647
|
-
|
|
55648
|
-
|
|
55649
|
-
|
|
55650
|
-
|
|
55651
|
-
|
|
55652
|
-
|
|
55653
|
-
|
|
55654
|
-
|
|
55655
|
-
|
|
55656
|
-
|
|
55657
|
-
|
|
55658
|
-
|
|
55659
|
-
|
|
55824
|
+
async getStudentXp(timebackId, user, options) {
|
|
55825
|
+
const client = this.requireClient();
|
|
55826
|
+
const db2 = this.deps.db;
|
|
55827
|
+
let courseIds = [];
|
|
55828
|
+
if (options?.gameId) {
|
|
55829
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
55830
|
+
const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
|
|
55831
|
+
if (options.grade !== undefined && options.subject) {
|
|
55832
|
+
conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
|
|
55833
|
+
conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
|
|
55834
|
+
}
|
|
55835
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55836
|
+
where: and(...conditions2)
|
|
55837
|
+
});
|
|
55838
|
+
courseIds = integrations.map((i2) => i2.courseId);
|
|
55839
|
+
if (courseIds.length === 0) {
|
|
55840
|
+
logger17.debug("No integrations found for game, returning 0 XP", {
|
|
55841
|
+
timebackId,
|
|
55842
|
+
gameId: options.gameId,
|
|
55843
|
+
grade: options.grade,
|
|
55844
|
+
subject: options.subject
|
|
55845
|
+
});
|
|
55846
|
+
return {
|
|
55847
|
+
totalXp: 0,
|
|
55848
|
+
...options?.include?.today && { todayXp: 0 },
|
|
55849
|
+
...options?.include?.perCourse && { courses: [] }
|
|
55850
|
+
};
|
|
55851
|
+
}
|
|
55852
|
+
}
|
|
55853
|
+
const result = await client.getStudentXp(timebackId, {
|
|
55854
|
+
courseIds: courseIds.length > 0 ? courseIds : undefined,
|
|
55855
|
+
include: options?.include
|
|
55856
|
+
});
|
|
55857
|
+
logger17.debug("Retrieved student XP", {
|
|
55858
|
+
timebackId,
|
|
55859
|
+
gameId: options?.gameId,
|
|
55860
|
+
grade: options?.grade,
|
|
55861
|
+
subject: options?.subject,
|
|
55862
|
+
totalXp: result.totalXp,
|
|
55863
|
+
courseCount: result.courses?.length
|
|
55864
|
+
});
|
|
55865
|
+
return result;
|
|
55866
|
+
}
|
|
55867
|
+
};
|
|
55660
55868
|
});
|
|
55661
55869
|
|
|
55662
55870
|
class UploadService {
|
|
@@ -55712,6 +55920,7 @@ function createPlatformServices(deps) {
|
|
|
55712
55920
|
alerts,
|
|
55713
55921
|
validateDeveloperAccessBySlug,
|
|
55714
55922
|
validateDeveloperAccess,
|
|
55923
|
+
validateGameManagementAccess,
|
|
55715
55924
|
validateOwnership
|
|
55716
55925
|
} = deps;
|
|
55717
55926
|
const bucket = new BucketService({
|
|
@@ -55746,12 +55955,14 @@ function createPlatformServices(deps) {
|
|
|
55746
55955
|
const timeback2 = new TimebackService({
|
|
55747
55956
|
db: db2,
|
|
55748
55957
|
timeback: timebackClient,
|
|
55749
|
-
validateDeveloperAccess
|
|
55958
|
+
validateDeveloperAccess,
|
|
55959
|
+
validateGameManagementAccess
|
|
55750
55960
|
});
|
|
55751
55961
|
const timebackAdmin = new TimebackAdminService({
|
|
55752
55962
|
db: db2,
|
|
55753
55963
|
timeback: timebackClient,
|
|
55754
|
-
validateDeveloperAccess
|
|
55964
|
+
validateDeveloperAccess,
|
|
55965
|
+
validateGameManagementAccess
|
|
55755
55966
|
});
|
|
55756
55967
|
return {
|
|
55757
55968
|
bucket,
|
|
@@ -58739,6 +58950,7 @@ function createCaliperNamespace(client) {
|
|
|
58739
58950
|
email: data.studentEmail
|
|
58740
58951
|
},
|
|
58741
58952
|
action: TIMEBACK_ACTIONS4.completed,
|
|
58953
|
+
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
58742
58954
|
object: {
|
|
58743
58955
|
id: data.objectId || caliper.buildActivityUrl(data),
|
|
58744
58956
|
type: TIMEBACK_TYPES4.activityContext,
|
|
@@ -58802,6 +59014,7 @@ function createCaliperNamespace(client) {
|
|
|
58802
59014
|
email: data.studentEmail
|
|
58803
59015
|
},
|
|
58804
59016
|
action: TIMEBACK_ACTIONS4.spentTime,
|
|
59017
|
+
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
58805
59018
|
object: {
|
|
58806
59019
|
id: caliper.buildActivityUrl(data),
|
|
58807
59020
|
type: TIMEBACK_TYPES4.activityContext,
|
|
@@ -58835,7 +59048,7 @@ function createCaliperNamespace(client) {
|
|
|
58835
59048
|
},
|
|
58836
59049
|
buildActivityUrl: (data) => {
|
|
58837
59050
|
const base = data.sensorUrl.replace(/\/$/, "");
|
|
58838
|
-
return `${base}/activities/${data.courseId}/${data.activityId
|
|
59051
|
+
return `${base}/activities/${encodeURIComponent(data.courseId)}/${encodeURIComponent(data.activityId)}`;
|
|
58839
59052
|
}
|
|
58840
59053
|
};
|
|
58841
59054
|
return caliper;
|
|
@@ -58845,6 +59058,34 @@ function createEduBridgeNamespace(client) {
|
|
|
58845
59058
|
listByUser: async (userId) => {
|
|
58846
59059
|
const response = await client["request"](`/edubridge/enrollments/user/${userId}`, "GET");
|
|
58847
59060
|
return response.data;
|
|
59061
|
+
},
|
|
59062
|
+
enroll: async (userId, courseId, options) => {
|
|
59063
|
+
const segments = [userId, courseId];
|
|
59064
|
+
if (options?.schoolId) {
|
|
59065
|
+
segments.push(options.schoolId);
|
|
59066
|
+
}
|
|
59067
|
+
const body2 = {};
|
|
59068
|
+
if (options?.role) {
|
|
59069
|
+
body2.role = options.role;
|
|
59070
|
+
}
|
|
59071
|
+
if (options?.sourcedId) {
|
|
59072
|
+
body2.sourcedId = options.sourcedId;
|
|
59073
|
+
}
|
|
59074
|
+
if (options?.beginDate) {
|
|
59075
|
+
body2.beginDate = options.beginDate;
|
|
59076
|
+
}
|
|
59077
|
+
if (options?.metadata) {
|
|
59078
|
+
body2.metadata = options.metadata;
|
|
59079
|
+
}
|
|
59080
|
+
const response = await client["request"](`/edubridge/enrollments/enroll/${segments.join("/")}`, "POST", body2);
|
|
59081
|
+
return response.data;
|
|
59082
|
+
},
|
|
59083
|
+
unenroll: async (userId, courseId, options) => {
|
|
59084
|
+
const segments = [userId, courseId];
|
|
59085
|
+
if (options?.schoolId) {
|
|
59086
|
+
segments.push(options.schoolId);
|
|
59087
|
+
}
|
|
59088
|
+
await client["request"](`/edubridge/enrollments/unenroll/${segments.join("/")}`, "DELETE");
|
|
58848
59089
|
}
|
|
58849
59090
|
};
|
|
58850
59091
|
const analytics = {
|
|
@@ -59020,6 +59261,10 @@ function createOneRosterNamespace(client) {
|
|
|
59020
59261
|
logTimebackError("list course roster", error, { courseSourcedId });
|
|
59021
59262
|
throw error;
|
|
59022
59263
|
}
|
|
59264
|
+
},
|
|
59265
|
+
create: async (data) => client["request"](ONEROSTER_ENDPOINTS4.enrollments, "POST", { enrollment: data }),
|
|
59266
|
+
delete: async (sourcedId) => {
|
|
59267
|
+
await client["request"](`${ONEROSTER_ENDPOINTS4.enrollments}/${sourcedId}`, "DELETE");
|
|
59023
59268
|
}
|
|
59024
59269
|
},
|
|
59025
59270
|
organizations: {
|
|
@@ -59853,7 +60098,8 @@ class ProgressRecorder {
|
|
|
59853
60098
|
masteredUnits,
|
|
59854
60099
|
attemptNumber: currentAttemptNumber,
|
|
59855
60100
|
progressData,
|
|
59856
|
-
extensions
|
|
60101
|
+
extensions,
|
|
60102
|
+
runId: progressData.runId
|
|
59857
60103
|
});
|
|
59858
60104
|
return {
|
|
59859
60105
|
xpAwarded: calculatedXp,
|
|
@@ -59991,7 +60237,8 @@ class ProgressRecorder {
|
|
|
59991
60237
|
masteredUnits,
|
|
59992
60238
|
attemptNumber,
|
|
59993
60239
|
progressData,
|
|
59994
|
-
extensions
|
|
60240
|
+
extensions,
|
|
60241
|
+
runId
|
|
59995
60242
|
}) {
|
|
59996
60243
|
await this.caliperNamespace.emitActivityEvent({
|
|
59997
60244
|
studentId,
|
|
@@ -60008,7 +60255,8 @@ class ProgressRecorder {
|
|
|
60008
60255
|
subject: progressData.subject,
|
|
60009
60256
|
appName: progressData.appName,
|
|
60010
60257
|
sensorUrl: progressData.sensorUrl,
|
|
60011
|
-
extensions: extensions || progressData.extensions
|
|
60258
|
+
extensions: extensions || progressData.extensions,
|
|
60259
|
+
...runId ? { runId } : {}
|
|
60012
60260
|
}).catch((error) => {
|
|
60013
60261
|
log.error("[ProgressRecorder] Failed to emit activity event", { error });
|
|
60014
60262
|
});
|
|
@@ -60062,7 +60310,7 @@ class SessionRecorder {
|
|
|
60062
60310
|
const courseName = sessionData.courseName || "Game Course";
|
|
60063
60311
|
const student = await this.studentResolver.resolve(studentIdentifier, sessionData.studentEmail);
|
|
60064
60312
|
const { id: studentId, email: studentEmail } = student;
|
|
60065
|
-
const { activeTimeSeconds, inactiveTimeSeconds, wasteTimeSeconds, extensions } = sessionData;
|
|
60313
|
+
const { activeTimeSeconds, inactiveTimeSeconds, wasteTimeSeconds, extensions, runId } = sessionData;
|
|
60066
60314
|
await this.caliperNamespace.emitTimeSpentEvent({
|
|
60067
60315
|
studentId,
|
|
60068
60316
|
studentEmail,
|
|
@@ -60076,6 +60324,7 @@ class SessionRecorder {
|
|
|
60076
60324
|
subject: sessionData.subject,
|
|
60077
60325
|
appName: sessionData.appName,
|
|
60078
60326
|
sensorUrl: sessionData.sensorUrl,
|
|
60327
|
+
...runId ? { runId } : {},
|
|
60079
60328
|
...extensions ? { extensions } : {}
|
|
60080
60329
|
});
|
|
60081
60330
|
}
|
|
@@ -120039,7 +120288,9 @@ var TIMEBACK_SUBJECTS5;
|
|
|
120039
120288
|
var TimebackGradeSchema;
|
|
120040
120289
|
var TimebackSubjectSchema;
|
|
120041
120290
|
var UpdateTimebackXpRequestSchema;
|
|
120291
|
+
var TimebackActivityDataSchema;
|
|
120042
120292
|
var EndActivityRequestSchema;
|
|
120293
|
+
var HeartbeatRequestSchema;
|
|
120043
120294
|
var PopulateStudentRequestSchema;
|
|
120044
120295
|
var DerivedPlatformCourseConfigSchema;
|
|
120045
120296
|
var TimebackBaseConfigSchema;
|
|
@@ -120050,6 +120301,8 @@ var GrantTimebackXpRequestSchema;
|
|
|
120050
120301
|
var AdjustTimebackTimeRequestSchema;
|
|
120051
120302
|
var AdjustTimebackMasteryRequestSchema;
|
|
120052
120303
|
var ToggleCourseCompletionRequestSchema;
|
|
120304
|
+
var EnrollStudentRequestSchema;
|
|
120305
|
+
var UnenrollStudentRequestSchema;
|
|
120053
120306
|
var init_schemas11 = __esm(() => {
|
|
120054
120307
|
init_esm();
|
|
120055
120308
|
TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
@@ -120072,31 +120325,49 @@ var init_schemas11 = __esm(() => {
|
|
|
120072
120325
|
xp: exports_external.number().min(0, "XP must be a non-negative number"),
|
|
120073
120326
|
userTimestamp: exports_external.string().datetime().optional()
|
|
120074
120327
|
});
|
|
120328
|
+
TimebackActivityDataSchema = exports_external.object({
|
|
120329
|
+
activityId: exports_external.string().min(1),
|
|
120330
|
+
activityName: exports_external.string().optional(),
|
|
120331
|
+
grade: TimebackGradeSchema,
|
|
120332
|
+
subject: TimebackSubjectSchema,
|
|
120333
|
+
appName: exports_external.string().optional(),
|
|
120334
|
+
sensorUrl: exports_external.string().url().optional(),
|
|
120335
|
+
courseId: exports_external.string().optional(),
|
|
120336
|
+
courseName: exports_external.string().optional(),
|
|
120337
|
+
studentEmail: exports_external.string().email().optional()
|
|
120338
|
+
});
|
|
120075
120339
|
EndActivityRequestSchema = exports_external.object({
|
|
120076
120340
|
gameId: exports_external.string().uuid(),
|
|
120077
120341
|
studentId: exports_external.string().min(1),
|
|
120078
|
-
|
|
120079
|
-
|
|
120080
|
-
activityName: exports_external.string().optional(),
|
|
120081
|
-
grade: TimebackGradeSchema,
|
|
120082
|
-
subject: TimebackSubjectSchema,
|
|
120083
|
-
appName: exports_external.string().optional(),
|
|
120084
|
-
sensorUrl: exports_external.string().url().optional(),
|
|
120085
|
-
courseId: exports_external.string().optional(),
|
|
120086
|
-
courseName: exports_external.string().optional(),
|
|
120087
|
-
studentEmail: exports_external.string().email().optional()
|
|
120088
|
-
}),
|
|
120342
|
+
runId: exports_external.string().uuid().optional(),
|
|
120343
|
+
activityData: TimebackActivityDataSchema,
|
|
120089
120344
|
scoreData: exports_external.object({
|
|
120090
120345
|
correctQuestions: exports_external.number().int().min(0),
|
|
120091
120346
|
totalQuestions: exports_external.number().int().min(0)
|
|
120092
120347
|
}),
|
|
120093
120348
|
timingData: exports_external.object({
|
|
120094
|
-
durationSeconds: exports_external.number().
|
|
120349
|
+
durationSeconds: exports_external.number().nonnegative()
|
|
120095
120350
|
}),
|
|
120351
|
+
sessionTimingData: exports_external.object({
|
|
120352
|
+
activeSeconds: exports_external.number().nonnegative(),
|
|
120353
|
+
inactiveSeconds: exports_external.number().nonnegative().optional()
|
|
120354
|
+
}).optional(),
|
|
120096
120355
|
xpEarned: exports_external.number().optional(),
|
|
120097
120356
|
masteredUnits: exports_external.number().nonnegative().optional(),
|
|
120098
120357
|
extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
120099
120358
|
});
|
|
120359
|
+
HeartbeatRequestSchema = exports_external.object({
|
|
120360
|
+
gameId: exports_external.string().uuid(),
|
|
120361
|
+
studentId: exports_external.string().min(1),
|
|
120362
|
+
runId: exports_external.string().uuid(),
|
|
120363
|
+
activityData: TimebackActivityDataSchema,
|
|
120364
|
+
timingData: exports_external.object({
|
|
120365
|
+
activeMs: exports_external.number().nonnegative(),
|
|
120366
|
+
pausedMs: exports_external.number().nonnegative()
|
|
120367
|
+
}),
|
|
120368
|
+
windowSequence: exports_external.number().int().nonnegative(),
|
|
120369
|
+
isFinal: exports_external.boolean().optional()
|
|
120370
|
+
});
|
|
120100
120371
|
PopulateStudentRequestSchema = exports_external.object({
|
|
120101
120372
|
firstName: exports_external.string().min(1).optional(),
|
|
120102
120373
|
lastName: exports_external.string().min(1).optional()
|
|
@@ -120192,6 +120463,16 @@ var init_schemas11 = __esm(() => {
|
|
|
120192
120463
|
studentId: exports_external.string().min(1),
|
|
120193
120464
|
action: exports_external.enum(["complete", "resume"])
|
|
120194
120465
|
});
|
|
120466
|
+
EnrollStudentRequestSchema = exports_external.object({
|
|
120467
|
+
gameId: exports_external.string().uuid(),
|
|
120468
|
+
courseId: exports_external.string().min(1),
|
|
120469
|
+
studentId: exports_external.string().min(1)
|
|
120470
|
+
});
|
|
120471
|
+
UnenrollStudentRequestSchema = exports_external.object({
|
|
120472
|
+
gameId: exports_external.string().uuid(),
|
|
120473
|
+
courseId: exports_external.string().min(1),
|
|
120474
|
+
studentId: exports_external.string().min(1)
|
|
120475
|
+
});
|
|
120195
120476
|
});
|
|
120196
120477
|
var init_schemas_index = __esm(() => {
|
|
120197
120478
|
init_schemas();
|
|
@@ -120210,6 +120491,9 @@ function isAuthenticated(ctx) {
|
|
|
120210
120491
|
return ctx.user != null;
|
|
120211
120492
|
}
|
|
120212
120493
|
var init_types9 = () => {};
|
|
120494
|
+
function hasGameManagementAccess(user) {
|
|
120495
|
+
return user.role === "admin" || user.role === "teacher" || user.role === "developer" && user.developerStatus === "approved";
|
|
120496
|
+
}
|
|
120213
120497
|
function requireAuth(handler) {
|
|
120214
120498
|
return async (ctx) => {
|
|
120215
120499
|
if (!isAuthenticated(ctx)) {
|
|
@@ -120253,6 +120537,17 @@ function requireDeveloper(handler) {
|
|
|
120253
120537
|
return handler(ctx);
|
|
120254
120538
|
};
|
|
120255
120539
|
}
|
|
120540
|
+
function requireGameManagementAccess(handler) {
|
|
120541
|
+
return async (ctx) => {
|
|
120542
|
+
if (!isAuthenticated(ctx)) {
|
|
120543
|
+
throw ApiError.unauthorized("Valid session or bearer token required");
|
|
120544
|
+
}
|
|
120545
|
+
if (!hasGameManagementAccess(ctx.user)) {
|
|
120546
|
+
throw ApiError.forbidden("Game management access required");
|
|
120547
|
+
}
|
|
120548
|
+
return handler(ctx);
|
|
120549
|
+
};
|
|
120550
|
+
}
|
|
120256
120551
|
var init_auth_util = __esm(() => {
|
|
120257
120552
|
init_errors();
|
|
120258
120553
|
init_types9();
|
|
@@ -122378,6 +122673,7 @@ var verifyIntegration;
|
|
|
122378
122673
|
var getConfig2;
|
|
122379
122674
|
var deleteIntegrations;
|
|
122380
122675
|
var endActivity;
|
|
122676
|
+
var heartbeat;
|
|
122381
122677
|
var getStudentXp;
|
|
122382
122678
|
var getRoster;
|
|
122383
122679
|
var getStudentOverview;
|
|
@@ -122386,6 +122682,9 @@ var grantXp;
|
|
|
122386
122682
|
var adjustTime;
|
|
122387
122683
|
var adjustMastery;
|
|
122388
122684
|
var toggleCompletion;
|
|
122685
|
+
var searchStudents;
|
|
122686
|
+
var enrollStudent;
|
|
122687
|
+
var unenrollStudent;
|
|
122389
122688
|
var timeback2;
|
|
122390
122689
|
var init_timeback_controller = __esm(() => {
|
|
122391
122690
|
init_esm();
|
|
@@ -122476,7 +122775,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
122476
122775
|
});
|
|
122477
122776
|
return ctx.services.timeback.setupIntegration(body2.gameId, body2, ctx.user);
|
|
122478
122777
|
});
|
|
122479
|
-
getIntegrations =
|
|
122778
|
+
getIntegrations = requireGameManagementAccess(async (ctx) => {
|
|
122480
122779
|
const gameId = ctx.params.gameId;
|
|
122481
122780
|
if (!gameId) {
|
|
122482
122781
|
throw ApiError.badRequest("Missing gameId");
|
|
@@ -122536,9 +122835,11 @@ var init_timeback_controller = __esm(() => {
|
|
|
122536
122835
|
const {
|
|
122537
122836
|
gameId,
|
|
122538
122837
|
studentId,
|
|
122838
|
+
runId,
|
|
122539
122839
|
activityData,
|
|
122540
122840
|
scoreData,
|
|
122541
122841
|
timingData,
|
|
122842
|
+
sessionTimingData,
|
|
122542
122843
|
xpEarned,
|
|
122543
122844
|
masteredUnits,
|
|
122544
122845
|
extensions
|
|
@@ -122547,15 +122848,50 @@ var init_timeback_controller = __esm(() => {
|
|
|
122547
122848
|
return ctx.services.timeback.endActivity({
|
|
122548
122849
|
gameId,
|
|
122549
122850
|
studentId,
|
|
122851
|
+
runId,
|
|
122550
122852
|
activityData,
|
|
122551
122853
|
scoreData,
|
|
122552
122854
|
timingData,
|
|
122855
|
+
sessionTimingData,
|
|
122553
122856
|
xpEarned,
|
|
122554
122857
|
masteredUnits,
|
|
122555
122858
|
extensions,
|
|
122556
122859
|
user: ctx.user
|
|
122557
122860
|
});
|
|
122558
122861
|
});
|
|
122862
|
+
heartbeat = requireDeveloper(async (ctx) => {
|
|
122863
|
+
let body2;
|
|
122864
|
+
try {
|
|
122865
|
+
const json4 = await ctx.request.json();
|
|
122866
|
+
body2 = HeartbeatRequestSchema.parse(json4);
|
|
122867
|
+
} catch (error2) {
|
|
122868
|
+
if (error2 instanceof exports_external.ZodError) {
|
|
122869
|
+
const details = formatZodError(error2);
|
|
122870
|
+
logger63.warn("Heartbeat validation failed", { details });
|
|
122871
|
+
throw ApiError.unprocessableEntity("Validation failed", details);
|
|
122872
|
+
}
|
|
122873
|
+
throw ApiError.badRequest("Invalid JSON body");
|
|
122874
|
+
}
|
|
122875
|
+
const { gameId, studentId, runId, activityData, timingData, windowSequence, isFinal } = body2;
|
|
122876
|
+
logger63.debug("Recording heartbeat", {
|
|
122877
|
+
userId: ctx.user.id,
|
|
122878
|
+
gameId,
|
|
122879
|
+
runId,
|
|
122880
|
+
windowSequence,
|
|
122881
|
+
activeMs: timingData.activeMs,
|
|
122882
|
+
isFinal
|
|
122883
|
+
});
|
|
122884
|
+
return ctx.services.timeback.recordHeartbeat({
|
|
122885
|
+
gameId,
|
|
122886
|
+
studentId,
|
|
122887
|
+
runId,
|
|
122888
|
+
activityData,
|
|
122889
|
+
timingData,
|
|
122890
|
+
windowSequence,
|
|
122891
|
+
isFinal,
|
|
122892
|
+
user: ctx.user
|
|
122893
|
+
});
|
|
122894
|
+
});
|
|
122559
122895
|
getStudentXp = requireDeveloper(async (ctx) => {
|
|
122560
122896
|
const timebackId = ctx.params.timebackId;
|
|
122561
122897
|
if (!timebackId) {
|
|
@@ -122601,7 +122937,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
122601
122937
|
include
|
|
122602
122938
|
});
|
|
122603
122939
|
});
|
|
122604
|
-
getRoster =
|
|
122940
|
+
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
122605
122941
|
const gameId = ctx.params.gameId;
|
|
122606
122942
|
const courseId = ctx.params.courseId;
|
|
122607
122943
|
if (!gameId || !courseId) {
|
|
@@ -122614,7 +122950,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
122614
122950
|
});
|
|
122615
122951
|
return ctx.services.timebackAdmin.listStudentsForCourse(gameId, courseId, ctx.user);
|
|
122616
122952
|
});
|
|
122617
|
-
getStudentOverview =
|
|
122953
|
+
getStudentOverview = requireGameManagementAccess(async (ctx) => {
|
|
122618
122954
|
const timebackId = ctx.params.timebackId;
|
|
122619
122955
|
const gameId = ctx.url.searchParams.get("gameId") || undefined;
|
|
122620
122956
|
const courseId = ctx.url.searchParams.get("courseId") || undefined;
|
|
@@ -122629,7 +122965,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
122629
122965
|
});
|
|
122630
122966
|
return ctx.services.timebackAdmin.getStudentOverview(gameId, timebackId, ctx.user, courseId);
|
|
122631
122967
|
});
|
|
122632
|
-
getStudentActivity =
|
|
122968
|
+
getStudentActivity = requireGameManagementAccess(async (ctx) => {
|
|
122633
122969
|
const timebackId = ctx.params.timebackId;
|
|
122634
122970
|
const courseId = ctx.params.courseId;
|
|
122635
122971
|
const gameId = ctx.url.searchParams.get("gameId") || undefined;
|
|
@@ -122692,7 +123028,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
122692
123028
|
});
|
|
122693
123029
|
return ctx.services.timebackAdmin.adjustMasteredUnits(body2, ctx.user);
|
|
122694
123030
|
});
|
|
122695
|
-
toggleCompletion =
|
|
123031
|
+
toggleCompletion = requireGameManagementAccess(async (ctx) => {
|
|
122696
123032
|
const body2 = await parseRequestBody(ctx.request, ToggleCourseCompletionRequestSchema);
|
|
122697
123033
|
logger63.debug("Toggling course completion", {
|
|
122698
123034
|
requesterId: ctx.user.id,
|
|
@@ -122703,6 +123039,41 @@ var init_timeback_controller = __esm(() => {
|
|
|
122703
123039
|
});
|
|
122704
123040
|
return ctx.services.timebackAdmin.toggleCourseCompletion(body2, ctx.user);
|
|
122705
123041
|
});
|
|
123042
|
+
searchStudents = requireGameManagementAccess(async (ctx) => {
|
|
123043
|
+
const gameId = ctx.params.gameId;
|
|
123044
|
+
const courseId = ctx.params.courseId;
|
|
123045
|
+
const query = ctx.url.searchParams.get("q") || "";
|
|
123046
|
+
if (!gameId || !courseId) {
|
|
123047
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
123048
|
+
}
|
|
123049
|
+
logger63.debug("Searching students for enrollment", {
|
|
123050
|
+
requesterId: ctx.user.id,
|
|
123051
|
+
gameId,
|
|
123052
|
+
courseId,
|
|
123053
|
+
query
|
|
123054
|
+
});
|
|
123055
|
+
return ctx.services.timebackAdmin.searchStudentsForEnrollment(gameId, courseId, query, ctx.user);
|
|
123056
|
+
});
|
|
123057
|
+
enrollStudent = requireGameManagementAccess(async (ctx) => {
|
|
123058
|
+
const body2 = await parseRequestBody(ctx.request, EnrollStudentRequestSchema);
|
|
123059
|
+
logger63.debug("Enrolling student", {
|
|
123060
|
+
requesterId: ctx.user.id,
|
|
123061
|
+
gameId: body2.gameId,
|
|
123062
|
+
courseId: body2.courseId,
|
|
123063
|
+
studentId: body2.studentId
|
|
123064
|
+
});
|
|
123065
|
+
return ctx.services.timebackAdmin.enrollStudent(body2, ctx.user);
|
|
123066
|
+
});
|
|
123067
|
+
unenrollStudent = requireGameManagementAccess(async (ctx) => {
|
|
123068
|
+
const body2 = await parseRequestBody(ctx.request, UnenrollStudentRequestSchema);
|
|
123069
|
+
logger63.debug("Unenrolling student", {
|
|
123070
|
+
requesterId: ctx.user.id,
|
|
123071
|
+
gameId: body2.gameId,
|
|
123072
|
+
courseId: body2.courseId,
|
|
123073
|
+
studentId: body2.studentId
|
|
123074
|
+
});
|
|
123075
|
+
return ctx.services.timebackAdmin.unenrollStudent(body2, ctx.user);
|
|
123076
|
+
});
|
|
122706
123077
|
timeback2 = {
|
|
122707
123078
|
getTodayXp,
|
|
122708
123079
|
getTotalXp,
|
|
@@ -122717,6 +123088,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
122717
123088
|
getConfig: getConfig2,
|
|
122718
123089
|
deleteIntegrations,
|
|
122719
123090
|
endActivity,
|
|
123091
|
+
heartbeat,
|
|
122720
123092
|
getStudentXp,
|
|
122721
123093
|
getRoster,
|
|
122722
123094
|
getStudentOverview,
|
|
@@ -122724,7 +123096,10 @@ var init_timeback_controller = __esm(() => {
|
|
|
122724
123096
|
grantXp,
|
|
122725
123097
|
adjustTime,
|
|
122726
123098
|
adjustMastery,
|
|
122727
|
-
toggleCompletion
|
|
123099
|
+
toggleCompletion,
|
|
123100
|
+
searchStudents,
|
|
123101
|
+
enrollStudent,
|
|
123102
|
+
unenrollStudent
|
|
122728
123103
|
};
|
|
122729
123104
|
});
|
|
122730
123105
|
var logger64;
|
|
@@ -123700,6 +124075,7 @@ var init_timeback6 = __esm(() => {
|
|
|
123700
124075
|
timebackRouter.get("/config/:gameId", handle2(timeback2.getConfig));
|
|
123701
124076
|
timebackRouter.delete("/integrations/:gameId", handle2(timeback2.deleteIntegrations, { status: 204 }));
|
|
123702
124077
|
timebackRouter.post("/end-activity", handle2(timeback2.endActivity));
|
|
124078
|
+
timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
|
|
123703
124079
|
timebackRouter.get("/user", async (c2) => {
|
|
123704
124080
|
const user = c2.get("user");
|
|
123705
124081
|
const gameId = c2.get("gameId");
|
|
@@ -124634,7 +125010,8 @@ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2;
|
|
|
124634
125010
|
var init_timeback7 = __esm7(() => {
|
|
124635
125011
|
TIMEBACK_ROUTES2 = {
|
|
124636
125012
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
124637
|
-
GET_XP: "/integrations/timeback/xp"
|
|
125013
|
+
GET_XP: "/integrations/timeback/xp",
|
|
125014
|
+
HEARTBEAT: "/integrations/timeback/heartbeat"
|
|
124638
125015
|
};
|
|
124639
125016
|
TIMEBACK_COURSE_DEFAULTS2 = {
|
|
124640
125017
|
gradingScheme: "STANDARD",
|
|
@@ -125286,7 +125663,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
|
125286
125663
|
// package.json
|
|
125287
125664
|
var package_default2 = {
|
|
125288
125665
|
name: "@playcademy/vite-plugin",
|
|
125289
|
-
version: "0.2.24-beta.
|
|
125666
|
+
version: "0.2.24-beta.5",
|
|
125290
125667
|
type: "module",
|
|
125291
125668
|
exports: {
|
|
125292
125669
|
".": {
|