@@ -4,6 +4,8 @@ class VitruvianApp {
44 constructor ( ) {
55 this . device = new VitruvianDevice ( ) ;
66 this . loadHistory = [ ] ;
7+ this . frozenHistory = null ;
8+ this . isGraphFrozen = false ;
79 this . maxHistoryPoints = 300 ; // 30 seconds at 100ms polling
810 this . maxPosA = 1000 ; // Dynamic max for Right Cable (A)
911 this . maxPosB = 1000 ; // Dynamic max for Left Cable (B)
@@ -13,16 +15,27 @@ class VitruvianApp {
1315 this . targetReps = 0 ; // Target working reps
1416 this . workoutHistory = [ ] ; // Track completed workouts
1517 this . currentWorkout = null ; // Current workout info
18+ this . _isStopping = false ;
19+ this . _isCompleting = false ;
1620 this . setupLogging ( ) ;
1721 this . setupGraph ( ) ;
1822 this . resetRepCountersToEmpty ( ) ;
23+ this . updateResumeGraphButton ( false ) ;
1924 }
2025
2126 setupLogging ( ) {
2227 // Connect device logging to UI
2328 this . device . onLog = ( message , type ) => {
2429 this . addLogEntry ( message , type ) ;
2530 } ;
31+
32+ this . device . onDisconnect = ( ) => {
33+ this . updateConnectionStatus ( false ) ;
34+ this . addLogEntry (
35+ "Device disconnected. Please reconnect before starting another workout." ,
36+ "error" ,
37+ ) ;
38+ } ;
2639 }
2740
2841 setupGraph ( ) {
@@ -65,12 +78,64 @@ class VitruvianApp {
6578 this . drawGraph ( ) ;
6679 }
6780
81+ updateResumeGraphButton ( visible ) {
82+ const button = document . getElementById ( "resumeGraphBtn" ) ;
83+ if ( ! button ) return ;
84+
85+ if ( visible ) {
86+ button . classList . remove ( "hidden" ) ;
87+ } else {
88+ button . classList . add ( "hidden" ) ;
89+ }
90+ }
91+
92+ freezeGraphSnapshot ( message ) {
93+ if ( this . isGraphFrozen ) {
94+ return ;
95+ }
96+
97+ if ( this . loadHistory . length === 0 ) {
98+ return ;
99+ }
100+
101+ this . isGraphFrozen = true ;
102+ this . frozenHistory = this . loadHistory . map ( ( point ) => ( { ...point } ) ) ;
103+ this . updateResumeGraphButton ( true ) ;
104+
105+ const logMessage =
106+ message || "Graph frozen – click Resume Live Graph to return to live data." ;
107+ this . addLogEntry ( logMessage , "info" ) ;
108+
109+ this . drawGraph ( ) ;
110+ }
111+
112+ resumeLiveGraph ( silent = false ) {
113+ if ( ! this . isGraphFrozen ) {
114+ return ;
115+ }
116+
117+ this . isGraphFrozen = false ;
118+ this . frozenHistory = null ;
119+ this . loadHistory = [ ] ;
120+ this . updateResumeGraphButton ( false ) ;
121+
122+ if ( ! silent ) {
123+ this . addLogEntry ( "Live graph resumed" , "info" ) ;
124+ }
125+
126+ this . drawGraph ( ) ;
127+ }
128+
68129 drawGraph ( ) {
69130 if ( ! this . ctx || ! this . canvas ) return ;
70131
71132 const width = this . canvasDisplayWidth || this . canvas . width ;
72133 const height = this . canvasDisplayHeight || this . canvas . height ;
73134 const ctx = this . ctx ;
135+ const history =
136+ this . isGraphFrozen && this . frozenHistory && this . frozenHistory . length > 0
137+ ? this . frozenHistory
138+ : this . loadHistory ;
74139
75140 if ( width === 0 || height === 0 ) return ; // Canvas not sized yet
76141
@@ -81,7 +146,7 @@ class VitruvianApp {
81146 // Set text rendering for crisp fonts
82147 ctx . textRendering = "optimizeLegibility" ;
83148
84- if ( this . loadHistory . length < 2 ) {
149+ if ( history . length < 2 ) {
85150 // Not enough data to draw
86151 ctx . fillStyle = "#6c757d" ;
87152 ctx . font = "14px -apple-system, sans-serif" ;
@@ -96,7 +161,7 @@ class VitruvianApp {
96161
97162 // Find max load for scaling
98163 let maxLoad = 0 ;
99- for ( const point of this . loadHistory ) {
164+ for ( const point of history ) {
100165 const totalLoad = point . loadA + point . loadB ;
101166 if ( totalLoad > maxLoad ) maxLoad = totalLoad ;
102167 }
@@ -149,7 +214,7 @@ class VitruvianApp {
149214
150215 // Draw lines for each cable and total
151216 // Calculate spacing based on actual number of points to fill the graph width
152- const numPoints = this . loadHistory . length ;
217+ const numPoints = history . length ;
153218 const pointSpacing = numPoints > 1 ? graphWidth / ( numPoints - 1 ) : 0 ;
154219
155220 // Helper to draw a line
@@ -161,8 +226,8 @@ class VitruvianApp {
161226 ctx . beginPath ( ) ;
162227
163228 let started = false ;
164- for ( let i = 0 ; i < this . loadHistory . length ; i ++ ) {
165- const point = this . loadHistory [ i ] ;
229+ for ( let i = 0 ; i < history . length ; i ++ ) {
230+ const point = history [ i ] ;
166231 const value = getData ( point ) ;
167232 const x = padding + i * pointSpacing ;
168233 const y = padding + graphHeight - ( value / maxLoad ) * graphHeight ;
@@ -284,6 +349,10 @@ class VitruvianApp {
284349 document . getElementById ( "barA" ) . style . height = heightA + "%" ;
285350 document . getElementById ( "barB" ) . style . height = heightB + "%" ;
286351
352+ if ( this . isGraphFrozen ) {
353+ return ;
354+ }
355+
287356 // Add to load history
288357 this . loadHistory . push ( {
289358 timestamp : sample . timestamp ,
@@ -383,7 +452,13 @@ class VitruvianApp {
383452 }
384453
385454 completeWorkout ( ) {
386- if ( this . currentWorkout ) {
455+ if ( ! this . currentWorkout || this . _isCompleting ) {
456+ return ;
457+ }
458+
459+ this . _isCompleting = true ;
460+
461+ try {
387462 // Add to history
388463 this . addToWorkoutHistory ( {
389464 mode : this . currentWorkout . mode ,
@@ -395,6 +470,12 @@ class VitruvianApp {
395470 // Reset to empty state
396471 this . resetRepCountersToEmpty ( ) ;
397472 this . addLogEntry ( "Workout completed and saved to history" , "success" ) ;
473+
474+ this . freezeGraphSnapshot (
475+ "Workout completed – graph frozen. Click Resume Live Graph to review the last set." ,
476+ ) ;
477+ } finally {
478+ this . _isCompleting = false ;
398479 }
399480 }
400481
@@ -508,16 +589,50 @@ class VitruvianApp {
508589 }
509590
510591 async stopWorkout ( ) {
592+ if ( this . _isStopping ) {
593+ return ;
594+ }
595+
596+ this . _isStopping = true ;
597+ const stopBtn = document . getElementById ( "stopBtn" ) ;
598+ if ( stopBtn ) {
599+ stopBtn . disabled = true ;
600+ }
601+
602+ const startedAt = Date . now ( ) ;
603+
511604 try {
512605 await this . device . sendStopCommand ( ) ;
513606 this . addLogEntry ( "Workout stopped by user" , "info" ) ;
514607
515608 // Complete the workout and save to history
516609 this . completeWorkout ( ) ;
610+
611+ this . freezeGraphSnapshot (
612+ "STOP acknowledged – graph frozen. Click Resume Live Graph to continue live telemetry." ,
613+ ) ;
517614 } catch ( error ) {
518615 console . error ( "Stop workout error:" , error ) ;
519616 this . addLogEntry ( `Failed to stop workout: ${ error . message } ` , "error" ) ;
617+
618+ if ( ! this . device . isConnected ) {
619+ this . updateConnectionStatus ( false ) ;
620+ this . addLogEntry (
621+ "Device disconnected during STOP safety fallback. Please reconnect before continuing." ,
622+ "error" ,
623+ ) ;
624+ }
625+
520626 alert ( `Failed to stop workout: ${ error . message } ` ) ;
627+ } finally {
628+ const duration = Date . now ( ) - startedAt ;
629+ this . addLogEntry ( `STOP flow completed in ${ duration } ms` , "info" ) ;
630+
631+ if ( stopBtn && this . device . isConnected ) {
632+ stopBtn . disabled = false ;
633+ }
634+
635+ this . _isStopping = false ;
521636 }
522637 }
523638
@@ -572,6 +687,8 @@ class VitruvianApp {
572687 } ;
573688 this . updateRepCounters ( ) ;
574689
690+ this . resumeLiveGraph ( true ) ;
691+
575692 await this . device . startProgram ( params ) ;
576693
577694 // Set up monitor listener
@@ -638,6 +755,8 @@ class VitruvianApp {
638755 } ;
639756 this . updateRepCounters ( ) ;
640757
758+ this . resumeLiveGraph ( true ) ;
759+
641760 await this . device . startEcho ( params ) ;
642761
643762 // Set up monitor listener
0 commit comments