1- import  {  HttpClient  }  from  '@angular/common/http' ; 
1+ import  {  HttpClient ,   HttpErrorResponse  }  from  '@angular/common/http' ; 
22import  {  Component ,  OnDestroy ,  OnInit ,  ViewChild  }  from  '@angular/core' ; 
33import  {  FormBuilder ,  FormGroup ,  Validators ,  FormControl  }  from  '@angular/forms' ; 
44import  {  ToastrService  }  from  'ngx-toastr' ; 
@@ -10,7 +10,7 @@ const SWARM_DATA = 'SWARM_DATA';
1010const  SWARM_REFRESH_TIME  =  'SWARM_REFRESH_TIME' ; 
1111const  SWARM_SORTING  =  'SWARM_SORTING' ; 
1212
13- type  SwarmDevice  =  {  IP : string ;  ASICModel : string ;  deviceModel : string ;  swarmColor : string ;  asicCount : number ;  [ key : string ] : any  } ; 
13+ type  SwarmDevice  =  {  address : string ;  ASICModel : string ;  deviceModel : string ;  swarmColor : string ;  asicCount : number ;  [ key : string ] : any  } ; 
1414
1515@Component ( { 
1616  selector : 'app-swarm' , 
@@ -50,7 +50,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
5050  )  { 
5151
5252    this . form  =  this . fb . group ( { 
53-       manualAddIp : [ null ,  [ Validators . required ,  Validators . pattern ( '(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' ) ] ] 
53+       manualAddAddress : [ null ,  [ Validators . required ,  Validators . pattern ( '^ (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|[a-zA-Z0-9-]+\\.local$ ' ) ] ] 
5454    } ) ; 
5555
5656    const  storedRefreshTime  =  this . localStorageService . getNumber ( SWARM_REFRESH_TIME )  ??  30 ; 
@@ -65,7 +65,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
6565    } ) ; 
6666
6767    const  storedSorting  =  this . localStorageService . getObject ( SWARM_SORTING )  ??  { 
68-       sortField : 'IP ' , 
68+       sortField : 'address ' , 
6969      sortDirection : 'asc' 
7070    } ; 
7171    this . sortField  =  storedSorting . sortField ; 
@@ -101,6 +101,11 @@ export class SwarmComponent implements OnInit, OnDestroy {
101101    return  ip . split ( '.' ) . reduce ( ( acc ,  octet )  =>  ( acc  <<  8 )  +  parseInt ( octet ,  10 ) ,  0 )  >>>  0 ; 
102102  } 
103103
104+   private  isIpAddress ( value : string ) : boolean  { 
105+     const  ipRegex  =  / ^ (?: (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) \\ .) { 3 } (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) $ / ; 
106+     return  ipRegex . test ( value ) ; 
107+   } 
108+ 
104109  private  intToIp ( int : number ) : string  { 
105110    return  `${ ( int  >>>  24 )  &  255 }  .${ ( int  >>>  16 )  &  255 }  .${ ( int  >>>  8 )  &  255 }  .${ int  &  255 }  ` ; 
106111  } 
@@ -116,15 +121,37 @@ export class SwarmComponent implements OnInit, OnDestroy {
116121  scanNetwork ( )  { 
117122    this . scanning  =  true ; 
118123
119-     const  {  start,  end }  =  this . calculateIpRange ( window . location . hostname ,  '255.255.255.0' ) ; 
120-     const  ips  =  Array . from ( {  length : end  -  start  +  1  } ,  ( _ ,  i )  =>  this . intToIp ( start  +  i ) ) ; 
124+     if  ( this . isIpAddress ( window . location . hostname ) )  { 
125+       // Direct IP access - scan the subnet 
126+       const  {  start,  end }  =  this . calculateIpRange ( window . location . hostname ,  '255.255.255.0' ) ; 
127+       const  ips  =  Array . from ( {  length : end  -  start  +  1  } ,  ( _ ,  i )  =>  this . intToIp ( start  +  i ) ) ; 
128+       this . performNetworkScan ( ips ) ; 
129+     }  else  { 
130+       // mDNS hostname - fetch server IP first, then scan its subnet 
131+       this . httpClient . get ( `http://${ window . location . hostname }  /api/system/info` ) 
132+         . subscribe ( { 
133+           next : ( response : any )  =>  { 
134+             const  serverIp  =  response . ip ; 
135+             const  {  start,  end }  =  this . calculateIpRange ( serverIp ,  '255.255.255.0' ) ; 
136+             const  ips  =  Array . from ( {  length : end  -  start  +  1  } ,  ( _ ,  i )  =>  this . intToIp ( start  +  i ) ) ; 
137+             this . performNetworkScan ( ips ) ; 
138+           } , 
139+           error : ( )  =>  { 
140+             // Fallback: skip scanning if we can't get the IP 
141+             this . scanning  =  false ; 
142+           } 
143+         } ) ; 
144+     } 
145+   } 
146+ 
147+   private  performNetworkScan ( ips : string [ ] )  { 
121148    this . getAllDeviceInfo ( ips ,  ( )  =>  of ( null ) ) . subscribe ( { 
122149      next : ( result )  =>  { 
123150        // Filter out null items first 
124151        const  validResults  =  result . filter ( ( item ) : item  is SwarmDevice  =>  item  !==  null ) ; 
125152        // Merge new results with existing swarm entries 
126-         const  existingIps  =  new  Set ( this . swarm . map ( item  =>  item . IP ) ) ; 
127-         const  newItems  =  validResults . filter ( item  =>  ! existingIps . has ( item . IP ) ) ; 
153+         const  existingAddresses  =  new  Set ( this . swarm . map ( item  =>  item . address ) ) ; 
154+         const  newItems  =  validResults . filter ( item  =>  ! existingAddresses . has ( item . address ) ) ; 
128155        this . swarm  =  [ ...this . swarm ,  ...newItems ] ; 
129156        this . sortSwarm ( ) ; 
130157        this . localStorageService . setObject ( SWARM_DATA ,  this . swarm ) ; 
@@ -136,19 +163,31 @@ export class SwarmComponent implements OnInit, OnDestroy {
136163    } ) ; 
137164  } 
138165
139-   private  getAllDeviceInfo ( ips : string [ ] ,  errorHandler : ( error : any ,  ip : string )  =>  Observable < SwarmDevice [ ]  |  null > ,  fetchAsic : boolean  =  true )  { 
140-     return  from ( ips ) . pipe ( 
141-       mergeMap ( IP  =>  forkJoin ( { 
142-         info : this . httpClient . get ( `http://${ IP }  /api/system/info` ) , 
143-         asic : fetchAsic  ? this . httpClient . get ( `http://${ IP }  /api/system/asic` ) . pipe ( catchError ( ( )  =>  of ( { } ) ) )  : of ( { } ) 
166+   private  getAllDeviceInfo ( addresses : string [ ] ,  errorHandler : ( error : any ,  address : string )  =>  Observable < SwarmDevice [ ]  |  null > ,  fetchAsic : boolean  =  true )  { 
167+     return  from ( addresses ) . pipe ( 
168+       mergeMap ( address  =>  forkJoin ( { 
169+         info : this . httpClient . get ( `http://${ address }  /api/system/info` ) . pipe ( catchError ( error  =>  { 
170+           if  ( error . status  ===  401  ||  error . status  ===  0 )  { 
171+             // Show warning for potential older device or CORS blocked (likely 401) 
172+             this . toastr . warning ( `Potential swarm peer detected at ${ address }   - upgrade its firmware to be able to add it.` ) ; 
173+             return  of ( {  _corsError : 401  } ) ; 
174+           } 
175+           throw  error ; 
176+         } ) ) , 
177+         asic : fetchAsic  ? this . httpClient . get ( `http://${ address }  /api/system/asic` ) . pipe ( catchError ( ( )  =>  of ( { } ) ) )  : of ( { } ) 
144178      } ) . pipe ( 
145179        map ( ( {  info,  asic } )  =>  { 
146-           const  existingDevice  =  this . swarm . find ( device  =>  device . IP  ===  IP ) ; 
147-           const  result  =  {  IP ,  ...( existingDevice  ? existingDevice  : { } ) ,  ...info ,  ...asic  } ; 
180+           // Skip processing if we already showed the warning 
181+           if  ( ( info  as  any ) . _corsError  ===  401 )  { 
182+             return  null ; 
183+           } 
184+ 
185+           const  existingDevice  =  this . swarm . find ( device  =>  device . address  ===  address ) ; 
186+           const  result  =  {  address,  ...( existingDevice  ? existingDevice  : { } ) ,  ...info ,  ...asic  } ; 
148187          return  this . fallbackDeviceModel ( result ) ; 
149188        } ) , 
150189        timeout ( 5000 ) , 
151-         catchError ( error  =>  errorHandler ( error ,  IP ) ) 
190+         catchError ( error  =>  errorHandler ( error ,  address ) ) 
152191      ) , 
153192        128 
154193      ) , 
@@ -157,61 +196,70 @@ export class SwarmComponent implements OnInit, OnDestroy {
157196  } 
158197
159198  public  add ( )  { 
160-     const  IP  =  this . form . value . manualAddIp ; 
199+     const  address  =  this . form . value . manualAddAddress ; 
161200
162-     // Check if IP  already exists 
163-     if  ( this . swarm . some ( item  =>  item . IP  ===  IP ) )  { 
164-       this . toastr . warning ( 'This IP  address already exists in the swarm.' ) ; 
201+     // Check if address  already exists 
202+     if  ( this . swarm . some ( item  =>  item . address  ===  address ) )  { 
203+       this . toastr . warning ( 'This address already exists in the swarm.' ) ; 
165204      return ; 
166205    } 
167206
168207    forkJoin ( { 
169-       info : this . httpClient . get < any > ( `http://${ IP }  /api/system/info` ) , 
170-       asic : this . httpClient . get < any > ( `http://${ IP }  /api/system/asic` ) . pipe ( catchError ( ( )  =>  of ( { } ) ) ) 
208+       info : this . httpClient . get < any > ( `http://${ address }  /api/system/info` ) . pipe ( catchError ( error  =>  { 
209+         if  ( error . status  ===  401  ||  error . status  ===  0 )  { 
210+           this . toastr . warning ( `Potential swarm peer detected at ${ address }   - upgrade its firmware to be able to add it.` ) ; 
211+           return  of ( {  _corsError : 401  } ) ; 
212+         } 
213+         throw  error ; 
214+       } ) ) , 
215+       asic : this . httpClient . get < any > ( `http://${ address }  /api/system/asic` ) . pipe ( catchError ( ( )  =>  of ( { } ) ) ) 
171216    } ) . subscribe ( ( {  info,  asic } )  =>  { 
217+       if  ( ( info  as  any ) . _corsError  ===  401 )  { 
218+         return ;  // Already showed warning 
219+       } 
172220      if  ( ! info . ASICModel  ||  ! asic . ASICModel )  { 
173221        return ; 
174222      } 
175223
176-       this . swarm . push ( {  IP ,  ...asic ,  ...info  } ) ; 
224+       this . swarm . push ( {  address ,  ...asic ,  ...info  } ) ; 
177225      this . sortSwarm ( ) ; 
178226      this . localStorageService . setObject ( SWARM_DATA ,  this . swarm ) ; 
179227      this . calculateTotals ( ) ; 
180228    } ) ; 
181229  } 
182230
183-   public  edit ( axe : any )  { 
184-     this . selectedAxeOs  =  axe ; 
231+   public  edit ( device : any )  { 
232+     this . selectedAxeOs  =  device ; 
185233    this . modalComponent . isVisible  =  true ; 
186234  } 
187235
188-   public  restart ( axe : any )  { 
189-     this . httpClient . post ( `http://${ axe . IP }  /api/system/restart` ,  { } ) . pipe ( 
236+   public  restart ( device : any )  { 
237+     this . httpClient . post ( `http://${ device . address }  /api/system/restart` ,  { } ) . pipe ( 
190238      catchError ( error  =>  { 
191239        if  ( error . status  ===  0  ||  error . status  ===  200  ||  error . name  ===  'HttpErrorResponse' )  { 
192240          return  of ( 'success' ) ; 
193241        }  else  { 
194-           this . toastr . error ( `Failed to restart device at ${ axe . IP }  ` ) ; 
242+           this . toastr . error ( `Failed to restart device at ${ device . address }  ` ) ; 
195243          return  of ( null ) ; 
196244        } 
197245      } ) 
198246    ) . subscribe ( res  =>  { 
199247      if  ( res  !==  null  &&  res  ==  'success' )  { 
200-         this . toastr . success ( `Device at ${ axe . IP }   restarted` ) ; 
248+         this . toastr . success ( `Device at ${ device . address }   restarted` ) ; 
201249      } 
202250    } ) ; 
203251  } 
204252
205-   public  remove ( axeOs : any )  { 
206-     this . swarm  =  this . swarm . filter ( axe  =>  axe . IP  !==  axeOs . IP ) ; 
253+   public  remove ( device : any )  { 
254+     this . swarm  =  this . swarm . filter ( axe  =>  axe . address  !==  device . address ) ; 
207255    this . localStorageService . setObject ( SWARM_DATA ,  this . swarm ) ; 
208256    this . calculateTotals ( ) ; 
209257  } 
210258
211-   public  refreshErrorHandler  =  ( error : any ,  ip : string )  =>  { 
259+   public  refreshErrorHandler  =  ( error : any ,  address : string )  =>  { 
212260    const  errorMessage  =  error ?. message  ||  error ?. statusText  ||  error ?. toString ( )  ||  'Unknown error' ; 
213-     this . toastr . error ( `Failed to get info from ${ ip }  ` ) ; 
214-     const  existingDevice  =  this . swarm . find ( axeOs  =>  axeOs . IP  ===  ip ) ; 
261+     this . toastr . error ( `Failed to get info from ${ address }  ` ) ; 
262+     const  existingDevice  =  this . swarm . find ( axeOs  =>  axeOs . address  ===  address ) ; 
215263    return  of ( { 
216264      ...existingDevice , 
217265      hashRate : 0 , 
@@ -232,10 +280,10 @@ export class SwarmComponent implements OnInit, OnDestroy {
232280    } 
233281
234282    this . refreshIntervalTime  =  this . refreshTimeSet ; 
235-     const  ips  =  this . swarm . map ( axeOs  =>  axeOs . IP ) ; 
283+     const  addresses  =  this . swarm . map ( axeOs  =>  axeOs . address ) ; 
236284    this . isRefreshing  =  true ; 
237285
238-     this . getAllDeviceInfo ( ips ,  this . refreshErrorHandler ,  fetchAsic ) . subscribe ( { 
286+     this . getAllDeviceInfo ( addresses ,  this . refreshErrorHandler ,  fetchAsic ) . subscribe ( { 
239287      next : ( result )  =>  { 
240288        this . swarm  =  result ; 
241289        this . sortSwarm ( ) ; 
@@ -272,15 +320,28 @@ export class SwarmComponent implements OnInit, OnDestroy {
272320      let  comparison  =  0 ; 
273321      const  fieldType  =  typeof  a [ this . sortField ] ; 
274322
275-       if  ( this . sortField  ===  'IP' )  { 
276-         // Split IP into octets and compare numerically 
277-         const  aOctets  =  a [ this . sortField ] . split ( '.' ) . map ( Number ) ; 
278-         const  bOctets  =  b [ this . sortField ] . split ( '.' ) . map ( Number ) ; 
279-         for  ( let  i  =  0 ;  i  <  4 ;  i ++ )  { 
280-           if  ( aOctets [ i ]  !==  bOctets [ i ] )  { 
281-             comparison  =  aOctets [ i ]  -  bOctets [ i ] ; 
282-             break ; 
323+       if  ( this . sortField  ===  'address' )  { 
324+         const  aValue  =  a [ this . sortField ] ; 
325+         const  bValue  =  b [ this . sortField ] ; 
326+         const  aIsIp  =  this . isIpAddress ( aValue ) ; 
327+         const  bIsIp  =  this . isIpAddress ( bValue ) ; 
328+ 
329+         if  ( aIsIp  &&  bIsIp )  { 
330+           // Both are IPs, sort numerically 
331+           const  aOctets  =  aValue . split ( '.' ) . map ( Number ) ; 
332+           const  bOctets  =  bValue . split ( '.' ) . map ( Number ) ; 
333+           for  ( let  i  =  0 ;  i  <  4 ;  i ++ )  { 
334+             if  ( aOctets [ i ]  !==  bOctets [ i ] )  { 
335+               comparison  =  aOctets [ i ]  -  bOctets [ i ] ; 
336+               break ; 
337+             } 
283338          } 
339+         }  else  if  ( ! aIsIp  &&  ! bIsIp )  { 
340+           // Both are hostnames, sort alphabetically 
341+           comparison  =  aValue . localeCompare ( bValue ) ; 
342+         }  else  { 
343+           // Mixed, sort IPs before hostnames 
344+           comparison  =  aIsIp  ? - 1  : 1 ; 
284345        } 
285346      }  else  if  ( fieldType  ===  'number' )  { 
286347        comparison  =  a [ this . sortField ]  -  b [ this . sortField ] ; 
0 commit comments