@patch-adams/core 1.5.11 → 1.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -389,7 +389,9 @@ var LrsBridgeConfigSchema = z.object({
389
389
  /** Document GUID for xAPI statement aggregation - baked into package at wrap time */
390
390
  documentGuid: z.string().optional(),
391
391
  /** Version GUID for xAPI statement aggregation - unique per export */
392
- versionGuid: z.string().optional()
392
+ versionGuid: z.string().optional(),
393
+ /** Original SCORM package filename - baked in at wrap time for LRS searchability */
394
+ packageName: z.string().optional()
393
395
  });
394
396
  var BlockingAssetConfigSchema = z.object({
395
397
  /** Filename for the asset */
@@ -738,6 +740,7 @@ function generateLrsBridgeCode(options) {
738
740
  const lrsProxyEndpoint = options.lrsProxyEndpoint || "";
739
741
  const documentGuid = options.documentGuid || "";
740
742
  const versionGuid = options.versionGuid || "";
743
+ const packageName = options.packageName || "";
741
744
  return `
742
745
  // ==========================================================================
743
746
  // PATCH-ADAMS LRS BRIDGE v2.2.0
@@ -770,6 +773,7 @@ function generateLrsBridgeCode(options) {
770
773
  var LRS_PROXY_ENDPOINT = '${lrsProxyEndpoint}';
771
774
  var DOCUMENT_GUID = '${documentGuid}';
772
775
  var VERSION_GUID = '${versionGuid}';
776
+ var PACKAGE_NAME = '${packageName}';
773
777
 
774
778
  // Bravais LRS endpoint pattern: https://lrs-{tenant}.bravais.com/XAPI/statements
775
779
  // Example: core-acme.bravais.com -> lrs-acme.bravais.com
@@ -2453,7 +2457,9 @@ function generateLrsBridgeCode(options) {
2453
2457
  sharedLinkName: null,
2454
2458
  // Tenant info
2455
2459
  homepage: COURSE_HOMEPAGE || window.location.origin,
2456
- tenantHomepage: null // https://{tenant}.bravais.com format
2460
+ tenantHomepage: null, // https://{tenant}.bravais.com format
2461
+ // Package info
2462
+ packageName: null // Original SCORM zip filename
2457
2463
  };
2458
2464
 
2459
2465
  // 0. Use baked-in GUIDs from PA-Patcher config (highest priority - set at wrap time)
@@ -2466,6 +2472,10 @@ function generateLrsBridgeCode(options) {
2466
2472
  info.versionGuid = VERSION_GUID;
2467
2473
  log('Version GUID from baked-in config:', VERSION_GUID);
2468
2474
  }
2475
+ if (PACKAGE_NAME) {
2476
+ info.packageName = PACKAGE_NAME;
2477
+ log('Package name from baked-in config:', PACKAGE_NAME);
2478
+ }
2469
2479
 
2470
2480
  // 1. Extract shared link token and document ID from URL
2471
2481
  info.sharedLinkToken = extractSharedLinkToken();
@@ -2896,6 +2906,11 @@ function generateLrsBridgeCode(options) {
2896
2906
  // Resource type
2897
2907
  obj.definition.extensions['resourceType'] = LRS.courseInfo.resourceType || 'Course';
2898
2908
 
2909
+ // Package name (original SCORM zip filename)
2910
+ if (LRS.courseInfo.packageName) {
2911
+ obj.definition.extensions['packageName'] = LRS.courseInfo.packageName;
2912
+ }
2913
+
2899
2914
  return obj;
2900
2915
  }
2901
2916
 
@@ -3729,11 +3744,11 @@ function generateLrsBridgeCode(options) {
3729
3744
  // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3730
3745
  LRS.sendCompletionStatement = function(data) {
3731
3746
  data = data || {};
3732
- if (scormCompletionSent) {
3733
- log('Completion statement already sent (SCORM auto-detected). Skipping duplicate.');
3734
- return;
3747
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3748
+ if (scormCompletionDebounce) {
3749
+ clearTimeout(scormCompletionDebounce);
3750
+ scormCompletionDebounce = null;
3735
3751
  }
3736
- scormCompletionSent = true;
3737
3752
 
3738
3753
  var status = data.status || 'completed';
3739
3754
  var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
@@ -3763,11 +3778,58 @@ function generateLrsBridgeCode(options) {
3763
3778
  };
3764
3779
  if (data.employeeName) activityDetails.employeeName = data.employeeName;
3765
3780
 
3766
- log('Sending completion statement:', verbKey, 'score:', result.score, 'title:', courseTitle);
3767
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
3781
+ // 1. Always send "completed" statement
3782
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3783
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3784
+ sendStatement(completedStatement);
3785
+
3786
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3787
+ if (status === 'passed' || status === 'failed') {
3788
+ log('Sending', status, 'statement');
3789
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3790
+ sendStatement(outcomeStatement);
3791
+ }
3792
+ };
3793
+
3794
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3795
+ var exitStatementSent = false;
3796
+ LRS.sendExitStatement = function(data) {
3797
+ if (exitStatementSent) return;
3798
+ exitStatementSent = true;
3799
+ data = data || {};
3800
+
3801
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3802
+ var duration = null;
3803
+ if (LRS.launchTime) {
3804
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3805
+ var secs = Math.floor(ms / 1000);
3806
+ var mins = Math.floor(secs / 60);
3807
+ var hrs = Math.floor(mins / 60);
3808
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3809
+ }
3810
+
3811
+ var activityDetails = {
3812
+ courseTitle: courseTitle,
3813
+ exitSource: data.source || 'page-unload',
3814
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3815
+ employeeId: LRS.employeeId || undefined,
3816
+ sessionDuration: duration,
3817
+ statementsSent: LRS.stats.statementsSent
3818
+ };
3819
+
3820
+ var result = duration ? { duration: duration } : null;
3821
+ log('Sending exit statement, duration:', duration);
3822
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3768
3823
  sendStatement(statement);
3769
3824
  };
3770
3825
 
3826
+ // Auto-send exit statement on page unload
3827
+ window.addEventListener('beforeunload', function() {
3828
+ if (!exitStatementSent && LRS.actor) {
3829
+ LRS.sendExitStatement({ source: 'page-unload' });
3830
+ }
3831
+ });
3832
+
3771
3833
  /**
3772
3834
  * Re-extract actor from SCORM after LMSInitialize has been called.
3773
3835
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3874,6 +3936,23 @@ function generateLrsBridgeCode(options) {
3874
3936
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3875
3937
  };
3876
3938
 
3939
+ // ========================================================================
3940
+ // PUBLIC API \u2014 expose core internals for skin scripts
3941
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3942
+ // ========================================================================
3943
+ LRS.api = {
3944
+ sendStatement: sendStatement,
3945
+ buildStatement: buildStatement,
3946
+ buildCourseActivityObject: buildCourseActivityObject,
3947
+ buildXylemeContext: buildXylemeContext,
3948
+ generateUUID: generateUUID,
3949
+ decodeEntities: decodeEntities,
3950
+ extractActor: extractActor,
3951
+ getCachedLessonInfo: getCachedLessonInfo,
3952
+ VERBS: VERBS,
3953
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3954
+ };
3955
+
3877
3956
  // ========================================================================
3878
3957
  // 8. RISE EVENT INTERCEPTORS
3879
3958
  // ========================================================================
@@ -4439,7 +4518,7 @@ function generateLrsBridgeCode(options) {
4439
4518
  var scormInteractionsSent = {}; // Track which interactions were already sent
4440
4519
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4441
4520
  var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4442
- var scormCompletionSent = false; // Prevent duplicate completion statements
4521
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4443
4522
 
4444
4523
  function interceptScormSetValue(key, value) {
4445
4524
  if (typeof key !== 'string') return;
@@ -4459,12 +4538,15 @@ function generateLrsBridgeCode(options) {
4459
4538
  if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4460
4539
 
4461
4540
  // Fire course completion statement when status indicates pass/fail/complete
4462
- if ((key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') && !scormCompletionSent) {
4541
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4542
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4463
4543
  var status = String(value).toLowerCase();
4464
4544
  if (status === 'passed' || status === 'failed' || status === 'completed') {
4465
- scormCompletionSent = true;
4466
- // Slight delay to let score values arrive (Rise often sets score just before status)
4467
- setTimeout(function() { sendCourseCompletionStatement(status); }, 200);
4545
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4546
+ scormCompletionDebounce = setTimeout(function() {
4547
+ scormCompletionDebounce = null;
4548
+ sendCourseCompletionStatement(status);
4549
+ }, 500);
4468
4550
  }
4469
4551
  }
4470
4552
 
@@ -4575,11 +4657,6 @@ function generateLrsBridgeCode(options) {
4575
4657
 
4576
4658
  var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4577
4659
 
4578
- // Determine verb
4579
- var verbKey = 'completed';
4580
- if (status === 'passed') verbKey = 'passed';
4581
- if (status === 'failed') verbKey = 'failed';
4582
-
4583
4660
  // Build result with score
4584
4661
  var result = { completion: true };
4585
4662
 
@@ -4605,10 +4682,17 @@ function generateLrsBridgeCode(options) {
4605
4682
  employeeId: LRS.employeeId || undefined
4606
4683
  };
4607
4684
 
4608
- log('Sending course completion:', verbKey, 'score:', result.score, 'title:', courseTitle);
4685
+ // 1. Send "completed" statement (course was finished)
4686
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4687
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4688
+ sendStatement(completedStatement);
4609
4689
 
4610
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
4611
- sendStatement(statement);
4690
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4691
+ if (status === 'passed' || status === 'failed') {
4692
+ log('Sending', status, 'statement');
4693
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4694
+ sendStatement(outcomeStatement);
4695
+ }
4612
4696
 
4613
4697
  logGroupEnd();
4614
4698
  }
@@ -5292,7 +5376,8 @@ function buildJsBeforeOptions(config, metadata) {
5292
5376
  lrsProxyEndpoint: lrsBridgeConfig.lrsProxyEndpoint,
5293
5377
  employeeApiEndpoint: lrsBridgeConfig.employeeApiEndpoint,
5294
5378
  documentGuid: lrsBridgeConfig.documentGuid,
5295
- versionGuid: lrsBridgeConfig.versionGuid
5379
+ versionGuid: lrsBridgeConfig.versionGuid,
5380
+ packageName: lrsBridgeConfig.packageName
5296
5381
  };
5297
5382
  return {
5298
5383
  remoteUrl: `${config.remoteDomain}/${config.localFolders.js}/${config.jsBefore.filename}`,
@@ -6672,6 +6757,11 @@ var Patcher = class {
6672
6757
  if (effectiveSkin) {
6673
6758
  console.log(`[Patcher] Skin: ${effectiveSkin}`);
6674
6759
  }
6760
+ if (options.packageName) {
6761
+ this.config.lrsBridge = this.config.lrsBridge ?? {};
6762
+ this.config.lrsBridge.packageName = options.packageName;
6763
+ console.log(`[Patcher] Package name: ${options.packageName}`);
6764
+ }
6675
6765
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
6676
6766
  htmlInjector.setMetadata(metadata);
6677
6767
  let fetchedFallbacks = {};
@@ -6965,6 +7055,7 @@ program.command("patch <input>").description("Patch a Rise course ZIP file").opt
6965
7055
  if (options.jsAfter) {
6966
7056
  patchOptions.jsAfterContent = readFileSync(resolve(options.jsAfter), "utf-8");
6967
7057
  }
7058
+ patchOptions.packageName = basename(inputPath);
6968
7059
  spinner.text = "Patching package...";
6969
7060
  const patcher = new Patcher(validatedConfig);
6970
7061
  const { buffer, result } = await patcher.patch(inputBuffer, patchOptions);