@playcademy/sandbox 0.6.0 → 0.6.1-beta.1
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 +121 -8
- package/dist/constants.js +1 -1
- package/dist/mocks/timeback.d.ts +1 -1
- package/dist/server.js +121 -8
- 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.1",
|
|
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"),
|
|
@@ -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 = [];
|
|
@@ -81568,13 +81574,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
81568
81574
|
};
|
|
81569
81575
|
}
|
|
81570
81576
|
}
|
|
81577
|
+
const timezone2 = await this.getLearningTimeZoneForStudent(timebackId);
|
|
81571
81578
|
const result = await client.analytics.getXp(timebackId, {
|
|
81572
81579
|
courseIds: courseIds.length > 0 ? courseIds : undefined,
|
|
81580
|
+
timezone: timezone2,
|
|
81573
81581
|
include: options?.include
|
|
81574
81582
|
});
|
|
81575
81583
|
return result;
|
|
81576
81584
|
});
|
|
81577
81585
|
}
|
|
81586
|
+
async getLearningTimeZoneForStudent(timebackId) {
|
|
81587
|
+
const user = await this.deps.db.query.users.findFirst({
|
|
81588
|
+
where: eq(users.timebackId, timebackId),
|
|
81589
|
+
columns: {
|
|
81590
|
+
learningTimeZone: true
|
|
81591
|
+
}
|
|
81592
|
+
});
|
|
81593
|
+
return user?.learningTimeZone ?? PLATFORM_TIMEZONE;
|
|
81594
|
+
}
|
|
81578
81595
|
async getStudentMastery(timebackId, user, options) {
|
|
81579
81596
|
return this.withClientTelemetry(async () => {
|
|
81580
81597
|
const client = this.requireClient();
|
|
@@ -82356,13 +82373,28 @@ var init_session_service = __esm(() => {
|
|
|
82356
82373
|
init_errors();
|
|
82357
82374
|
});
|
|
82358
82375
|
|
|
82376
|
+
// ../api-core/src/utils/timezone.util.ts
|
|
82377
|
+
function normalizeIanaTimeZone(value) {
|
|
82378
|
+
const timeZone = value?.trim();
|
|
82379
|
+
if (!timeZone || timeZone.length > MAX_IANA_TIME_ZONE_LENGTH) {
|
|
82380
|
+
return;
|
|
82381
|
+
}
|
|
82382
|
+
try {
|
|
82383
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date);
|
|
82384
|
+
return timeZone;
|
|
82385
|
+
} catch {
|
|
82386
|
+
return;
|
|
82387
|
+
}
|
|
82388
|
+
}
|
|
82389
|
+
var MAX_IANA_TIME_ZONE_LENGTH = 100;
|
|
82390
|
+
|
|
82359
82391
|
// ../api-core/src/services/user.service.ts
|
|
82360
82392
|
class UserService {
|
|
82361
82393
|
deps;
|
|
82362
82394
|
constructor(deps) {
|
|
82363
82395
|
this.deps = deps;
|
|
82364
82396
|
}
|
|
82365
|
-
async getMe(user, gameId) {
|
|
82397
|
+
async getMe(user, gameId, observedTimeZone) {
|
|
82366
82398
|
const db2 = this.deps.db;
|
|
82367
82399
|
const userData = await db2.query.users.findFirst({
|
|
82368
82400
|
where: eq(users.id, user.id)
|
|
@@ -82375,6 +82407,7 @@ class UserService {
|
|
|
82375
82407
|
"app.user.has_timeback_account": Boolean(userData.timebackId),
|
|
82376
82408
|
"app.user.timeback_enriched": false
|
|
82377
82409
|
});
|
|
82410
|
+
const localDay = await this.resolveLocalDay(userData, observedTimeZone);
|
|
82378
82411
|
const timeback2 = userData.timebackId ? await this.fetchTimebackData(userData.timebackId, gameId) : undefined;
|
|
82379
82412
|
setAttribute("app.user.timeback_enriched", Boolean(timeback2));
|
|
82380
82413
|
if (gameId) {
|
|
@@ -82384,6 +82417,7 @@ class UserService {
|
|
|
82384
82417
|
role: userData.role,
|
|
82385
82418
|
username: userData.username,
|
|
82386
82419
|
email: userData.email,
|
|
82420
|
+
localDay,
|
|
82387
82421
|
timeback: timeback2
|
|
82388
82422
|
};
|
|
82389
82423
|
}
|
|
@@ -82403,6 +82437,7 @@ class UserService {
|
|
|
82403
82437
|
createdAt: userData.createdAt,
|
|
82404
82438
|
updatedAt: userData.updatedAt,
|
|
82405
82439
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
82440
|
+
localDay,
|
|
82406
82441
|
timeback: timeback2
|
|
82407
82442
|
};
|
|
82408
82443
|
}
|
|
@@ -82440,6 +82475,44 @@ class UserService {
|
|
|
82440
82475
|
isDefault: updatedUser.name === DEMO_DISPLAY_NAME_PLACEHOLDER
|
|
82441
82476
|
};
|
|
82442
82477
|
}
|
|
82478
|
+
async resolveLocalDay(userData, observedTimeZone) {
|
|
82479
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
82480
|
+
if (normalizedTimeZone) {
|
|
82481
|
+
const observedAt = new Date;
|
|
82482
|
+
const shouldPersist = userData.learningTimeZone !== normalizedTimeZone || !userData.learningTimeZoneUpdatedAt || observedAt.getTime() - userData.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82483
|
+
if (shouldPersist) {
|
|
82484
|
+
await this.deps.db.update(users).set({
|
|
82485
|
+
learningTimeZone: normalizedTimeZone,
|
|
82486
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
82487
|
+
}).where(eq(users.id, userData.id));
|
|
82488
|
+
}
|
|
82489
|
+
setAttributes({
|
|
82490
|
+
"app.user.learning_time_zone_observed": true,
|
|
82491
|
+
"app.user.learning_time_zone_persisted": shouldPersist
|
|
82492
|
+
});
|
|
82493
|
+
return {
|
|
82494
|
+
timeZone: normalizedTimeZone,
|
|
82495
|
+
source: "browser_last_seen",
|
|
82496
|
+
observedAt: (shouldPersist ? observedAt : userData.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
82497
|
+
};
|
|
82498
|
+
}
|
|
82499
|
+
setAttributes({
|
|
82500
|
+
"app.user.learning_time_zone_observed": false,
|
|
82501
|
+
"app.user.learning_time_zone_persisted": false
|
|
82502
|
+
});
|
|
82503
|
+
if (userData.learningTimeZone) {
|
|
82504
|
+
return {
|
|
82505
|
+
timeZone: userData.learningTimeZone,
|
|
82506
|
+
source: "browser_last_seen",
|
|
82507
|
+
observedAt: userData.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
82508
|
+
};
|
|
82509
|
+
}
|
|
82510
|
+
return {
|
|
82511
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
82512
|
+
source: "platform_default",
|
|
82513
|
+
observedAt: null
|
|
82514
|
+
};
|
|
82515
|
+
}
|
|
82443
82516
|
async fetchTimebackData(timebackId, gameId) {
|
|
82444
82517
|
const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
|
|
82445
82518
|
withSpan("timeback.fetch_profile", () => this.fetchStudentProfile(timebackId)),
|
|
@@ -82521,6 +82594,7 @@ class UserService {
|
|
|
82521
82594
|
return organizations.filter((o) => enrollmentOrgIds.has(o.id));
|
|
82522
82595
|
}
|
|
82523
82596
|
}
|
|
82597
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82524
82598
|
var init_user_service = __esm(() => {
|
|
82525
82599
|
init_drizzle_orm();
|
|
82526
82600
|
init_src();
|
|
@@ -82529,6 +82603,7 @@ var init_user_service = __esm(() => {
|
|
|
82529
82603
|
init_spans();
|
|
82530
82604
|
init_errors();
|
|
82531
82605
|
init_timeback_util();
|
|
82606
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
82532
82607
|
});
|
|
82533
82608
|
|
|
82534
82609
|
// ../api-core/src/services/verify.service.ts
|
|
@@ -140159,9 +140234,10 @@ var init_upload_controller = __esm(() => {
|
|
|
140159
140234
|
// ../api-core/src/controllers/user.controller.ts
|
|
140160
140235
|
var getMe, getDemoProfile, updateDemoProfile, users2;
|
|
140161
140236
|
var init_user_controller = __esm(() => {
|
|
140237
|
+
init_src();
|
|
140162
140238
|
init_schemas_index();
|
|
140163
140239
|
init_utils11();
|
|
140164
|
-
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId));
|
|
140240
|
+
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId, ctx.request.headers.get(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER)));
|
|
140165
140241
|
getDemoProfile = requireAnonymous(async (ctx) => ctx.services.user.getDemoProfile(ctx.user.id));
|
|
140166
140242
|
updateDemoProfile = requireAnonymous(async (ctx) => {
|
|
140167
140243
|
const body2 = await parseRequestBody(ctx.request, DemoProfileSchema);
|
|
@@ -140316,8 +140392,39 @@ function getMockHighestGradeMastered(enrollments, subject) {
|
|
|
140316
140392
|
const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
|
|
140317
140393
|
return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
|
|
140318
140394
|
}
|
|
140319
|
-
async function
|
|
140395
|
+
async function buildLocalDayContext(db2, user, observedTimeZone) {
|
|
140396
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
140397
|
+
if (normalizedTimeZone) {
|
|
140398
|
+
const observedAt = new Date;
|
|
140399
|
+
const shouldPersist = user.learningTimeZone !== normalizedTimeZone || !user.learningTimeZoneUpdatedAt || observedAt.getTime() - user.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140400
|
+
if (shouldPersist) {
|
|
140401
|
+
await db2.update(users).set({
|
|
140402
|
+
learningTimeZone: normalizedTimeZone,
|
|
140403
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
140404
|
+
}).where(eq(users.id, user.id));
|
|
140405
|
+
}
|
|
140406
|
+
return {
|
|
140407
|
+
timeZone: normalizedTimeZone,
|
|
140408
|
+
source: "browser_last_seen",
|
|
140409
|
+
observedAt: (shouldPersist ? observedAt : user.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
140410
|
+
};
|
|
140411
|
+
}
|
|
140412
|
+
if (user.learningTimeZone) {
|
|
140413
|
+
return {
|
|
140414
|
+
timeZone: user.learningTimeZone,
|
|
140415
|
+
source: "browser_last_seen",
|
|
140416
|
+
observedAt: user.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
140417
|
+
};
|
|
140418
|
+
}
|
|
140419
|
+
return {
|
|
140420
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
140421
|
+
source: "platform_default",
|
|
140422
|
+
observedAt: null
|
|
140423
|
+
};
|
|
140424
|
+
}
|
|
140425
|
+
async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
|
|
140320
140426
|
const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
|
|
140427
|
+
const localDay = await buildLocalDayContext(db2, user, observedTimeZone);
|
|
140321
140428
|
if (gameId) {
|
|
140322
140429
|
return {
|
|
140323
140430
|
id: user.id,
|
|
@@ -140325,6 +140432,7 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140325
140432
|
role: user.role,
|
|
140326
140433
|
username: user.username,
|
|
140327
140434
|
email: user.email,
|
|
140435
|
+
localDay,
|
|
140328
140436
|
timeback: timeback3
|
|
140329
140437
|
};
|
|
140330
140438
|
}
|
|
@@ -140343,15 +140451,19 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140343
140451
|
createdAt: user.createdAt,
|
|
140344
140452
|
updatedAt: user.updatedAt,
|
|
140345
140453
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
140454
|
+
localDay,
|
|
140346
140455
|
timeback: timeback3
|
|
140347
140456
|
};
|
|
140348
140457
|
}
|
|
140458
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140349
140459
|
var init_timeback6 = __esm(() => {
|
|
140350
140460
|
init_drizzle_orm();
|
|
140351
140461
|
init_utils11();
|
|
140462
|
+
init_src();
|
|
140352
140463
|
init_tables_index();
|
|
140353
140464
|
init_src2();
|
|
140354
140465
|
init_config();
|
|
140466
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2 = 24 * 60 * 60 * 1000;
|
|
140355
140467
|
});
|
|
140356
140468
|
|
|
140357
140469
|
// src/routes/platform/users.ts
|
|
@@ -140361,6 +140473,7 @@ var init_users = __esm(async () => {
|
|
|
140361
140473
|
init_dist7();
|
|
140362
140474
|
init_controllers();
|
|
140363
140475
|
init_errors();
|
|
140476
|
+
init_src();
|
|
140364
140477
|
init_tables_index();
|
|
140365
140478
|
init_error_handler();
|
|
140366
140479
|
init_timeback6();
|
|
@@ -140382,7 +140495,7 @@ var init_users = __esm(async () => {
|
|
|
140382
140495
|
const error89 = ApiError.notFound("User not found");
|
|
140383
140496
|
return c2.json(createErrorResponse(error89), error89.status);
|
|
140384
140497
|
}
|
|
140385
|
-
const response = await buildMockUserResponse(db2, userData, gameId);
|
|
140498
|
+
const response = await buildMockUserResponse(db2, userData, gameId, c2.req.header(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER));
|
|
140386
140499
|
return c2.json(response);
|
|
140387
140500
|
}
|
|
140388
140501
|
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.1",
|
|
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"),
|
|
@@ -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 = [];
|
|
@@ -81567,13 +81573,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
81567
81573
|
};
|
|
81568
81574
|
}
|
|
81569
81575
|
}
|
|
81576
|
+
const timezone2 = await this.getLearningTimeZoneForStudent(timebackId);
|
|
81570
81577
|
const result = await client.analytics.getXp(timebackId, {
|
|
81571
81578
|
courseIds: courseIds.length > 0 ? courseIds : undefined,
|
|
81579
|
+
timezone: timezone2,
|
|
81572
81580
|
include: options?.include
|
|
81573
81581
|
});
|
|
81574
81582
|
return result;
|
|
81575
81583
|
});
|
|
81576
81584
|
}
|
|
81585
|
+
async getLearningTimeZoneForStudent(timebackId) {
|
|
81586
|
+
const user = await this.deps.db.query.users.findFirst({
|
|
81587
|
+
where: eq(users.timebackId, timebackId),
|
|
81588
|
+
columns: {
|
|
81589
|
+
learningTimeZone: true
|
|
81590
|
+
}
|
|
81591
|
+
});
|
|
81592
|
+
return user?.learningTimeZone ?? PLATFORM_TIMEZONE;
|
|
81593
|
+
}
|
|
81577
81594
|
async getStudentMastery(timebackId, user, options) {
|
|
81578
81595
|
return this.withClientTelemetry(async () => {
|
|
81579
81596
|
const client = this.requireClient();
|
|
@@ -82355,13 +82372,28 @@ var init_session_service = __esm(() => {
|
|
|
82355
82372
|
init_errors();
|
|
82356
82373
|
});
|
|
82357
82374
|
|
|
82375
|
+
// ../api-core/src/utils/timezone.util.ts
|
|
82376
|
+
function normalizeIanaTimeZone(value) {
|
|
82377
|
+
const timeZone = value?.trim();
|
|
82378
|
+
if (!timeZone || timeZone.length > MAX_IANA_TIME_ZONE_LENGTH) {
|
|
82379
|
+
return;
|
|
82380
|
+
}
|
|
82381
|
+
try {
|
|
82382
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date);
|
|
82383
|
+
return timeZone;
|
|
82384
|
+
} catch {
|
|
82385
|
+
return;
|
|
82386
|
+
}
|
|
82387
|
+
}
|
|
82388
|
+
var MAX_IANA_TIME_ZONE_LENGTH = 100;
|
|
82389
|
+
|
|
82358
82390
|
// ../api-core/src/services/user.service.ts
|
|
82359
82391
|
class UserService {
|
|
82360
82392
|
deps;
|
|
82361
82393
|
constructor(deps) {
|
|
82362
82394
|
this.deps = deps;
|
|
82363
82395
|
}
|
|
82364
|
-
async getMe(user, gameId) {
|
|
82396
|
+
async getMe(user, gameId, observedTimeZone) {
|
|
82365
82397
|
const db2 = this.deps.db;
|
|
82366
82398
|
const userData = await db2.query.users.findFirst({
|
|
82367
82399
|
where: eq(users.id, user.id)
|
|
@@ -82374,6 +82406,7 @@ class UserService {
|
|
|
82374
82406
|
"app.user.has_timeback_account": Boolean(userData.timebackId),
|
|
82375
82407
|
"app.user.timeback_enriched": false
|
|
82376
82408
|
});
|
|
82409
|
+
const localDay = await this.resolveLocalDay(userData, observedTimeZone);
|
|
82377
82410
|
const timeback2 = userData.timebackId ? await this.fetchTimebackData(userData.timebackId, gameId) : undefined;
|
|
82378
82411
|
setAttribute("app.user.timeback_enriched", Boolean(timeback2));
|
|
82379
82412
|
if (gameId) {
|
|
@@ -82383,6 +82416,7 @@ class UserService {
|
|
|
82383
82416
|
role: userData.role,
|
|
82384
82417
|
username: userData.username,
|
|
82385
82418
|
email: userData.email,
|
|
82419
|
+
localDay,
|
|
82386
82420
|
timeback: timeback2
|
|
82387
82421
|
};
|
|
82388
82422
|
}
|
|
@@ -82402,6 +82436,7 @@ class UserService {
|
|
|
82402
82436
|
createdAt: userData.createdAt,
|
|
82403
82437
|
updatedAt: userData.updatedAt,
|
|
82404
82438
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
82439
|
+
localDay,
|
|
82405
82440
|
timeback: timeback2
|
|
82406
82441
|
};
|
|
82407
82442
|
}
|
|
@@ -82439,6 +82474,44 @@ class UserService {
|
|
|
82439
82474
|
isDefault: updatedUser.name === DEMO_DISPLAY_NAME_PLACEHOLDER
|
|
82440
82475
|
};
|
|
82441
82476
|
}
|
|
82477
|
+
async resolveLocalDay(userData, observedTimeZone) {
|
|
82478
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
82479
|
+
if (normalizedTimeZone) {
|
|
82480
|
+
const observedAt = new Date;
|
|
82481
|
+
const shouldPersist = userData.learningTimeZone !== normalizedTimeZone || !userData.learningTimeZoneUpdatedAt || observedAt.getTime() - userData.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82482
|
+
if (shouldPersist) {
|
|
82483
|
+
await this.deps.db.update(users).set({
|
|
82484
|
+
learningTimeZone: normalizedTimeZone,
|
|
82485
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
82486
|
+
}).where(eq(users.id, userData.id));
|
|
82487
|
+
}
|
|
82488
|
+
setAttributes({
|
|
82489
|
+
"app.user.learning_time_zone_observed": true,
|
|
82490
|
+
"app.user.learning_time_zone_persisted": shouldPersist
|
|
82491
|
+
});
|
|
82492
|
+
return {
|
|
82493
|
+
timeZone: normalizedTimeZone,
|
|
82494
|
+
source: "browser_last_seen",
|
|
82495
|
+
observedAt: (shouldPersist ? observedAt : userData.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
82496
|
+
};
|
|
82497
|
+
}
|
|
82498
|
+
setAttributes({
|
|
82499
|
+
"app.user.learning_time_zone_observed": false,
|
|
82500
|
+
"app.user.learning_time_zone_persisted": false
|
|
82501
|
+
});
|
|
82502
|
+
if (userData.learningTimeZone) {
|
|
82503
|
+
return {
|
|
82504
|
+
timeZone: userData.learningTimeZone,
|
|
82505
|
+
source: "browser_last_seen",
|
|
82506
|
+
observedAt: userData.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
82507
|
+
};
|
|
82508
|
+
}
|
|
82509
|
+
return {
|
|
82510
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
82511
|
+
source: "platform_default",
|
|
82512
|
+
observedAt: null
|
|
82513
|
+
};
|
|
82514
|
+
}
|
|
82442
82515
|
async fetchTimebackData(timebackId, gameId) {
|
|
82443
82516
|
const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
|
|
82444
82517
|
withSpan("timeback.fetch_profile", () => this.fetchStudentProfile(timebackId)),
|
|
@@ -82520,6 +82593,7 @@ class UserService {
|
|
|
82520
82593
|
return organizations.filter((o) => enrollmentOrgIds.has(o.id));
|
|
82521
82594
|
}
|
|
82522
82595
|
}
|
|
82596
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
82523
82597
|
var init_user_service = __esm(() => {
|
|
82524
82598
|
init_drizzle_orm();
|
|
82525
82599
|
init_src();
|
|
@@ -82528,6 +82602,7 @@ var init_user_service = __esm(() => {
|
|
|
82528
82602
|
init_spans();
|
|
82529
82603
|
init_errors();
|
|
82530
82604
|
init_timeback_util();
|
|
82605
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
82531
82606
|
});
|
|
82532
82607
|
|
|
82533
82608
|
// ../api-core/src/services/verify.service.ts
|
|
@@ -140158,9 +140233,10 @@ var init_upload_controller = __esm(() => {
|
|
|
140158
140233
|
// ../api-core/src/controllers/user.controller.ts
|
|
140159
140234
|
var getMe, getDemoProfile, updateDemoProfile, users2;
|
|
140160
140235
|
var init_user_controller = __esm(() => {
|
|
140236
|
+
init_src();
|
|
140161
140237
|
init_schemas_index();
|
|
140162
140238
|
init_utils11();
|
|
140163
|
-
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId));
|
|
140239
|
+
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId, ctx.request.headers.get(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER)));
|
|
140164
140240
|
getDemoProfile = requireAnonymous(async (ctx) => ctx.services.user.getDemoProfile(ctx.user.id));
|
|
140165
140241
|
updateDemoProfile = requireAnonymous(async (ctx) => {
|
|
140166
140242
|
const body2 = await parseRequestBody(ctx.request, DemoProfileSchema);
|
|
@@ -140315,8 +140391,39 @@ function getMockHighestGradeMastered(enrollments, subject) {
|
|
|
140315
140391
|
const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
|
|
140316
140392
|
return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
|
|
140317
140393
|
}
|
|
140318
|
-
async function
|
|
140394
|
+
async function buildLocalDayContext(db2, user, observedTimeZone) {
|
|
140395
|
+
const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
|
|
140396
|
+
if (normalizedTimeZone) {
|
|
140397
|
+
const observedAt = new Date;
|
|
140398
|
+
const shouldPersist = user.learningTimeZone !== normalizedTimeZone || !user.learningTimeZoneUpdatedAt || observedAt.getTime() - user.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140399
|
+
if (shouldPersist) {
|
|
140400
|
+
await db2.update(users).set({
|
|
140401
|
+
learningTimeZone: normalizedTimeZone,
|
|
140402
|
+
learningTimeZoneUpdatedAt: observedAt
|
|
140403
|
+
}).where(eq(users.id, user.id));
|
|
140404
|
+
}
|
|
140405
|
+
return {
|
|
140406
|
+
timeZone: normalizedTimeZone,
|
|
140407
|
+
source: "browser_last_seen",
|
|
140408
|
+
observedAt: (shouldPersist ? observedAt : user.learningTimeZoneUpdatedAt)?.toISOString() ?? null
|
|
140409
|
+
};
|
|
140410
|
+
}
|
|
140411
|
+
if (user.learningTimeZone) {
|
|
140412
|
+
return {
|
|
140413
|
+
timeZone: user.learningTimeZone,
|
|
140414
|
+
source: "browser_last_seen",
|
|
140415
|
+
observedAt: user.learningTimeZoneUpdatedAt?.toISOString() ?? null
|
|
140416
|
+
};
|
|
140417
|
+
}
|
|
140418
|
+
return {
|
|
140419
|
+
timeZone: PLATFORM_TIMEZONE,
|
|
140420
|
+
source: "platform_default",
|
|
140421
|
+
observedAt: null
|
|
140422
|
+
};
|
|
140423
|
+
}
|
|
140424
|
+
async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
|
|
140319
140425
|
const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
|
|
140426
|
+
const localDay = await buildLocalDayContext(db2, user, observedTimeZone);
|
|
140320
140427
|
if (gameId) {
|
|
140321
140428
|
return {
|
|
140322
140429
|
id: user.id,
|
|
@@ -140324,6 +140431,7 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140324
140431
|
role: user.role,
|
|
140325
140432
|
username: user.username,
|
|
140326
140433
|
email: user.email,
|
|
140434
|
+
localDay,
|
|
140327
140435
|
timeback: timeback3
|
|
140328
140436
|
};
|
|
140329
140437
|
}
|
|
@@ -140342,15 +140450,19 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
140342
140450
|
createdAt: user.createdAt,
|
|
140343
140451
|
updatedAt: user.updatedAt,
|
|
140344
140452
|
hasTimebackAccount: Boolean(timebackAccount),
|
|
140453
|
+
localDay,
|
|
140345
140454
|
timeback: timeback3
|
|
140346
140455
|
};
|
|
140347
140456
|
}
|
|
140457
|
+
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
140348
140458
|
var init_timeback6 = __esm(() => {
|
|
140349
140459
|
init_drizzle_orm();
|
|
140350
140460
|
init_utils11();
|
|
140461
|
+
init_src();
|
|
140351
140462
|
init_tables_index();
|
|
140352
140463
|
init_src2();
|
|
140353
140464
|
init_config();
|
|
140465
|
+
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2 = 24 * 60 * 60 * 1000;
|
|
140354
140466
|
});
|
|
140355
140467
|
|
|
140356
140468
|
// src/routes/platform/users.ts
|
|
@@ -140360,6 +140472,7 @@ var init_users = __esm(async () => {
|
|
|
140360
140472
|
init_dist7();
|
|
140361
140473
|
init_controllers();
|
|
140362
140474
|
init_errors();
|
|
140475
|
+
init_src();
|
|
140363
140476
|
init_tables_index();
|
|
140364
140477
|
init_error_handler();
|
|
140365
140478
|
init_timeback6();
|
|
@@ -140381,7 +140494,7 @@ var init_users = __esm(async () => {
|
|
|
140381
140494
|
const error89 = ApiError.notFound("User not found");
|
|
140382
140495
|
return c2.json(createErrorResponse(error89), error89.status);
|
|
140383
140496
|
}
|
|
140384
|
-
const response = await buildMockUserResponse(db2, userData, gameId);
|
|
140497
|
+
const response = await buildMockUserResponse(db2, userData, gameId, c2.req.header(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER));
|
|
140385
140498
|
return c2.json(response);
|
|
140386
140499
|
}
|
|
140387
140500
|
return handle2(users2.getMe)(c2);
|