@playcademy/vite-plugin 1.1.2-beta.1 → 1.1.2-beta.3

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.
Files changed (2) hide show
  1. package/dist/index.js +161 -21
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -23746,7 +23746,7 @@ import path from "node:path";
23746
23746
  // package.json
23747
23747
  var package_default = {
23748
23748
  name: "@playcademy/vite-plugin",
23749
- version: "1.1.2-beta.1",
23749
+ version: "1.1.2-beta.3",
23750
23750
  type: "module",
23751
23751
  exports: {
23752
23752
  ".": {
@@ -24476,6 +24476,7 @@ var init_game = __esm(() => {
24476
24476
  COLLABORATOR: "collaborator"
24477
24477
  };
24478
24478
  });
24479
+ var PLAYCADEMY_BROWSER_TIME_ZONE_HEADER = "x-playcademy-browser-time-zone";
24479
24480
  var CORE_GAME_UUIDS;
24480
24481
  var init_platform = __esm(() => {
24481
24482
  CORE_GAME_UUIDS = {
@@ -25344,7 +25345,7 @@ var package_default2;
25344
25345
  var init_package = __esm(() => {
25345
25346
  package_default2 = {
25346
25347
  name: "@playcademy/sandbox",
25347
- version: "0.6.0",
25348
+ version: "0.6.1-beta.2",
25348
25349
  description: "Local development server for Playcademy game development",
25349
25350
  type: "module",
25350
25351
  exports: {
@@ -35335,6 +35336,11 @@ var init_table3 = __esm(() => {
35335
35336
  email: text("email").notNull().unique(),
35336
35337
  isAnonymous: boolean("is_anonymous").notNull().default(false),
35337
35338
  timebackId: text("timeback_id").unique(),
35339
+ learningTimeZone: text("learning_time_zone"),
35340
+ learningTimeZoneUpdatedAt: timestamp("learning_time_zone_updated_at", {
35341
+ mode: "date",
35342
+ withTimezone: true
35343
+ }),
35338
35344
  emailVerified: boolean("email_verified").notNull().default(false),
35339
35345
  image: text("image"),
35340
35346
  role: userRoleEnum("role").notNull().default("player"),
@@ -55047,7 +55053,7 @@ var init_schemas4 = __esm(() => {
55047
55053
  subject: TimebackSubjectSchema,
55048
55054
  appName: exports_external.string().optional(),
55049
55055
  sensorUrl: exports_external.string().url().optional(),
55050
- courseId: exports_external.string().optional(),
55056
+ courseId: exports_external.string().min(1).optional(),
55051
55057
  courseName: exports_external.string().optional(),
55052
55058
  studentEmail: exports_external.string().email().optional()
55053
55059
  });
@@ -100786,6 +100792,7 @@ class AnalyticsXp {
100786
100792
  this.core = core3;
100787
100793
  }
100788
100794
  async get(studentId, options) {
100795
+ const timezone2 = options?.timezone ?? PLATFORM_TIMEZONE;
100789
100796
  const enrollments = await this.core.api.edubridge.enrollments.list({ userId: studentId });
100790
100797
  const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
100791
100798
  if (filteredEnrollments.length === 0) {
@@ -100799,7 +100806,7 @@ class AnalyticsXp {
100799
100806
  try {
100800
100807
  const analytics = await this.core.api.edubridge.analytics.getEnrollmentFacts({
100801
100808
  enrollmentId: enrollment.id,
100802
- timezone: PLATFORM_TIMEZONE
100809
+ timezone: timezone2
100803
100810
  });
100804
100811
  return { enrollment, analytics };
100805
100812
  } catch (error88) {
@@ -100811,7 +100818,7 @@ class AnalyticsXp {
100811
100818
  return { enrollment, analytics: null };
100812
100819
  }
100813
100820
  }));
100814
- const today = formatDateYMDInTimezone(PLATFORM_TIMEZONE);
100821
+ const today = formatDateYMDInTimezone(timezone2);
100815
100822
  let totalXp = 0;
100816
100823
  let todayXp = 0;
100817
100824
  const courses = [];
@@ -105571,6 +105578,24 @@ var init_timeback_service = __esm(async () => {
105571
105578
  }
105572
105579
  };
105573
105580
  }
105581
+ static resolveRuntimeCourse(resolvedCourseId, requestedCourseId, courseName) {
105582
+ let requestedCourseIdStatus = "absent";
105583
+ if (requestedCourseId === resolvedCourseId) {
105584
+ requestedCourseIdStatus = "accepted";
105585
+ } else if (requestedCourseId !== undefined) {
105586
+ requestedCourseIdStatus = "ignored_mismatch";
105587
+ }
105588
+ return {
105589
+ courseId: resolvedCourseId,
105590
+ courseName: requestedCourseIdStatus === "ignored_mismatch" ? undefined : courseName,
105591
+ attributes: {
105592
+ "app.timeback.course_id": resolvedCourseId,
105593
+ "app.timeback.resolved_course_id": resolvedCourseId,
105594
+ "app.timeback.requested_course_id": requestedCourseId,
105595
+ "app.timeback.requested_course_id_status": requestedCourseIdStatus
105596
+ }
105597
+ };
105598
+ }
105574
105599
  static recordRuntimeContext({
105575
105600
  operation,
105576
105601
  studentId,
@@ -106436,8 +106461,13 @@ var init_timeback_service = __esm(async () => {
106436
106461
  if (!integration) {
106437
106462
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
106438
106463
  }
106464
+ const {
106465
+ courseId: runtimeCourseId,
106466
+ courseName: runtimeCourseName,
106467
+ attributes: courseAttributes
106468
+ } = TimebackService2.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
106439
106469
  const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
106440
- const result = await client.activity.record(integration.courseId, studentId, {
106470
+ const result = await client.activity.record(runtimeCourseId, studentId, {
106441
106471
  gameId,
106442
106472
  score: scorePercentage,
106443
106473
  totalQuestions: scoreData.totalQuestions,
@@ -106452,8 +106482,8 @@ var init_timeback_service = __esm(async () => {
106452
106482
  subject: activityData.subject,
106453
106483
  appName: activityData.appName,
106454
106484
  sensorUrl: activityData.sensorUrl,
106455
- courseId: activityData.courseId,
106456
- courseName: activityData.courseName,
106485
+ courseId: runtimeCourseId,
106486
+ courseName: runtimeCourseName,
106457
106487
  studentEmail: activityData.studentEmail,
106458
106488
  courseTotalXp: integration.totalXp,
106459
106489
  ...runId ? { runId } : {}
@@ -106462,7 +106492,7 @@ var init_timeback_service = __esm(async () => {
106462
106492
  const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
106463
106493
  const sessionEndEmitted = sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0;
106464
106494
  if (sessionEndEmitted) {
106465
- await client.activity.session(integration.courseId, studentId, {
106495
+ await client.activity.session(runtimeCourseId, studentId, {
106466
106496
  gameId,
106467
106497
  activeTimeSeconds: sessionEndActiveSeconds,
106468
106498
  ...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {},
@@ -106471,15 +106501,15 @@ var init_timeback_service = __esm(async () => {
106471
106501
  subject: activityData.subject,
106472
106502
  appName: activityData.appName,
106473
106503
  sensorUrl: activityData.sensorUrl,
106474
- courseId: activityData.courseId,
106475
- courseName: activityData.courseName,
106504
+ courseId: runtimeCourseId,
106505
+ courseName: runtimeCourseName,
106476
106506
  studentEmail: activityData.studentEmail,
106477
106507
  extensions: extensionsWithResumeId,
106478
106508
  ...runId ? { runId } : {}
106479
106509
  });
106480
106510
  }
106481
106511
  setAttributes({
106482
- "app.timeback.course_id": integration.courseId,
106512
+ ...courseAttributes,
106483
106513
  "app.timeback.grade": activityData.grade,
106484
106514
  "app.timeback.subject": activityData.subject,
106485
106515
  "app.timeback.total_questions": scoreData.totalQuestions,
@@ -106496,7 +106526,7 @@ var init_timeback_service = __esm(async () => {
106496
106526
  });
106497
106527
  return {
106498
106528
  status: "ok",
106499
- courseId: integration.courseId,
106529
+ courseId: runtimeCourseId,
106500
106530
  xpAwarded: result.xpAwarded,
106501
106531
  masteredUnits: result.masteredUnitsApplied,
106502
106532
  pctCompleteApp: result.pctCompleteApp,
@@ -106707,10 +106737,15 @@ var init_timeback_service = __esm(async () => {
106707
106737
  if (!integration) {
106708
106738
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
106709
106739
  }
106740
+ const {
106741
+ courseId: runtimeCourseId,
106742
+ courseName: runtimeCourseName,
106743
+ attributes: courseAttributes
106744
+ } = TimebackService2.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
106710
106745
  const activeTimeSeconds = timingData.activeMs / 1000;
106711
106746
  const inactiveTimeSeconds = timingData.pausedMs / 1000;
106712
106747
  if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
106713
- await client.activity.session(integration.courseId, studentId, {
106748
+ await client.activity.session(runtimeCourseId, studentId, {
106714
106749
  gameId,
106715
106750
  activeTimeSeconds,
106716
106751
  ...inactiveTimeSeconds > 0 ? { inactiveTimeSeconds } : {},
@@ -106719,15 +106754,15 @@ var init_timeback_service = __esm(async () => {
106719
106754
  subject: activityData.subject,
106720
106755
  appName: activityData.appName,
106721
106756
  sensorUrl: activityData.sensorUrl,
106722
- courseId: activityData.courseId,
106723
- courseName: activityData.courseName,
106757
+ courseId: runtimeCourseId,
106758
+ courseName: runtimeCourseName,
106724
106759
  studentEmail: activityData.studentEmail,
106725
106760
  extensions: TimebackService2.addResumeIdToExtensions(undefined, effectiveResumeId),
106726
106761
  ...runId ? { runId } : {}
106727
106762
  });
106728
106763
  }
106729
106764
  setAttributes({
106730
- "app.timeback.course_id": integration.courseId,
106765
+ ...courseAttributes,
106731
106766
  "app.timeback.grade": activityData.grade,
106732
106767
  "app.timeback.subject": activityData.subject,
106733
106768
  "app.timeback.active_time_seconds": activeTimeSeconds,
@@ -106772,13 +106807,24 @@ var init_timeback_service = __esm(async () => {
106772
106807
  };
106773
106808
  }
106774
106809
  }
106810
+ const timezone2 = await this.getLearningTimeZoneForStudent(timebackId);
106775
106811
  const result = await client.analytics.getXp(timebackId, {
106776
106812
  courseIds: courseIds.length > 0 ? courseIds : undefined,
106813
+ timezone: timezone2,
106777
106814
  include: options?.include
106778
106815
  });
106779
106816
  return result;
106780
106817
  });
106781
106818
  }
106819
+ async getLearningTimeZoneForStudent(timebackId) {
106820
+ const user = await this.deps.db.query.users.findFirst({
106821
+ where: eq(users.timebackId, timebackId),
106822
+ columns: {
106823
+ learningTimeZone: true
106824
+ }
106825
+ });
106826
+ return user?.learningTimeZone ?? PLATFORM_TIMEZONE;
106827
+ }
106782
106828
  async getStudentMastery(timebackId, user, options) {
106783
106829
  return this.withClientTelemetry(async () => {
106784
106830
  const client = this.requireClient();
@@ -107540,13 +107586,26 @@ var init_session_service = __esm(() => {
107540
107586
  init_spans();
107541
107587
  init_errors();
107542
107588
  });
107589
+ function normalizeIanaTimeZone(value) {
107590
+ const timeZone = value?.trim();
107591
+ if (!timeZone || timeZone.length > MAX_IANA_TIME_ZONE_LENGTH) {
107592
+ return;
107593
+ }
107594
+ try {
107595
+ new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date);
107596
+ return timeZone;
107597
+ } catch {
107598
+ return;
107599
+ }
107600
+ }
107601
+ var MAX_IANA_TIME_ZONE_LENGTH = 100;
107543
107602
 
107544
107603
  class UserService {
107545
107604
  deps;
107546
107605
  constructor(deps) {
107547
107606
  this.deps = deps;
107548
107607
  }
107549
- async getMe(user, gameId) {
107608
+ async getMe(user, gameId, observedTimeZone) {
107550
107609
  const db2 = this.deps.db;
107551
107610
  const userData = await db2.query.users.findFirst({
107552
107611
  where: eq(users.id, user.id)
@@ -107559,6 +107618,7 @@ class UserService {
107559
107618
  "app.user.has_timeback_account": Boolean(userData.timebackId),
107560
107619
  "app.user.timeback_enriched": false
107561
107620
  });
107621
+ const localDay = await this.resolveLocalDay(userData, observedTimeZone);
107562
107622
  const timeback2 = userData.timebackId ? await this.fetchTimebackData(userData.timebackId, gameId) : undefined;
107563
107623
  setAttribute("app.user.timeback_enriched", Boolean(timeback2));
107564
107624
  if (gameId) {
@@ -107568,6 +107628,7 @@ class UserService {
107568
107628
  role: userData.role,
107569
107629
  username: userData.username,
107570
107630
  email: userData.email,
107631
+ localDay,
107571
107632
  timeback: timeback2
107572
107633
  };
107573
107634
  }
@@ -107587,6 +107648,7 @@ class UserService {
107587
107648
  createdAt: userData.createdAt,
107588
107649
  updatedAt: userData.updatedAt,
107589
107650
  hasTimebackAccount: Boolean(timebackAccount),
107651
+ localDay,
107590
107652
  timeback: timeback2
107591
107653
  };
107592
107654
  }
@@ -107624,6 +107686,44 @@ class UserService {
107624
107686
  isDefault: updatedUser.name === DEMO_DISPLAY_NAME_PLACEHOLDER
107625
107687
  };
107626
107688
  }
107689
+ async resolveLocalDay(userData, observedTimeZone) {
107690
+ const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
107691
+ if (normalizedTimeZone) {
107692
+ const observedAt = new Date;
107693
+ const shouldPersist = userData.learningTimeZone !== normalizedTimeZone || !userData.learningTimeZoneUpdatedAt || observedAt.getTime() - userData.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
107694
+ if (shouldPersist) {
107695
+ await this.deps.db.update(users).set({
107696
+ learningTimeZone: normalizedTimeZone,
107697
+ learningTimeZoneUpdatedAt: observedAt
107698
+ }).where(eq(users.id, userData.id));
107699
+ }
107700
+ setAttributes({
107701
+ "app.user.learning_time_zone_observed": true,
107702
+ "app.user.learning_time_zone_persisted": shouldPersist
107703
+ });
107704
+ return {
107705
+ timeZone: normalizedTimeZone,
107706
+ source: "browser_last_seen",
107707
+ observedAt: (shouldPersist ? observedAt : userData.learningTimeZoneUpdatedAt)?.toISOString() ?? null
107708
+ };
107709
+ }
107710
+ setAttributes({
107711
+ "app.user.learning_time_zone_observed": false,
107712
+ "app.user.learning_time_zone_persisted": false
107713
+ });
107714
+ if (userData.learningTimeZone) {
107715
+ return {
107716
+ timeZone: userData.learningTimeZone,
107717
+ source: "browser_last_seen",
107718
+ observedAt: userData.learningTimeZoneUpdatedAt?.toISOString() ?? null
107719
+ };
107720
+ }
107721
+ return {
107722
+ timeZone: PLATFORM_TIMEZONE,
107723
+ source: "platform_default",
107724
+ observedAt: null
107725
+ };
107726
+ }
107627
107727
  async fetchTimebackData(timebackId, gameId) {
107628
107728
  const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
107629
107729
  withSpan("timeback.fetch_profile", () => this.fetchStudentProfile(timebackId)),
@@ -107705,6 +107805,7 @@ class UserService {
107705
107805
  return organizations.filter((o) => enrollmentOrgIds.has(o.id));
107706
107806
  }
107707
107807
  }
107808
+ var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
107708
107809
  var init_user_service = __esm(() => {
107709
107810
  init_drizzle_orm();
107710
107811
  init_src();
@@ -107713,6 +107814,7 @@ var init_user_service = __esm(() => {
107713
107814
  init_spans();
107714
107815
  init_errors();
107715
107816
  init_timeback_util();
107817
+ LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS = 86400000;
107716
107818
  });
107717
107819
 
107718
107820
  class VerifyService {
@@ -168001,9 +168103,10 @@ var getDemoProfile;
168001
168103
  var updateDemoProfile;
168002
168104
  var users2;
168003
168105
  var init_user_controller = __esm(() => {
168106
+ init_src();
168004
168107
  init_schemas_index();
168005
168108
  init_utils11();
168006
- getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId));
168109
+ getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId, ctx.request.headers.get(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER)));
168007
168110
  getDemoProfile = requireAnonymous(async (ctx) => ctx.services.user.getDemoProfile(ctx.user.id));
168008
168111
  updateDemoProfile = requireAnonymous(async (ctx) => {
168009
168112
  const body2 = await parseRequestBody(ctx.request, DemoProfileSchema);
@@ -168148,8 +168251,39 @@ function getMockHighestGradeMastered(enrollments, subject) {
168148
168251
  const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
168149
168252
  return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
168150
168253
  }
168151
- async function buildMockUserResponse(db2, user, gameId) {
168254
+ async function buildLocalDayContext(db2, user, observedTimeZone) {
168255
+ const normalizedTimeZone = normalizeIanaTimeZone(observedTimeZone);
168256
+ if (normalizedTimeZone) {
168257
+ const observedAt = new Date;
168258
+ const shouldPersist = user.learningTimeZone !== normalizedTimeZone || !user.learningTimeZoneUpdatedAt || observedAt.getTime() - user.learningTimeZoneUpdatedAt.getTime() >= LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
168259
+ if (shouldPersist) {
168260
+ await db2.update(users).set({
168261
+ learningTimeZone: normalizedTimeZone,
168262
+ learningTimeZoneUpdatedAt: observedAt
168263
+ }).where(eq(users.id, user.id));
168264
+ }
168265
+ return {
168266
+ timeZone: normalizedTimeZone,
168267
+ source: "browser_last_seen",
168268
+ observedAt: (shouldPersist ? observedAt : user.learningTimeZoneUpdatedAt)?.toISOString() ?? null
168269
+ };
168270
+ }
168271
+ if (user.learningTimeZone) {
168272
+ return {
168273
+ timeZone: user.learningTimeZone,
168274
+ source: "browser_last_seen",
168275
+ observedAt: user.learningTimeZoneUpdatedAt?.toISOString() ?? null
168276
+ };
168277
+ }
168278
+ return {
168279
+ timeZone: PLATFORM_TIMEZONE,
168280
+ source: "platform_default",
168281
+ observedAt: null
168282
+ };
168283
+ }
168284
+ async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
168152
168285
  const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
168286
+ const localDay = await buildLocalDayContext(db2, user, observedTimeZone);
168153
168287
  if (gameId) {
168154
168288
  return {
168155
168289
  id: user.id,
@@ -168157,6 +168291,7 @@ async function buildMockUserResponse(db2, user, gameId) {
168157
168291
  role: user.role,
168158
168292
  username: user.username,
168159
168293
  email: user.email,
168294
+ localDay,
168160
168295
  timeback: timeback3
168161
168296
  };
168162
168297
  }
@@ -168175,15 +168310,19 @@ async function buildMockUserResponse(db2, user, gameId) {
168175
168310
  createdAt: user.createdAt,
168176
168311
  updatedAt: user.updatedAt,
168177
168312
  hasTimebackAccount: Boolean(timebackAccount),
168313
+ localDay,
168178
168314
  timeback: timeback3
168179
168315
  };
168180
168316
  }
168317
+ var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
168181
168318
  var init_timeback6 = __esm(() => {
168182
168319
  init_drizzle_orm();
168183
168320
  init_utils11();
168321
+ init_src();
168184
168322
  init_tables_index();
168185
168323
  init_src2();
168186
168324
  init_config();
168325
+ LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2 = 86400000;
168187
168326
  });
168188
168327
  var usersRouter;
168189
168328
  var init_users = __esm(async () => {
@@ -168191,6 +168330,7 @@ var init_users = __esm(async () => {
168191
168330
  init_dist7();
168192
168331
  init_controllers();
168193
168332
  init_errors();
168333
+ init_src();
168194
168334
  init_tables_index();
168195
168335
  init_error_handler();
168196
168336
  init_timeback6();
@@ -168212,7 +168352,7 @@ var init_users = __esm(async () => {
168212
168352
  const error89 = ApiError.notFound("User not found");
168213
168353
  return c2.json(createErrorResponse(error89), error89.status);
168214
168354
  }
168215
- const response = await buildMockUserResponse(db2, userData, gameId);
168355
+ const response = await buildMockUserResponse(db2, userData, gameId, c2.req.header(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER));
168216
168356
  return c2.json(response);
168217
168357
  }
168218
168358
  return handle2(users2.getMe)(c2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "1.1.2-beta.1",
3
+ "version": "1.1.2-beta.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -19,14 +19,14 @@
19
19
  "dependencies": {
20
20
  "archiver": "^7.0.1",
21
21
  "picocolors": "^1.1.1",
22
- "playcademy": "0.26.1-beta.1"
22
+ "playcademy": "0.26.1-beta.3"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@electric-sql/pglite": "^0.3.16",
26
26
  "@inquirer/prompts": "^7.8.6",
27
27
  "@playcademy/constants": "0.0.1",
28
- "@playcademy/sandbox": "0.6.0",
29
- "@playcademy/sdk": "0.14.0",
28
+ "@playcademy/sandbox": "0.6.1-beta.2",
29
+ "@playcademy/sdk": "0.14.1-beta.2",
30
30
  "@playcademy/types": "0.0.1",
31
31
  "@playcademy/utils": "0.0.1",
32
32
  "@types/archiver": "^6.0.3",