@patch-adams/core 1.5.10 → 1.5.13

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
@@ -3726,6 +3726,95 @@ function generateLrsBridgeCode(options) {
3726
3726
  sendStatement(statement);
3727
3727
  };
3728
3728
 
3729
+ // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3730
+ LRS.sendCompletionStatement = function(data) {
3731
+ data = data || {};
3732
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3733
+ if (scormCompletionDebounce) {
3734
+ clearTimeout(scormCompletionDebounce);
3735
+ scormCompletionDebounce = null;
3736
+ }
3737
+
3738
+ var status = data.status || 'completed';
3739
+ var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
3740
+
3741
+ var verbKey = 'completed';
3742
+ if (status === 'passed') verbKey = 'passed';
3743
+ if (status === 'failed') verbKey = 'failed';
3744
+
3745
+ var result = { completion: true, success: (status === 'passed') };
3746
+
3747
+ if (typeof data.score === 'number') {
3748
+ var max = data.scoreMax || 100;
3749
+ var min = data.scoreMin || 0;
3750
+ result.score = {
3751
+ raw: data.score,
3752
+ max: max,
3753
+ min: min,
3754
+ scaled: max > min ? (data.score - min) / (max - min) : 0
3755
+ };
3756
+ }
3757
+
3758
+ var activityDetails = {
3759
+ courseTitle: courseTitle,
3760
+ completionStatus: status,
3761
+ employeeEmail: data.email || (LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined),
3762
+ employeeId: data.employeeId || LRS.employeeId || undefined
3763
+ };
3764
+ if (data.employeeName) activityDetails.employeeName = data.employeeName;
3765
+
3766
+ // 1. Always send "completed" statement
3767
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3768
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3769
+ sendStatement(completedStatement);
3770
+
3771
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3772
+ if (status === 'passed' || status === 'failed') {
3773
+ log('Sending', status, 'statement');
3774
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3775
+ sendStatement(outcomeStatement);
3776
+ }
3777
+ };
3778
+
3779
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3780
+ var exitStatementSent = false;
3781
+ LRS.sendExitStatement = function(data) {
3782
+ if (exitStatementSent) return;
3783
+ exitStatementSent = true;
3784
+ data = data || {};
3785
+
3786
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3787
+ var duration = null;
3788
+ if (LRS.launchTime) {
3789
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3790
+ var secs = Math.floor(ms / 1000);
3791
+ var mins = Math.floor(secs / 60);
3792
+ var hrs = Math.floor(mins / 60);
3793
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3794
+ }
3795
+
3796
+ var activityDetails = {
3797
+ courseTitle: courseTitle,
3798
+ exitSource: data.source || 'page-unload',
3799
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3800
+ employeeId: LRS.employeeId || undefined,
3801
+ sessionDuration: duration,
3802
+ statementsSent: LRS.stats.statementsSent
3803
+ };
3804
+
3805
+ var result = duration ? { duration: duration } : null;
3806
+ log('Sending exit statement, duration:', duration);
3807
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3808
+ sendStatement(statement);
3809
+ };
3810
+
3811
+ // Auto-send exit statement on page unload
3812
+ window.addEventListener('beforeunload', function() {
3813
+ if (!exitStatementSent && LRS.actor) {
3814
+ LRS.sendExitStatement({ source: 'page-unload' });
3815
+ }
3816
+ });
3817
+
3729
3818
  /**
3730
3819
  * Re-extract actor from SCORM after LMSInitialize has been called.
3731
3820
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3832,6 +3921,23 @@ function generateLrsBridgeCode(options) {
3832
3921
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3833
3922
  };
3834
3923
 
3924
+ // ========================================================================
3925
+ // PUBLIC API \u2014 expose core internals for skin scripts
3926
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3927
+ // ========================================================================
3928
+ LRS.api = {
3929
+ sendStatement: sendStatement,
3930
+ buildStatement: buildStatement,
3931
+ buildCourseActivityObject: buildCourseActivityObject,
3932
+ buildXylemeContext: buildXylemeContext,
3933
+ generateUUID: generateUUID,
3934
+ decodeEntities: decodeEntities,
3935
+ extractActor: extractActor,
3936
+ getCachedLessonInfo: getCachedLessonInfo,
3937
+ VERBS: VERBS,
3938
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3939
+ };
3940
+
3835
3941
  // ========================================================================
3836
3942
  // 8. RISE EVENT INTERCEPTORS
3837
3943
  // ========================================================================
@@ -4396,9 +4502,40 @@ function generateLrsBridgeCode(options) {
4396
4502
  var scormInteractions = {}; // Pending interactions keyed by index N
4397
4503
  var scormInteractionsSent = {}; // Track which interactions were already sent
4398
4504
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4505
+ var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4506
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4399
4507
 
4400
4508
  function interceptScormSetValue(key, value) {
4401
4509
  if (typeof key !== 'string') return;
4510
+
4511
+ // ---- Course-level SCORM data (score, status, completion) ----
4512
+ // SCORM 1.2
4513
+ if (key === 'cmi.core.score.raw') { scormCourseData.scoreRaw = parseFloat(value); }
4514
+ if (key === 'cmi.core.score.max') { scormCourseData.scoreMax = parseFloat(value); }
4515
+ if (key === 'cmi.core.score.min') { scormCourseData.scoreMin = parseFloat(value); }
4516
+ if (key === 'cmi.core.lesson_status') { scormCourseData.status = String(value).toLowerCase(); }
4517
+ // SCORM 2004
4518
+ if (key === 'cmi.score.raw') { scormCourseData.scoreRaw = parseFloat(value); }
4519
+ if (key === 'cmi.score.max') { scormCourseData.scoreMax = parseFloat(value); }
4520
+ if (key === 'cmi.score.min') { scormCourseData.scoreMin = parseFloat(value); }
4521
+ if (key === 'cmi.score.scaled') { scormCourseData.scoreScaled = parseFloat(value); }
4522
+ if (key === 'cmi.completion_status') { scormCourseData.completionStatus = String(value).toLowerCase(); }
4523
+ if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4524
+
4525
+ // Fire course completion statement when status indicates pass/fail/complete
4526
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4527
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4528
+ var status = String(value).toLowerCase();
4529
+ if (status === 'passed' || status === 'failed' || status === 'completed') {
4530
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4531
+ scormCompletionDebounce = setTimeout(function() {
4532
+ scormCompletionDebounce = null;
4533
+ sendCourseCompletionStatement(status);
4534
+ }, 500);
4535
+ }
4536
+ }
4537
+
4538
+ // ---- Interaction-level tracking (existing) ----
4402
4539
  var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4403
4540
  var match = key.match(interactionPattern);
4404
4541
  if (!match) return;
@@ -4498,6 +4635,53 @@ function generateLrsBridgeCode(options) {
4498
4635
  }
4499
4636
  }
4500
4637
 
4638
+ function sendCourseCompletionStatement(status) {
4639
+ logGroup('Course Completion');
4640
+ log('Status:', status);
4641
+ log('SCORM course data:', scormCourseData);
4642
+
4643
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4644
+
4645
+ // Build result with score
4646
+ var result = { completion: true };
4647
+
4648
+ var raw = scormCourseData.scoreRaw;
4649
+ var max = scormCourseData.scoreMax || 100;
4650
+ var min = scormCourseData.scoreMin || 0;
4651
+
4652
+ if (typeof raw === 'number' && !isNaN(raw)) {
4653
+ result.score = {
4654
+ raw: raw,
4655
+ max: max,
4656
+ min: min,
4657
+ scaled: scormCourseData.scoreScaled != null ? scormCourseData.scoreScaled : (max > min ? (raw - min) / (max - min) : 0)
4658
+ };
4659
+ }
4660
+
4661
+ result.success = (status === 'passed');
4662
+
4663
+ var activityDetails = {
4664
+ courseTitle: courseTitle,
4665
+ completionStatus: status,
4666
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
4667
+ employeeId: LRS.employeeId || undefined
4668
+ };
4669
+
4670
+ // 1. Send "completed" statement (course was finished)
4671
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4672
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4673
+ sendStatement(completedStatement);
4674
+
4675
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4676
+ if (status === 'passed' || status === 'failed') {
4677
+ log('Sending', status, 'statement');
4678
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4679
+ sendStatement(outcomeStatement);
4680
+ }
4681
+
4682
+ logGroupEnd();
4683
+ }
4684
+
4501
4685
  function setupScormInteractionTracker() {
4502
4686
  if (!TRACK_QUIZZES) return;
4503
4687