@@ -133,7 +133,12 @@ export class DataUtil {
133133 _ . each ( data , this . joinBolusAndDosingDecision ) ;
134134 this . endTimer ( 'joinBolusAndDosingDecision' ) ;
135135
136- // Filter out any data that failed validation, and and duplicates by `id`
136+ // Add missing suppressed basals to select basal datums
137+ this . startTimer ( 'addMissingSuppressedBasals' ) ;
138+ this . addMissingSuppressedBasals ( data ) ;
139+ this . endTimer ( 'addMissingSuppressedBasals' ) ;
140+
141+ // Filter out any data that failed validation, and any duplicates by `id`
137142 this . startTimer ( 'filterValidData' ) ;
138143 this . clearFilters ( ) ;
139144 const validData = _ . uniqBy ( data , 'id' ) ;
@@ -337,6 +342,191 @@ export class DataUtil {
337342 }
338343 } ;
339344
345+ addMissingSuppressedBasals = data => {
346+ // Mapping function to get the basal schedules for a given pumpSettings datum,
347+ // ordered by start time in descending order for easier comparison with basals
348+ const getOrderedPumpSettingsSchedules = ( { activeSchedule, basalSchedules, deviceId, time } ) => ( {
349+ basalSchedule : _ . orderBy ( basalSchedules ?. [ activeSchedule ] , 'start' , 'desc' ) ,
350+ time,
351+ deviceId,
352+ } ) ;
353+
354+ const shouldGenerateSuppressedBasal = ( d ) => (
355+ d . type === 'basal' &&
356+ ! d . suppressed &&
357+ _ . includes ( [ 'automated' , 'temp' ] , d . deliveryType ) &&
358+ isTwiistLoop ( d )
359+ ) ;
360+
361+ // Get the pump settings datums ordered by start time in descending order
362+ const pumpSettingsByStartTimes = _ . orderBy (
363+ _ . map ( _ . values ( this . pumpSettingsDatumsByIdMap ) , getOrderedPumpSettingsSchedules ) ,
364+ 'time' ,
365+ 'desc'
366+ ) ;
367+
368+ // We will need add additional basals when we split existing basals that overlap with pump settings schedules
369+ const basalsToAdd = [ ] ;
370+
371+ _ . each ( data , d => {
372+ if ( shouldGenerateSuppressedBasal ( d ) ) {
373+ // Get the pump settings datum that is active at the time of the basal by grabbing the first
374+ // datum with a time less than or equal to the basal's time
375+ const pumpSettingsDatum = _ . find ( pumpSettingsByStartTimes , ps => ps . deviceId === d . deviceId && ps . time <= d . time ) ;
376+ const activeSchedule = pumpSettingsDatum ?. basalSchedule ;
377+
378+ if ( ! activeSchedule ?. length ) {
379+ // No schedule available, skip this basal
380+ return ;
381+ }
382+
383+ // Calculate the end time of the basal
384+ const basalEndTime = d . time + ( d . duration || 0 ) ;
385+
386+ // Get the msPer24 for the utc start and end times of the basal datum, then adjust for timezone offset
387+ const timezoneOffsetMs = ( d . timezoneOffset || 0 ) * MS_IN_MIN ;
388+ const basalMsPer24WithOffset = {
389+ start : getMsPer24 ( d . time ) + timezoneOffsetMs ,
390+ end : getMsPer24 ( basalEndTime ) + timezoneOffsetMs ,
391+ } ;
392+
393+ // If the offset msPer24 is negative, as can happen with negative timezone offsets,
394+ // we need to add a day to adjust it to be within the 24-hour range
395+ if ( basalMsPer24WithOffset . start < 0 ) basalMsPer24WithOffset . start += MS_IN_DAY ;
396+ if ( basalMsPer24WithOffset . end < 0 ) basalMsPer24WithOffset . end += MS_IN_DAY ;
397+
398+ // If end time crosses midnight (is less than start), extend it to next day
399+ if ( basalMsPer24WithOffset . end < basalMsPer24WithOffset . start ) {
400+ basalMsPer24WithOffset . end += MS_IN_DAY ;
401+ }
402+
403+ // Find all schedule segments that this basal overlaps
404+ const currentDaySegments = [ ] ;
405+ const nextDaySegments = [ ] ;
406+
407+ // Sort schedule by start time ascending for easier processing
408+ const sortedSchedule = _ . sortBy ( activeSchedule , 'start' ) ;
409+
410+ for ( let i = 0 ; i < sortedSchedule . length ; i ++ ) {
411+ const segment = sortedSchedule [ i ] ;
412+ const nextSegment = sortedSchedule [ i + 1 ] ;
413+
414+ const segmentStart = segment . start ;
415+ const segmentEnd = nextSegment ? nextSegment . start : MS_IN_DAY ;
416+
417+ // Check if basal overlaps with this segment in the current day
418+ const basalOverlapsCurrentDay = (
419+ basalMsPer24WithOffset . start < segmentEnd &&
420+ basalMsPer24WithOffset . end > segmentStart
421+ ) ;
422+
423+ if ( basalOverlapsCurrentDay ) {
424+ currentDaySegments . push ( {
425+ ...segment ,
426+ segmentEnd,
427+ overlapStart : Math . max ( basalMsPer24WithOffset . start , segmentStart ) ,
428+ overlapEnd : Math . min ( basalMsPer24WithOffset . end , segmentEnd )
429+ } ) ;
430+ }
431+
432+ // Also check if basal overlaps with this segment when extended to next day
433+ // This handles cases where the basal crosses midnight
434+ if ( basalMsPer24WithOffset . end > MS_IN_DAY ) {
435+ const extendedSegmentStart = segmentStart + MS_IN_DAY ;
436+ const extendedSegmentEnd = segmentEnd + MS_IN_DAY ;
437+
438+ const basalOverlapsNextDay = (
439+ basalMsPer24WithOffset . start < extendedSegmentEnd &&
440+ basalMsPer24WithOffset . end > extendedSegmentStart
441+ ) ;
442+
443+ if ( basalOverlapsNextDay ) {
444+ nextDaySegments . push ( {
445+ ...segment ,
446+ segmentEnd : extendedSegmentEnd ,
447+ overlapStart : Math . max ( basalMsPer24WithOffset . start , extendedSegmentStart ) ,
448+ overlapEnd : Math . min ( basalMsPer24WithOffset . end , extendedSegmentEnd )
449+ } ) ;
450+ }
451+ }
452+ }
453+
454+ // Combine segments in chronological order: current day first, then next day
455+ const overlappingSegments = [ ...currentDaySegments , ...nextDaySegments ] ;
456+
457+ // It's expected that a basal will overlap with at least one segment
458+ if ( overlappingSegments . length === 1 ) {
459+ // Simple case: basal is within a single schedule segment
460+ const segment = overlappingSegments [ 0 ] ;
461+ d . suppressed = {
462+ ...d ,
463+ id : `${ d . id } _suppressed` ,
464+ deliveryType : 'scheduled' ,
465+ rate : segment . rate || 0 ,
466+ } ;
467+ } else if ( overlappingSegments . length > 1 ) {
468+ // Complex case: basal crosses schedule boundaries, need to split
469+ const originalDuration = d . duration || 0 ;
470+ const basalDurationMs24 = basalMsPer24WithOffset . end - basalMsPer24WithOffset . start ;
471+
472+ // Update the original basal to cover only the first segment
473+ const firstSegment = overlappingSegments [ 0 ] ;
474+ const firstSegmentMs24Duration = firstSegment . overlapEnd - firstSegment . overlapStart ;
475+ const firstSegmentDuration = ( firstSegmentMs24Duration / basalDurationMs24 ) * originalDuration ;
476+
477+ d . duration = firstSegmentDuration ;
478+ d . suppressed = {
479+ ...d ,
480+ id : `${ d . id } _suppressed` ,
481+ deliveryType : 'scheduled' ,
482+ rate : firstSegment . rate || 0 ,
483+ duration : firstSegmentDuration ,
484+ time : d . time , // Keep original time
485+ } ;
486+
487+ // Create additional basal datums for the remaining segments
488+ let cumulativeDuration = firstSegmentDuration ;
489+
490+ for ( let i = 1 ; i < overlappingSegments . length ; i ++ ) {
491+ const segment = overlappingSegments [ i ] ;
492+ const segmentMs24Duration = segment . overlapEnd - segment . overlapStart ;
493+ const segmentDuration = ( segmentMs24Duration / basalDurationMs24 ) * originalDuration ;
494+
495+ const newBasal = {
496+ ...d ,
497+ id : `${ d . id } _split_${ i } ` ,
498+ time : d . time + cumulativeDuration ,
499+ duration : segmentDuration ,
500+ suppressed : {
501+ ...d ,
502+ id : `${ d . id } _suppressed_split_${ i } ` ,
503+ deliveryType : 'scheduled' ,
504+ rate : segment . rate || 0 ,
505+ duration : segmentDuration ,
506+ time : d . time + cumulativeDuration ,
507+ }
508+ } ;
509+
510+ // Update deviceTime if it exists
511+ if ( d . deviceTime ) {
512+ newBasal . deviceTime = d . deviceTime + cumulativeDuration ;
513+ newBasal . suppressed . deviceTime = newBasal . deviceTime ;
514+ }
515+
516+ basalsToAdd . push ( newBasal ) ;
517+ cumulativeDuration += segmentDuration ;
518+ }
519+ } else {
520+ // Fallback: no overlapping segments found (shouldn't happen normally)
521+ this . log ( 'Warning: No overlapping segments found for basal' , d . id ) ;
522+ }
523+ }
524+ } ) ;
525+
526+ // Add the new split basal datums to the data array
527+ data . push ( ...basalsToAdd ) ;
528+ } ;
529+
340530 /**
341531 * Medtronic 5 and 7 series (which always have a deviceId starting with 'MedT-') carb exchange
342532 * data is converted to carbs at a rounded 1:15 ratio in the uploader, and needs to be
0 commit comments