@@ -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