@@ -319,3 +319,213 @@ function countNights(timestamps: Date[]): number {
319319 }
320320 return uniqueDays . size ;
321321}
322+
323+ export interface DebugReport {
324+ timestamp : string ;
325+ platform : string ;
326+ sleepStages : {
327+ total : number ;
328+ byStage : Record < string , number > ;
329+ samples : Array < { stage : string ; start : string ; end : string ; durationMin : number } > ;
330+ } ;
331+ heartRate : {
332+ total : number ;
333+ oldest : string | null ;
334+ newest : string | null ;
335+ samples : Array < { bpm : number ; time : string } > ;
336+ } ;
337+ hrv : {
338+ total : number ;
339+ oldest : string | null ;
340+ newest : string | null ;
341+ samples : Array < { ms : number ; time : string } > ;
342+ } ;
343+ matching : {
344+ stagesWithHR : Record < string , number > ;
345+ stagesWithHRV : Record < string , number > ;
346+ } ;
347+ model : LearnedModel | null ;
348+ errors : string [ ] ;
349+ }
350+
351+ export async function runDebugReport ( hoursBack : number = 48 ) : Promise < DebugReport > {
352+ const platform = Platform . OS ;
353+ const errors : string [ ] = [ ] ;
354+ const report : DebugReport = {
355+ timestamp : new Date ( ) . toISOString ( ) ,
356+ platform,
357+ sleepStages : { total : 0 , byStage : { } , samples : [ ] } ,
358+ heartRate : { total : 0 , oldest : null , newest : null , samples : [ ] } ,
359+ hrv : { total : 0 , oldest : null , newest : null , samples : [ ] } ,
360+ matching : { stagesWithHR : { } , stagesWithHRV : { } } ,
361+ model : null ,
362+ errors : [ ] ,
363+ } ;
364+
365+ if ( platform !== 'android' && platform !== 'ios' ) {
366+ errors . push ( `Unsupported platform: ${ platform } ` ) ;
367+ report . errors = errors ;
368+ return report ;
369+ }
370+
371+ try {
372+ const sleepStages =
373+ platform === 'ios'
374+ ? await healthKit . getRecentSleepSessions ( hoursBack )
375+ : await healthConnect . getRecentSleepSessions ( hoursBack ) ;
376+
377+ report . sleepStages . total = sleepStages . length ;
378+
379+ const byStage : Record < string , number > = { } ;
380+ for ( const s of sleepStages ) {
381+ byStage [ s . stage ] = ( byStage [ s . stage ] || 0 ) + 1 ;
382+ }
383+ report . sleepStages . byStage = byStage ;
384+
385+ report . sleepStages . samples = sleepStages . slice ( 0 , 20 ) . map ( ( s ) => ( {
386+ stage : s . stage ,
387+ start : s . startTime ,
388+ end : s . endTime ,
389+ durationMin : Math . round (
390+ ( new Date ( s . endTime ) . getTime ( ) - new Date ( s . startTime ) . getTime ( ) ) / 60000
391+ ) ,
392+ } ) ) ;
393+
394+ const hrSamples =
395+ platform === 'ios'
396+ ? await healthKit . getRecentHeartRate ( hoursBack * 60 )
397+ : await healthConnect . getRecentHeartRate ( hoursBack * 60 ) ;
398+
399+ report . heartRate . total = hrSamples . length ;
400+ if ( hrSamples . length > 0 ) {
401+ const sorted = [ ...hrSamples ] . sort (
402+ ( a , b ) => new Date ( a . time ) . getTime ( ) - new Date ( b . time ) . getTime ( )
403+ ) ;
404+ report . heartRate . oldest = sorted [ 0 ] . time ;
405+ report . heartRate . newest = sorted [ sorted . length - 1 ] . time ;
406+ report . heartRate . samples = hrSamples . slice ( 0 , 10 ) . map ( ( s ) => ( {
407+ bpm : s . beatsPerMinute ,
408+ time : s . time ,
409+ } ) ) ;
410+ }
411+
412+ const hrvSamples =
413+ platform === 'ios'
414+ ? await healthKit . getRecentHRV ( hoursBack * 60 )
415+ : await healthConnect . getRecentHRV ( hoursBack * 60 ) ;
416+
417+ report . hrv . total = hrvSamples . length ;
418+ if ( hrvSamples . length > 0 ) {
419+ const sorted = [ ...hrvSamples ] . sort (
420+ ( a , b ) => new Date ( a . time ) . getTime ( ) - new Date ( b . time ) . getTime ( )
421+ ) ;
422+ report . hrv . oldest = sorted [ 0 ] . time ;
423+ report . hrv . newest = sorted [ sorted . length - 1 ] . time ;
424+ report . hrv . samples = hrvSamples . slice ( 0 , 10 ) . map ( ( s ) => ( {
425+ ms : s . heartRateVariabilityMillis ,
426+ time : s . time ,
427+ } ) ) ;
428+ }
429+
430+ const stagesWithHR : Record < string , number > = { } ;
431+ const stagesWithHRV : Record < string , number > = { } ;
432+
433+ for ( const stageRecord of sleepStages ) {
434+ const stageStart = new Date ( stageRecord . startTime ) ;
435+ const stageEnd = new Date ( stageRecord . endTime ) ;
436+ const stage = stageRecord . stage ;
437+
438+ const matchingHr = hrSamples . filter ( ( hr ) => {
439+ const t = new Date ( hr . time ) ;
440+ return t >= stageStart && t <= stageEnd ;
441+ } ) ;
442+ stagesWithHR [ stage ] = ( stagesWithHR [ stage ] || 0 ) + matchingHr . length ;
443+
444+ const matchingHrv = hrvSamples . filter ( ( hrv ) => {
445+ const t = new Date ( hrv . time ) ;
446+ return t >= stageStart && t <= stageEnd ;
447+ } ) ;
448+ stagesWithHRV [ stage ] = ( stagesWithHRV [ stage ] || 0 ) + matchingHrv . length ;
449+ }
450+
451+ report . matching . stagesWithHR = stagesWithHR ;
452+ report . matching . stagesWithHRV = stagesWithHRV ;
453+
454+ report . model = await loadModel ( ) ;
455+ } catch ( error ) {
456+ errors . push ( `Error: ${ error } ` ) ;
457+ }
458+
459+ report . errors = errors ;
460+ return report ;
461+ }
462+
463+ export function formatDebugReport ( report : DebugReport ) : string {
464+ const lines : string [ ] = [ ] ;
465+
466+ lines . push ( `=== Sleep Stage Learning Debug Report ===` ) ;
467+ lines . push ( `Time: ${ report . timestamp } ` ) ;
468+ lines . push ( `Platform: ${ report . platform } ` ) ;
469+ lines . push ( '' ) ;
470+
471+ lines . push ( `--- Sleep Stages ---` ) ;
472+ lines . push ( `Total stages: ${ report . sleepStages . total } ` ) ;
473+ lines . push ( `By stage: ${ JSON . stringify ( report . sleepStages . byStage ) } ` ) ;
474+ if ( report . sleepStages . samples . length > 0 ) {
475+ lines . push ( `Recent samples:` ) ;
476+ for ( const s of report . sleepStages . samples . slice ( 0 , 5 ) ) {
477+ lines . push ( ` ${ s . stage } : ${ s . durationMin } min (${ s . start } )` ) ;
478+ }
479+ }
480+ lines . push ( '' ) ;
481+
482+ lines . push ( `--- Heart Rate ---` ) ;
483+ lines . push ( `Total samples: ${ report . heartRate . total } ` ) ;
484+ if ( report . heartRate . oldest && report . heartRate . newest ) {
485+ lines . push ( `Range: ${ report . heartRate . oldest } to ${ report . heartRate . newest } ` ) ;
486+ }
487+ if ( report . heartRate . samples . length > 0 ) {
488+ lines . push ( `Recent: ${ report . heartRate . samples . map ( ( s ) => s . bpm ) . join ( ', ' ) } bpm` ) ;
489+ }
490+ lines . push ( '' ) ;
491+
492+ lines . push ( `--- HRV ---` ) ;
493+ lines . push ( `Total samples: ${ report . hrv . total } ` ) ;
494+ if ( report . hrv . oldest && report . hrv . newest ) {
495+ lines . push ( `Range: ${ report . hrv . oldest } to ${ report . hrv . newest } ` ) ;
496+ }
497+ if ( report . hrv . samples . length > 0 ) {
498+ lines . push ( `Recent: ${ report . hrv . samples . map ( ( s ) => s . ms ) . join ( ', ' ) } ms` ) ;
499+ }
500+ lines . push ( '' ) ;
501+
502+ lines . push ( `--- Matching (HR/HRV samples per sleep stage) ---` ) ;
503+ lines . push ( `HR per stage: ${ JSON . stringify ( report . matching . stagesWithHR ) } ` ) ;
504+ lines . push ( `HRV per stage: ${ JSON . stringify ( report . matching . stagesWithHRV ) } ` ) ;
505+ lines . push ( '' ) ;
506+
507+ lines . push ( `--- Learned Model ---` ) ;
508+ if ( report . model ) {
509+ lines . push ( `Last updated: ${ report . model . lastUpdated } ` ) ;
510+ lines . push ( `Nights analyzed: ${ report . model . nightsAnalyzed } ` ) ;
511+ for ( const [ stage , profile ] of Object . entries ( report . model . profiles ) ) {
512+ if ( profile && stage !== 'any' ) {
513+ lines . push (
514+ ` ${ stage } : HR=${ profile . hrMean . toFixed ( 1 ) } ±${ profile . hrStd . toFixed ( 1 ) } , HRV=${ profile . hrvMean . toFixed ( 1 ) } ±${ profile . hrvStd . toFixed ( 1 ) } (n=${ profile . sampleCount } )`
515+ ) ;
516+ }
517+ }
518+ } else {
519+ lines . push ( 'No model loaded' ) ;
520+ }
521+ lines . push ( '' ) ;
522+
523+ if ( report . errors . length > 0 ) {
524+ lines . push ( `--- Errors ---` ) ;
525+ for ( const e of report . errors ) {
526+ lines . push ( ` ${ e } ` ) ;
527+ }
528+ }
529+
530+ return lines . join ( '\n' ) ;
531+ }
0 commit comments