@playcademy/vite-plugin 0.2.24-beta.4 → 0.2.24-beta.6
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 +737 -554
- 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.9",
|
|
25339
25340
|
description: "Local development server for Playcademy game development",
|
|
25340
25341
|
type: "module",
|
|
25341
25342
|
exports: {
|
|
@@ -52700,7 +52701,8 @@ var init_constants3 = __esm(() => {
|
|
|
52700
52701
|
HEALTH: "/api/health",
|
|
52701
52702
|
TIMEBACK: {
|
|
52702
52703
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
52703
|
-
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}
|
|
52704
|
+
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
52705
|
+
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`
|
|
52704
52706
|
}
|
|
52705
52707
|
};
|
|
52706
52708
|
});
|
|
@@ -55165,590 +55167,704 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
55165
55167
|
init_timeback_util();
|
|
55166
55168
|
logger16 = log.scope("TimebackAdminService");
|
|
55167
55169
|
});
|
|
55168
|
-
|
|
55169
|
-
|
|
55170
|
-
|
|
55171
|
-
|
|
55172
|
-
|
|
55173
|
-
|
|
55174
|
-
|
|
55175
|
-
|
|
55176
|
-
|
|
55177
|
-
|
|
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
|
+
}
|
|
55178
55193
|
}
|
|
55179
|
-
|
|
55180
|
-
|
|
55181
|
-
|
|
55182
|
-
const db2 = this.deps.db;
|
|
55183
|
-
const tz = timezone2 || PLATFORM_TIMEZONE;
|
|
55184
|
-
const base = date3 ? new Date(date3) : new Date;
|
|
55185
|
-
if (isNaN(base.getTime())) {
|
|
55186
|
-
throw new ValidationError("Invalid date format. Use ISO 8601 format.");
|
|
55194
|
+
static isDuplicateHeartbeatWindow(key) {
|
|
55195
|
+
this.cleanHeartbeatDedupeCache();
|
|
55196
|
+
return this.processedHeartbeatWindows.has(key);
|
|
55187
55197
|
}
|
|
55188
|
-
|
|
55189
|
-
|
|
55190
|
-
} catch {
|
|
55191
|
-
throw new ValidationError(`Invalid timezone: ${tz}`);
|
|
55198
|
+
static getInFlightHeartbeatWindow(key) {
|
|
55199
|
+
return this.inFlightHeartbeatWindows.get(key);
|
|
55192
55200
|
}
|
|
55193
|
-
|
|
55194
|
-
|
|
55195
|
-
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);
|
|
55196
|
-
if (result2.length === 0) {
|
|
55197
|
-
return { xp: 0, date: todayMidnight.toISOString() };
|
|
55198
|
-
}
|
|
55199
|
-
return { xp: result2[0].xp, date: result2[0].date.toISOString() };
|
|
55201
|
+
static markHeartbeatWindowProcessed(key) {
|
|
55202
|
+
this.processedHeartbeatWindows.set(key, Date.now());
|
|
55200
55203
|
}
|
|
55201
|
-
|
|
55202
|
-
|
|
55203
|
-
return { xp: Number(result[0]?.totalXp) || 0, date: startOfDay.toISOString() };
|
|
55204
|
-
}
|
|
55205
|
-
async getTotalXp(userId) {
|
|
55206
|
-
const db2 = this.deps.db;
|
|
55207
|
-
const result = await db2.select({ totalXp: sum(timebackDailyXp.xp) }).from(timebackDailyXp).where(eq(timebackDailyXp.userId, userId));
|
|
55208
|
-
return { totalXp: Number(result[0]?.totalXp) || 0 };
|
|
55209
|
-
}
|
|
55210
|
-
async updateTodayXp(userId, data) {
|
|
55211
|
-
const db2 = this.deps.db;
|
|
55212
|
-
const { xp, userTimestamp } = data;
|
|
55213
|
-
let targetDate;
|
|
55214
|
-
if (userTimestamp) {
|
|
55215
|
-
targetDate = new Date(userTimestamp);
|
|
55216
|
-
if (isNaN(targetDate.getTime())) {
|
|
55217
|
-
throw new ValidationError("Invalid userTimestamp format. Use ISO 8601 format.");
|
|
55218
|
-
}
|
|
55219
|
-
targetDate.setHours(0, 0, 0, 0);
|
|
55220
|
-
} else {
|
|
55221
|
-
targetDate = new Date;
|
|
55222
|
-
targetDate.setUTCHours(0, 0, 0, 0);
|
|
55204
|
+
static markHeartbeatWindowInFlight(key, promise) {
|
|
55205
|
+
this.inFlightHeartbeatWindows.set(key, promise);
|
|
55223
55206
|
}
|
|
55224
|
-
|
|
55225
|
-
|
|
55226
|
-
set: { xp: sql`excluded.xp`, updatedAt: new Date }
|
|
55227
|
-
}).returning({ xp: timebackDailyXp.xp, date: timebackDailyXp.date });
|
|
55228
|
-
if (!result) {
|
|
55229
|
-
logger17.error("Daily XP upsert returned no rows", { userId, date: targetDate });
|
|
55230
|
-
throw new InternalError("Failed to update daily XP record");
|
|
55207
|
+
static clearInFlightHeartbeatWindow(key) {
|
|
55208
|
+
this.inFlightHeartbeatWindows.delete(key);
|
|
55231
55209
|
}
|
|
55232
|
-
|
|
55233
|
-
|
|
55234
|
-
async getXpHistory(userId, startDate, endDate) {
|
|
55235
|
-
const db2 = this.deps.db;
|
|
55236
|
-
const whereConditions = [eq(timebackDailyXp.userId, userId)];
|
|
55237
|
-
if (startDate) {
|
|
55238
|
-
const start2 = new Date(startDate);
|
|
55239
|
-
start2.setUTCHours(0, 0, 0, 0);
|
|
55240
|
-
whereConditions.push(gte(timebackDailyXp.date, start2));
|
|
55210
|
+
constructor(deps) {
|
|
55211
|
+
this.deps = deps;
|
|
55241
55212
|
}
|
|
55242
|
-
|
|
55243
|
-
|
|
55244
|
-
|
|
55245
|
-
|
|
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;
|
|
55246
55219
|
}
|
|
55247
|
-
|
|
55248
|
-
|
|
55249
|
-
|
|
55250
|
-
|
|
55251
|
-
|
|
55252
|
-
|
|
55253
|
-
|
|
55254
|
-
|
|
55255
|
-
|
|
55256
|
-
|
|
55257
|
-
|
|
55258
|
-
|
|
55259
|
-
|
|
55260
|
-
|
|
55261
|
-
|
|
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() };
|
|
55262
55243
|
}
|
|
55263
|
-
|
|
55264
|
-
|
|
55265
|
-
|
|
55266
|
-
|
|
55267
|
-
|
|
55268
|
-
|
|
55269
|
-
|
|
55270
|
-
|
|
55271
|
-
|
|
55272
|
-
|
|
55273
|
-
|
|
55274
|
-
|
|
55275
|
-
|
|
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);
|
|
55276
55262
|
}
|
|
55277
|
-
const
|
|
55278
|
-
|
|
55279
|
-
|
|
55280
|
-
|
|
55281
|
-
|
|
55282
|
-
|
|
55283
|
-
|
|
55284
|
-
email: user.email,
|
|
55285
|
-
roles: [
|
|
55286
|
-
{
|
|
55287
|
-
roleType: "primary",
|
|
55288
|
-
role: "student",
|
|
55289
|
-
org: { sourcedId: TIMEBACK_ORG_SOURCED_ID }
|
|
55290
|
-
}
|
|
55291
|
-
]
|
|
55292
|
-
});
|
|
55293
|
-
if (!response.sourcedIdPairs?.allocatedSourcedId) {
|
|
55294
|
-
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");
|
|
55295
55270
|
}
|
|
55296
|
-
|
|
55297
|
-
name3 = `${providedNames.firstName} ${providedNames.lastName}`;
|
|
55298
|
-
logger17.info("Created student in OneRoster", { userId: user.id, timebackId });
|
|
55271
|
+
return { xp: result.xp, date: result.date.toISOString() };
|
|
55299
55272
|
}
|
|
55300
|
-
|
|
55301
|
-
|
|
55302
|
-
|
|
55303
|
-
|
|
55304
|
-
|
|
55305
|
-
|
|
55306
|
-
|
|
55307
|
-
|
|
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" };
|
|
55308
55315
|
}
|
|
55309
|
-
const
|
|
55310
|
-
|
|
55311
|
-
|
|
55312
|
-
|
|
55313
|
-
|
|
55314
|
-
|
|
55315
|
-
|
|
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
|
+
});
|
|
55316
55367
|
}
|
|
55317
55368
|
}
|
|
55318
|
-
|
|
55319
|
-
|
|
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", {
|
|
55320
55372
|
userId: user.id,
|
|
55321
|
-
|
|
55322
|
-
xp
|
|
55323
|
-
}));
|
|
55324
|
-
await tx.insert(timebackDailyXp).values(dailyRecords).onConflictDoUpdate({
|
|
55325
|
-
target: [timebackDailyXp.userId, timebackDailyXp.date],
|
|
55326
|
-
set: { xp: sql`excluded.xp`, updatedAt: new Date }
|
|
55373
|
+
timebackId
|
|
55327
55374
|
});
|
|
55375
|
+
throw new InternalError("Failed to update user with Timeback ID");
|
|
55328
55376
|
}
|
|
55329
|
-
}
|
|
55330
|
-
const [updated] = await tx.update(users).set({ timebackId, name: name3 }).where(eq(users.id, user.id)).returning({ id: users.id });
|
|
55331
|
-
if (!updated) {
|
|
55332
|
-
logger17.error("User Timeback ID update returned no rows", {
|
|
55333
|
-
userId: user.id,
|
|
55334
|
-
timebackId
|
|
55335
|
-
});
|
|
55336
|
-
throw new InternalError("Failed to update user with Timeback ID");
|
|
55337
|
-
}
|
|
55338
|
-
});
|
|
55339
|
-
return { status: "ok" };
|
|
55340
|
-
}
|
|
55341
|
-
async fetchAssessments(studentSourcedId) {
|
|
55342
|
-
const client = this.requireClient();
|
|
55343
|
-
const allAssessments = [];
|
|
55344
|
-
const limit = 3000;
|
|
55345
|
-
const fields = "sourcedId,assessmentLineItem,student,score,textScore,scoreDate,scoreStatus,scorePercentile,comment,metadata,inProgress,incomplete,late,missing";
|
|
55346
|
-
let offset = 0;
|
|
55347
|
-
try {
|
|
55348
|
-
while (true) {
|
|
55349
|
-
const results = await client.oneroster.assessmentResults.listByStudent(studentSourcedId, { limit, offset, fields });
|
|
55350
|
-
allAssessments.push(...results);
|
|
55351
|
-
if (results.length < limit) {
|
|
55352
|
-
break;
|
|
55353
|
-
}
|
|
55354
|
-
offset += limit;
|
|
55355
|
-
}
|
|
55356
|
-
logger17.debug("Fetched assessments", {
|
|
55357
|
-
studentSourcedId,
|
|
55358
|
-
totalCount: allAssessments.length
|
|
55359
55377
|
});
|
|
55360
|
-
return
|
|
55361
|
-
} catch (error) {
|
|
55362
|
-
logger17.warn("Failed to fetch assessments", { studentSourcedId, error });
|
|
55363
|
-
return [];
|
|
55364
|
-
}
|
|
55365
|
-
}
|
|
55366
|
-
async getUserData(userId, gameId) {
|
|
55367
|
-
const db2 = this.deps.db;
|
|
55368
|
-
const userData = await db2.query.users.findFirst({ where: eq(users.id, userId) });
|
|
55369
|
-
if (!userData) {
|
|
55370
|
-
throw new NotFoundError("User", userId);
|
|
55371
|
-
}
|
|
55372
|
-
if (!userData.timebackId) {
|
|
55373
|
-
throw new NotFoundError("Timeback account not found for user");
|
|
55378
|
+
return { status: "ok" };
|
|
55374
55379
|
}
|
|
55375
|
-
|
|
55376
|
-
this.
|
|
55377
|
-
|
|
55378
|
-
|
|
55379
|
-
|
|
55380
|
-
|
|
55381
|
-
|
|
55382
|
-
|
|
55383
|
-
|
|
55384
|
-
|
|
55385
|
-
|
|
55386
|
-
|
|
55387
|
-
|
|
55388
|
-
|
|
55389
|
-
|
|
55390
|
-
|
|
55391
|
-
|
|
55392
|
-
|
|
55393
|
-
organizations: profile.organizations
|
|
55394
|
-
};
|
|
55395
|
-
}
|
|
55396
|
-
async fetchStudentProfile(timebackId) {
|
|
55397
|
-
const client = this.requireClient();
|
|
55398
|
-
try {
|
|
55399
|
-
const user = await client.oneroster.users.get(timebackId);
|
|
55400
|
-
const primaryRole = user.roles.find((r) => r.roleType === "primary");
|
|
55401
|
-
const role = primaryRole?.role ?? user.roles[0]?.role ?? "student";
|
|
55402
|
-
const orgMap = new Map;
|
|
55403
|
-
if (user.primaryOrg) {
|
|
55404
|
-
orgMap.set(user.primaryOrg.sourcedId, {
|
|
55405
|
-
id: user.primaryOrg.sourcedId,
|
|
55406
|
-
name: user.primaryOrg.name ?? null,
|
|
55407
|
-
type: user.primaryOrg.type || "school",
|
|
55408
|
-
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
|
|
55409
55398
|
});
|
|
55399
|
+
return allAssessments;
|
|
55400
|
+
} catch (error) {
|
|
55401
|
+
logger17.warn("Failed to fetch assessments", { studentSourcedId, error });
|
|
55402
|
+
return [];
|
|
55410
55403
|
}
|
|
55411
|
-
|
|
55412
|
-
|
|
55413
|
-
|
|
55414
|
-
|
|
55415
|
-
|
|
55416
|
-
|
|
55417
|
-
|
|
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
|
|
55418
55448
|
});
|
|
55419
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: [] };
|
|
55420
55463
|
}
|
|
55421
|
-
return { role, organizations: [...orgMap.values()] };
|
|
55422
|
-
} catch {
|
|
55423
|
-
return { role: "student", organizations: [] };
|
|
55424
55464
|
}
|
|
55425
|
-
|
|
55426
|
-
|
|
55427
|
-
|
|
55428
|
-
|
|
55429
|
-
|
|
55430
|
-
|
|
55431
|
-
|
|
55432
|
-
|
|
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 {
|
|
55433
55486
|
return [];
|
|
55434
55487
|
}
|
|
55435
|
-
const courseToSchool = new Map(enrollments.filter((e) => e.school?.id).map((e) => [e.courseId, e.school.id]));
|
|
55436
|
-
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55437
|
-
where: inArray(gameTimebackIntegrations.courseId, courseIds)
|
|
55438
|
-
});
|
|
55439
|
-
return integrations.map((i2) => ({
|
|
55440
|
-
gameId: i2.gameId,
|
|
55441
|
-
grade: i2.grade,
|
|
55442
|
-
subject: i2.subject,
|
|
55443
|
-
courseId: i2.courseId,
|
|
55444
|
-
orgId: courseToSchool.get(i2.courseId)
|
|
55445
|
-
}));
|
|
55446
|
-
} catch {
|
|
55447
|
-
return [];
|
|
55448
55488
|
}
|
|
55449
|
-
|
|
55450
|
-
|
|
55451
|
-
|
|
55452
|
-
|
|
55453
|
-
|
|
55454
|
-
|
|
55455
|
-
|
|
55456
|
-
|
|
55457
|
-
|
|
55458
|
-
|
|
55459
|
-
|
|
55460
|
-
|
|
55461
|
-
|
|
55462
|
-
|
|
55463
|
-
|
|
55464
|
-
const {
|
|
55465
|
-
subject: subjectInput,
|
|
55466
|
-
grade,
|
|
55467
|
-
title,
|
|
55468
|
-
courseCode,
|
|
55469
|
-
level,
|
|
55470
|
-
metadata: metadata2,
|
|
55471
|
-
totalXp: derivedTotalXp,
|
|
55472
|
-
masterableUnits: derivedMasterableUnits
|
|
55473
|
-
} = courseConfig;
|
|
55474
|
-
if (!isTimebackSubject(subjectInput)) {
|
|
55475
|
-
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 {
|
|
55476
55504
|
subject: subjectInput,
|
|
55477
|
-
courseCode,
|
|
55478
|
-
title
|
|
55479
|
-
});
|
|
55480
|
-
throw new ValidationError(`Invalid subject "${subjectInput}"`);
|
|
55481
|
-
}
|
|
55482
|
-
if (!isTimebackGrade(grade)) {
|
|
55483
|
-
logger17.warn("Invalid Timeback grade in course config", {
|
|
55484
55505
|
grade,
|
|
55485
|
-
courseCode,
|
|
55486
|
-
title
|
|
55487
|
-
});
|
|
55488
|
-
throw new ValidationError(`Invalid grade "${grade}"`);
|
|
55489
|
-
}
|
|
55490
|
-
const subject = subjectInput;
|
|
55491
|
-
const courseMetadata = isCourseMetadata(metadata2) ? metadata2 : undefined;
|
|
55492
|
-
const totalXp = derivedTotalXp ?? courseMetadata?.metrics?.totalXp;
|
|
55493
|
-
const masterableUnits = derivedMasterableUnits ?? (isPlaycademyResourceMetadata(courseMetadata?.playcademy) ? courseMetadata?.playcademy?.mastery?.masterableUnits : undefined);
|
|
55494
|
-
if (typeof totalXp !== "number") {
|
|
55495
|
-
logger17.warn("Course missing totalXp in Timeback config", {
|
|
55496
|
-
courseCode,
|
|
55497
|
-
title
|
|
55498
|
-
});
|
|
55499
|
-
throw new ValidationError(`Course "${title}" is missing totalXp`);
|
|
55500
|
-
}
|
|
55501
|
-
const suffix = baseConfig.component.titleSuffix || "";
|
|
55502
|
-
const fullConfig = {
|
|
55503
|
-
organization: baseConfig.organization,
|
|
55504
|
-
course: {
|
|
55505
55506
|
title,
|
|
55506
|
-
subjects: [subject],
|
|
55507
|
-
grades: [grade],
|
|
55508
55507
|
courseCode,
|
|
55509
55508
|
level,
|
|
55510
|
-
|
|
55511
|
-
|
|
55512
|
-
|
|
55513
|
-
|
|
55514
|
-
|
|
55515
|
-
|
|
55516
|
-
|
|
55517
|
-
|
|
55518
|
-
|
|
55519
|
-
|
|
55520
|
-
|
|
55521
|
-
baseMetadata: baseConfig.resource.metadata,
|
|
55522
|
-
subject,
|
|
55523
|
-
grade,
|
|
55524
|
-
totalXp,
|
|
55525
|
-
masterableUnits
|
|
55526
|
-
})
|
|
55527
|
-
},
|
|
55528
|
-
componentResource: {
|
|
55529
|
-
...baseConfig.componentResource,
|
|
55530
|
-
title: applySuffix(baseConfig.componentResource.title || "")
|
|
55531
|
-
}
|
|
55532
|
-
};
|
|
55533
|
-
const existingIntegration = existing.find((i2) => i2.grade === grade && i2.subject === subject);
|
|
55534
|
-
if (existingIntegration) {
|
|
55535
|
-
await client.update(existingIntegration.courseId, fullConfig);
|
|
55536
|
-
const [updated] = await db2.update(gameTimebackIntegrations).set({ totalXp, updatedAt: new Date }).where(eq(gameTimebackIntegrations.id, existingIntegration.id)).returning();
|
|
55537
|
-
if (updated) {
|
|
55538
|
-
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}"`);
|
|
55539
55520
|
}
|
|
55540
|
-
|
|
55541
|
-
|
|
55542
|
-
|
|
55543
|
-
|
|
55544
|
-
|
|
55545
|
-
|
|
55546
|
-
|
|
55547
|
-
|
|
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
|
+
}
|
|
55548
55588
|
}
|
|
55549
55589
|
}
|
|
55550
55590
|
}
|
|
55591
|
+
return { integrations, ...verbose && verboseData.length > 0 && { verbose: verboseData } };
|
|
55551
55592
|
}
|
|
55552
|
-
|
|
55553
|
-
|
|
55554
|
-
|
|
55555
|
-
|
|
55556
|
-
|
|
55557
|
-
|
|
55558
|
-
});
|
|
55559
|
-
return rows.map((row) => this.toGameTimebackIntegration(row));
|
|
55560
|
-
}
|
|
55561
|
-
async verifyIntegration(gameId, user) {
|
|
55562
|
-
const client = this.requireClient();
|
|
55563
|
-
const db2 = this.deps.db;
|
|
55564
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55565
|
-
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
55566
|
-
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
55567
|
-
});
|
|
55568
|
-
if (integrations.length === 0) {
|
|
55569
|
-
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));
|
|
55570
55599
|
}
|
|
55571
|
-
|
|
55572
|
-
|
|
55573
|
-
const
|
|
55574
|
-
|
|
55575
|
-
const
|
|
55576
|
-
|
|
55577
|
-
|
|
55578
|
-
|
|
55579
|
-
integration
|
|
55580
|
-
|
|
55581
|
-
|
|
55582
|
-
|
|
55583
|
-
resources
|
|
55584
|
-
|
|
55585
|
-
|
|
55586
|
-
|
|
55587
|
-
|
|
55588
|
-
|
|
55589
|
-
|
|
55590
|
-
|
|
55591
|
-
|
|
55592
|
-
|
|
55593
|
-
|
|
55594
|
-
|
|
55595
|
-
|
|
55596
|
-
|
|
55597
|
-
|
|
55598
|
-
|
|
55599
|
-
|
|
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 };
|
|
55600
55630
|
}
|
|
55601
|
-
|
|
55602
|
-
|
|
55603
|
-
|
|
55604
|
-
|
|
55605
|
-
|
|
55606
|
-
|
|
55607
|
-
|
|
55608
|
-
|
|
55609
|
-
|
|
55610
|
-
|
|
55611
|
-
throw new NotFoundError("Timeback integration", gameId);
|
|
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);
|
|
55612
55641
|
}
|
|
55613
|
-
|
|
55614
|
-
|
|
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));
|
|
55615
55656
|
}
|
|
55616
|
-
|
|
55617
|
-
|
|
55618
|
-
|
|
55619
|
-
|
|
55620
|
-
|
|
55621
|
-
|
|
55622
|
-
|
|
55623
|
-
|
|
55624
|
-
|
|
55625
|
-
|
|
55626
|
-
|
|
55627
|
-
|
|
55628
|
-
|
|
55629
|
-
|
|
55630
|
-
|
|
55631
|
-
|
|
55632
|
-
|
|
55633
|
-
|
|
55634
|
-
|
|
55635
|
-
|
|
55636
|
-
|
|
55637
|
-
xpEarned,
|
|
55638
|
-
masteredUnits,
|
|
55639
|
-
extensions,
|
|
55640
|
-
user
|
|
55641
|
-
}) {
|
|
55642
|
-
const client = this.requireClient();
|
|
55643
|
-
const db2 = this.deps.db;
|
|
55644
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
55645
|
-
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
55646
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
|
|
55647
|
-
});
|
|
55648
|
-
if (!integration) {
|
|
55649
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
55650
|
-
}
|
|
55651
|
-
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
55652
|
-
const result = await client.recordProgress(integration.courseId, studentId, {
|
|
55653
|
-
score: scorePercentage,
|
|
55654
|
-
totalQuestions: scoreData.totalQuestions,
|
|
55655
|
-
correctQuestions: scoreData.correctQuestions,
|
|
55656
|
-
durationSeconds: timingData.durationSeconds,
|
|
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
|
+
};
|
|
55669
|
+
}
|
|
55670
|
+
async endActivity({
|
|
55671
|
+
gameId,
|
|
55672
|
+
studentId,
|
|
55673
|
+
runId,
|
|
55674
|
+
activityData,
|
|
55675
|
+
scoreData,
|
|
55676
|
+
timingData,
|
|
55677
|
+
sessionTimingData,
|
|
55657
55678
|
xpEarned,
|
|
55658
55679
|
masteredUnits,
|
|
55659
55680
|
extensions,
|
|
55660
|
-
|
|
55661
|
-
|
|
55662
|
-
|
|
55663
|
-
|
|
55664
|
-
|
|
55665
|
-
|
|
55666
|
-
|
|
55667
|
-
|
|
55668
|
-
|
|
55669
|
-
|
|
55670
|
-
|
|
55671
|
-
|
|
55672
|
-
|
|
55673
|
-
|
|
55674
|
-
|
|
55675
|
-
|
|
55676
|
-
|
|
55677
|
-
|
|
55678
|
-
|
|
55679
|
-
|
|
55680
|
-
|
|
55681
|
-
|
|
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({
|
|
55682
55747
|
gameId,
|
|
55683
|
-
courseId: integration.courseId,
|
|
55684
55748
|
studentId,
|
|
55685
|
-
|
|
55686
|
-
|
|
55687
|
-
|
|
55688
|
-
|
|
55689
|
-
|
|
55690
|
-
|
|
55691
|
-
|
|
55692
|
-
|
|
55693
|
-
|
|
55694
|
-
|
|
55695
|
-
|
|
55696
|
-
|
|
55697
|
-
|
|
55698
|
-
|
|
55699
|
-
|
|
55700
|
-
|
|
55701
|
-
|
|
55702
|
-
|
|
55703
|
-
|
|
55704
|
-
if (options.grade !== undefined && options.subject) {
|
|
55705
|
-
conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
|
|
55706
|
-
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" };
|
|
55707
55768
|
}
|
|
55708
|
-
|
|
55709
|
-
|
|
55710
|
-
|
|
55711
|
-
|
|
55712
|
-
|
|
55713
|
-
|
|
55714
|
-
|
|
55715
|
-
|
|
55716
|
-
|
|
55717
|
-
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
|
|
55718
55778
|
});
|
|
55719
|
-
return
|
|
55720
|
-
|
|
55721
|
-
|
|
55722
|
-
|
|
55723
|
-
|
|
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);
|
|
55724
55822
|
}
|
|
55725
55823
|
}
|
|
55726
|
-
|
|
55727
|
-
|
|
55728
|
-
|
|
55729
|
-
|
|
55730
|
-
|
|
55731
|
-
|
|
55732
|
-
|
|
55733
|
-
|
|
55734
|
-
|
|
55735
|
-
|
|
55736
|
-
|
|
55737
|
-
|
|
55738
|
-
|
|
55739
|
-
|
|
55740
|
-
|
|
55741
|
-
|
|
55742
|
-
|
|
55743
|
-
|
|
55744
|
-
|
|
55745
|
-
|
|
55746
|
-
|
|
55747
|
-
|
|
55748
|
-
|
|
55749
|
-
|
|
55750
|
-
|
|
55751
|
-
|
|
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
|
+
};
|
|
55752
55868
|
});
|
|
55753
55869
|
|
|
55754
55870
|
class UploadService {
|
|
@@ -58834,6 +58950,7 @@ function createCaliperNamespace(client) {
|
|
|
58834
58950
|
email: data.studentEmail
|
|
58835
58951
|
},
|
|
58836
58952
|
action: TIMEBACK_ACTIONS4.completed,
|
|
58953
|
+
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
58837
58954
|
object: {
|
|
58838
58955
|
id: data.objectId || caliper.buildActivityUrl(data),
|
|
58839
58956
|
type: TIMEBACK_TYPES4.activityContext,
|
|
@@ -58897,6 +59014,7 @@ function createCaliperNamespace(client) {
|
|
|
58897
59014
|
email: data.studentEmail
|
|
58898
59015
|
},
|
|
58899
59016
|
action: TIMEBACK_ACTIONS4.spentTime,
|
|
59017
|
+
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
58900
59018
|
object: {
|
|
58901
59019
|
id: caliper.buildActivityUrl(data),
|
|
58902
59020
|
type: TIMEBACK_TYPES4.activityContext,
|
|
@@ -58930,7 +59048,7 @@ function createCaliperNamespace(client) {
|
|
|
58930
59048
|
},
|
|
58931
59049
|
buildActivityUrl: (data) => {
|
|
58932
59050
|
const base = data.sensorUrl.replace(/\/$/, "");
|
|
58933
|
-
return `${base}/activities/${data.courseId}/${data.activityId
|
|
59051
|
+
return `${base}/activities/${encodeURIComponent(data.courseId)}/${encodeURIComponent(data.activityId)}`;
|
|
58934
59052
|
}
|
|
58935
59053
|
};
|
|
58936
59054
|
return caliper;
|
|
@@ -59980,7 +60098,8 @@ class ProgressRecorder {
|
|
|
59980
60098
|
masteredUnits,
|
|
59981
60099
|
attemptNumber: currentAttemptNumber,
|
|
59982
60100
|
progressData,
|
|
59983
|
-
extensions
|
|
60101
|
+
extensions,
|
|
60102
|
+
runId: progressData.runId
|
|
59984
60103
|
});
|
|
59985
60104
|
return {
|
|
59986
60105
|
xpAwarded: calculatedXp,
|
|
@@ -60118,7 +60237,8 @@ class ProgressRecorder {
|
|
|
60118
60237
|
masteredUnits,
|
|
60119
60238
|
attemptNumber,
|
|
60120
60239
|
progressData,
|
|
60121
|
-
extensions
|
|
60240
|
+
extensions,
|
|
60241
|
+
runId
|
|
60122
60242
|
}) {
|
|
60123
60243
|
await this.caliperNamespace.emitActivityEvent({
|
|
60124
60244
|
studentId,
|
|
@@ -60135,7 +60255,8 @@ class ProgressRecorder {
|
|
|
60135
60255
|
subject: progressData.subject,
|
|
60136
60256
|
appName: progressData.appName,
|
|
60137
60257
|
sensorUrl: progressData.sensorUrl,
|
|
60138
|
-
extensions: extensions || progressData.extensions
|
|
60258
|
+
extensions: extensions || progressData.extensions,
|
|
60259
|
+
...runId ? { runId } : {}
|
|
60139
60260
|
}).catch((error) => {
|
|
60140
60261
|
log.error("[ProgressRecorder] Failed to emit activity event", { error });
|
|
60141
60262
|
});
|
|
@@ -60189,7 +60310,7 @@ class SessionRecorder {
|
|
|
60189
60310
|
const courseName = sessionData.courseName || "Game Course";
|
|
60190
60311
|
const student = await this.studentResolver.resolve(studentIdentifier, sessionData.studentEmail);
|
|
60191
60312
|
const { id: studentId, email: studentEmail } = student;
|
|
60192
|
-
const { activeTimeSeconds, inactiveTimeSeconds, wasteTimeSeconds, extensions } = sessionData;
|
|
60313
|
+
const { activeTimeSeconds, inactiveTimeSeconds, wasteTimeSeconds, extensions, runId } = sessionData;
|
|
60193
60314
|
await this.caliperNamespace.emitTimeSpentEvent({
|
|
60194
60315
|
studentId,
|
|
60195
60316
|
studentEmail,
|
|
@@ -60203,6 +60324,7 @@ class SessionRecorder {
|
|
|
60203
60324
|
subject: sessionData.subject,
|
|
60204
60325
|
appName: sessionData.appName,
|
|
60205
60326
|
sensorUrl: sessionData.sensorUrl,
|
|
60327
|
+
...runId ? { runId } : {},
|
|
60206
60328
|
...extensions ? { extensions } : {}
|
|
60207
60329
|
});
|
|
60208
60330
|
}
|
|
@@ -120166,7 +120288,9 @@ var TIMEBACK_SUBJECTS5;
|
|
|
120166
120288
|
var TimebackGradeSchema;
|
|
120167
120289
|
var TimebackSubjectSchema;
|
|
120168
120290
|
var UpdateTimebackXpRequestSchema;
|
|
120291
|
+
var TimebackActivityDataSchema;
|
|
120169
120292
|
var EndActivityRequestSchema;
|
|
120293
|
+
var HeartbeatRequestSchema;
|
|
120170
120294
|
var PopulateStudentRequestSchema;
|
|
120171
120295
|
var DerivedPlatformCourseConfigSchema;
|
|
120172
120296
|
var TimebackBaseConfigSchema;
|
|
@@ -120201,31 +120325,49 @@ var init_schemas11 = __esm(() => {
|
|
|
120201
120325
|
xp: exports_external.number().min(0, "XP must be a non-negative number"),
|
|
120202
120326
|
userTimestamp: exports_external.string().datetime().optional()
|
|
120203
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
|
+
});
|
|
120204
120339
|
EndActivityRequestSchema = exports_external.object({
|
|
120205
120340
|
gameId: exports_external.string().uuid(),
|
|
120206
120341
|
studentId: exports_external.string().min(1),
|
|
120207
|
-
|
|
120208
|
-
|
|
120209
|
-
activityName: exports_external.string().optional(),
|
|
120210
|
-
grade: TimebackGradeSchema,
|
|
120211
|
-
subject: TimebackSubjectSchema,
|
|
120212
|
-
appName: exports_external.string().optional(),
|
|
120213
|
-
sensorUrl: exports_external.string().url().optional(),
|
|
120214
|
-
courseId: exports_external.string().optional(),
|
|
120215
|
-
courseName: exports_external.string().optional(),
|
|
120216
|
-
studentEmail: exports_external.string().email().optional()
|
|
120217
|
-
}),
|
|
120342
|
+
runId: exports_external.string().uuid().optional(),
|
|
120343
|
+
activityData: TimebackActivityDataSchema,
|
|
120218
120344
|
scoreData: exports_external.object({
|
|
120219
120345
|
correctQuestions: exports_external.number().int().min(0),
|
|
120220
120346
|
totalQuestions: exports_external.number().int().min(0)
|
|
120221
120347
|
}),
|
|
120222
120348
|
timingData: exports_external.object({
|
|
120223
|
-
durationSeconds: exports_external.number().
|
|
120349
|
+
durationSeconds: exports_external.number().nonnegative()
|
|
120224
120350
|
}),
|
|
120351
|
+
sessionTimingData: exports_external.object({
|
|
120352
|
+
activeSeconds: exports_external.number().nonnegative(),
|
|
120353
|
+
inactiveSeconds: exports_external.number().nonnegative().optional()
|
|
120354
|
+
}).optional(),
|
|
120225
120355
|
xpEarned: exports_external.number().optional(),
|
|
120226
120356
|
masteredUnits: exports_external.number().nonnegative().optional(),
|
|
120227
120357
|
extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
120228
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
|
+
});
|
|
120229
120371
|
PopulateStudentRequestSchema = exports_external.object({
|
|
120230
120372
|
firstName: exports_external.string().min(1).optional(),
|
|
120231
120373
|
lastName: exports_external.string().min(1).optional()
|
|
@@ -122531,6 +122673,7 @@ var verifyIntegration;
|
|
|
122531
122673
|
var getConfig2;
|
|
122532
122674
|
var deleteIntegrations;
|
|
122533
122675
|
var endActivity;
|
|
122676
|
+
var heartbeat;
|
|
122534
122677
|
var getStudentXp;
|
|
122535
122678
|
var getRoster;
|
|
122536
122679
|
var getStudentOverview;
|
|
@@ -122692,9 +122835,11 @@ var init_timeback_controller = __esm(() => {
|
|
|
122692
122835
|
const {
|
|
122693
122836
|
gameId,
|
|
122694
122837
|
studentId,
|
|
122838
|
+
runId,
|
|
122695
122839
|
activityData,
|
|
122696
122840
|
scoreData,
|
|
122697
122841
|
timingData,
|
|
122842
|
+
sessionTimingData,
|
|
122698
122843
|
xpEarned,
|
|
122699
122844
|
masteredUnits,
|
|
122700
122845
|
extensions
|
|
@@ -122703,15 +122848,50 @@ var init_timeback_controller = __esm(() => {
|
|
|
122703
122848
|
return ctx.services.timeback.endActivity({
|
|
122704
122849
|
gameId,
|
|
122705
122850
|
studentId,
|
|
122851
|
+
runId,
|
|
122706
122852
|
activityData,
|
|
122707
122853
|
scoreData,
|
|
122708
122854
|
timingData,
|
|
122855
|
+
sessionTimingData,
|
|
122709
122856
|
xpEarned,
|
|
122710
122857
|
masteredUnits,
|
|
122711
122858
|
extensions,
|
|
122712
122859
|
user: ctx.user
|
|
122713
122860
|
});
|
|
122714
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
|
+
});
|
|
122715
122895
|
getStudentXp = requireDeveloper(async (ctx) => {
|
|
122716
122896
|
const timebackId = ctx.params.timebackId;
|
|
122717
122897
|
if (!timebackId) {
|
|
@@ -122908,6 +123088,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
122908
123088
|
getConfig: getConfig2,
|
|
122909
123089
|
deleteIntegrations,
|
|
122910
123090
|
endActivity,
|
|
123091
|
+
heartbeat,
|
|
122911
123092
|
getStudentXp,
|
|
122912
123093
|
getRoster,
|
|
122913
123094
|
getStudentOverview,
|
|
@@ -123894,6 +124075,7 @@ var init_timeback6 = __esm(() => {
|
|
|
123894
124075
|
timebackRouter.get("/config/:gameId", handle2(timeback2.getConfig));
|
|
123895
124076
|
timebackRouter.delete("/integrations/:gameId", handle2(timeback2.deleteIntegrations, { status: 204 }));
|
|
123896
124077
|
timebackRouter.post("/end-activity", handle2(timeback2.endActivity));
|
|
124078
|
+
timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
|
|
123897
124079
|
timebackRouter.get("/user", async (c2) => {
|
|
123898
124080
|
const user = c2.get("user");
|
|
123899
124081
|
const gameId = c2.get("gameId");
|
|
@@ -124828,7 +125010,8 @@ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2;
|
|
|
124828
125010
|
var init_timeback7 = __esm7(() => {
|
|
124829
125011
|
TIMEBACK_ROUTES2 = {
|
|
124830
125012
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
124831
|
-
GET_XP: "/integrations/timeback/xp"
|
|
125013
|
+
GET_XP: "/integrations/timeback/xp",
|
|
125014
|
+
HEARTBEAT: "/integrations/timeback/heartbeat"
|
|
124832
125015
|
};
|
|
124833
125016
|
TIMEBACK_COURSE_DEFAULTS2 = {
|
|
124834
125017
|
gradingScheme: "STANDARD",
|
|
@@ -125480,7 +125663,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
|
125480
125663
|
// package.json
|
|
125481
125664
|
var package_default2 = {
|
|
125482
125665
|
name: "@playcademy/vite-plugin",
|
|
125483
|
-
version: "0.2.24-beta.
|
|
125666
|
+
version: "0.2.24-beta.6",
|
|
125484
125667
|
type: "module",
|
|
125485
125668
|
exports: {
|
|
125486
125669
|
".": {
|