@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 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.0",
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: PLATFORM_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(PLATFORM_TIMEZONE);
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 buildMockUserResponse(db2, user, gameId) {
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"
@@ -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.0",
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: PLATFORM_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(PLATFORM_TIMEZONE);
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 buildMockUserResponse(db2, user, gameId) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-beta.1",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {