@playcademy/sandbox 0.6.0 → 0.6.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +162 -21
- package/dist/constants.js +1 -1
- package/dist/mocks/timeback.d.ts +1 -1
- package/dist/server.js +162 -21
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -234,7 +234,7 @@ var init_game = __esm(() => {
|
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
// ../constants/src/platform.ts
|
|
237
|
-
var CORE_GAME_UUIDS;
|
|
237
|
+
var PLAYCADEMY_BROWSER_TIME_ZONE_HEADER = "x-playcademy-browser-time-zone", CORE_GAME_UUIDS;
|
|
238
238
|
var init_platform = __esm(() => {
|
|
239
239
|
CORE_GAME_UUIDS = {
|
|
240
240
|
DEMO: "00000000-0000-0000-0000-000000000001"
|
|
@@ -1080,7 +1080,7 @@ var package_default;
|
|
|
1080
1080
|
var init_package = __esm(() => {
|
|
1081
1081
|
package_default = {
|
|
1082
1082
|
name: "@playcademy/sandbox",
|
|
1083
|
-
version: "0.6.
|
|
1083
|
+
version: "0.6.1-beta.2",
|
|
1084
1084
|
description: "Local development server for Playcademy game development",
|
|
1085
1085
|
type: "module",
|
|
1086
1086
|
exports: {
|
|
@@ -11057,6 +11057,11 @@ var init_table3 = __esm(() => {
|
|
|
11057
11057
|
email: text("email").notNull().unique(),
|
|
11058
11058
|
isAnonymous: boolean("is_anonymous").notNull().default(false),
|
|
11059
11059
|
timebackId: text("timeback_id").unique(),
|
|
11060
|
+
learningTimeZone: text("learning_time_zone"),
|
|
11061
|
+
learningTimeZoneUpdatedAt: timestamp("learning_time_zone_updated_at", {
|
|
11062
|
+
mode: "date",
|
|
11063
|
+
withTimezone: true
|
|
11064
|
+
}),
|
|
11060
11065
|
emailVerified: boolean("email_verified").notNull().default(false),
|
|
11061
11066
|
image: text("image"),
|
|
11062
11067
|
role: userRoleEnum("role").notNull().default("player"),
|
|
@@ -31042,7 +31047,7 @@ var init_schemas4 = __esm(() => {
|
|
|
31042
31047
|
subject: TimebackSubjectSchema,
|
|
31043
31048
|
appName: exports_external.string().optional(),
|
|
31044
31049
|
sensorUrl: exports_external.string().url().optional(),
|
|
31045
|
-
courseId: exports_external.string().optional(),
|
|
31050
|
+
courseId: exports_external.string().min(1).optional(),
|
|
31046
31051
|
courseName: exports_external.string().optional(),
|
|
31047
31052
|
studentEmail: exports_external.string().email().optional()
|
|
31048
31053
|
});
|
|
@@ -75585,6 +75590,7 @@ class AnalyticsXp {
|
|
|
75585
75590
|
this.core = core3;
|
|
75586
75591
|
}
|
|
75587
75592
|
async get(studentId, options) {
|
|
75593
|
+
const timezone2 = options?.timezone ?? PLATFORM_TIMEZONE;
|
|
75588
75594
|
const enrollments = await this.core.api.edubridge.enrollments.list({ userId: studentId });
|
|
75589
75595
|
const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
|
|
75590
75596
|
if (filteredEnrollments.length === 0) {
|
|
@@ -75598,7 +75604,7 @@ class AnalyticsXp {
|
|
|
75598
75604
|
try {
|
|
75599
75605
|
const analytics = await this.core.api.edubridge.analytics.getEnrollmentFacts({
|
|
75600
75606
|
enrollmentId: enrollment.id,
|
|
75601
|
-
timezone:
|
|
75607
|
+
timezone: timezone2
|
|
75602
75608
|
});
|
|
75603
75609
|
return { enrollment, analytics };
|
|
75604
75610
|
} catch (error88) {
|
|
@@ -75610,7 +75616,7 @@ class AnalyticsXp {
|
|
|
75610
75616
|
return { enrollment, analytics: null };
|
|
75611
75617
|
}
|
|
75612
75618
|
}));
|
|
75613
|
-
const today = formatDateYMDInTimezone(
|
|
75619
|
+
const today = formatDateYMDInTimezone(timezone2);
|
|
75614
75620
|
let totalXp = 0;
|
|
75615
75621
|
let todayXp = 0;
|
|
75616
75622
|
const courses = [];
|
|
@@ -80367,6 +80373,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
80367
80373
|
}
|
|
80368
80374
|
};
|
|
80369
80375
|
}
|
|
80376
|
+
static resolveRuntimeCourse(resolvedCourseId, requestedCourseId, courseName) {
|
|
80377
|
+
let requestedCourseIdStatus = "absent";
|
|
80378
|
+
if (requestedCourseId === resolvedCourseId) {
|
|
80379
|
+
requestedCourseIdStatus = "accepted";
|
|
80380
|
+
} else if (requestedCourseId !== undefined) {
|
|
80381
|
+
requestedCourseIdStatus = "ignored_mismatch";
|
|
80382
|
+
}
|
|
80383
|
+
return {
|
|
80384
|
+
courseId: resolvedCourseId,
|
|
80385
|
+
courseName: requestedCourseIdStatus === "ignored_mismatch" ? undefined : courseName,
|
|
80386
|
+
attributes: {
|
|
80387
|
+
"app.timeback.course_id": resolvedCourseId,
|
|
80388
|
+
"app.timeback.resolved_course_id": resolvedCourseId,
|
|
80389
|
+
"app.timeback.requested_course_id": requestedCourseId,
|
|
80390
|
+
"app.timeback.requested_course_id_status": requestedCourseIdStatus
|
|
80391
|
+
}
|
|
80392
|
+
};
|
|
80393
|
+
}
|
|
80370
80394
|
static recordRuntimeContext({
|
|
80371
80395
|
operation,
|
|
80372
80396
|
studentId,
|
|
@@ -81232,8 +81256,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
81232
81256
|
if (!integration) {
|
|
81233
81257
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
81234
81258
|
}
|
|
81259
|
+
const {
|
|
81260
|
+
courseId: runtimeCourseId,
|
|
81261
|
+
courseName: runtimeCourseName,
|
|
81262
|
+
attributes: courseAttributes
|
|
81263
|
+
} = TimebackService.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
81235
81264
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
81236
|
-
const result = await client.activity.record(
|
|
81265
|
+
const result = await client.activity.record(runtimeCourseId, studentId, {
|
|
81237
81266
|
gameId,
|
|
81238
81267
|
score: scorePercentage,
|
|
81239
81268
|
totalQuestions: scoreData.totalQuestions,
|
|
@@ -81248,8 +81277,8 @@ var init_timeback_service = __esm(async () => {
|
|
|
81248
81277
|
subject: activityData.subject,
|
|
81249
81278
|
appName: activityData.appName,
|
|
81250
81279
|
sensorUrl: activityData.sensorUrl,
|
|
81251
|
-
courseId:
|
|
81252
|
-
courseName:
|
|
81280
|
+
courseId: runtimeCourseId,
|
|
81281
|
+
courseName: runtimeCourseName,
|
|
81253
81282
|
studentEmail: activityData.studentEmail,
|
|
81254
81283
|
courseTotalXp: integration.totalXp,
|
|
81255
81284
|
...runId ? { runId } : {}
|
|
@@ -81258,7 +81287,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
81258
81287
|
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
81259
81288
|
const sessionEndEmitted = sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0;
|
|
81260
81289
|
if (sessionEndEmitted) {
|
|
81261
|
-
await client.activity.session(
|
|
81290
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
81262
81291
|
gameId,
|
|
81263
81292
|
activeTimeSeconds: sessionEndActiveSeconds,
|
|
81264
81293
|
...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {},
|
|
@@ -81267,15 +81296,15 @@ var init_timeback_service = __esm(async () => {
|
|
|
81267
81296
|
subject: activityData.subject,
|
|
81268
81297
|
appName: activityData.appName,
|
|
81269
81298
|
sensorUrl: activityData.sensorUrl,
|
|
81270
|
-
courseId:
|
|
81271
|
-
courseName:
|
|
81299
|
+
courseId: runtimeCourseId,
|
|
81300
|
+
courseName: runtimeCourseName,
|
|
81272
81301
|
studentEmail: activityData.studentEmail,
|
|
81273
81302
|
extensions: extensionsWithResumeId,
|
|
81274
81303
|
...runId ? { runId } : {}
|
|
81275
81304
|
});
|
|
81276
81305
|
}
|
|
81277
81306
|
setAttributes({
|
|
81278
|
-
|
|
81307
|
+
...courseAttributes,
|
|
81279
81308
|
"app.timeback.grade": activityData.grade,
|
|
81280
81309
|
"app.timeback.subject": activityData.subject,
|
|
81281
81310
|
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
@@ -81292,7 +81321,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
81292
81321
|
});
|
|
81293
81322
|
return {
|
|
81294
81323
|
status: "ok",
|
|
81295
|
-
courseId:
|
|
81324
|
+
courseId: runtimeCourseId,
|
|
81296
81325
|
xpAwarded: result.xpAwarded,
|
|
81297
81326
|
masteredUnits: result.masteredUnitsApplied,
|
|
81298
81327
|
pctCompleteApp: result.pctCompleteApp,
|
|
@@ -81503,10 +81532,15 @@ var init_timeback_service = __esm(async () => {
|
|
|
81503
81532
|
if (!integration) {
|
|
81504
81533
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
81505
81534
|
}
|
|
81535
|
+
const {
|
|
81536
|
+
courseId: runtimeCourseId,
|
|
81537
|
+
courseName: runtimeCourseName,
|
|
81538
|
+
attributes: courseAttributes
|
|
81539
|
+
} = TimebackService.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
81506
81540
|
const activeTimeSeconds = timingData.activeMs / 1000;
|
|
81507
81541
|
const inactiveTimeSeconds = timingData.pausedMs / 1000;
|
|
81508
81542
|
if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
|
|
81509
|
-
await client.activity.session(
|
|
81543
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
81510
81544
|
gameId,
|
|
81511
81545
|
activeTimeSeconds,
|
|
81512
81546
|
...inactiveTimeSeconds > 0 ? { inactiveTimeSeconds } : {},
|
|
@@ -81515,15 +81549,15 @@ var init_timeback_service = __esm(async () => {
|
|
|
81515
81549
|
subject: activityData.subject,
|
|
81516
81550
|
appName: activityData.appName,
|
|
81517
81551
|
sensorUrl: activityData.sensorUrl,
|
|
81518
|
-
courseId:
|
|
81519
|
-
courseName:
|
|
81552
|
+
courseId: runtimeCourseId,
|
|
81553
|
+
courseName: runtimeCourseName,
|
|
81520
81554
|
studentEmail: activityData.studentEmail,
|
|
81521
81555
|
extensions: TimebackService.addResumeIdToExtensions(undefined, effectiveResumeId),
|
|
81522
81556
|
...runId ? { runId } : {}
|
|
81523
81557
|
});
|
|
81524
81558
|
}
|
|
81525
81559
|
setAttributes({
|
|
81526
|
-
|
|
81560
|
+
...courseAttributes,
|
|
81527
81561
|
"app.timeback.grade": activityData.grade,
|
|
81528
81562
|
"app.timeback.subject": activityData.subject,
|
|
81529
81563
|
"app.timeback.active_time_seconds": activeTimeSeconds,
|
|
@@ -81568,13 +81602,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
81568
81602
|
};
|
|
81569
81603
|
}
|
|
81570
81604
|
}
|
|
81605
|
+
const timezone2 = await this.getLearningTimeZoneForStudent(timebackId);
|
|
81571
81606
|
const result = await client.analytics.getXp(timebackId, {
|
|
81572
81607
|
courseIds: courseIds.length > 0 ? courseIds : undefined,
|
|
81608
|
+
timezone: timezone2,
|
|
81573
81609
|
include: options?.include
|
|
81574
81610
|
});
|
|
81575
81611
|
return result;
|
|
81576
81612
|
});
|
|
81577
81613
|
}
|
|
81614
|
+
async getLearningTimeZoneForStudent(timebackId) {
|
|
81615
|
+
const user = await this.deps.db.query.users.findFirst({
|
|
81616
|
+
where: eq(users.timebackId, timebackId),
|
|
81617
|
+
columns: {
|
|
81618
|
+
learningTimeZone: true
|
|
81619
|
+
}
|
|
81620
|
+
});
|
|
81621
|
+
return user?.learningTimeZone ?? PLATFORM_TIMEZONE;
|
|
81622
|
+
}
|
|
81578
81623
|
async getStudentMastery(timebackId, user, options) {
|
|
81579
81624
|
return this.withClientTelemetry(async () => {
|
|
81580
81625
|
const client = this.requireClient();
|
|
@@ -82356,13 +82401,28 @@ var init_session_service = __esm(() => {
|
|
|
82356
82401
|
init_errors();
|
|
82357
82402
|
});
|
|
82358
82403
|
|
|
82404
|
+
// ../api-core/src/utils/timezone.util.ts
|
|
82405
|
+
function normalizeIanaTimeZone(value) {
|
|
82406
|
+
const timeZone = value?.trim();
|
|
82407
|
+
if (!timeZone || timeZone.length > MAX_IANA_TIME_ZONE_LENGTH) {
|
|
82408
|
+
return;
|
|
82409
|
+
}
|
|
82410
|
+
try {
|
|
82411
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date);
|
|
82412
|
+
return timeZone;
|
|
82413
|
+
} catch {
|
|
82414
|
+
return;
|
|
82415
|
+
}
|
|
82416
|
+
}
|
|
82417
|
+
var MAX_IANA_TIME_ZONE_LENGTH = 100;
|
|
82418
|
+
|
|
82359
82419
|
// ../api-core/src/services/user.service.ts
|
|
82360
82420
|
class UserService {
|
|
82361
82421
|
deps;
|
|
82362
82422
|
constructor(deps) {
|
|
82363
82423
|
this.deps = deps;
|
|
82364
82424
|
}
|
|
82365
|
-
async getMe(user, gameId) {
|
|
82425
|
+
async getMe(user, gameId, observedTimeZone) {
|
|
82366
82426
|
const db2 = this.deps.db;
|
|
82367
82427
|
const userData = await db2.query.users.findFirst({
|
|
82368
82428
|
where: eq(users.id, user.id)
|
|
@@ -82375,6 +82435,7 @@ class UserService {
|
|
|
82375
82435
|
"app.user.has_timeback_account": Boolean(userData.timebackId),
|
|
82376
82436
|
"app.user.timeback_enriched": false
|
|
82377
82437
|
});
|
|
82438
|
+
const localDay = await this.resolveLocalDay(userData, observedTimeZone);
|
|
82378
82439
|
const timeback2 = userData.timebackId ? await this.fetchTimebackData(userData.timebackId, gameId) : undefined;
|
|
82379
82440
|
setAttribute("app.user.timeback_enriched", Boolean(timeback2));
|
|
82380
82441
|
if (gameId) {
|
|
@@ -82384,6 +82445,7 @@ class UserService {
|
|
|
82384
82445
|
role: userData.role,
|
|
82385
82446
|
username: userData.username,
|
|
82386
82447
|
email: userData.email,
|
|
82448
|
+
localDay,
|
|
82387
82449
|
timeback: timeback2
|
|
82388
82450
|
};
|
|
82389
82451
|
}
|
|
@@ -82403,6 +82465,7 @@ class UserService {
|
|
|
82403
82465
|
createdAt: userData.createdAt,
|
|
82404
82466
|
updatedAt: userData.updatedAt,
|
|
82405
82467
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
82468
|
+
localDay,
|
|
82406
82469
|
timeback: timeback2
|
|
82407
82470
|
};
|
|
82408
82471
|
}
|
|
@@ -82440,6 +82503,44 @@ class UserService {
|
|
|
82440
82503
|
isDefault: updatedUser.name === DEMO_DISPLAY_NAME_PLACEHOLDER
|
|
82441
82504
|
};
|
|
82442
82505
|
}
|
|
82506
|
+
async resolveLocalDay(userData, observedTimeZone) {
|
|
82507
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
82508
|
+
if (normalizedTimeZone) {
|
|
82509
|
+
const observedAt = new Date;
|
|
82510
|
+
const shouldPersist = userData.learningTimeZone !== normalizedTimeZone || !userData.learningTimeZoneUpdatedAt || observedAt.getTime() - userData.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82511
|
+
if (shouldPersist) {
|
|
82512
|
+
await this.deps.db.update(users).set({
|
|
82513
|
+
learningTimeZone: normalizedTimeZone,
|
|
82514
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
82515
|
+
}).where(eq(users.id, userData.id));
|
|
82516
|
+
}
|
|
82517
|
+
setAttributes({
|
|
82518
|
+
"app.user.learning_time_zone_observed": true,
|
|
82519
|
+
"app.user.learning_time_zone_persisted": shouldPersist
|
|
82520
|
+
});
|
|
82521
|
+
return {
|
|
82522
|
+
timeZone: normalizedTimeZone,
|
|
82523
|
+
source: "browser_last_seen",
|
|
82524
|
+
observedAt: (shouldPersist ? observedAt : userData.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
82525
|
+
};
|
|
82526
|
+
}
|
|
82527
|
+
setAttributes({
|
|
82528
|
+
"app.user.learning_time_zone_observed": false,
|
|
82529
|
+
"app.user.learning_time_zone_persisted": false
|
|
82530
|
+
});
|
|
82531
|
+
if (userData.learningTimeZone) {
|
|
82532
|
+
return {
|
|
82533
|
+
timeZone: userData.learningTimeZone,
|
|
82534
|
+
source: "browser_last_seen",
|
|
82535
|
+
observedAt: userData.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
82536
|
+
};
|
|
82537
|
+
}
|
|
82538
|
+
return {
|
|
82539
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
82540
|
+
source: "platform_default",
|
|
82541
|
+
observedAt: null
|
|
82542
|
+
};
|
|
82543
|
+
}
|
|
82443
82544
|
async fetchTimebackData(timebackId, gameId) {
|
|
82444
82545
|
const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
|
|
82445
82546
|
withSpan("timeback.fetch_profile", () => this.fetchStudentProfile(timebackId)),
|
|
@@ -82521,6 +82622,7 @@ class UserService {
|
|
|
82521
82622
|
return organizations.filter((o) => enrollmentOrgIds.has(o.id));
|
|
82522
82623
|
}
|
|
82523
82624
|
}
|
|
82625
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82524
82626
|
var init_user_service = __esm(() => {
|
|
82525
82627
|
init_drizzle_orm();
|
|
82526
82628
|
init_src();
|
|
@@ -82529,6 +82631,7 @@ var init_user_service = __esm(() => {
|
|
|
82529
82631
|
init_spans();
|
|
82530
82632
|
init_errors();
|
|
82531
82633
|
init_timeback_util();
|
|
82634
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
82532
82635
|
});
|
|
82533
82636
|
|
|
82534
82637
|
// ../api-core/src/services/verify.service.ts
|
|
@@ -140159,9 +140262,10 @@ var init_upload_controller = __esm(() => {
|
|
|
140159
140262
|
// ../api-core/src/controllers/user.controller.ts
|
|
140160
140263
|
var getMe, getDemoProfile, updateDemoProfile, users2;
|
|
140161
140264
|
var init_user_controller = __esm(() => {
|
|
140265
|
+
init_src();
|
|
140162
140266
|
init_schemas_index();
|
|
140163
140267
|
init_utils11();
|
|
140164
|
-
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId));
|
|
140268
|
+
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId, ctx.request.headers.get(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER)));
|
|
140165
140269
|
getDemoProfile = requireAnonymous(async (ctx) => ctx.services.user.getDemoProfile(ctx.user.id));
|
|
140166
140270
|
updateDemoProfile = requireAnonymous(async (ctx) => {
|
|
140167
140271
|
const body2 = await parseRequestBody(ctx.request, DemoProfileSchema);
|
|
@@ -140316,8 +140420,39 @@ function getMockHighestGradeMastered(enrollments, subject) {
|
|
|
140316
140420
|
const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
|
|
140317
140421
|
return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
|
|
140318
140422
|
}
|
|
140319
|
-
async function
|
|
140423
|
+
async function buildLocalDayContext(db2, user, observedTimeZone) {
|
|
140424
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
140425
|
+
if (normalizedTimeZone) {
|
|
140426
|
+
const observedAt = new Date;
|
|
140427
|
+
const shouldPersist = user.learningTimeZone !== normalizedTimeZone || !user.learningTimeZoneUpdatedAt || observedAt.getTime() - user.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140428
|
+
if (shouldPersist) {
|
|
140429
|
+
await db2.update(users).set({
|
|
140430
|
+
learningTimeZone: normalizedTimeZone,
|
|
140431
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
140432
|
+
}).where(eq(users.id, user.id));
|
|
140433
|
+
}
|
|
140434
|
+
return {
|
|
140435
|
+
timeZone: normalizedTimeZone,
|
|
140436
|
+
source: "browser_last_seen",
|
|
140437
|
+
observedAt: (shouldPersist ? observedAt : user.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
140438
|
+
};
|
|
140439
|
+
}
|
|
140440
|
+
if (user.learningTimeZone) {
|
|
140441
|
+
return {
|
|
140442
|
+
timeZone: user.learningTimeZone,
|
|
140443
|
+
source: "browser_last_seen",
|
|
140444
|
+
observedAt: user.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
140445
|
+
};
|
|
140446
|
+
}
|
|
140447
|
+
return {
|
|
140448
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
140449
|
+
source: "platform_default",
|
|
140450
|
+
observedAt: null
|
|
140451
|
+
};
|
|
140452
|
+
}
|
|
140453
|
+
async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
|
|
140320
140454
|
const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
|
|
140455
|
+
const localDay = await buildLocalDayContext(db2, user, observedTimeZone);
|
|
140321
140456
|
if (gameId) {
|
|
140322
140457
|
return {
|
|
140323
140458
|
id: user.id,
|
|
@@ -140325,6 +140460,7 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140325
140460
|
role: user.role,
|
|
140326
140461
|
username: user.username,
|
|
140327
140462
|
email: user.email,
|
|
140463
|
+
localDay,
|
|
140328
140464
|
timeback: timeback3
|
|
140329
140465
|
};
|
|
140330
140466
|
}
|
|
@@ -140343,15 +140479,19 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140343
140479
|
createdAt: user.createdAt,
|
|
140344
140480
|
updatedAt: user.updatedAt,
|
|
140345
140481
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
140482
|
+
localDay,
|
|
140346
140483
|
timeback: timeback3
|
|
140347
140484
|
};
|
|
140348
140485
|
}
|
|
140486
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140349
140487
|
var init_timeback6 = __esm(() => {
|
|
140350
140488
|
init_drizzle_orm();
|
|
140351
140489
|
init_utils11();
|
|
140490
|
+
init_src();
|
|
140352
140491
|
init_tables_index();
|
|
140353
140492
|
init_src2();
|
|
140354
140493
|
init_config();
|
|
140494
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2 = 24 * 60 * 60 * 1000;
|
|
140355
140495
|
});
|
|
140356
140496
|
|
|
140357
140497
|
// src/routes/platform/users.ts
|
|
@@ -140361,6 +140501,7 @@ var init_users = __esm(async () => {
|
|
|
140361
140501
|
init_dist7();
|
|
140362
140502
|
init_controllers();
|
|
140363
140503
|
init_errors();
|
|
140504
|
+
init_src();
|
|
140364
140505
|
init_tables_index();
|
|
140365
140506
|
init_error_handler();
|
|
140366
140507
|
init_timeback6();
|
|
@@ -140382,7 +140523,7 @@ var init_users = __esm(async () => {
|
|
|
140382
140523
|
const error89 = ApiError.notFound("User not found");
|
|
140383
140524
|
return c2.json(createErrorResponse(error89), error89.status);
|
|
140384
140525
|
}
|
|
140385
|
-
const response = await buildMockUserResponse(db2, userData, gameId);
|
|
140526
|
+
const response = await buildMockUserResponse(db2, userData, gameId, c2.req.header(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER));
|
|
140386
140527
|
return c2.json(response);
|
|
140387
140528
|
}
|
|
140388
140529
|
return handle2(users2.getMe)(c2);
|
package/dist/constants.js
CHANGED
|
@@ -69,7 +69,7 @@ var init_game = __esm(() => {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
// ../constants/src/platform.ts
|
|
72
|
-
var CORE_GAME_UUIDS;
|
|
72
|
+
var PLAYCADEMY_BROWSER_TIME_ZONE_HEADER = "x-playcademy-browser-time-zone", CORE_GAME_UUIDS;
|
|
73
73
|
var init_platform = __esm(() => {
|
|
74
74
|
CORE_GAME_UUIDS = {
|
|
75
75
|
DEMO: "00000000-0000-0000-0000-000000000001"
|
package/dist/mocks/timeback.d.ts
CHANGED
|
@@ -35,4 +35,4 @@ export declare function getMockHighestGradeMastered(enrollments: UserEnrollment[
|
|
|
35
35
|
* Build a complete user response with mock timeback data.
|
|
36
36
|
* Used to bypass api-core when in mock mode (avoids real API calls).
|
|
37
37
|
*/
|
|
38
|
-
export declare function buildMockUserResponse(db: DatabaseInstance, user: User, gameId?: string): Promise<AuthenticatedUser | GameUser>;
|
|
38
|
+
export declare function buildMockUserResponse(db: DatabaseInstance, user: User, gameId?: string, observedTimeZone?: string | null): Promise<AuthenticatedUser | GameUser>;
|
package/dist/server.js
CHANGED
|
@@ -233,7 +233,7 @@ var init_game = __esm(() => {
|
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
// ../constants/src/platform.ts
|
|
236
|
-
var CORE_GAME_UUIDS;
|
|
236
|
+
var PLAYCADEMY_BROWSER_TIME_ZONE_HEADER = "x-playcademy-browser-time-zone", CORE_GAME_UUIDS;
|
|
237
237
|
var init_platform = __esm(() => {
|
|
238
238
|
CORE_GAME_UUIDS = {
|
|
239
239
|
DEMO: "00000000-0000-0000-0000-000000000001"
|
|
@@ -1079,7 +1079,7 @@ var package_default;
|
|
|
1079
1079
|
var init_package = __esm(() => {
|
|
1080
1080
|
package_default = {
|
|
1081
1081
|
name: "@playcademy/sandbox",
|
|
1082
|
-
version: "0.6.
|
|
1082
|
+
version: "0.6.1-beta.2",
|
|
1083
1083
|
description: "Local development server for Playcademy game development",
|
|
1084
1084
|
type: "module",
|
|
1085
1085
|
exports: {
|
|
@@ -11056,6 +11056,11 @@ var init_table3 = __esm(() => {
|
|
|
11056
11056
|
email: text("email").notNull().unique(),
|
|
11057
11057
|
isAnonymous: boolean("is_anonymous").notNull().default(false),
|
|
11058
11058
|
timebackId: text("timeback_id").unique(),
|
|
11059
|
+
learningTimeZone: text("learning_time_zone"),
|
|
11060
|
+
learningTimeZoneUpdatedAt: timestamp("learning_time_zone_updated_at", {
|
|
11061
|
+
mode: "date",
|
|
11062
|
+
withTimezone: true
|
|
11063
|
+
}),
|
|
11059
11064
|
emailVerified: boolean("email_verified").notNull().default(false),
|
|
11060
11065
|
image: text("image"),
|
|
11061
11066
|
role: userRoleEnum("role").notNull().default("player"),
|
|
@@ -31041,7 +31046,7 @@ var init_schemas4 = __esm(() => {
|
|
|
31041
31046
|
subject: TimebackSubjectSchema,
|
|
31042
31047
|
appName: exports_external.string().optional(),
|
|
31043
31048
|
sensorUrl: exports_external.string().url().optional(),
|
|
31044
|
-
courseId: exports_external.string().optional(),
|
|
31049
|
+
courseId: exports_external.string().min(1).optional(),
|
|
31045
31050
|
courseName: exports_external.string().optional(),
|
|
31046
31051
|
studentEmail: exports_external.string().email().optional()
|
|
31047
31052
|
});
|
|
@@ -75584,6 +75589,7 @@ class AnalyticsXp {
|
|
|
75584
75589
|
this.core = core3;
|
|
75585
75590
|
}
|
|
75586
75591
|
async get(studentId, options) {
|
|
75592
|
+
const timezone2 = options?.timezone ?? PLATFORM_TIMEZONE;
|
|
75587
75593
|
const enrollments = await this.core.api.edubridge.enrollments.list({ userId: studentId });
|
|
75588
75594
|
const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
|
|
75589
75595
|
if (filteredEnrollments.length === 0) {
|
|
@@ -75597,7 +75603,7 @@ class AnalyticsXp {
|
|
|
75597
75603
|
try {
|
|
75598
75604
|
const analytics = await this.core.api.edubridge.analytics.getEnrollmentFacts({
|
|
75599
75605
|
enrollmentId: enrollment.id,
|
|
75600
|
-
timezone:
|
|
75606
|
+
timezone: timezone2
|
|
75601
75607
|
});
|
|
75602
75608
|
return { enrollment, analytics };
|
|
75603
75609
|
} catch (error88) {
|
|
@@ -75609,7 +75615,7 @@ class AnalyticsXp {
|
|
|
75609
75615
|
return { enrollment, analytics: null };
|
|
75610
75616
|
}
|
|
75611
75617
|
}));
|
|
75612
|
-
const today = formatDateYMDInTimezone(
|
|
75618
|
+
const today = formatDateYMDInTimezone(timezone2);
|
|
75613
75619
|
let totalXp = 0;
|
|
75614
75620
|
let todayXp = 0;
|
|
75615
75621
|
const courses = [];
|
|
@@ -80366,6 +80372,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
80366
80372
|
}
|
|
80367
80373
|
};
|
|
80368
80374
|
}
|
|
80375
|
+
static resolveRuntimeCourse(resolvedCourseId, requestedCourseId, courseName) {
|
|
80376
|
+
let requestedCourseIdStatus = "absent";
|
|
80377
|
+
if (requestedCourseId === resolvedCourseId) {
|
|
80378
|
+
requestedCourseIdStatus = "accepted";
|
|
80379
|
+
} else if (requestedCourseId !== undefined) {
|
|
80380
|
+
requestedCourseIdStatus = "ignored_mismatch";
|
|
80381
|
+
}
|
|
80382
|
+
return {
|
|
80383
|
+
courseId: resolvedCourseId,
|
|
80384
|
+
courseName: requestedCourseIdStatus === "ignored_mismatch" ? undefined : courseName,
|
|
80385
|
+
attributes: {
|
|
80386
|
+
"app.timeback.course_id": resolvedCourseId,
|
|
80387
|
+
"app.timeback.resolved_course_id": resolvedCourseId,
|
|
80388
|
+
"app.timeback.requested_course_id": requestedCourseId,
|
|
80389
|
+
"app.timeback.requested_course_id_status": requestedCourseIdStatus
|
|
80390
|
+
}
|
|
80391
|
+
};
|
|
80392
|
+
}
|
|
80369
80393
|
static recordRuntimeContext({
|
|
80370
80394
|
operation,
|
|
80371
80395
|
studentId,
|
|
@@ -81231,8 +81255,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
81231
81255
|
if (!integration) {
|
|
81232
81256
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
81233
81257
|
}
|
|
81258
|
+
const {
|
|
81259
|
+
courseId: runtimeCourseId,
|
|
81260
|
+
courseName: runtimeCourseName,
|
|
81261
|
+
attributes: courseAttributes
|
|
81262
|
+
} = TimebackService.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
81234
81263
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
81235
|
-
const result = await client.activity.record(
|
|
81264
|
+
const result = await client.activity.record(runtimeCourseId, studentId, {
|
|
81236
81265
|
gameId,
|
|
81237
81266
|
score: scorePercentage,
|
|
81238
81267
|
totalQuestions: scoreData.totalQuestions,
|
|
@@ -81247,8 +81276,8 @@ var init_timeback_service = __esm(async () => {
|
|
|
81247
81276
|
subject: activityData.subject,
|
|
81248
81277
|
appName: activityData.appName,
|
|
81249
81278
|
sensorUrl: activityData.sensorUrl,
|
|
81250
|
-
courseId:
|
|
81251
|
-
courseName:
|
|
81279
|
+
courseId: runtimeCourseId,
|
|
81280
|
+
courseName: runtimeCourseName,
|
|
81252
81281
|
studentEmail: activityData.studentEmail,
|
|
81253
81282
|
courseTotalXp: integration.totalXp,
|
|
81254
81283
|
...runId ? { runId } : {}
|
|
@@ -81257,7 +81286,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
81257
81286
|
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
81258
81287
|
const sessionEndEmitted = sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0;
|
|
81259
81288
|
if (sessionEndEmitted) {
|
|
81260
|
-
await client.activity.session(
|
|
81289
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
81261
81290
|
gameId,
|
|
81262
81291
|
activeTimeSeconds: sessionEndActiveSeconds,
|
|
81263
81292
|
...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {},
|
|
@@ -81266,15 +81295,15 @@ var init_timeback_service = __esm(async () => {
|
|
|
81266
81295
|
subject: activityData.subject,
|
|
81267
81296
|
appName: activityData.appName,
|
|
81268
81297
|
sensorUrl: activityData.sensorUrl,
|
|
81269
|
-
courseId:
|
|
81270
|
-
courseName:
|
|
81298
|
+
courseId: runtimeCourseId,
|
|
81299
|
+
courseName: runtimeCourseName,
|
|
81271
81300
|
studentEmail: activityData.studentEmail,
|
|
81272
81301
|
extensions: extensionsWithResumeId,
|
|
81273
81302
|
...runId ? { runId } : {}
|
|
81274
81303
|
});
|
|
81275
81304
|
}
|
|
81276
81305
|
setAttributes({
|
|
81277
|
-
|
|
81306
|
+
...courseAttributes,
|
|
81278
81307
|
"app.timeback.grade": activityData.grade,
|
|
81279
81308
|
"app.timeback.subject": activityData.subject,
|
|
81280
81309
|
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
@@ -81291,7 +81320,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
81291
81320
|
});
|
|
81292
81321
|
return {
|
|
81293
81322
|
status: "ok",
|
|
81294
|
-
courseId:
|
|
81323
|
+
courseId: runtimeCourseId,
|
|
81295
81324
|
xpAwarded: result.xpAwarded,
|
|
81296
81325
|
masteredUnits: result.masteredUnitsApplied,
|
|
81297
81326
|
pctCompleteApp: result.pctCompleteApp,
|
|
@@ -81502,10 +81531,15 @@ var init_timeback_service = __esm(async () => {
|
|
|
81502
81531
|
if (!integration) {
|
|
81503
81532
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
81504
81533
|
}
|
|
81534
|
+
const {
|
|
81535
|
+
courseId: runtimeCourseId,
|
|
81536
|
+
courseName: runtimeCourseName,
|
|
81537
|
+
attributes: courseAttributes
|
|
81538
|
+
} = TimebackService.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
81505
81539
|
const activeTimeSeconds = timingData.activeMs / 1000;
|
|
81506
81540
|
const inactiveTimeSeconds = timingData.pausedMs / 1000;
|
|
81507
81541
|
if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
|
|
81508
|
-
await client.activity.session(
|
|
81542
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
81509
81543
|
gameId,
|
|
81510
81544
|
activeTimeSeconds,
|
|
81511
81545
|
...inactiveTimeSeconds > 0 ? { inactiveTimeSeconds } : {},
|
|
@@ -81514,15 +81548,15 @@ var init_timeback_service = __esm(async () => {
|
|
|
81514
81548
|
subject: activityData.subject,
|
|
81515
81549
|
appName: activityData.appName,
|
|
81516
81550
|
sensorUrl: activityData.sensorUrl,
|
|
81517
|
-
courseId:
|
|
81518
|
-
courseName:
|
|
81551
|
+
courseId: runtimeCourseId,
|
|
81552
|
+
courseName: runtimeCourseName,
|
|
81519
81553
|
studentEmail: activityData.studentEmail,
|
|
81520
81554
|
extensions: TimebackService.addResumeIdToExtensions(undefined, effectiveResumeId),
|
|
81521
81555
|
...runId ? { runId } : {}
|
|
81522
81556
|
});
|
|
81523
81557
|
}
|
|
81524
81558
|
setAttributes({
|
|
81525
|
-
|
|
81559
|
+
...courseAttributes,
|
|
81526
81560
|
"app.timeback.grade": activityData.grade,
|
|
81527
81561
|
"app.timeback.subject": activityData.subject,
|
|
81528
81562
|
"app.timeback.active_time_seconds": activeTimeSeconds,
|
|
@@ -81567,13 +81601,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
81567
81601
|
};
|
|
81568
81602
|
}
|
|
81569
81603
|
}
|
|
81604
|
+
const timezone2 = await this.getLearningTimeZoneForStudent(timebackId);
|
|
81570
81605
|
const result = await client.analytics.getXp(timebackId, {
|
|
81571
81606
|
courseIds: courseIds.length > 0 ? courseIds : undefined,
|
|
81607
|
+
timezone: timezone2,
|
|
81572
81608
|
include: options?.include
|
|
81573
81609
|
});
|
|
81574
81610
|
return result;
|
|
81575
81611
|
});
|
|
81576
81612
|
}
|
|
81613
|
+
async getLearningTimeZoneForStudent(timebackId) {
|
|
81614
|
+
const user = await this.deps.db.query.users.findFirst({
|
|
81615
|
+
where: eq(users.timebackId, timebackId),
|
|
81616
|
+
columns: {
|
|
81617
|
+
learningTimeZone: true
|
|
81618
|
+
}
|
|
81619
|
+
});
|
|
81620
|
+
return user?.learningTimeZone ?? PLATFORM_TIMEZONE;
|
|
81621
|
+
}
|
|
81577
81622
|
async getStudentMastery(timebackId, user, options) {
|
|
81578
81623
|
return this.withClientTelemetry(async () => {
|
|
81579
81624
|
const client = this.requireClient();
|
|
@@ -82355,13 +82400,28 @@ var init_session_service = __esm(() => {
|
|
|
82355
82400
|
init_errors();
|
|
82356
82401
|
});
|
|
82357
82402
|
|
|
82403
|
+
// ../api-core/src/utils/timezone.util.ts
|
|
82404
|
+
function normalizeIanaTimeZone(value) {
|
|
82405
|
+
const timeZone = value?.trim();
|
|
82406
|
+
if (!timeZone || timeZone.length > MAX_IANA_TIME_ZONE_LENGTH) {
|
|
82407
|
+
return;
|
|
82408
|
+
}
|
|
82409
|
+
try {
|
|
82410
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date);
|
|
82411
|
+
return timeZone;
|
|
82412
|
+
} catch {
|
|
82413
|
+
return;
|
|
82414
|
+
}
|
|
82415
|
+
}
|
|
82416
|
+
var MAX_IANA_TIME_ZONE_LENGTH = 100;
|
|
82417
|
+
|
|
82358
82418
|
// ../api-core/src/services/user.service.ts
|
|
82359
82419
|
class UserService {
|
|
82360
82420
|
deps;
|
|
82361
82421
|
constructor(deps) {
|
|
82362
82422
|
this.deps = deps;
|
|
82363
82423
|
}
|
|
82364
|
-
async getMe(user, gameId) {
|
|
82424
|
+
async getMe(user, gameId, observedTimeZone) {
|
|
82365
82425
|
const db2 = this.deps.db;
|
|
82366
82426
|
const userData = await db2.query.users.findFirst({
|
|
82367
82427
|
where: eq(users.id, user.id)
|
|
@@ -82374,6 +82434,7 @@ class UserService {
|
|
|
82374
82434
|
"app.user.has_timeback_account": Boolean(userData.timebackId),
|
|
82375
82435
|
"app.user.timeback_enriched": false
|
|
82376
82436
|
});
|
|
82437
|
+
const localDay = await this.resolveLocalDay(userData, observedTimeZone);
|
|
82377
82438
|
const timeback2 = userData.timebackId ? await this.fetchTimebackData(userData.timebackId, gameId) : undefined;
|
|
82378
82439
|
setAttribute("app.user.timeback_enriched", Boolean(timeback2));
|
|
82379
82440
|
if (gameId) {
|
|
@@ -82383,6 +82444,7 @@ class UserService {
|
|
|
82383
82444
|
role: userData.role,
|
|
82384
82445
|
username: userData.username,
|
|
82385
82446
|
email: userData.email,
|
|
82447
|
+
localDay,
|
|
82386
82448
|
timeback: timeback2
|
|
82387
82449
|
};
|
|
82388
82450
|
}
|
|
@@ -82402,6 +82464,7 @@ class UserService {
|
|
|
82402
82464
|
createdAt: userData.createdAt,
|
|
82403
82465
|
updatedAt: userData.updatedAt,
|
|
82404
82466
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
82467
|
+
localDay,
|
|
82405
82468
|
timeback: timeback2
|
|
82406
82469
|
};
|
|
82407
82470
|
}
|
|
@@ -82439,6 +82502,44 @@ class UserService {
|
|
|
82439
82502
|
isDefault: updatedUser.name === DEMO_DISPLAY_NAME_PLACEHOLDER
|
|
82440
82503
|
};
|
|
82441
82504
|
}
|
|
82505
|
+
async resolveLocalDay(userData, observedTimeZone) {
|
|
82506
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
82507
|
+
if (normalizedTimeZone) {
|
|
82508
|
+
const observedAt = new Date;
|
|
82509
|
+
const shouldPersist = userData.learningTimeZone !== normalizedTimeZone || !userData.learningTimeZoneUpdatedAt || observedAt.getTime() - userData.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82510
|
+
if (shouldPersist) {
|
|
82511
|
+
await this.deps.db.update(users).set({
|
|
82512
|
+
learningTimeZone: normalizedTimeZone,
|
|
82513
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
82514
|
+
}).where(eq(users.id, userData.id));
|
|
82515
|
+
}
|
|
82516
|
+
setAttributes({
|
|
82517
|
+
"app.user.learning_time_zone_observed": true,
|
|
82518
|
+
"app.user.learning_time_zone_persisted": shouldPersist
|
|
82519
|
+
});
|
|
82520
|
+
return {
|
|
82521
|
+
timeZone: normalizedTimeZone,
|
|
82522
|
+
source: "browser_last_seen",
|
|
82523
|
+
observedAt: (shouldPersist ? observedAt : userData.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
82524
|
+
};
|
|
82525
|
+
}
|
|
82526
|
+
setAttributes({
|
|
82527
|
+
"app.user.learning_time_zone_observed": false,
|
|
82528
|
+
"app.user.learning_time_zone_persisted": false
|
|
82529
|
+
});
|
|
82530
|
+
if (userData.learningTimeZone) {
|
|
82531
|
+
return {
|
|
82532
|
+
timeZone: userData.learningTimeZone,
|
|
82533
|
+
source: "browser_last_seen",
|
|
82534
|
+
observedAt: userData.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
82535
|
+
};
|
|
82536
|
+
}
|
|
82537
|
+
return {
|
|
82538
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
82539
|
+
source: "platform_default",
|
|
82540
|
+
observedAt: null
|
|
82541
|
+
};
|
|
82542
|
+
}
|
|
82442
82543
|
async fetchTimebackData(timebackId, gameId) {
|
|
82443
82544
|
const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
|
|
82444
82545
|
withSpan("timeback.fetch_profile", () => this.fetchStudentProfile(timebackId)),
|
|
@@ -82520,6 +82621,7 @@ class UserService {
|
|
|
82520
82621
|
return organizations.filter((o) => enrollmentOrgIds.has(o.id));
|
|
82521
82622
|
}
|
|
82522
82623
|
}
|
|
82624
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82523
82625
|
var init_user_service = __esm(() => {
|
|
82524
82626
|
init_drizzle_orm();
|
|
82525
82627
|
init_src();
|
|
@@ -82528,6 +82630,7 @@ var init_user_service = __esm(() => {
|
|
|
82528
82630
|
init_spans();
|
|
82529
82631
|
init_errors();
|
|
82530
82632
|
init_timeback_util();
|
|
82633
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
82531
82634
|
});
|
|
82532
82635
|
|
|
82533
82636
|
// ../api-core/src/services/verify.service.ts
|
|
@@ -140158,9 +140261,10 @@ var init_upload_controller = __esm(() => {
|
|
|
140158
140261
|
// ../api-core/src/controllers/user.controller.ts
|
|
140159
140262
|
var getMe, getDemoProfile, updateDemoProfile, users2;
|
|
140160
140263
|
var init_user_controller = __esm(() => {
|
|
140264
|
+
init_src();
|
|
140161
140265
|
init_schemas_index();
|
|
140162
140266
|
init_utils11();
|
|
140163
|
-
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId));
|
|
140267
|
+
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId, ctx.request.headers.get(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER)));
|
|
140164
140268
|
getDemoProfile = requireAnonymous(async (ctx) => ctx.services.user.getDemoProfile(ctx.user.id));
|
|
140165
140269
|
updateDemoProfile = requireAnonymous(async (ctx) => {
|
|
140166
140270
|
const body2 = await parseRequestBody(ctx.request, DemoProfileSchema);
|
|
@@ -140315,8 +140419,39 @@ function getMockHighestGradeMastered(enrollments, subject) {
|
|
|
140315
140419
|
const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
|
|
140316
140420
|
return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
|
|
140317
140421
|
}
|
|
140318
|
-
async function
|
|
140422
|
+
async function buildLocalDayContext(db2, user, observedTimeZone) {
|
|
140423
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
140424
|
+
if (normalizedTimeZone) {
|
|
140425
|
+
const observedAt = new Date;
|
|
140426
|
+
const shouldPersist = user.learningTimeZone !== normalizedTimeZone || !user.learningTimeZoneUpdatedAt || observedAt.getTime() - user.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140427
|
+
if (shouldPersist) {
|
|
140428
|
+
await db2.update(users).set({
|
|
140429
|
+
learningTimeZone: normalizedTimeZone,
|
|
140430
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
140431
|
+
}).where(eq(users.id, user.id));
|
|
140432
|
+
}
|
|
140433
|
+
return {
|
|
140434
|
+
timeZone: normalizedTimeZone,
|
|
140435
|
+
source: "browser_last_seen",
|
|
140436
|
+
observedAt: (shouldPersist ? observedAt : user.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
140437
|
+
};
|
|
140438
|
+
}
|
|
140439
|
+
if (user.learningTimeZone) {
|
|
140440
|
+
return {
|
|
140441
|
+
timeZone: user.learningTimeZone,
|
|
140442
|
+
source: "browser_last_seen",
|
|
140443
|
+
observedAt: user.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
140444
|
+
};
|
|
140445
|
+
}
|
|
140446
|
+
return {
|
|
140447
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
140448
|
+
source: "platform_default",
|
|
140449
|
+
observedAt: null
|
|
140450
|
+
};
|
|
140451
|
+
}
|
|
140452
|
+
async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
|
|
140319
140453
|
const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
|
|
140454
|
+
const localDay = await buildLocalDayContext(db2, user, observedTimeZone);
|
|
140320
140455
|
if (gameId) {
|
|
140321
140456
|
return {
|
|
140322
140457
|
id: user.id,
|
|
@@ -140324,6 +140459,7 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140324
140459
|
role: user.role,
|
|
140325
140460
|
username: user.username,
|
|
140326
140461
|
email: user.email,
|
|
140462
|
+
localDay,
|
|
140327
140463
|
timeback: timeback3
|
|
140328
140464
|
};
|
|
140329
140465
|
}
|
|
@@ -140342,15 +140478,19 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140342
140478
|
createdAt: user.createdAt,
|
|
140343
140479
|
updatedAt: user.updatedAt,
|
|
140344
140480
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
140481
|
+
localDay,
|
|
140345
140482
|
timeback: timeback3
|
|
140346
140483
|
};
|
|
140347
140484
|
}
|
|
140485
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140348
140486
|
var init_timeback6 = __esm(() => {
|
|
140349
140487
|
init_drizzle_orm();
|
|
140350
140488
|
init_utils11();
|
|
140489
|
+
init_src();
|
|
140351
140490
|
init_tables_index();
|
|
140352
140491
|
init_src2();
|
|
140353
140492
|
init_config();
|
|
140493
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2 = 24 * 60 * 60 * 1000;
|
|
140354
140494
|
});
|
|
140355
140495
|
|
|
140356
140496
|
// src/routes/platform/users.ts
|
|
@@ -140360,6 +140500,7 @@ var init_users = __esm(async () => {
|
|
|
140360
140500
|
init_dist7();
|
|
140361
140501
|
init_controllers();
|
|
140362
140502
|
init_errors();
|
|
140503
|
+
init_src();
|
|
140363
140504
|
init_tables_index();
|
|
140364
140505
|
init_error_handler();
|
|
140365
140506
|
init_timeback6();
|
|
@@ -140381,7 +140522,7 @@ var init_users = __esm(async () => {
|
|
|
140381
140522
|
const error89 = ApiError.notFound("User not found");
|
|
140382
140523
|
return c2.json(createErrorResponse(error89), error89.status);
|
|
140383
140524
|
}
|
|
140384
|
-
const response = await buildMockUserResponse(db2, userData, gameId);
|
|
140525
|
+
const response = await buildMockUserResponse(db2, userData, gameId, c2.req.header(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER));
|
|
140385
140526
|
return c2.json(response);
|
|
140386
140527
|
}
|
|
140387
140528
|
return handle2(users2.getMe)(c2);
|