@playcademy/vite-plugin 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -41140,7 +41140,8 @@ var serverState = {
41140
41140
  sandbox: null,
41141
41141
  backend: null,
41142
41142
  viteServer: null,
41143
- currentMode: "platform"
41143
+ currentMode: "platform",
41144
+ timebackRoleOverride: null
41144
41145
  };
41145
41146
  function hasActiveServers() {
41146
41147
  return !!(serverState.backend || serverState.sandbox);
@@ -41157,6 +41158,12 @@ function getViteServerRef() {
41157
41158
  function setViteServerRef(server) {
41158
41159
  serverState.viteServer = server;
41159
41160
  }
41161
+ function getTimebackRoleOverride() {
41162
+ return serverState.timebackRoleOverride;
41163
+ }
41164
+ function setTimebackRoleOverride(role) {
41165
+ serverState.timebackRoleOverride = role;
41166
+ }
41160
41167
 
41161
41168
  // src/server/cleanup.ts
41162
41169
  async function cleanupServers() {
@@ -41176,73 +41183,12 @@ async function cleanupServers() {
41176
41183
  }
41177
41184
  }
41178
41185
 
41179
- // src/server/lifecycle.ts
41180
- var shutdownHandlersRegistered = false;
41181
- function setupProcessShutdownHandlers() {
41182
- if (shutdownHandlersRegistered)
41183
- return;
41184
- shutdownHandlersRegistered = true;
41185
- let isShuttingDown = false;
41186
- const shutdown = async () => {
41187
- if (isShuttingDown)
41188
- return;
41189
- isShuttingDown = true;
41190
- await cleanupServers();
41191
- process.exit(0);
41192
- };
41193
- process.on("SIGINT", shutdown);
41194
- process.on("SIGTERM", shutdown);
41195
- }
41196
-
41197
- // src/server/mode-switcher.ts
41198
- var import_picocolors7 = __toESM(require_picocolors(), 1);
41199
- // package.json
41200
- var package_default = {
41201
- name: "@playcademy/vite-plugin",
41202
- version: "0.1.35",
41203
- type: "module",
41204
- exports: {
41205
- ".": {
41206
- import: "./dist/index.js",
41207
- types: "./dist/index.d.ts"
41208
- }
41209
- },
41210
- main: "dist/index.js",
41211
- module: "dist/index.js",
41212
- files: [
41213
- "dist"
41214
- ],
41215
- scripts: {
41216
- build: "rm -rf dist && bun build.ts",
41217
- docs: "typedoc --skipErrorChecking",
41218
- pub: "bun publish.ts"
41219
- },
41220
- dependencies: {
41221
- archiver: "^7.0.1",
41222
- picocolors: "^1.1.1",
41223
- playcademy: "workspace:*"
41224
- },
41225
- devDependencies: {
41226
- "@inquirer/prompts": "^7.8.6",
41227
- "@playcademy/sandbox": "workspace:*",
41228
- "@types/archiver": "^6.0.3",
41229
- "@types/bun": "latest"
41230
- },
41231
- peerDependencies: {
41232
- typescript: "^5",
41233
- vite: "^5 || ^6"
41234
- }
41235
- };
41236
-
41237
- // src/lib/backend/server.ts
41238
- import {
41239
- loadPlaycademyConfigAndSetWorkspace as loadConfigAndSetWorkspace,
41240
- startPlaycademyDevServer,
41241
- startPlaycademyHotReload
41242
- } from "playcademy/utils";
41243
-
41244
- // src/lib/logging/adapter.ts
41186
+ // src/server/hotkeys/cycle-timeback-role.ts
41245
41187
  var import_picocolors2 = __toESM(require_picocolors(), 1);
41188
+
41189
+ // src/types/internal.ts
41190
+ var TIMEBACK_ROLES = ["student", "parent", "teacher", "administrator"];
41191
+ // src/server/hotkeys/cycle-timeback-role.ts
41246
41192
  function formatTimestamp() {
41247
41193
  const now = new Date;
41248
41194
  const hours = now.getHours();
@@ -41250,117 +41196,32 @@ function formatTimestamp() {
41250
41196
  const seconds = now.getSeconds().toString().padStart(2, "0");
41251
41197
  const ampm = hours >= 12 ? "PM" : "AM";
41252
41198
  const displayHours = hours % 12 || 12;
41253
- return import_picocolors2.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
41254
- }
41255
- function createLoggerAdapter(prefix) {
41256
- const formattedPrefix = import_picocolors2.dim(`(${prefix})`);
41257
- const label = import_picocolors2.cyan(import_picocolors2.bold("[playcademy]"));
41258
- return {
41259
- info: (msg) => console.log(`${formatTimestamp()} ${label} ${formattedPrefix} ${msg}`),
41260
- warn: (msg) => console.warn(`${formatTimestamp()} ${label} ${formattedPrefix} ${msg}`),
41261
- error: (msg) => console.error(`${formatTimestamp()} ${label} ${formattedPrefix} ${msg}`)
41262
- };
41263
- }
41264
- // src/lib/logging/utils.ts
41265
- var import_picocolors3 = __toESM(require_picocolors(), 1);
41266
- function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
41267
- const INDENT = " ".repeat(2);
41268
- viteConfig.logger.info("");
41269
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green(import_picocolors3.bold("PLAYCADEMY"))} ${import_picocolors3.green(`v${pluginVersion}`)}`);
41270
- viteConfig.logger.info("");
41271
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Game:")} ${import_picocolors3.cyan(projectInfo.slug)}`);
41272
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Sandbox:")} ${import_picocolors3.cyan(`http://localhost:${import_picocolors3.bold(servers.sandbox.toString())}/api`)}`);
41273
- if (servers.backend) {
41274
- const backendUrl = servers.vite ? `http://localhost:${import_picocolors3.bold(servers.vite.toString())}/api ${import_picocolors3.dim(`(via ${servers.backend})`)}` : `http://localhost:${import_picocolors3.bold(servers.backend.toString())}/api`;
41275
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Backend:")} ${import_picocolors3.cyan(backendUrl)}`);
41276
- }
41277
- if (servers.realtime) {
41278
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Realtime:")} ${import_picocolors3.cyan(`ws://localhost:${import_picocolors3.bold(servers.realtime.toString())}`)}`);
41279
- }
41280
- viteConfig.logger.info("");
41281
- }
41282
- // src/lib/backend/hot-reload.ts
41283
- var import_picocolors4 = __toESM(require_picocolors(), 1);
41284
- function formatChangedPath(changedPath) {
41285
- if (!changedPath)
41286
- return;
41287
- if (changedPath.includes("/api/")) {
41288
- return changedPath.substring(changedPath.indexOf("/api/"));
41199
+ return import_picocolors2.default.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
41200
+ }
41201
+ function cycleTimebackRole(logger) {
41202
+ const currentRole = getTimebackRoleOverride() ?? "student";
41203
+ const currentIndex = TIMEBACK_ROLES.indexOf(currentRole);
41204
+ const nextIndex = (currentIndex + 1) % TIMEBACK_ROLES.length;
41205
+ const nextRole = TIMEBACK_ROLES[nextIndex];
41206
+ setTimebackRoleOverride(nextRole);
41207
+ logger.info(`${formatTimestamp()} ${import_picocolors2.default.cyan(import_picocolors2.default.bold("[playcademy]"))} ${import_picocolors2.default.dim("(timeback)")} ${import_picocolors2.default.red(currentRole)} → ${import_picocolors2.default.green(nextRole)}`);
41208
+ if (getViteServerRef()) {
41209
+ getViteServerRef()?.ws.send({ type: "full-reload", path: "*" });
41210
+ } else {
41211
+ logger.warn(`${formatTimestamp()} ${import_picocolors2.default.red(import_picocolors2.bold("[playcademy]"))} ${import_picocolors2.dim("(timeback)")} ${import_picocolors2.yellow("Cannot cycle TimeBack role: no Vite server reference")}`);
41289
41212
  }
41290
- return changedPath;
41291
- }
41292
- function createHotReloadCallbacks(viteConfig) {
41293
- return {
41294
- onSuccess: (changedPath) => {
41295
- const relativePath = formatChangedPath(changedPath);
41296
- if (relativePath) {
41297
- viteConfig.logger.info(`${import_picocolors4.dim("(backend)")} ${import_picocolors4.green("hmr update")} ${import_picocolors4.dim(relativePath)}`, { timestamp: true });
41298
- } else {
41299
- viteConfig.logger.info("backend reloaded", { timestamp: true });
41300
- }
41301
- },
41302
- onError: (error) => {
41303
- viteConfig.logger.error(`backend reload failed: ${error instanceof Error ? error.message : String(error)}`);
41304
- }
41305
- };
41306
41213
  }
41214
+ var cycleTimebackRoleHotkey = (options) => ({
41215
+ key: "t",
41216
+ description: "cycle TimeBack role",
41217
+ action: () => cycleTimebackRole(options.viteConfig.logger)
41218
+ });
41219
+
41220
+ // src/server/hotkeys/recreate-database.ts
41221
+ var import_picocolors7 = __toESM(require_picocolors(), 1);
41307
41222
 
41308
- // src/lib/backend/server.ts
41309
- async function tryLoadConfig(viteConfig, configPath) {
41310
- try {
41311
- return await loadConfigAndSetWorkspace(configPath);
41312
- } catch (error) {
41313
- if (error instanceof Error && !error.message.includes("Could not find")) {
41314
- viteConfig.logger.warn(`Could not load playcademy.config.js: ${error.message}`);
41315
- }
41316
- return null;
41317
- }
41318
- }
41319
- function needsCliDevServer(config) {
41320
- return !!config.integrations;
41321
- }
41322
- async function startServer(options) {
41323
- const { port, config, platformUrl } = options;
41324
- return startPlaycademyDevServer({
41325
- port,
41326
- config,
41327
- quiet: true,
41328
- platformUrl,
41329
- customLogger: createLoggerAdapter("backend")
41330
- });
41331
- }
41332
- function setupHotReload(serverRef, options) {
41333
- const watcher = startPlaycademyHotReload(async () => {
41334
- await serverRef.current.server.dispose();
41335
- serverRef.current = await startServer(options);
41336
- }, createHotReloadCallbacks(options.viteConfig));
41337
- return () => watcher.close();
41338
- }
41339
- async function setupCliDevServer(options) {
41340
- const { preferredPort, viteConfig, platformUrl, configPath } = options;
41341
- const config = await tryLoadConfig(viteConfig, configPath);
41342
- if (!config)
41343
- return null;
41344
- if (!needsCliDevServer(config))
41345
- return null;
41346
- try {
41347
- const port = await findAvailablePort(preferredPort);
41348
- const serverOptions = { port, config, platformUrl, viteConfig };
41349
- const serverRef = { current: await startServer(serverOptions) };
41350
- const stopHotReload = setupHotReload(serverRef, serverOptions);
41351
- return {
41352
- server: serverRef.current.server,
41353
- port: serverRef.current.port,
41354
- stopHotReload,
41355
- cleanup: () => cleanupServerInfo("backend", viteConfig.root, process.pid)
41356
- };
41357
- } catch (error) {
41358
- viteConfig.logger.error(`Failed to start game backend: ${error instanceof Error ? error.message : String(error)}`);
41359
- return null;
41360
- }
41361
- }
41362
41223
  // src/lib/sandbox/server.ts
41363
- var import_picocolors5 = __toESM(require_picocolors(), 1);
41224
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
41364
41225
  import { DEFAULT_PORTS as DEFAULT_PORTS2 } from "playcademy/constants";
41365
41226
 
41366
41227
  // ../sandbox/dist/server.js
@@ -50427,8 +50288,8 @@ function assembleStyles() {
50427
50288
  styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
50428
50289
  Object.defineProperties(styles, {
50429
50290
  rgbToAnsi256: {
50430
- value(red, green3, blue2) {
50431
- if (red === green3 && green3 === blue2) {
50291
+ value(red, green, blue) {
50292
+ if (red === green && green === blue) {
50432
50293
  if (red < 8) {
50433
50294
  return 16;
50434
50295
  }
@@ -50437,7 +50298,7 @@ function assembleStyles() {
50437
50298
  }
50438
50299
  return Math.round((red - 8) / 247 * 24) + 232;
50439
50300
  }
50440
- return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green3 / 255 * 5) + Math.round(blue2 / 255 * 5);
50301
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
50441
50302
  },
50442
50303
  enumerable: false
50443
50304
  },
@@ -50473,24 +50334,24 @@ function assembleStyles() {
50473
50334
  return 90 + (code - 8);
50474
50335
  }
50475
50336
  let red;
50476
- let green3;
50477
- let blue2;
50337
+ let green;
50338
+ let blue;
50478
50339
  if (code >= 232) {
50479
50340
  red = ((code - 232) * 10 + 8) / 255;
50480
- green3 = red;
50481
- blue2 = red;
50341
+ green = red;
50342
+ blue = red;
50482
50343
  } else {
50483
50344
  code -= 16;
50484
50345
  const remainder = code % 36;
50485
50346
  red = Math.floor(code / 36) / 5;
50486
- green3 = Math.floor(remainder / 6) / 5;
50487
- blue2 = remainder % 6 / 5;
50347
+ green = Math.floor(remainder / 6) / 5;
50348
+ blue = remainder % 6 / 5;
50488
50349
  }
50489
- const value = Math.max(red, green3, blue2) * 2;
50350
+ const value = Math.max(red, green, blue) * 2;
50490
50351
  if (value === 0) {
50491
50352
  return 30;
50492
50353
  }
50493
- let result = 30 + (Math.round(blue2) << 2 | Math.round(green3) << 1 | Math.round(red));
50354
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
50494
50355
  if (value === 2) {
50495
50356
  result += 60;
50496
50357
  }
@@ -50499,7 +50360,7 @@ function assembleStyles() {
50499
50360
  enumerable: false
50500
50361
  },
50501
50362
  rgbToAnsi: {
50502
- value: (red, green3, blue2) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green3, blue2)),
50363
+ value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
50503
50364
  enumerable: false
50504
50365
  },
50505
50366
  hexToAnsi: {
@@ -55966,7 +55827,7 @@ var init_api = __esm(() => {
55966
55827
  ANSI_BACKGROUND_OFFSET = 10;
55967
55828
  wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
55968
55829
  wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
55969
- wrapAnsi16m = (offset = 0) => (red, green3, blue2) => `\x1B[${38 + offset};2;${red};${green3};${blue2}m`;
55830
+ wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
55970
55831
  styles = {
55971
55832
  modifier: {
55972
55833
  reset: [0, 0],
@@ -162180,8 +162041,8 @@ var require_colorette = __commonJS2((exports) => {
162180
162041
  var createColors = ({ useColor = isColorSupported } = {}) => useColor ? colors4 : Object.keys(colors4).reduce((colors5, key) => ({ ...colors5, [key]: String }), {});
162181
162042
  var {
162182
162043
  reset,
162183
- bold: bold22,
162184
- dim: dim22,
162044
+ bold: bold2,
162045
+ dim: dim2,
162185
162046
  italic,
162186
162047
  underline,
162187
162048
  inverse,
@@ -162189,11 +162050,11 @@ var require_colorette = __commonJS2((exports) => {
162189
162050
  strikethrough,
162190
162051
  black,
162191
162052
  red,
162192
- green: green3,
162193
- yellow,
162194
- blue: blue2,
162053
+ green,
162054
+ yellow: yellow2,
162055
+ blue,
162195
162056
  magenta,
162196
- cyan: cyan3,
162057
+ cyan,
162197
162058
  white,
162198
162059
  gray,
162199
162060
  bgBlack,
@@ -162239,15 +162100,15 @@ var require_colorette = __commonJS2((exports) => {
162239
162100
  exports.bgYellowBright = bgYellowBright;
162240
162101
  exports.black = black;
162241
162102
  exports.blackBright = blackBright;
162242
- exports.blue = blue2;
162103
+ exports.blue = blue;
162243
162104
  exports.blueBright = blueBright;
162244
- exports.bold = bold22;
162105
+ exports.bold = bold2;
162245
162106
  exports.createColors = createColors;
162246
- exports.cyan = cyan3;
162107
+ exports.cyan = cyan;
162247
162108
  exports.cyanBright = cyanBright;
162248
- exports.dim = dim22;
162109
+ exports.dim = dim2;
162249
162110
  exports.gray = gray;
162250
- exports.green = green3;
162111
+ exports.green = green;
162251
162112
  exports.greenBright = greenBright;
162252
162113
  exports.hidden = hidden;
162253
162114
  exports.inverse = inverse;
@@ -162262,7 +162123,7 @@ var require_colorette = __commonJS2((exports) => {
162262
162123
  exports.underline = underline;
162263
162124
  exports.white = white;
162264
162125
  exports.whiteBright = whiteBright;
162265
- exports.yellow = yellow;
162126
+ exports.yellow = yellow2;
162266
162127
  exports.yellowBright = yellowBright;
162267
162128
  });
162268
162129
  var require_dist22 = __commonJS2((exports) => {
@@ -175287,6 +175148,7 @@ var config = {
175287
175148
  };
175288
175149
  process.env.BETTER_AUTH_SECRET = config.auth.betterAuthSecret;
175289
175150
  process.env.GAME_JWT_SECRET = config.auth.gameJwtSecret;
175151
+ process.env.PUBLIC_IS_LOCAL = "true";
175290
175152
  var RequestError = class extends Error {
175291
175153
  constructor(message3, options) {
175292
175154
  super(message3, options);
@@ -175822,9 +175684,9 @@ var serve = (options, listeningListener) => {
175822
175684
  });
175823
175685
  return server;
175824
175686
  };
175825
- var package_default2 = {
175687
+ var package_default = {
175826
175688
  name: "@playcademy/sandbox",
175827
- version: "0.1.11",
175689
+ version: "0.2.0",
175828
175690
  description: "Local development server for Playcademy game development",
175829
175691
  type: "module",
175830
175692
  exports: {
@@ -177412,7 +177274,7 @@ var colorStatus = async (status) => {
177412
177274
  }
177413
177275
  return `${status}`;
177414
177276
  };
177415
- async function log3(fn, prefix2, method, path4, status = 0, elapsed) {
177277
+ async function log(fn, prefix2, method, path4, status = 0, elapsed) {
177416
177278
  const out2 = prefix2 === "<--" ? `${prefix2} ${method} ${path4}` : `${prefix2} ${method} ${path4} ${await colorStatus(status)} ${elapsed}`;
177417
177279
  fn(out2);
177418
177280
  }
@@ -177420,10 +177282,10 @@ var logger2 = (fn = console.log) => {
177420
177282
  return async function logger2(c3, next) {
177421
177283
  const { method, url } = c3.req;
177422
177284
  const path4 = url.slice(url.indexOf("/", 8));
177423
- await log3(fn, "<--", method, path4);
177285
+ await log(fn, "<--", method, path4);
177424
177286
  const start2 = Date.now();
177425
177287
  await next();
177426
- await log3(fn, "-->", method, path4, c3.res.status, time(start2));
177288
+ await log(fn, "-->", method, path4, c3.res.status, time(start2));
177427
177289
  };
177428
177290
  };
177429
177291
  var entityKind = Symbol.for("drizzle:entityKind");
@@ -189515,7 +189377,9 @@ var init_constants = __esm3(() => {
189515
189377
  studentTTL: 600000,
189516
189378
  studentMaxSize: 500,
189517
189379
  assessmentTTL: 1800000,
189518
- assessmentMaxSize: 200
189380
+ assessmentMaxSize: 200,
189381
+ enrollmentTTL: 5000,
189382
+ enrollmentMaxSize: 100
189519
189383
  };
189520
189384
  CONFIG_DEFAULTS = {
189521
189385
  fileNames: ["timeback.config.js", "timeback.config.json"]
@@ -189733,6 +189597,25 @@ class ResourceNotFoundError extends TimebackError {
189733
189597
  Object.setPrototypeOf(this, ResourceNotFoundError.prototype);
189734
189598
  }
189735
189599
  }
189600
+ init_constants();
189601
+ var isObject2 = (value) => typeof value === "object" && value !== null;
189602
+ var SUBJECT_VALUES = TIMEBACK_SUBJECTS;
189603
+ var GRADE_VALUES = TIMEBACK_GRADE_LEVELS;
189604
+ function isPlaycademyResourceMetadata(value) {
189605
+ if (!isObject2(value)) {
189606
+ return false;
189607
+ }
189608
+ if (!("mastery" in value) || value.mastery === undefined) {
189609
+ return true;
189610
+ }
189611
+ return isObject2(value.mastery);
189612
+ }
189613
+ function isTimebackSubject(value) {
189614
+ return typeof value === "string" && SUBJECT_VALUES.includes(value);
189615
+ }
189616
+ function isTimebackGrade(value) {
189617
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
189618
+ }
189736
189619
  var isBrowser2 = () => {
189737
189620
  const g52 = globalThis;
189738
189621
  return typeof g52.window !== "undefined" && typeof g52.document !== "undefined";
@@ -189907,7 +189790,7 @@ var createLogger2 = () => {
189907
189790
  log: performLog2
189908
189791
  };
189909
189792
  };
189910
- var log32 = createLogger2();
189793
+ var log3 = createLogger2();
189911
189794
  async function deleteTimebackResources(client2, courseId) {
189912
189795
  const sourcedIds = deriveSourcedIds(courseId);
189913
189796
  const { fetchTimebackConfig: fetchTimebackConfig2 } = await Promise.resolve().then(() => (init_verify5(), exports_verify));
@@ -189955,7 +189838,7 @@ async function deleteTimebackResources(client2, courseId) {
189955
189838
  lessonType: config2.componentResource.lessonType
189956
189839
  })
189957
189840
  ]);
189958
- log32.info("[TimeBack] Resources soft-deleted", { courseId });
189841
+ log3.info("[TimeBack] Resources soft-deleted", { courseId });
189959
189842
  }
189960
189843
  init_verify5();
189961
189844
  async function createCourse(client2, config2) {
@@ -189975,12 +189858,12 @@ async function createCourse(client2, config2) {
189975
189858
  if (!response.sourcedIdPairs?.allocatedSourcedId) {
189976
189859
  throw new Error("Course created but TimeBack did not return allocatedSourcedId");
189977
189860
  }
189978
- log32.info("[TimeBack] Course created", {
189861
+ log3.info("[TimeBack] Course created", {
189979
189862
  courseId: response.sourcedIdPairs.allocatedSourcedId
189980
189863
  });
189981
189864
  return response.sourcedIdPairs.allocatedSourcedId;
189982
189865
  } catch (error2) {
189983
- log32.error("[TimeBack] Failed to create course", {
189866
+ log3.error("[TimeBack] Failed to create course", {
189984
189867
  title: config2.course.title,
189985
189868
  error: error2
189986
189869
  });
@@ -189999,9 +189882,9 @@ async function createComponent(client2, config2, sourcedIds) {
189999
189882
  prerequisiteCriteria: config2.component.prerequisiteCriteria
190000
189883
  };
190001
189884
  await client2.oneroster.courseComponents.create({ courseComponent: componentData });
190002
- log32.info("[TimeBack] Component created", { componentId: sourcedIds.component });
189885
+ log3.info("[TimeBack] Component created", { componentId: sourcedIds.component });
190003
189886
  } catch (error2) {
190004
- log32.error("[TimeBack] Failed to create component", {
189887
+ log3.error("[TimeBack] Failed to create component", {
190005
189888
  componentId: sourcedIds.component,
190006
189889
  error: error2
190007
189890
  });
@@ -190021,11 +189904,11 @@ async function createResource(client2, config2, sourcedIds) {
190021
189904
  importance: config2.resource.importance,
190022
189905
  metadata: config2.resource.metadata
190023
189906
  };
190024
- log32.debug("[TimeBack] Creating resource", { resourceId: sourcedIds.resource });
189907
+ log3.debug("[TimeBack] Creating resource", { resourceId: sourcedIds.resource });
190025
189908
  await client2.oneroster.resources.create({ resource: resourceData });
190026
- log32.info("[TimeBack] Resource created", { resourceId: sourcedIds.resource });
189909
+ log3.info("[TimeBack] Resource created", { resourceId: sourcedIds.resource });
190027
189910
  } catch (error2) {
190028
- log32.error("[TimeBack] Failed to create resource", {
189911
+ log3.error("[TimeBack] Failed to create resource", {
190029
189912
  resourceId: sourcedIds.resource,
190030
189913
  error: error2
190031
189914
  });
@@ -190043,17 +189926,17 @@ async function createComponentResourceLink(client2, config2, sourcedIds) {
190043
189926
  sortOrder: config2.componentResource.sortOrder,
190044
189927
  lessonType: config2.componentResource.lessonType
190045
189928
  };
190046
- log32.debug("[TimeBack] Creating component-resource link", {
189929
+ log3.debug("[TimeBack] Creating component-resource link", {
190047
189930
  componentResourceId: sourcedIds.componentResource
190048
189931
  });
190049
189932
  await client2.oneroster.componentResources.create({
190050
189933
  componentResource: componentResourceData
190051
189934
  });
190052
- log32.info("[TimeBack] Component-resource link created", {
189935
+ log3.info("[TimeBack] Component-resource link created", {
190053
189936
  componentResourceId: sourcedIds.componentResource
190054
189937
  });
190055
189938
  } catch (error2) {
190056
- log32.error("[TimeBack] Failed to create component-resource link", {
189939
+ log3.error("[TimeBack] Failed to create component-resource link", {
190057
189940
  componentResourceId: sourcedIds.componentResource,
190058
189941
  error: error2
190059
189942
  });
@@ -190077,7 +189960,7 @@ async function setupTimebackResources(client2, config2, options) {
190077
189960
  ...verboseData && { verboseData }
190078
189961
  };
190079
189962
  } catch (error2) {
190080
- log32.error("[TimeBack] Setup failed", { error: error2 });
189963
+ log3.error("[TimeBack] Setup failed", { error: error2 });
190081
189964
  throw error2;
190082
189965
  }
190083
189966
  }
@@ -190096,9 +189979,9 @@ async function updateCourse(client2, config2, courseId) {
190096
189979
  metadata: config2.course.metadata
190097
189980
  };
190098
189981
  await client2.oneroster.courses.update(courseId, courseData);
190099
- log32.info("[TimeBack] Course updated", { courseId });
189982
+ log3.info("[TimeBack] Course updated", { courseId });
190100
189983
  } catch (error2) {
190101
- log32.error("[TimeBack] Failed to update course", {
189984
+ log3.error("[TimeBack] Failed to update course", {
190102
189985
  courseId,
190103
189986
  error: error2
190104
189987
  });
@@ -190117,9 +190000,9 @@ async function updateComponent(client2, config2, sourcedIds) {
190117
190000
  prerequisiteCriteria: config2.component.prerequisiteCriteria
190118
190001
  };
190119
190002
  await client2.oneroster.courseComponents.update(sourcedIds.component, componentData);
190120
- log32.info("[TimeBack] Component updated", { componentId: sourcedIds.component });
190003
+ log3.info("[TimeBack] Component updated", { componentId: sourcedIds.component });
190121
190004
  } catch (error2) {
190122
- log32.error("[TimeBack] Failed to update component", {
190005
+ log3.error("[TimeBack] Failed to update component", {
190123
190006
  componentId: sourcedIds.component,
190124
190007
  error: error2
190125
190008
  });
@@ -190140,9 +190023,9 @@ async function updateResource(client2, config2, resourceId) {
190140
190023
  metadata: config2.resource.metadata
190141
190024
  };
190142
190025
  await client2.oneroster.resources.update(resourceId, resourceData);
190143
- log32.info("[TimeBack] Resource updated", { resourceId });
190026
+ log3.info("[TimeBack] Resource updated", { resourceId });
190144
190027
  } catch (error2) {
190145
- log32.error("[TimeBack] Failed to update resource", {
190028
+ log3.error("[TimeBack] Failed to update resource", {
190146
190029
  resourceId,
190147
190030
  error: error2
190148
190031
  });
@@ -190161,11 +190044,11 @@ async function updateComponentResourceLink(client2, config2, sourcedIds) {
190161
190044
  lessonType: config2.componentResource.lessonType
190162
190045
  };
190163
190046
  await client2.oneroster.componentResources.update(sourcedIds.componentResource, componentResourceData);
190164
- log32.info("[TimeBack] Component-resource link updated", {
190047
+ log3.info("[TimeBack] Component-resource link updated", {
190165
190048
  componentResourceId: sourcedIds.componentResource
190166
190049
  });
190167
190050
  } catch (error2) {
190168
- log32.error("[TimeBack] Failed to update component-resource link", {
190051
+ log3.error("[TimeBack] Failed to update component-resource link", {
190169
190052
  componentResourceId: sourcedIds.componentResource,
190170
190053
  error: error2
190171
190054
  });
@@ -190334,7 +190217,7 @@ async function request({
190334
190217
  lastError = error2;
190335
190218
  if (attempt < retries) {
190336
190219
  const delay = Math.pow(HTTP_DEFAULTS.retryBackoffBase, attempt) * 1000;
190337
- log32.warn(`[TimebackClient] Request failed, retrying in ${delay}ms`, {
190220
+ log3.warn(`[TimebackClient] Request failed, retrying in ${delay}ms`, {
190338
190221
  attempt: attempt + 1,
190339
190222
  maxRetries: retries,
190340
190223
  status: res.status,
@@ -190366,7 +190249,7 @@ async function request({
190366
190249
  lastError = error2 instanceof Error ? error2 : new Error(String(error2));
190367
190250
  if (attempt < retries) {
190368
190251
  const delay = Math.pow(HTTP_DEFAULTS.retryBackoffBase, attempt) * 1000;
190369
- log32.warn(`[TimebackClient] Network error, retrying in ${delay}ms`, {
190252
+ log3.warn(`[TimebackClient] Network error, retrying in ${delay}ms`, {
190370
190253
  attempt: attempt + 1,
190371
190254
  maxRetries: retries,
190372
190255
  error: lastError.message,
@@ -190389,11 +190272,158 @@ async function requestCaliper(options) {
190389
190272
  });
190390
190273
  }
190391
190274
  init_constants();
190275
+ function createCaliperNamespace(client2) {
190276
+ const urls = createOneRosterUrls(client2.getBaseUrl());
190277
+ const caliper = {
190278
+ emit: async (event, sensorUrl) => {
190279
+ const envelope = {
190280
+ sensor: sensorUrl,
190281
+ sendTime: new Date().toISOString(),
190282
+ dataVersion: CALIPER_CONSTANTS.dataVersion,
190283
+ data: [event]
190284
+ };
190285
+ return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190286
+ },
190287
+ emitBatch: async (events, sensorUrl) => {
190288
+ if (events.length === 0)
190289
+ return;
190290
+ const envelope = {
190291
+ sensor: sensorUrl,
190292
+ sendTime: new Date().toISOString(),
190293
+ dataVersion: CALIPER_CONSTANTS.dataVersion,
190294
+ data: events
190295
+ };
190296
+ return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190297
+ },
190298
+ emitActivityEvent: async (data) => {
190299
+ const event = {
190300
+ "@context": CALIPER_CONSTANTS.context,
190301
+ id: `urn:uuid:${crypto.randomUUID()}`,
190302
+ type: TIMEBACK_EVENT_TYPES.activityEvent,
190303
+ eventTime: new Date().toISOString(),
190304
+ profile: CALIPER_CONSTANTS.profile,
190305
+ actor: {
190306
+ id: urls.user(data.studentId),
190307
+ type: TIMEBACK_TYPES.user,
190308
+ email: data.studentEmail
190309
+ },
190310
+ action: TIMEBACK_ACTIONS.completed,
190311
+ object: {
190312
+ id: caliper.buildActivityUrl(data),
190313
+ type: TIMEBACK_TYPES.activityContext,
190314
+ subject: data.subject,
190315
+ app: {
190316
+ name: data.appName
190317
+ },
190318
+ activity: {
190319
+ name: data.activityName
190320
+ },
190321
+ course: { id: urls.course(data.courseId), name: data.activityName },
190322
+ process: false
190323
+ },
190324
+ generated: {
190325
+ id: `urn:timeback:metrics:activity-completion-${crypto.randomUUID()}`,
190326
+ type: TIMEBACK_TYPES.activityMetricsCollection,
190327
+ attempt: data.attemptNumber || 1,
190328
+ items: [
190329
+ ...data.totalQuestions !== undefined ? [
190330
+ {
190331
+ type: ACTIVITY_METRIC_TYPES.totalQuestions,
190332
+ value: data.totalQuestions
190333
+ }
190334
+ ] : [],
190335
+ ...data.correctQuestions !== undefined ? [
190336
+ {
190337
+ type: ACTIVITY_METRIC_TYPES.correctQuestions,
190338
+ value: data.correctQuestions
190339
+ }
190340
+ ] : [],
190341
+ ...data.xpEarned !== undefined ? [{ type: ACTIVITY_METRIC_TYPES.xpEarned, value: data.xpEarned }] : [],
190342
+ ...data.masteredUnits !== undefined ? [
190343
+ {
190344
+ type: ACTIVITY_METRIC_TYPES.masteredUnits,
190345
+ value: data.masteredUnits
190346
+ }
190347
+ ] : []
190348
+ ],
190349
+ ...data.extensions ? { extensions: data.extensions } : {}
190350
+ }
190351
+ };
190352
+ return caliper.emit(event, data.sensorUrl);
190353
+ },
190354
+ emitTimeSpentEvent: async (data) => {
190355
+ const event = {
190356
+ "@context": CALIPER_CONSTANTS.context,
190357
+ id: `urn:uuid:${crypto.randomUUID()}`,
190358
+ type: TIMEBACK_EVENT_TYPES.timeSpentEvent,
190359
+ eventTime: new Date().toISOString(),
190360
+ profile: CALIPER_CONSTANTS.profile,
190361
+ actor: {
190362
+ id: urls.user(data.studentId),
190363
+ type: TIMEBACK_TYPES.user,
190364
+ email: data.studentEmail
190365
+ },
190366
+ action: TIMEBACK_ACTIONS.spentTime,
190367
+ object: {
190368
+ id: caliper.buildActivityUrl(data),
190369
+ type: TIMEBACK_TYPES.activityContext,
190370
+ subject: data.subject,
190371
+ app: {
190372
+ name: data.appName
190373
+ },
190374
+ activity: {
190375
+ name: data.activityName
190376
+ },
190377
+ course: { id: urls.course(data.courseId), name: data.activityName },
190378
+ process: false
190379
+ },
190380
+ generated: {
190381
+ id: `urn:timeback:metrics:time-spent-${crypto.randomUUID()}`,
190382
+ type: TIMEBACK_TYPES.timeSpentMetricsCollection,
190383
+ items: [
190384
+ { type: TIME_METRIC_TYPES.active, value: data.activeTimeSeconds },
190385
+ ...data.inactiveTimeSeconds !== undefined ? [
190386
+ {
190387
+ type: TIME_METRIC_TYPES.inactive,
190388
+ value: data.inactiveTimeSeconds
190389
+ }
190390
+ ] : [],
190391
+ ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES.waste, value: data.wasteTimeSeconds }] : []
190392
+ ]
190393
+ }
190394
+ };
190395
+ return caliper.emit(event, data.sensorUrl);
190396
+ },
190397
+ buildActivityUrl: (data) => {
190398
+ const base = data.sensorUrl.replace(/\/$/, "");
190399
+ return `${base}/activities/${data.courseId}/${data.activityId}/${crypto.randomUUID()}`;
190400
+ }
190401
+ };
190402
+ return caliper;
190403
+ }
190404
+ function createEduBridgeNamespace(client2) {
190405
+ const enrollments = {
190406
+ listByUser: async (userId) => {
190407
+ const response = await client2["request"](`/edubridge/enrollments/user/${userId}`, "GET");
190408
+ return response.data;
190409
+ }
190410
+ };
190411
+ const analytics = {
190412
+ getEnrollmentFacts: async (enrollmentId) => {
190413
+ return client2["request"](`/edubridge/analytics/enrollment/${enrollmentId}`, "GET");
190414
+ }
190415
+ };
190416
+ return {
190417
+ enrollments,
190418
+ analytics
190419
+ };
190420
+ }
190421
+ init_constants();
190392
190422
  function logTimebackError(operation, error2, context) {
190393
190423
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
190394
190424
  if (error2 instanceof TimebackApiError) {
190395
190425
  const imsError = error2.details;
190396
- log32.error(`[TimeBack ${operation}] ${errorMessage}`, {
190426
+ log3.error(`[TimeBack ${operation}] ${errorMessage}`, {
190397
190427
  ...context,
190398
190428
  status: error2.status,
190399
190429
  imsx_codeMajor: imsError?.imsx_codeMajor,
@@ -190404,7 +190434,7 @@ function logTimebackError(operation, error2, context) {
190404
190434
  console.error(`[TimeBack Error during ${operation}] Full details:`, JSON.stringify(error2.details, null, 2));
190405
190435
  }
190406
190436
  } else {
190407
- log32.error(`[TimeBack ${operation}] ${errorMessage}`, context);
190437
+ log3.error(`[TimeBack ${operation}] ${errorMessage}`, context);
190408
190438
  }
190409
190439
  }
190410
190440
  function createOneRosterNamespace(client2) {
@@ -190603,153 +190633,6 @@ function createOneRosterNamespace(client2) {
190603
190633
  };
190604
190634
  }
190605
190635
  init_constants();
190606
- function createCaliperNamespace(client2) {
190607
- const urls = createOneRosterUrls(client2.getBaseUrl());
190608
- const caliper = {
190609
- emit: async (event, sensorUrl) => {
190610
- const envelope = {
190611
- sensor: sensorUrl,
190612
- sendTime: new Date().toISOString(),
190613
- dataVersion: CALIPER_CONSTANTS.dataVersion,
190614
- data: [event]
190615
- };
190616
- return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190617
- },
190618
- emitBatch: async (events, sensorUrl) => {
190619
- if (events.length === 0)
190620
- return;
190621
- const envelope = {
190622
- sensor: sensorUrl,
190623
- sendTime: new Date().toISOString(),
190624
- dataVersion: CALIPER_CONSTANTS.dataVersion,
190625
- data: events
190626
- };
190627
- return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190628
- },
190629
- emitActivityEvent: async (data) => {
190630
- const event = {
190631
- "@context": CALIPER_CONSTANTS.context,
190632
- id: `urn:uuid:${crypto.randomUUID()}`,
190633
- type: TIMEBACK_EVENT_TYPES.activityEvent,
190634
- eventTime: new Date().toISOString(),
190635
- profile: CALIPER_CONSTANTS.profile,
190636
- actor: {
190637
- id: urls.user(data.studentId),
190638
- type: TIMEBACK_TYPES.user,
190639
- email: data.studentEmail
190640
- },
190641
- action: TIMEBACK_ACTIONS.completed,
190642
- object: {
190643
- id: caliper.buildActivityUrl(data),
190644
- type: TIMEBACK_TYPES.activityContext,
190645
- subject: data.subject,
190646
- app: {
190647
- name: data.appName
190648
- },
190649
- activity: {
190650
- name: data.activityName
190651
- },
190652
- course: { id: urls.course(data.courseId), name: data.activityName },
190653
- process: false
190654
- },
190655
- generated: {
190656
- id: `urn:timeback:metrics:activity-completion-${crypto.randomUUID()}`,
190657
- type: TIMEBACK_TYPES.activityMetricsCollection,
190658
- attempt: data.attemptNumber || 1,
190659
- items: [
190660
- ...data.totalQuestions !== undefined ? [
190661
- {
190662
- type: ACTIVITY_METRIC_TYPES.totalQuestions,
190663
- value: data.totalQuestions
190664
- }
190665
- ] : [],
190666
- ...data.correctQuestions !== undefined ? [
190667
- {
190668
- type: ACTIVITY_METRIC_TYPES.correctQuestions,
190669
- value: data.correctQuestions
190670
- }
190671
- ] : [],
190672
- ...data.xpEarned !== undefined ? [{ type: ACTIVITY_METRIC_TYPES.xpEarned, value: data.xpEarned }] : [],
190673
- ...data.masteredUnits !== undefined ? [
190674
- {
190675
- type: ACTIVITY_METRIC_TYPES.masteredUnits,
190676
- value: data.masteredUnits
190677
- }
190678
- ] : []
190679
- ],
190680
- ...data.extensions ? { extensions: data.extensions } : {}
190681
- }
190682
- };
190683
- return caliper.emit(event, data.sensorUrl);
190684
- },
190685
- emitTimeSpentEvent: async (data) => {
190686
- const event = {
190687
- "@context": CALIPER_CONSTANTS.context,
190688
- id: `urn:uuid:${crypto.randomUUID()}`,
190689
- type: TIMEBACK_EVENT_TYPES.timeSpentEvent,
190690
- eventTime: new Date().toISOString(),
190691
- profile: CALIPER_CONSTANTS.profile,
190692
- actor: {
190693
- id: urls.user(data.studentId),
190694
- type: TIMEBACK_TYPES.user,
190695
- email: data.studentEmail
190696
- },
190697
- action: TIMEBACK_ACTIONS.spentTime,
190698
- object: {
190699
- id: caliper.buildActivityUrl(data),
190700
- type: TIMEBACK_TYPES.activityContext,
190701
- subject: data.subject,
190702
- app: {
190703
- name: data.appName
190704
- },
190705
- activity: {
190706
- name: data.activityName
190707
- },
190708
- course: { id: urls.course(data.courseId), name: data.activityName },
190709
- process: false
190710
- },
190711
- generated: {
190712
- id: `urn:timeback:metrics:time-spent-${crypto.randomUUID()}`,
190713
- type: TIMEBACK_TYPES.timeSpentMetricsCollection,
190714
- items: [
190715
- { type: TIME_METRIC_TYPES.active, value: data.activeTimeSeconds },
190716
- ...data.inactiveTimeSeconds !== undefined ? [
190717
- {
190718
- type: TIME_METRIC_TYPES.inactive,
190719
- value: data.inactiveTimeSeconds
190720
- }
190721
- ] : [],
190722
- ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES.waste, value: data.wasteTimeSeconds }] : []
190723
- ]
190724
- }
190725
- };
190726
- return caliper.emit(event, data.sensorUrl);
190727
- },
190728
- buildActivityUrl: (data) => {
190729
- const base = data.sensorUrl.replace(/\/$/, "");
190730
- return `${base}/activities/${data.courseId}/${data.activityId}/${crypto.randomUUID()}`;
190731
- }
190732
- };
190733
- return caliper;
190734
- }
190735
- function createEduBridgeNamespace(client2) {
190736
- const enrollments = {
190737
- listByUser: async (userId) => {
190738
- const response = await client2["request"](`/edubridge/enrollments/user/${userId}`, "GET");
190739
- return response.data;
190740
- }
190741
- };
190742
- const analytics = {
190743
- getEnrollmentFacts: async (enrollmentId) => {
190744
- return client2["request"](`/edubridge/analytics/enrollment/${enrollmentId}`, "GET");
190745
- }
190746
- };
190747
- return {
190748
- enrollments,
190749
- analytics
190750
- };
190751
- }
190752
- init_constants();
190753
190636
  init_constants();
190754
190637
 
190755
190638
  class TimebackCache {
@@ -190771,7 +190654,7 @@ class TimebackCache {
190771
190654
  }
190772
190655
  if (Date.now() > entry.expiresAt) {
190773
190656
  this.delete(key);
190774
- log32.debug(`[${this.name}] Cache entry expired`, { key });
190657
+ log3.debug(`[${this.name}] Cache entry expired`, { key });
190775
190658
  this.misses++;
190776
190659
  return;
190777
190660
  }
@@ -190786,7 +190669,7 @@ class TimebackCache {
190786
190669
  }
190787
190670
  this.cache.set(key, { value, expiresAt });
190788
190671
  this._updateAccessOrder(key);
190789
- log32.debug(`[${this.name}] Cache entry set`, {
190672
+ log3.debug(`[${this.name}] Cache entry set`, {
190790
190673
  key,
190791
190674
  expiresAt: new Date(expiresAt).toISOString()
190792
190675
  });
@@ -190801,7 +190684,7 @@ class TimebackCache {
190801
190684
  clear() {
190802
190685
  this.cache.clear();
190803
190686
  this.accessOrder = [];
190804
- log32.debug(`[${this.name}] Cache cleared`);
190687
+ log3.debug(`[${this.name}] Cache cleared`);
190805
190688
  }
190806
190689
  size() {
190807
190690
  return this.cache.size;
@@ -190824,7 +190707,7 @@ class TimebackCache {
190824
190707
  this.delete(key);
190825
190708
  }
190826
190709
  if (expiredKeys.length > 0) {
190827
- log32.debug(`[${this.name}] Cleaned up expired entries`, {
190710
+ log3.debug(`[${this.name}] Cleaned up expired entries`, {
190828
190711
  count: expiredKeys.length
190829
190712
  });
190830
190713
  }
@@ -190849,7 +190732,7 @@ class TimebackCache {
190849
190732
  return;
190850
190733
  const oldestKey = this.accessOrder[0];
190851
190734
  this.delete(oldestKey);
190852
- log32.debug(`[${this.name}] Evicted LRU entry`, { key: oldestKey });
190735
+ log3.debug(`[${this.name}] Evicted LRU entry`, { key: oldestKey });
190853
190736
  }
190854
190737
  }
190855
190738
 
@@ -190857,6 +190740,7 @@ class TimebackCacheManager {
190857
190740
  studentCache;
190858
190741
  assessmentLineItemCache;
190859
190742
  resourceMasteryCache;
190743
+ enrollmentCache;
190860
190744
  constructor() {
190861
190745
  this.studentCache = new TimebackCache({
190862
190746
  defaultTTL: CACHE_DEFAULTS.studentTTL,
@@ -190873,6 +190757,11 @@ class TimebackCacheManager {
190873
190757
  maxSize: CACHE_DEFAULTS.assessmentMaxSize,
190874
190758
  name: "ResourceMasteryCache"
190875
190759
  });
190760
+ this.enrollmentCache = new TimebackCache({
190761
+ defaultTTL: CACHE_DEFAULTS.enrollmentTTL,
190762
+ maxSize: CACHE_DEFAULTS.enrollmentMaxSize,
190763
+ name: "EnrollmentCache"
190764
+ });
190876
190765
  }
190877
190766
  getStudent(key) {
190878
190767
  return this.studentCache.get(key);
@@ -190892,37 +190781,36 @@ class TimebackCacheManager {
190892
190781
  setResourceMasterableUnits(key, value) {
190893
190782
  this.resourceMasteryCache.set(key, value);
190894
190783
  }
190784
+ getEnrollments(studentId) {
190785
+ return this.enrollmentCache.get(studentId);
190786
+ }
190787
+ setEnrollments(studentId, enrollments) {
190788
+ this.enrollmentCache.set(studentId, enrollments);
190789
+ }
190895
190790
  clearAll() {
190896
190791
  this.studentCache.clear();
190897
190792
  this.assessmentLineItemCache.clear();
190898
- log32.info("[TimebackCacheManager] All caches cleared");
190793
+ this.resourceMasteryCache.clear();
190794
+ this.enrollmentCache.clear();
190795
+ log3.info("[TimebackCacheManager] All caches cleared");
190899
190796
  }
190900
190797
  getStats() {
190901
190798
  return {
190902
190799
  studentCache: this.studentCache.stats(),
190903
190800
  assessmentLineItemCache: this.assessmentLineItemCache.stats(),
190904
- resourceMasteryCache: this.resourceMasteryCache.stats()
190801
+ resourceMasteryCache: this.resourceMasteryCache.stats(),
190802
+ enrollmentCache: this.enrollmentCache.stats()
190905
190803
  };
190906
190804
  }
190907
190805
  cleanup() {
190908
190806
  this.studentCache.cleanup();
190909
190807
  this.assessmentLineItemCache.cleanup();
190910
190808
  this.resourceMasteryCache.cleanup();
190911
- log32.debug("[TimebackCacheManager] Cache cleanup completed");
190809
+ this.enrollmentCache.cleanup();
190810
+ log3.debug("[TimebackCacheManager] Cache cleanup completed");
190912
190811
  }
190913
190812
  }
190914
190813
  init_constants();
190915
- init_constants();
190916
- var isObject2 = (value) => typeof value === "object" && value !== null;
190917
- function isPlaycademyResourceMetadata(value) {
190918
- if (!isObject2(value)) {
190919
- return false;
190920
- }
190921
- if (!("mastery" in value) || value.mastery === undefined) {
190922
- return true;
190923
- }
190924
- return isObject2(value.mastery);
190925
- }
190926
190814
 
190927
190815
  class MasteryTracker {
190928
190816
  cacheManager;
@@ -190940,7 +190828,7 @@ class MasteryTracker {
190940
190828
  }
190941
190829
  const masterableUnits = await this.resolveMasterableUnits(resourceId);
190942
190830
  if (!masterableUnits || masterableUnits <= 0) {
190943
- log32.warn("[MasteryTracker] No masterableUnits configured for course", {
190831
+ log3.warn("[MasteryTracker] No masterableUnits configured for course", {
190944
190832
  courseId,
190945
190833
  resourceId
190946
190834
  });
@@ -190948,7 +190836,7 @@ class MasteryTracker {
190948
190836
  }
190949
190837
  const facts = await this.fetchEnrollmentAnalyticsFacts(studentId, courseId);
190950
190838
  if (!facts) {
190951
- log32.warn("[MasteryTracker] Unable to retrieve analytics for mastery-based completion", {
190839
+ log3.warn("[MasteryTracker] Unable to retrieve analytics for mastery-based completion", {
190952
190840
  studentId,
190953
190841
  courseId
190954
190842
  });
@@ -190988,13 +190876,13 @@ class MasteryTracker {
190988
190876
  appName
190989
190877
  }
190990
190878
  });
190991
- log32.info("[MasteryTracker] Created mastery completion entry", {
190879
+ log3.info("[MasteryTracker] Created mastery completion entry", {
190992
190880
  studentId,
190993
190881
  lineItemId,
190994
190882
  resultId
190995
190883
  });
190996
190884
  } catch (error2) {
190997
- log32.error("[MasteryTracker] Failed to create mastery completion entry", {
190885
+ log3.error("[MasteryTracker] Failed to create mastery completion entry", {
190998
190886
  studentId,
190999
190887
  lineItemId,
191000
190888
  error: error2
@@ -191019,7 +190907,7 @@ class MasteryTracker {
191019
190907
  this.cacheManager.setResourceMasterableUnits(resourceId, masterableUnits ?? null);
191020
190908
  return masterableUnits;
191021
190909
  } catch (error2) {
191022
- log32.error("[MasteryTracker] Failed to fetch resource metadata for mastery config", {
190910
+ log3.error("[MasteryTracker] Failed to fetch resource metadata for mastery config", {
191023
190911
  resourceId,
191024
190912
  error: error2
191025
190913
  });
@@ -191032,7 +190920,7 @@ class MasteryTracker {
191032
190920
  const enrollments = await this.edubridgeNamespace.enrollments.listByUser(studentId);
191033
190921
  const enrollment = enrollments.find((e2) => e2.course.id === courseId);
191034
190922
  if (!enrollment) {
191035
- log32.warn("[MasteryTracker] Enrollment not found for student/course", {
190923
+ log3.warn("[MasteryTracker] Enrollment not found for student/course", {
191036
190924
  studentId,
191037
190925
  courseId
191038
190926
  });
@@ -191041,7 +190929,7 @@ class MasteryTracker {
191041
190929
  const analytics = await this.edubridgeNamespace.analytics.getEnrollmentFacts(enrollment.id);
191042
190930
  return analytics.facts;
191043
190931
  } catch (error2) {
191044
- log32.error("[MasteryTracker] Failed to load enrollment analytics facts", {
190932
+ log3.error("[MasteryTracker] Failed to load enrollment analytics facts", {
191045
190933
  studentId,
191046
190934
  courseId,
191047
190935
  error: error2
@@ -191164,7 +191052,7 @@ class ProgressRecorder {
191164
191052
  if (score !== undefined) {
191165
191053
  await this.createGradebookEntry(actualLineItemId, studentId, currentAttemptNumber, score, totalQuestions, correctQuestions, calculatedXp, masteredUnits, scoreStatus, inProgress, progressData.appName);
191166
191054
  } else {
191167
- log32.warn("[ProgressRecorder] Score not provided, skipping gradebook entry", {
191055
+ log3.warn("[ProgressRecorder] Score not provided, skipping gradebook entry", {
191168
191056
  studentId,
191169
191057
  activityId,
191170
191058
  attemptNumber: currentAttemptNumber
@@ -191211,13 +191099,13 @@ class ProgressRecorder {
191211
191099
  }
191212
191100
  calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, isFirstAttempt) {
191213
191101
  if (xpEarned !== undefined) {
191214
- log32.debug("[ProgressRecorder] Using provided XP", { xpEarned });
191102
+ log3.debug("[ProgressRecorder] Using provided XP", { xpEarned });
191215
191103
  return xpEarned;
191216
191104
  }
191217
191105
  if (progressData.sessionDurationSeconds && totalQuestions && correctQuestions) {
191218
191106
  const accuracy = correctQuestions / totalQuestions;
191219
191107
  const calculatedXp = calculateXp(progressData.sessionDurationSeconds, accuracy, isFirstAttempt);
191220
- log32.debug("[ProgressRecorder] Calculated XP", {
191108
+ log3.debug("[ProgressRecorder] Calculated XP", {
191221
191109
  durationSeconds: progressData.sessionDurationSeconds,
191222
191110
  accuracy,
191223
191111
  isFirstAttempt,
@@ -191299,7 +191187,7 @@ class ProgressRecorder {
191299
191187
  sensorUrl: progressData.sensorUrl,
191300
191188
  extensions: extensions || progressData.extensions
191301
191189
  }).catch((error2) => {
191302
- log32.error("[ProgressRecorder] Failed to emit activity event", { error: error2 });
191190
+ log3.error("[ProgressRecorder] Failed to emit activity event", { error: error2 });
191303
191191
  });
191304
191192
  }
191305
191193
  }
@@ -194030,9 +193918,9 @@ class ZodUnion2 extends ZodType2 {
194030
193918
  return this._def.options;
194031
193919
  }
194032
193920
  }
194033
- ZodUnion2.create = (types4, params) => {
193921
+ ZodUnion2.create = (types22, params) => {
194034
193922
  return new ZodUnion2({
194035
- options: types4,
193923
+ options: types22,
194036
193924
  typeName: ZodFirstPartyTypeKind2.ZodUnion,
194037
193925
  ...processCreateParams2(params)
194038
193926
  });
@@ -195345,7 +195233,7 @@ class StudentResolver {
195345
195233
  if (error2 instanceof TimebackAuthenticationError || error2 instanceof StudentNotFoundError || error2 instanceof TimebackError) {
195346
195234
  throw error2;
195347
195235
  }
195348
- log32.error("[StudentResolver] Failed to find student by email", {
195236
+ log3.error("[StudentResolver] Failed to find student by email", {
195349
195237
  email,
195350
195238
  error: error2
195351
195239
  });
@@ -195369,7 +195257,7 @@ class StudentResolver {
195369
195257
  this.cacheStudent(email, studentId, studentData);
195370
195258
  return studentData;
195371
195259
  } catch (error2) {
195372
- log32.error("[StudentResolver] Failed to fetch student by ID, using fallback", {
195260
+ log3.error("[StudentResolver] Failed to fetch student by ID, using fallback", {
195373
195261
  studentId,
195374
195262
  error: error2,
195375
195263
  errorMessage: error2 instanceof Error ? error2.message : String(error2),
@@ -195420,7 +195308,7 @@ class TimebackClient {
195420
195308
  this.sessionRecorder = new SessionRecorder(this.studentResolver, this.caliper);
195421
195309
  if (this.credentials) {
195422
195310
  this._ensureAuthenticated().catch((error2) => {
195423
- log32.error("[TimebackClient] Auto-authentication failed", { error: error2 });
195311
+ log3.error("[TimebackClient] Auto-authentication failed", { error: error2 });
195424
195312
  });
195425
195313
  }
195426
195314
  }
@@ -195480,11 +195368,11 @@ class TimebackClient {
195480
195368
  };
195481
195369
  const tokenData = await getTimebackTokenResponse(authConfig);
195482
195370
  this.setToken(tokenData.access_token, tokenData.expires_in);
195483
- log32.debug("[TimebackClient] Authentication successful", {
195371
+ log3.debug("[TimebackClient] Authentication successful", {
195484
195372
  expiresIn: tokenData.expires_in
195485
195373
  });
195486
195374
  } catch (error2) {
195487
- log32.error("[TimebackClient] Authentication failed", { error: error2 });
195375
+ log3.error("[TimebackClient] Authentication failed", { error: error2 });
195488
195376
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
195489
195377
  throw new TimebackAuthenticationError(`TimeBack authentication failed: ${errorMessage}. Verify that your client credentials are correct and not expired. Environment: ${this.baseUrl}.`);
195490
195378
  }
@@ -195536,18 +195424,27 @@ class TimebackClient {
195536
195424
  await this._ensureAuthenticated();
195537
195425
  return this.sessionRecorder.record(courseId, studentIdentifier, sessionData);
195538
195426
  }
195539
- async getEnrollments(studentId, options) {
195427
+ async getEnrollments(studentId) {
195428
+ const cached = this.cacheManager.getEnrollments(studentId);
195429
+ if (cached) {
195430
+ return cached;
195431
+ }
195540
195432
  await this._ensureAuthenticated();
195541
- const classes = await this.oneroster.classes.listByStudent(studentId, options);
195542
- return classes.filter((cls) => cls.sourcedId && cls.status && cls.course?.sourcedId).map((cls) => ({
195543
- sourcedId: cls.sourcedId,
195544
- title: cls.title,
195545
- classCode: cls.classCode ?? null,
195546
- courseId: cls.course.sourcedId,
195547
- status: cls.status,
195548
- grades: cls.grades ?? null,
195549
- subjects: cls.subjects ?? null
195550
- }));
195433
+ const edubridgeEnrollments = await this.edubridge.enrollments.listByUser(studentId);
195434
+ const enrollments = edubridgeEnrollments.map((enrollment) => {
195435
+ const grades = enrollment.course.grades ? enrollment.course.grades.map((g52) => parseInt(g52, 10)).filter(isTimebackGrade) : null;
195436
+ const subjects = enrollment.course.subjects ? enrollment.course.subjects.filter(isTimebackSubject) : null;
195437
+ return {
195438
+ sourcedId: enrollment.id,
195439
+ title: enrollment.course.title,
195440
+ courseId: enrollment.course.id,
195441
+ status: "active",
195442
+ grades,
195443
+ subjects
195444
+ };
195445
+ });
195446
+ this.cacheManager.setEnrollments(studentId, enrollments);
195447
+ return enrollments;
195551
195448
  }
195552
195449
  clearCaches() {
195553
195450
  this.cacheManager.clearAll();
@@ -195786,7 +195683,9 @@ var init_constants2 = __esm4(() => {
195786
195683
  studentTTL: 600000,
195787
195684
  studentMaxSize: 500,
195788
195685
  assessmentTTL: 1800000,
195789
- assessmentMaxSize: 200
195686
+ assessmentMaxSize: 200,
195687
+ enrollmentTTL: 5000,
195688
+ enrollmentMaxSize: 100
195790
195689
  };
195791
195690
  CONFIG_DEFAULTS2 = {
195792
195691
  fileNames: ["timeback.config.js", "timeback.config.json"]
@@ -195854,8 +195753,8 @@ var init_constants2 = __esm4(() => {
195854
195753
  });
195855
195754
  init_constants2();
195856
195755
  var isObject3 = (value) => typeof value === "object" && value !== null;
195857
- var SUBJECT_VALUES = TIMEBACK_SUBJECTS2;
195858
- var GRADE_VALUES = TIMEBACK_GRADE_LEVELS2;
195756
+ var SUBJECT_VALUES2 = TIMEBACK_SUBJECTS2;
195757
+ var GRADE_VALUES2 = TIMEBACK_GRADE_LEVELS2;
195859
195758
  function isCourseMetadata(value) {
195860
195759
  return isObject3(value);
195861
195760
  }
@@ -195871,11 +195770,11 @@ function isPlaycademyResourceMetadata2(value) {
195871
195770
  }
195872
195771
  return isObject3(value.mastery);
195873
195772
  }
195874
- function isTimebackSubject(value) {
195875
- return typeof value === "string" && SUBJECT_VALUES.includes(value);
195773
+ function isTimebackSubject2(value) {
195774
+ return typeof value === "string" && SUBJECT_VALUES2.includes(value);
195876
195775
  }
195877
- function isTimebackGrade(value) {
195878
- return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
195776
+ function isTimebackGrade2(value) {
195777
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES2.includes(value);
195879
195778
  }
195880
195779
  var UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
195881
195780
  function isValidUUID(value) {
@@ -196179,6 +196078,68 @@ function buildResourceMetadata({
196179
196078
  }
196180
196079
  return metadata2;
196181
196080
  }
196081
+ init_src();
196082
+ async function fetchEnrollmentsForUser(timebackId) {
196083
+ const db = getDatabase();
196084
+ const isLocal = process.env.PUBLIC_IS_LOCAL === "true";
196085
+ if (isLocal) {
196086
+ const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
196087
+ return allIntegrations.map((integration) => ({
196088
+ gameId: integration.gameId,
196089
+ grade: integration.grade,
196090
+ subject: integration.subject,
196091
+ courseId: integration.courseId
196092
+ }));
196093
+ }
196094
+ log2.debug("[timeback-enrollments] Fetching student enrollments from TimeBack", { timebackId });
196095
+ try {
196096
+ const client2 = await getTimebackClient();
196097
+ const classes = await client2.getEnrollments(timebackId);
196098
+ const courseIds = classes.map((cls) => cls.courseId).filter((id) => Boolean(id));
196099
+ if (courseIds.length === 0) {
196100
+ return [];
196101
+ }
196102
+ const integrations = await db.query.gameTimebackIntegrations.findMany({
196103
+ where: inArray(gameTimebackIntegrations.courseId, courseIds)
196104
+ });
196105
+ return integrations.map((integration) => ({
196106
+ gameId: integration.gameId,
196107
+ grade: integration.grade,
196108
+ subject: integration.subject,
196109
+ courseId: integration.courseId
196110
+ }));
196111
+ } catch (error2) {
196112
+ log2.warn("[timeback-enrollments] Failed to fetch TimeBack enrollments:", {
196113
+ error: error2,
196114
+ timebackId
196115
+ });
196116
+ return [];
196117
+ }
196118
+ }
196119
+ async function fetchUserRole(timebackId) {
196120
+ log2.debug("[timeback] Fetching user role from TimeBack", { timebackId });
196121
+ try {
196122
+ const client2 = await getTimebackClient();
196123
+ const user = await client2.oneroster.users.get(timebackId);
196124
+ const primaryRole = user.roles.find((r22) => r22.roleType === "primary");
196125
+ const role = primaryRole?.role ?? user.roles[0]?.role ?? "student";
196126
+ log2.debug("[timeback] Resolved user role", { timebackId, role });
196127
+ return role;
196128
+ } catch (error2) {
196129
+ log2.warn("[timeback] Failed to fetch user role, defaulting to student:", {
196130
+ error: error2,
196131
+ timebackId
196132
+ });
196133
+ return "student";
196134
+ }
196135
+ }
196136
+ async function fetchUserTimebackData(timebackId) {
196137
+ const [role, enrollments] = await Promise.all([
196138
+ fetchUserRole(timebackId),
196139
+ fetchEnrollmentsForUser(timebackId)
196140
+ ]);
196141
+ return { role, enrollments };
196142
+ }
196182
196143
  var AchievementCompletionType;
196183
196144
  ((AchievementCompletionType2) => {
196184
196145
  AchievementCompletionType2["TIME_PLAYED_SESSION"] = "time_played_session";
@@ -199484,21 +199445,6 @@ var logger3 = {
199484
199445
  warn: (msg) => getLogger().warn(msg),
199485
199446
  error: (msg) => getLogger().error(msg)
199486
199447
  };
199487
- function detectTimebackCourses() {
199488
- const coursePattern = /^SANDBOX_TIMEBACK_COURSE_(\d+)_([A-Z_]+)$/i;
199489
- const courses = [];
199490
- for (const [key, value] of Object.entries(process.env)) {
199491
- const match2 = key.match(coursePattern);
199492
- if (match2 && value) {
199493
- const gradeStr = match2[1];
199494
- const subjectStr = match2[2];
199495
- const grade = parseInt(gradeStr, 10);
199496
- const subject = subjectStr.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
199497
- courses.push({ grade, subject, courseId: value });
199498
- }
199499
- }
199500
- return courses;
199501
- }
199502
199448
  async function seedCoreGames(db) {
199503
199449
  const now2 = new Date;
199504
199450
  const coreGames = [
@@ -199523,28 +199469,6 @@ async function seedCoreGames(db) {
199523
199469
  console.error(`Error seeding core game '${gameData.slug}':`, error2);
199524
199470
  }
199525
199471
  }
199526
- if (hasTimebackCredentials()) {
199527
- const courses = detectTimebackCourses();
199528
- if (courses.length > 0) {
199529
- for (const course of courses) {
199530
- try {
199531
- await db.insert(gameTimebackIntegrations).values({
199532
- id: crypto.randomUUID(),
199533
- gameId: CORE_GAME_UUIDS.PLAYGROUND,
199534
- courseId: course.courseId,
199535
- grade: course.grade,
199536
- subject: course.subject,
199537
- totalXp: null,
199538
- lastVerifiedAt: null,
199539
- createdAt: now2,
199540
- updatedAt: now2
199541
- }).onConflictDoNothing();
199542
- } catch (error2) {
199543
- console.error(`Error seeding TimeBack integration for playground (${course.subject} grade ${course.grade}):`, error2);
199544
- }
199545
- }
199546
- }
199547
- }
199548
199472
  }
199549
199473
  async function seedCurrentProjectGame(db, project) {
199550
199474
  const now2 = new Date;
@@ -199573,24 +199497,6 @@ async function seedCurrentProjectGame(db, project) {
199573
199497
  updatedAt: now2
199574
199498
  };
199575
199499
  const [newGame] = await db.insert(games).values(gameRecord).returning();
199576
- if (hasTimebackCredentials()) {
199577
- const courses = detectTimebackCourses();
199578
- if (courses.length > 0) {
199579
- for (const course of courses) {
199580
- await db.insert(gameTimebackIntegrations).values({
199581
- id: crypto.randomUUID(),
199582
- gameId: newGame.id,
199583
- courseId: course.courseId,
199584
- grade: course.grade,
199585
- subject: course.subject,
199586
- totalXp: null,
199587
- lastVerifiedAt: null,
199588
- createdAt: now2,
199589
- updatedAt: now2
199590
- }).onConflictDoNothing();
199591
- }
199592
- }
199593
- }
199594
199500
  return newGame;
199595
199501
  } catch (error2) {
199596
199502
  console.error("❌ Error seeding project game:", error2);
@@ -199647,12 +199553,23 @@ async function seedSpriteTemplates(db) {
199647
199553
  }
199648
199554
  }
199649
199555
  }
199556
+ function resolveStudentId(studentId) {
199557
+ if (!studentId)
199558
+ return null;
199559
+ if (studentId === "mock")
199560
+ return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
199561
+ return studentId;
199562
+ }
199563
+ function getAdminTimebackId() {
199564
+ return resolveStudentId(config.timeback.studentId);
199565
+ }
199650
199566
  async function seedDemoData(db) {
199651
199567
  try {
199568
+ const adminTimebackId = getAdminTimebackId();
199652
199569
  for (const [role, user] of Object.entries(DEMO_USERS)) {
199653
199570
  const userValues = {
199654
199571
  ...user,
199655
- timebackId: role === "admin" && config.timeback.studentId ? config.timeback.studentId : null
199572
+ timebackId: role === "admin" ? adminTimebackId : null
199656
199573
  };
199657
199574
  await db.insert(users).values(userValues).onConflictDoNothing();
199658
199575
  }
@@ -205015,9 +204932,11 @@ async function getUserMe(ctx) {
205015
204932
  const timebackAccount = await db.query.accounts.findFirst({
205016
204933
  where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
205017
204934
  });
204935
+ const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId) : undefined;
205018
204936
  return {
205019
204937
  ...userData,
205020
- hasTimebackAccount: !!timebackAccount
204938
+ hasTimebackAccount: !!timebackAccount,
204939
+ timeback: timeback3
205021
204940
  };
205022
204941
  } catch (error2) {
205023
204942
  if (error2 instanceof ApiError)
@@ -209128,7 +209047,7 @@ async function getPlayerCharacter(ctx) {
209128
209047
  throw ApiError.unauthorized("Login required");
209129
209048
  try {
209130
209049
  const db = getDatabase();
209131
- const pc2 = await db.query.playerCharacters.findFirst({
209050
+ const pc3 = await db.query.playerCharacters.findFirst({
209132
209051
  where: eq(playerCharacters.userId, user.id),
209133
209052
  with: {
209134
209053
  accessories: {
@@ -209138,7 +209057,7 @@ async function getPlayerCharacter(ctx) {
209138
209057
  }
209139
209058
  }
209140
209059
  });
209141
- return pc2 ?? null;
209060
+ return pc3 ?? null;
209142
209061
  } catch (error2) {
209143
209062
  log2.error("Error fetching player character", { error: error2 });
209144
209063
  throw ApiError.internal("Failed to get player character", error2);
@@ -209154,7 +209073,7 @@ async function getPlayerCharacterById(ctx) {
209154
209073
  }
209155
209074
  try {
209156
209075
  const db = getDatabase();
209157
- const pc2 = await db.query.playerCharacters.findFirst({
209076
+ const pc3 = await db.query.playerCharacters.findFirst({
209158
209077
  where: eq(playerCharacters.userId, userId),
209159
209078
  with: {
209160
209079
  accessories: {
@@ -209164,7 +209083,7 @@ async function getPlayerCharacterById(ctx) {
209164
209083
  }
209165
209084
  }
209166
209085
  });
209167
- return pc2 ?? null;
209086
+ return pc3 ?? null;
209168
209087
  } catch (error2) {
209169
209088
  log2.error("Error fetching player character by ID", { userId, error: error2 });
209170
209089
  throw ApiError.internal("Failed to get player character", error2);
@@ -209999,11 +209918,11 @@ async function setupTimebackIntegration(ctx) {
209999
209918
  totalXp: derivedTotalXp,
210000
209919
  masterableUnits: derivedMasterableUnits
210001
209920
  } = courseConfig;
210002
- if (!isTimebackSubject(subject)) {
209921
+ if (!isTimebackSubject2(subject)) {
210003
209922
  log2.error("[API] Invalid subject in TimeBack request", { subject });
210004
209923
  throw ApiError.badRequest(`Invalid subject "${subject}" provided for course "${title}".`);
210005
209924
  }
210006
- if (!isTimebackGrade(grade)) {
209925
+ if (!isTimebackGrade2(grade)) {
210007
209926
  log2.error("[API] Invalid grade in TimeBack request", { grade });
210008
209927
  throw ApiError.badRequest(`Invalid grade "${grade}" provided for course "${title}".`);
210009
209928
  }
@@ -210400,27 +210319,11 @@ async function getStudentEnrollments(ctx) {
210400
210319
  throw ApiError.badRequest("Missing timebackId parameter");
210401
210320
  }
210402
210321
  log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
210403
- const client2 = await getTimebackClient();
210404
- const classes = await client2.getEnrollments(timebackId, { limit: 100 });
210405
- const courseIds = classes.map((cls) => cls.courseId).filter((id) => Boolean(id));
210406
- if (courseIds.length === 0) {
210407
- return { enrollments: [] };
210408
- }
210409
- const db = getDatabase();
210410
- const integrations = await db.query.gameTimebackIntegrations.findMany({
210411
- where: inArray(gameTimebackIntegrations.courseId, courseIds)
210412
- });
210413
- const enrollments = integrations.map((integration) => ({
210414
- gameId: integration.gameId,
210415
- grade: integration.grade,
210416
- subject: integration.subject,
210417
- courseId: integration.courseId
210418
- }));
210322
+ const enrollments = await fetchEnrollmentsForUser(timebackId);
210419
210323
  log2.info("[API] Retrieved student enrollments", {
210420
210324
  userId: user.id,
210421
210325
  timebackId,
210422
- totalClasses: classes.length,
210423
- mappedEnrollments: enrollments.length
210326
+ enrollmentCount: enrollments.length
210424
210327
  });
210425
210328
  return { enrollments };
210426
210329
  }
@@ -210614,11 +210517,11 @@ timebackRouter.post("/end-activity", async (c3) => {
210614
210517
  return c3.json(createUnknownErrorResponse(error2), 500);
210615
210518
  }
210616
210519
  });
210617
- timebackRouter.get("/enrollments/:studentId", async (c3) => {
210618
- const studentId = c3.req.param("studentId");
210520
+ timebackRouter.get("/enrollments/:timebackId", async (c3) => {
210521
+ const timebackId = c3.req.param("timebackId");
210619
210522
  const ctx = {
210620
210523
  user: c3.get("user"),
210621
- params: { studentId },
210524
+ params: { timebackId },
210622
210525
  url: new URL(c3.req.url),
210623
210526
  request: c3.req.raw
210624
210527
  };
@@ -210928,8 +210831,8 @@ function registerRoutes(app) {
210928
210831
  return c3.json({ error: "Not Found", message: `Route ${c3.req.path} not found` }, 404);
210929
210832
  });
210930
210833
  }
210931
- var version3 = package_default2.version;
210932
- async function startServer2(port, project, options = {}) {
210834
+ var version3 = package_default.version;
210835
+ async function startServer(port, project, options = {}) {
210933
210836
  const processedOptions = processServerOptions(port, options);
210934
210837
  const db = await setupServerDatabase(processedOptions, project);
210935
210838
  const app = createApp(db, {
@@ -210951,6 +210854,44 @@ async function startServer2(port, project, options = {}) {
210951
210854
  };
210952
210855
  }
210953
210856
 
210857
+ // src/lib/logging/adapter.ts
210858
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
210859
+ function formatTimestamp2() {
210860
+ const now2 = new Date;
210861
+ const hours = now2.getHours();
210862
+ const minutes = now2.getMinutes().toString().padStart(2, "0");
210863
+ const seconds = now2.getSeconds().toString().padStart(2, "0");
210864
+ const ampm = hours >= 12 ? "PM" : "AM";
210865
+ const displayHours = hours % 12 || 12;
210866
+ return import_picocolors3.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
210867
+ }
210868
+ function createLoggerAdapter(prefix2) {
210869
+ const formattedPrefix = import_picocolors3.dim(`(${prefix2})`);
210870
+ const label = import_picocolors3.cyan(import_picocolors3.bold("[playcademy]"));
210871
+ return {
210872
+ info: (msg) => console.log(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`),
210873
+ warn: (msg) => console.warn(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`),
210874
+ error: (msg) => console.error(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`)
210875
+ };
210876
+ }
210877
+ // src/lib/logging/utils.ts
210878
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
210879
+ function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
210880
+ const INDENT = " ".repeat(2);
210881
+ viteConfig.logger.info("");
210882
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green(import_picocolors4.bold("PLAYCADEMY"))} ${import_picocolors4.green(`v${pluginVersion}`)}`);
210883
+ viteConfig.logger.info("");
210884
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Game:")} ${import_picocolors4.cyan(projectInfo.slug)}`);
210885
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Sandbox:")} ${import_picocolors4.cyan(`http://localhost:${import_picocolors4.bold(servers.sandbox.toString())}/api`)}`);
210886
+ if (servers.backend) {
210887
+ const backendUrl = servers.vite ? `http://localhost:${import_picocolors4.bold(servers.vite.toString())}/api ${import_picocolors4.dim(`(via ${servers.backend})`)}` : `http://localhost:${import_picocolors4.bold(servers.backend.toString())}/api`;
210888
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Backend:")} ${import_picocolors4.cyan(backendUrl)}`);
210889
+ }
210890
+ if (servers.realtime) {
210891
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Realtime:")} ${import_picocolors4.cyan(`ws://localhost:${import_picocolors4.bold(servers.realtime.toString())}`)}`);
210892
+ }
210893
+ viteConfig.logger.info("");
210894
+ }
210954
210895
  // src/lib/sandbox/project-info.ts
210955
210896
  import fs6 from "node:fs";
210956
210897
  import path5 from "node:path";
@@ -210989,6 +210930,14 @@ async function extractProjectInfo(viteConfig) {
210989
210930
  }
210990
210931
 
210991
210932
  // src/lib/sandbox/timeback.ts
210933
+ var import_picocolors5 = __toESM(require_picocolors(), 1);
210934
+ function getEffectiveTimebackId(timeback) {
210935
+ if (timeback?.timebackId)
210936
+ return timeback.timebackId;
210937
+ if (timeback?.courses && Object.keys(timeback.courses).length > 0)
210938
+ return "mock";
210939
+ return;
210940
+ }
210992
210941
  function detectTimebackOptions() {
210993
210942
  if (process.env.TIMEBACK_LOCAL === "true") {
210994
210943
  return {
@@ -211013,16 +210962,60 @@ function detectTimebackOptions() {
211013
210962
  }
211014
210963
  return;
211015
210964
  }
210965
+ function hasTimebackCredentials2() {
210966
+ const config2 = detectTimebackOptions();
210967
+ if (!config2)
210968
+ return false;
210969
+ if (config2.mode === "local") {
210970
+ return !!(config2.onerosterApiUrl && config2.caliperApiUrl);
210971
+ }
210972
+ return !!(config2.onerosterApiUrl && config2.clientId && config2.clientSecret && config2.authUrl);
210973
+ }
210974
+ function validateTimebackConfig(viteConfig, timeback) {
210975
+ if (!timeback?.courses)
210976
+ return;
210977
+ const realCourses = Object.entries(timeback.courses).filter(([, value]) => value !== "mock");
210978
+ if (realCourses.length > 0 && !hasTimebackCredentials2()) {
210979
+ const courseList = realCourses.map(([key]) => key).join(", ");
210980
+ viteConfig.logger.warn("");
210981
+ viteConfig.logger.warn(import_picocolors5.default.yellow(`⚠️ TimeBack: Real course IDs for ${import_picocolors5.default.bold(courseList)} but credentials missing.`));
210982
+ viteConfig.logger.warn(import_picocolors5.default.dim(` Required: TIMEBACK_API_CLIENT_ID, TIMEBACK_API_CLIENT_SECRET, TIMEBACK_API_AUTH_URL, TIMEBACK_ONEROSTER_API_URL`));
210983
+ viteConfig.logger.warn(import_picocolors5.default.dim(` Or use 'mock' for local testing.`));
210984
+ viteConfig.logger.warn("");
210985
+ }
210986
+ }
210987
+ function generateTimebackJson(timeback) {
210988
+ if (!timeback?.courses || Object.keys(timeback.courses).length === 0) {
210989
+ return;
210990
+ }
210991
+ const enrollments = [];
210992
+ for (const [key, value] of Object.entries(timeback.courses)) {
210993
+ const parts2 = key.split(":");
210994
+ const subject = parts2[0];
210995
+ const gradeStr = parts2[1];
210996
+ const grade = gradeStr ? parseInt(gradeStr, 10) : NaN;
210997
+ if (!subject || isNaN(grade))
210998
+ continue;
210999
+ const courseId = value === "mock" ? `mock-${subject.toLowerCase()}-g${grade}` : value;
211000
+ enrollments.push({ subject, grade, courseId });
211001
+ }
211002
+ if (enrollments.length === 0) {
211003
+ return;
211004
+ }
211005
+ const roleOverride = getTimebackRoleOverride();
211006
+ const role = roleOverride ?? timeback.role ?? "student";
211007
+ return JSON.stringify({ role, enrollments });
211008
+ }
211016
211009
 
211017
211010
  // src/lib/sandbox/server.ts
211018
211011
  function printSandboxInfo(viteConfig, apiPort, realtimePort, projectInfo, realtimeEnabled) {
211019
211012
  viteConfig.logger.info("");
211020
- viteConfig.logger.info(` ${import_picocolors5.default.green(import_picocolors5.default.bold("PLAYCADEMY"))} ${import_picocolors5.default.green(`v${version3}`)}`);
211013
+ viteConfig.logger.info(` ${import_picocolors6.default.green(import_picocolors6.default.bold("PLAYCADEMY"))} ${import_picocolors6.default.green(`v${version3}`)}`);
211021
211014
  viteConfig.logger.info("");
211022
- viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Game:")} ${import_picocolors5.default.cyan(projectInfo.slug)}`);
211023
- viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Sandbox:")} ${import_picocolors5.default.cyan(`http://localhost:${import_picocolors5.default.bold(apiPort.toString())}/api`)}`);
211015
+ viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Game:")} ${import_picocolors6.default.cyan(projectInfo.slug)}`);
211016
+ viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Sandbox:")} ${import_picocolors6.default.cyan(`http://localhost:${import_picocolors6.default.bold(apiPort.toString())}/api`)}`);
211024
211017
  if (realtimeEnabled) {
211025
- viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Realtime:")} ${import_picocolors5.default.cyan(`ws://localhost:${import_picocolors5.default.bold(realtimePort.toString())}`)}`);
211018
+ viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Realtime:")} ${import_picocolors6.default.cyan(`ws://localhost:${import_picocolors6.default.bold(realtimePort.toString())}`)}`);
211026
211019
  }
211027
211020
  viteConfig.logger.info("");
211028
211021
  }
@@ -211037,7 +211030,8 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211037
211030
  databasePath,
211038
211031
  realtimeEnabled = false,
211039
211032
  realtimePort,
211040
- logLevel = "info"
211033
+ logLevel = "info",
211034
+ timebackId
211041
211035
  } = options;
211042
211036
  if (!autoStart || viteConfig.command !== "serve") {
211043
211037
  const baseUrl = customUrl ?? `http://localhost:${DEFAULT_PORTS2.SANDBOX}`;
@@ -211064,10 +211058,16 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211064
211058
  const sandboxPort = await findAvailablePort(DEFAULT_PORTS2.SANDBOX);
211065
211059
  const baseUrl = `http://localhost:${sandboxPort}`;
211066
211060
  const projectInfo = await extractProjectInfo(viteConfig);
211067
- const timebackOptions = detectTimebackOptions();
211061
+ let timebackOptions = detectTimebackOptions();
211062
+ if (timebackId) {
211063
+ timebackOptions = {
211064
+ ...timebackOptions,
211065
+ studentId: timebackId
211066
+ };
211067
+ }
211068
211068
  const finalRealtimePort = realtimePort ?? await findAvailablePort(sandboxPort + 1);
211069
211069
  const realtimeUrl = `ws://localhost:${finalRealtimePort}`;
211070
- const server = await startServer2(sandboxPort, projectInfo, {
211070
+ const server = await startServer(sandboxPort, projectInfo, {
211071
211071
  verbose,
211072
211072
  quiet,
211073
211073
  seed,
@@ -211105,7 +211105,7 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211105
211105
  }
211106
211106
  };
211107
211107
  } catch (error2) {
211108
- viteConfig.logger.error(import_picocolors5.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211108
+ viteConfig.logger.error(import_picocolors6.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211109
211109
  return {
211110
211110
  baseUrl: `http://localhost:${DEFAULT_PORTS2.SANDBOX}`,
211111
211111
  realtimeUrl: "ws://localhost:4322",
@@ -211330,16 +211330,13 @@ var shell_with_corner_badge_default = `<!doctype html>
211330
211330
  <head>
211331
211331
  <meta charset="UTF-8" />
211332
211332
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
211333
- <title>Playcademy - Development</title>
211333
+ <title>Playcademy Dev</title>
211334
211334
  <link rel="preconnect" href="https://fonts.googleapis.com" />
211335
211335
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
211336
211336
  <link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet" />
211337
+ <script type="module" src="/@vite/client"></script>
211337
211338
  <script type="importmap">
211338
- {
211339
- "imports": {
211340
- "@playcademy/sdk": "https://esm.sh/@playcademy/sdk@latest"
211341
- }
211342
- }
211339
+ { "imports": { "@playcademy/sdk": "https://esm.sh/@playcademy/sdk@latest" } }
211343
211340
  </script>
211344
211341
  <style>
211345
211342
  * {
@@ -211347,14 +211344,11 @@ var shell_with_corner_badge_default = `<!doctype html>
211347
211344
  padding: 0;
211348
211345
  box-sizing: border-box;
211349
211346
  }
211350
-
211351
211347
  body {
211352
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
211353
211348
  height: 100vh;
211354
211349
  overflow: hidden;
211355
211350
  }
211356
-
211357
- .dev-badge {
211351
+ #badge {
211358
211352
  position: fixed;
211359
211353
  top: 0.5rem;
211360
211354
  left: 0.5rem;
@@ -211369,202 +211363,141 @@ var shell_with_corner_badge_default = `<!doctype html>
211369
211363
  letter-spacing: 0.05em;
211370
211364
  transition: background 0.2s ease;
211371
211365
  }
211372
-
211373
- .dev-badge.offline {
211366
+ #badge.offline {
211374
211367
  background: rgba(211, 47, 47, 0.9);
211375
211368
  }
211376
-
211377
- .dev-badge-status {
211378
- font-size: 0.7rem;
211379
- }
211380
-
211381
- .dev-badge-status:not(:empty) {
211382
- margin-left: 0.5rem;
211383
- }
211384
-
211385
- .game-container {
211386
- width: 100vw;
211387
- height: 100vh;
211388
- position: relative;
211389
- overflow: hidden;
211390
- }
211391
-
211392
- .game-frame {
211369
+ #frame {
211393
211370
  position: absolute;
211394
211371
  inset: 0;
211395
211372
  border: none;
211396
211373
  width: 100%;
211397
211374
  height: 100%;
211398
211375
  }
211399
-
211400
- .error {
211401
- position: absolute;
211402
- inset: 0;
211403
- display: flex;
211404
- flex-direction: column;
211405
- align-items: center;
211406
- justify-content: center;
211407
- color: #d32f2f;
211408
- background-color: #ffebee;
211409
- }
211410
-
211411
211376
  .hidden {
211412
211377
  display: none;
211413
211378
  }
211414
211379
  </style>
211415
211380
  </head>
211416
211381
  <body>
211417
- <div class="dev-badge" id="devBadge">
211418
- PLAYCADEMY<span class="dev-badge-status" id="badgeStatus"></span>
211419
- </div>
211420
-
211421
- <div class="game-container">
211422
- <div class="error hidden" id="error">
211423
- <div>❌ Failed to load game</div>
211424
- <div
211425
- style="margin-top: 0.5rem; font-size: 0.9rem; opacity: 0.8"
211426
- id="errorMessage"
211427
- ></div>
211428
- </div>
211429
-
211430
- <iframe class="game-frame hidden" id="gameFrame" src="/"></iframe>
211431
- </div>
211382
+ <div id="badge">PLAYCADEMY</div>
211383
+ <iframe id="frame" class="hidden"></iframe>
211432
211384
 
211433
211385
  <script type="module">
211434
211386
  import { MessageEvents, messaging } from '@playcademy/sdk'
211435
211387
 
211436
- const devBadge = document.getElementById('devBadge')
211437
- const badgeStatus = document.getElementById('badgeStatus')
211438
- const loading = document.getElementById('loading')
211439
- const error = document.getElementById('error')
211440
- const errorMessage = document.getElementById('errorMessage')
211441
- const gameFrame = document.getElementById('gameFrame')
211388
+ // Config (injected by vite plugin)
211389
+ const CONFIG = {
211390
+ sandboxUrl: '{{SANDBOX_URL}}',
211391
+ gameId: '{{GAME_ID}}',
211392
+ gameUrl: '{{GAME_URL}}' || undefined,
211393
+ realtimeUrl: '{{REALTIME_URL}}',
211394
+ timebackJson: '{{TIMEBACK_DATA}}',
211395
+ }
211442
211396
 
211443
- const logIfDebug = (...args) => {
211444
- if (window.PLAYCADEMY_DEBUG) {
211445
- console.log('[PlaycademyDevShell]', ...args)
211397
+ // Parse timeback data (role + enrollments)
211398
+ const timeback = (() => {
211399
+ try {
211400
+ return JSON.parse(CONFIG.timebackJson)
211401
+ } catch {
211402
+ return null
211446
211403
  }
211447
- }
211404
+ })()
211448
211405
 
211449
- logIfDebug('[PlaycademyDevShell] Initialized')
211406
+ // Elements
211407
+ const badge = document.getElementById('badge')
211408
+ const frame = document.getElementById('frame')
211450
211409
 
211451
- async function checkSandboxConnection() {
211410
+ // Debug logging
211411
+ const log = (...args) => window.PLAYCADEMY_DEBUG && console.log('[DevShell]', ...args)
211412
+
211413
+ // Check sandbox connection
211414
+ async function checkSandbox() {
211452
211415
  try {
211453
- const response = await fetch('{{SANDBOX_URL}}/api/users/me', {
211454
- headers: {
211455
- Authorization: 'Bearer sandbox-demo-token',
211456
- },
211416
+ const res = await fetch(\`\${CONFIG.sandboxUrl}/api/users/me\`, {
211417
+ headers: { Authorization: 'Bearer sandbox-demo-token' },
211457
211418
  })
211458
-
211459
- if (response.ok) {
211460
- badgeStatus.textContent = ''
211461
- devBadge.classList.remove('offline')
211462
- logIfDebug('[PlaycademyDevShell] Sandbox API connection successful')
211463
- } else {
211464
- throw new Error('Sandbox API not available')
211465
- }
211466
- } catch (err) {
211467
- logIfDebug('[PlaycademyDevShell] Sandbox connection failed:', err)
211468
- badgeStatus.textContent = ' ⚠️'
211469
- devBadge.classList.add('offline')
211419
+ if (!res.ok) throw new Error('Sandbox unavailable')
211420
+ badge.classList.remove('offline')
211421
+ log('Sandbox connected')
211422
+ return true
211423
+ } catch (e) {
211424
+ badge.classList.add('offline')
211425
+ badge.textContent = 'PLAYCADEMY ⚠️'
211426
+ log('Sandbox failed:', e)
211427
+ return false
211470
211428
  }
211471
211429
  }
211472
211430
 
211473
- // Load the game iframe
211474
- function loadGame() {
211475
- let handshakeInterval = null
211476
- let handshakeTimeout = null
211477
-
211478
- const initPayload = {
211479
- baseUrl: '{{SANDBOX_URL}}',
211480
- gameUrl: '{{GAME_URL}}' || undefined,
211431
+ // Init handshake with game iframe
211432
+ function initHandshake(timebackData) {
211433
+ const payload = {
211434
+ baseUrl: CONFIG.sandboxUrl,
211435
+ gameUrl: CONFIG.gameUrl,
211436
+ gameId: CONFIG.gameId,
211437
+ realtimeUrl: CONFIG.realtimeUrl,
211481
211438
  token: 'sandbox-demo-token',
211482
- gameId: '{{GAME_ID}}',
211483
- realtimeUrl: '{{REALTIME_URL}}',
211439
+ timeback: timebackData,
211484
211440
  }
211485
211441
 
211486
- const sendInit = () => {
211487
- if (!gameFrame.contentWindow) return
211488
-
211489
- messaging.send(MessageEvents.INIT, initPayload, {
211490
- target: gameFrame.contentWindow,
211491
- origin: '*',
211492
- })
211493
- }
211442
+ let interval, timeout
211494
211443
 
211495
- const stopHandshake = () => {
211496
- if (handshakeInterval) {
211497
- clearInterval(handshakeInterval)
211498
- handshakeInterval = null
211499
- }
211500
- if (handshakeTimeout) {
211501
- clearTimeout(handshakeTimeout)
211502
- handshakeTimeout = null
211444
+ const send = () => {
211445
+ if (frame.contentWindow) {
211446
+ messaging.send(MessageEvents.INIT, payload, {
211447
+ target: frame.contentWindow,
211448
+ origin: '*',
211449
+ })
211503
211450
  }
211504
211451
  }
211505
211452
 
211506
- gameFrame.onload = () => {
211507
- gameFrame.classList.remove('hidden')
211508
-
211509
- logIfDebug('[PlaycademyDevShell] Game iframe loaded, beginning init handshake')
211510
-
211511
- // Begin handshake: send immediately, then every 300ms
211512
- sendInit()
211513
- handshakeInterval = setInterval(sendInit, 300)
211514
-
211515
- // Stop handshake after 10 seconds
211516
- handshakeTimeout = setTimeout(() => {
211517
- stopHandshake()
211518
- logIfDebug('[PlaycademyDevShell] Init handshake timeout reached')
211519
- }, 10000)
211453
+ const stop = () => {
211454
+ clearInterval(interval)
211455
+ clearTimeout(timeout)
211520
211456
  }
211521
211457
 
211522
- gameFrame.onerror = () => {
211523
- loading.classList.add('hidden')
211524
- error.classList.remove('hidden')
211525
- errorMessage.textContent = 'Game iframe failed to load'
211526
- logIfDebug('[PlaycademyDevShell] Game iframe failed to load')
211527
- stopHandshake()
211458
+ frame.onload = () => {
211459
+ frame.classList.remove('hidden')
211460
+ log('Frame loaded, starting handshake')
211461
+ send()
211462
+ interval = setInterval(send, 300)
211463
+ timeout = setTimeout(() => {
211464
+ stop()
211465
+ log('Handshake timeout')
211466
+ }, 10000)
211528
211467
  }
211529
211468
 
211530
- // Listen for READY message to stop handshake
211531
211469
  messaging.listen(MessageEvents.READY, () => {
211532
- logIfDebug('[PlaycademyDevShell] Received READY from game, stopping handshake')
211533
- stopHandshake()
211470
+ stop()
211471
+ log('Game ready')
211534
211472
  })
211535
211473
 
211536
- gameFrame.src = '/'
211474
+ frame.src = '/'
211537
211475
  }
211538
211476
 
211539
- window.addEventListener('message', event => {
211540
- if (event.source === gameFrame.contentWindow) {
211541
- const { type, ...payload } = event.data || {}
211542
- if (type && type.startsWith('PLAYCADEMY_')) {
211543
- logIfDebug(
211544
- '[PlaycademyDevShell] Received message from game:',
211545
- type,
211546
- payload,
211547
- )
211548
- // Bridge the message to the local context using CustomEvent
211549
- messaging.send(type, payload)
211550
- }
211477
+ // Bridge messages from game to shell context
211478
+ window.addEventListener('message', e => {
211479
+ if (e.source !== frame.contentWindow) return
211480
+ const { type, ...payload } = e.data || {}
211481
+ if (type?.startsWith('PLAYCADEMY_')) {
211482
+ log('Message:', type, payload)
211483
+ messaging.send(type, payload)
211551
211484
  }
211552
211485
  })
211553
211486
 
211554
- // Initialize
211555
- checkSandboxConnection()
211556
- loadGame()
211487
+ // Start
211488
+ checkSandbox().then(() => initHandshake(timeback))
211557
211489
  </script>
211558
211490
  </body>
211559
211491
  </html>
211560
211492
  `;
211561
211493
 
211562
211494
  // src/server/middleware.ts
211563
- function generateLoaderHTML(sandboxUrl, gameId, realtimeUrl, showBadge, gameUrl) {
211564
- const shell = showBadge ? shell_with_corner_badge_default : shell_no_badge_default;
211565
- return shell.replace(/{{SANDBOX_URL}}/g, sandboxUrl).replace(/{{GAME_ID}}/g, gameId).replace(/{{REALTIME_URL}}/g, realtimeUrl).replace(/{{GAME_URL}}/g, gameUrl || "");
211495
+ function generateLoaderHTML(sandboxUrl, gameId, realtimeUrl, options, gameUrl) {
211496
+ const shell = options.showBadge ? shell_with_corner_badge_default : shell_no_badge_default;
211497
+ const timebackJson = generateTimebackJson(options.timeback) ?? "null";
211498
+ return shell.replace(/{{SANDBOX_URL}}/g, sandboxUrl).replace(/{{GAME_ID}}/g, gameId).replace(/{{REALTIME_URL}}/g, realtimeUrl).replace(/{{GAME_URL}}/g, gameUrl || "").replace(/{{TIMEBACK_DATA}}/g, timebackJson);
211566
211499
  }
211567
- function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211500
+ function devServerMiddleware(server, sandbox, gameUrl, options) {
211568
211501
  server.middlewares.use("/", (req, res, next) => {
211569
211502
  if (getCurrentMode() !== "platform") {
211570
211503
  next();
@@ -211576,8 +211509,7 @@ function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211576
211509
  next();
211577
211510
  } else {
211578
211511
  res.setHeader("Content-Type", "text/html");
211579
- const gameId = sandbox.project?.slug;
211580
- res.end(generateLoaderHTML(sandbox.baseUrl, gameId ?? "", sandbox.realtimeUrl, showBadge, gameUrl));
211512
+ res.end(generateLoaderHTML(sandbox.baseUrl, sandbox.project?.slug ?? "", sandbox.realtimeUrl, options, gameUrl));
211581
211513
  }
211582
211514
  return;
211583
211515
  }
@@ -211585,6 +211517,190 @@ function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211585
211517
  });
211586
211518
  }
211587
211519
 
211520
+ // src/server/hotkeys/recreate-database.ts
211521
+ var { bold: bold4, cyan: cyan3, dim: dim4, green: green2, red, yellow: yellow2 } = import_picocolors7.default;
211522
+ function formatTimestamp3() {
211523
+ const now2 = new Date;
211524
+ const hours = now2.getHours();
211525
+ const minutes = now2.getMinutes().toString().padStart(2, "0");
211526
+ const seconds = now2.getSeconds().toString().padStart(2, "0");
211527
+ const ampm = hours >= 12 ? "PM" : "AM";
211528
+ const displayHours = hours % 12 || 12;
211529
+ return dim4(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211530
+ }
211531
+ async function recreateSandboxDatabase(options) {
211532
+ const currentMode = getCurrentMode();
211533
+ const viteServer = getViteServerRef();
211534
+ if (!viteServer) {
211535
+ options.viteConfig.logger.error(`${formatTimestamp3()} ${red(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${red("Cannot recreate sandbox database: no Vite server reference")}`);
211536
+ return;
211537
+ }
211538
+ if (currentMode !== "platform") {
211539
+ options.viteConfig.logger.warn(`${formatTimestamp3()} ${yellow2(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${yellow2("can only recreate sandbox database in platform mode (m + enter)")}`);
211540
+ return;
211541
+ }
211542
+ options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} recreating database...`);
211543
+ if (serverState.sandbox) {
211544
+ serverState.sandbox.cleanup();
211545
+ serverState.sandbox = null;
211546
+ }
211547
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
211548
+ const sandbox = await startSandbox(options.viteConfig, options.platformModeOptions.startSandbox, {
211549
+ verbose: options.platformModeOptions.verbose,
211550
+ logLevel: options.platformModeOptions.logLevel,
211551
+ customUrl: options.platformModeOptions.sandboxUrl,
211552
+ quiet: true,
211553
+ recreateDb: true,
211554
+ seed: options.platformModeOptions.seed,
211555
+ memoryOnly: options.platformModeOptions.memoryOnly,
211556
+ databasePath: options.platformModeOptions.databasePath,
211557
+ realtimeEnabled: options.platformModeOptions.realtimeEnabled,
211558
+ realtimePort: options.platformModeOptions.realtimePort,
211559
+ timebackId: getEffectiveTimebackId(options.platformModeOptions.timeback)
211560
+ });
211561
+ serverState.sandbox = sandbox;
211562
+ if (sandbox.project && serverState.backend) {
211563
+ const gameUrl = `http://localhost:${serverState.backend.port}`;
211564
+ devServerMiddleware(viteServer, sandbox, gameUrl, {
211565
+ showBadge: options.platformModeOptions.showBadge,
211566
+ timeback: options.platformModeOptions.timeback
211567
+ });
211568
+ }
211569
+ options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${green2("database recreated")}`);
211570
+ }
211571
+ var recreateDatabaseHotkey = (options) => ({
211572
+ key: "d",
211573
+ description: "recreate sandbox database",
211574
+ action: () => recreateSandboxDatabase(options)
211575
+ });
211576
+
211577
+ // src/server/hotkeys/toggle-mode.ts
211578
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
211579
+ // package.json
211580
+ var package_default2 = {
211581
+ name: "@playcademy/vite-plugin",
211582
+ version: "0.1.37",
211583
+ type: "module",
211584
+ exports: {
211585
+ ".": {
211586
+ import: "./dist/index.js",
211587
+ types: "./dist/index.d.ts"
211588
+ }
211589
+ },
211590
+ main: "dist/index.js",
211591
+ module: "dist/index.js",
211592
+ files: [
211593
+ "dist"
211594
+ ],
211595
+ scripts: {
211596
+ build: "rm -rf dist && bun build.ts",
211597
+ docs: "typedoc --skipErrorChecking",
211598
+ pub: "bun publish.ts"
211599
+ },
211600
+ dependencies: {
211601
+ archiver: "^7.0.1",
211602
+ picocolors: "^1.1.1",
211603
+ playcademy: "workspace:*"
211604
+ },
211605
+ devDependencies: {
211606
+ "@inquirer/prompts": "^7.8.6",
211607
+ "@playcademy/sandbox": "workspace:*",
211608
+ "@types/archiver": "^6.0.3",
211609
+ "@types/bun": "latest"
211610
+ },
211611
+ peerDependencies: {
211612
+ typescript: "^5",
211613
+ vite: "^5 || ^6"
211614
+ }
211615
+ };
211616
+
211617
+ // src/lib/backend/server.ts
211618
+ import {
211619
+ loadPlaycademyConfigAndSetWorkspace as loadConfigAndSetWorkspace,
211620
+ startPlaycademyDevServer,
211621
+ startPlaycademyHotReload
211622
+ } from "playcademy/utils";
211623
+
211624
+ // src/lib/backend/hot-reload.ts
211625
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
211626
+ function formatChangedPath(changedPath) {
211627
+ if (!changedPath)
211628
+ return;
211629
+ if (changedPath.includes("/api/")) {
211630
+ return changedPath.substring(changedPath.indexOf("/api/"));
211631
+ }
211632
+ return changedPath;
211633
+ }
211634
+ function createHotReloadCallbacks(viteConfig) {
211635
+ return {
211636
+ onSuccess: (changedPath) => {
211637
+ const relativePath = formatChangedPath(changedPath);
211638
+ if (relativePath) {
211639
+ viteConfig.logger.info(`${import_picocolors8.dim("(backend)")} ${import_picocolors8.green("hmr update")} ${import_picocolors8.dim(relativePath)}`, { timestamp: true });
211640
+ } else {
211641
+ viteConfig.logger.info("backend reloaded", { timestamp: true });
211642
+ }
211643
+ },
211644
+ onError: (error2) => {
211645
+ viteConfig.logger.error(`backend reload failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
211646
+ }
211647
+ };
211648
+ }
211649
+
211650
+ // src/lib/backend/server.ts
211651
+ async function tryLoadConfig(viteConfig, configPath) {
211652
+ try {
211653
+ return await loadConfigAndSetWorkspace(configPath);
211654
+ } catch (error2) {
211655
+ if (error2 instanceof Error && !error2.message.includes("Could not find")) {
211656
+ viteConfig.logger.warn(`Could not load playcademy.config.js: ${error2.message}`);
211657
+ }
211658
+ return null;
211659
+ }
211660
+ }
211661
+ function needsCliDevServer(config2) {
211662
+ return !!config2.integrations;
211663
+ }
211664
+ async function startServer2(options) {
211665
+ const { port, config: config2, platformUrl } = options;
211666
+ return startPlaycademyDevServer({
211667
+ port,
211668
+ config: config2,
211669
+ quiet: true,
211670
+ platformUrl,
211671
+ customLogger: createLoggerAdapter("backend")
211672
+ });
211673
+ }
211674
+ function setupHotReload(serverRef, options) {
211675
+ const watcher = startPlaycademyHotReload(async () => {
211676
+ await serverRef.current.server.dispose();
211677
+ serverRef.current = await startServer2(options);
211678
+ }, createHotReloadCallbacks(options.viteConfig));
211679
+ return () => watcher.close();
211680
+ }
211681
+ async function setupCliDevServer(options) {
211682
+ const { preferredPort, viteConfig, platformUrl, configPath } = options;
211683
+ const config2 = await tryLoadConfig(viteConfig, configPath);
211684
+ if (!config2)
211685
+ return null;
211686
+ if (!needsCliDevServer(config2))
211687
+ return null;
211688
+ try {
211689
+ const port = await findAvailablePort(preferredPort);
211690
+ const serverOptions = { port, config: config2, platformUrl, viteConfig };
211691
+ const serverRef = { current: await startServer2(serverOptions) };
211692
+ const stopHotReload = setupHotReload(serverRef, serverOptions);
211693
+ return {
211694
+ server: serverRef.current.server,
211695
+ port: serverRef.current.port,
211696
+ stopHotReload,
211697
+ cleanup: () => cleanupServerInfo("backend", viteConfig.root, process.pid)
211698
+ };
211699
+ } catch (error2) {
211700
+ viteConfig.logger.error(`Failed to start game backend: ${error2 instanceof Error ? error2.message : String(error2)}`);
211701
+ return null;
211702
+ }
211703
+ }
211588
211704
  // src/server/platform-mode.ts
211589
211705
  async function configurePlatformMode(server, viteConfig, options) {
211590
211706
  const sandbox = await startSandbox(viteConfig, options.startSandbox, {
@@ -211597,7 +211713,8 @@ async function configurePlatformMode(server, viteConfig, options) {
211597
211713
  memoryOnly: options.memoryOnly,
211598
211714
  databasePath: options.databasePath,
211599
211715
  realtimeEnabled: options.realtimeEnabled,
211600
- realtimePort: options.realtimePort
211716
+ realtimePort: options.realtimePort,
211717
+ timebackId: getEffectiveTimebackId(options.timeback)
211601
211718
  });
211602
211719
  serverState.sandbox = sandbox;
211603
211720
  const backend = await setupCliDevServer({
@@ -211608,8 +211725,12 @@ async function configurePlatformMode(server, viteConfig, options) {
211608
211725
  });
211609
211726
  serverState.backend = backend;
211610
211727
  if (sandbox.project) {
211728
+ validateTimebackConfig(viteConfig, options.timeback);
211611
211729
  const gameUrl = backend ? `http://localhost:${backend.port}` : undefined;
211612
- devServerMiddleware(server, sandbox, gameUrl, options.showBadge);
211730
+ devServerMiddleware(server, sandbox, gameUrl, {
211731
+ showBadge: options.showBadge,
211732
+ timeback: options.timeback
211733
+ });
211613
211734
  }
211614
211735
  server.httpServer?.once("listening", () => {
211615
211736
  setTimeout(async () => {
@@ -211620,13 +211741,13 @@ async function configurePlatformMode(server, viteConfig, options) {
211620
211741
  backend: backend?.port,
211621
211742
  realtime: sandbox.realtimePort,
211622
211743
  vite: vitePort
211623
- }, projectInfo, package_default.version);
211744
+ }, projectInfo, package_default2.version);
211624
211745
  }, 100);
211625
211746
  });
211626
211747
  }
211627
211748
 
211628
211749
  // src/server/standalone-mode.ts
211629
- var import_picocolors6 = __toESM(require_picocolors(), 1);
211750
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
211630
211751
  async function configureStandaloneMode(server, viteConfig, options) {
211631
211752
  const backend = await setupCliDevServer({
211632
211753
  preferredPort: options.preferredPort,
@@ -211642,32 +211763,32 @@ async function configureStandaloneMode(server, viteConfig, options) {
211642
211763
  server.httpServer?.once("listening", () => {
211643
211764
  setTimeout(() => {
211644
211765
  viteConfig.logger.info("");
211645
- viteConfig.logger.info(` ${import_picocolors6.default.green(import_picocolors6.default.bold("PLAYCADEMY"))} ${import_picocolors6.default.green(`v${package_default.version}`)}`);
211766
+ viteConfig.logger.info(` ${import_picocolors9.default.green(import_picocolors9.default.bold("PLAYCADEMY"))} ${import_picocolors9.default.green(`v${package_default2.version}`)}`);
211646
211767
  viteConfig.logger.info("");
211647
- viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Backend:")} ${import_picocolors6.default.cyan(`http://localhost:${backend.port}`)}`);
211648
- viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Sandbox:")} ${import_picocolors6.default.cyan("Disabled")}`);
211768
+ viteConfig.logger.info(` ${import_picocolors9.default.green("➜")} ${import_picocolors9.default.bold("Backend:")} ${import_picocolors9.default.cyan(`http://localhost:${backend.port}`)}`);
211769
+ viteConfig.logger.info(` ${import_picocolors9.default.green("➜")} ${import_picocolors9.default.bold("Sandbox:")} ${import_picocolors9.default.cyan("Disabled")}`);
211649
211770
  viteConfig.logger.info("");
211650
211771
  }, 100);
211651
211772
  });
211652
211773
  }
211653
211774
 
211654
- // src/server/mode-switcher.ts
211655
- var { bold: bold3, cyan: cyan3, dim: dim4, green: green3, red } = import_picocolors7.default;
211656
- function formatTimestamp2() {
211775
+ // src/server/hotkeys/toggle-mode.ts
211776
+ var { bold: bold5, cyan: cyan4, dim: dim6, green: green4, red: red2 } = import_picocolors10.default;
211777
+ function formatTimestamp4() {
211657
211778
  const now2 = new Date;
211658
211779
  const hours = now2.getHours();
211659
211780
  const minutes = now2.getMinutes().toString().padStart(2, "0");
211660
211781
  const seconds = now2.getSeconds().toString().padStart(2, "0");
211661
211782
  const ampm = hours >= 12 ? "PM" : "AM";
211662
211783
  const displayHours = hours % 12 || 12;
211663
- return dim4(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211784
+ return dim6(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211664
211785
  }
211665
211786
  async function toggleMode(options) {
211666
211787
  const currentMode = getCurrentMode();
211667
211788
  const newMode = currentMode === "platform" ? "standalone" : "platform";
211668
211789
  const viteServer = getViteServerRef();
211669
211790
  if (!viteServer) {
211670
- options.viteConfig.logger.error(`${formatTimestamp2()} ${red(bold3("[playcademy]"))} ${red("Cannot toggle mode: no Vite server reference")}`);
211791
+ options.viteConfig.logger.error(`${formatTimestamp4()} ${red2(bold5("[playcademy]"))} ${red2("Cannot toggle mode: no Vite server reference")}`);
211671
211792
  return;
211672
211793
  }
211673
211794
  await cleanupServers();
@@ -211681,56 +211802,39 @@ async function toggleMode(options) {
211681
211802
  } else {
211682
211803
  await configurePlatformMode(viteServer, options.viteConfig, options.platformModeOptions);
211683
211804
  }
211684
- options.viteConfig.logger.info(`${formatTimestamp2()} ${cyan3(bold3("[playcademy]"))} ${green3("switched to")} ${green3(bold3(newMode))} ${green3("mode")}`);
211805
+ options.viteConfig.logger.info(`${formatTimestamp4()} ${cyan4(bold5("[playcademy]"))} ${green4("switched to")} ${green4(bold5(newMode))} ${green4("mode")}`);
211685
211806
  }
211807
+ var toggleModeHotkey = (options) => ({
211808
+ key: "m",
211809
+ description: "toggle platform/standalone mode",
211810
+ action: () => toggleMode(options)
211811
+ });
211686
211812
 
211687
- // src/server/recreate-sandbox-database.ts
211688
- var import_picocolors8 = __toESM(require_picocolors(), 1);
211689
- var { bold: bold4, cyan: cyan4, dim: dim5, green: green4, red: red2, yellow } = import_picocolors8.default;
211690
- function formatTimestamp3() {
211691
- const now2 = new Date;
211692
- const hours = now2.getHours();
211693
- const minutes = now2.getMinutes().toString().padStart(2, "0");
211694
- const seconds = now2.getSeconds().toString().padStart(2, "0");
211695
- const ampm = hours >= 12 ? "PM" : "AM";
211696
- const displayHours = hours % 12 || 12;
211697
- return dim5(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211813
+ // src/server/hotkeys/index.ts
211814
+ function getHotkeys(options) {
211815
+ return [
211816
+ toggleModeHotkey(options),
211817
+ recreateDatabaseHotkey(options),
211818
+ cycleTimebackRoleHotkey(options)
211819
+ ];
211698
211820
  }
211699
- async function recreateSandboxDatabase(options) {
211700
- const currentMode = getCurrentMode();
211701
- const viteServer = getViteServerRef();
211702
- if (!viteServer) {
211703
- options.viteConfig.logger.error(`${formatTimestamp3()} ${red2(bold4("[playcademy]"))} ${dim5("(sandbox)")} ${red2("Cannot recreate sandbox database: no Vite server reference")}`);
211704
- return;
211705
- }
211706
- if (currentMode !== "platform") {
211707
- options.viteConfig.logger.warn(`${formatTimestamp3()} ${yellow(bold4("[playcademy]"))} ${dim5("(sandbox)")} ${yellow("can only recreate sandbox database in platform mode (m + enter)")}`);
211821
+
211822
+ // src/server/lifecycle.ts
211823
+ var shutdownHandlersRegistered = false;
211824
+ function setupProcessShutdownHandlers() {
211825
+ if (shutdownHandlersRegistered)
211708
211826
  return;
211709
- }
211710
- options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan4(bold4("[playcademy]"))} ${dim5("(sandbox)")} recreating database...`);
211711
- if (serverState.sandbox) {
211712
- serverState.sandbox.cleanup();
211713
- serverState.sandbox = null;
211714
- }
211715
- await new Promise((resolve2) => setTimeout(resolve2, 100));
211716
- const sandbox = await startSandbox(options.viteConfig, options.platformModeOptions.startSandbox, {
211717
- verbose: options.platformModeOptions.verbose,
211718
- logLevel: options.platformModeOptions.logLevel,
211719
- customUrl: options.platformModeOptions.sandboxUrl,
211720
- quiet: true,
211721
- recreateDb: true,
211722
- seed: options.platformModeOptions.seed,
211723
- memoryOnly: options.platformModeOptions.memoryOnly,
211724
- databasePath: options.platformModeOptions.databasePath,
211725
- realtimeEnabled: options.platformModeOptions.realtimeEnabled,
211726
- realtimePort: options.platformModeOptions.realtimePort
211727
- });
211728
- serverState.sandbox = sandbox;
211729
- if (sandbox.project && serverState.backend) {
211730
- const gameUrl = `http://localhost:${serverState.backend.port}`;
211731
- devServerMiddleware(viteServer, sandbox, gameUrl, options.platformModeOptions.showBadge);
211732
- }
211733
- options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan4(bold4("[playcademy]"))} ${dim5("(sandbox)")} ${green4("database recreated")}`);
211827
+ shutdownHandlersRegistered = true;
211828
+ let isShuttingDown = false;
211829
+ const shutdown = async () => {
211830
+ if (isShuttingDown)
211831
+ return;
211832
+ isShuttingDown = true;
211833
+ await cleanupServers();
211834
+ process.exit(0);
211835
+ };
211836
+ process.on("SIGINT", shutdown);
211837
+ process.on("SIGTERM", shutdown);
211734
211838
  }
211735
211839
 
211736
211840
  // src/hooks/configure-server.ts
@@ -211759,7 +211863,8 @@ async function configureServerHook(server, context) {
211759
211863
  realtimePort: context.options.realtimePort,
211760
211864
  showBadge: context.options.showBadge,
211761
211865
  preferredBackendPort: preferredPort,
211762
- configPath: context.options.configPath
211866
+ configPath: context.options.configPath,
211867
+ timeback: context.options.timeback
211763
211868
  };
211764
211869
  if (context.options.mode === "standalone") {
211765
211870
  await configureStandaloneMode(server, context.viteConfig, {
@@ -211776,26 +211881,10 @@ async function configureServerHook(server, context) {
211776
211881
  ...options,
211777
211882
  customShortcuts: [
211778
211883
  ...options?.customShortcuts || [],
211779
- {
211780
- key: "m",
211781
- description: "toggle platform/standalone mode",
211782
- action: async () => {
211783
- await toggleMode({
211784
- viteConfig: context.viteConfig,
211785
- platformModeOptions
211786
- });
211787
- }
211788
- },
211789
- {
211790
- key: "d",
211791
- description: "recreate sandbox database",
211792
- action: async () => {
211793
- await recreateSandboxDatabase({
211794
- viteConfig: context.viteConfig,
211795
- platformModeOptions
211796
- });
211797
- }
211798
- }
211884
+ ...getHotkeys({
211885
+ viteConfig: context.viteConfig,
211886
+ platformModeOptions
211887
+ })
211799
211888
  ]
211800
211889
  });
211801
211890
  };
@@ -211843,7 +211932,8 @@ function resolveOptions(options) {
211843
211932
  databasePath: sandboxOptions.databasePath,
211844
211933
  realtimeEnabled: realtimeOptions.enabled ?? false,
211845
211934
  realtimePort: realtimeOptions.port,
211846
- showBadge: shellOptions.showBadge ?? true
211935
+ showBadge: shellOptions.showBadge ?? true,
211936
+ timeback: options.timeback
211847
211937
  };
211848
211938
  }
211849
211939
  function createPluginContext(options) {