11import React from 'react' ;
2- import { Block , MiningOn , Subtree , Handshake , RejectedTx } from '../types/Message' ;
2+ import { Block , MiningOn , Subtree , Handshake , RejectedTx , NodeStatus } from '../types/Message' ;
33import { ClickablePeerNameDisplay } from './ClickablePeerNameDisplay' ;
44
55interface MessageCardBaseProps {
@@ -323,7 +323,7 @@ export const RejectedTxCard: React.FC<RejectedTxCardProps> = ({ rejectedTx, isNe
323323 { rejectedTx . TxID || 'N/A' }
324324 </ p >
325325 </ div >
326-
326+
327327 < div >
328328 < span className = "text-xs sm:text-sm font-medium text-gray-500" > Reason</ span >
329329 < p className = "text-xs sm:text-sm text-gray-900 bg-red-50 p-2 rounded break-words" >
@@ -333,3 +333,199 @@ export const RejectedTxCard: React.FC<RejectedTxCardProps> = ({ rejectedTx, isNe
333333 </ MessageCardBase >
334334) ;
335335
336+ interface NodeStatusCardProps {
337+ nodeStatus : NodeStatus ;
338+ isNew ?: boolean ;
339+ }
340+
341+ export const NodeStatusCard : React . FC < NodeStatusCardProps > = ( { nodeStatus, isNew } ) => {
342+ const formatUptime = ( seconds : number ) : string => {
343+ if ( ! seconds || seconds <= 0 ) return 'N/A' ;
344+
345+ const days = Math . floor ( seconds / 86400 ) ;
346+ const hours = Math . floor ( ( seconds % 86400 ) / 3600 ) ;
347+ const minutes = Math . floor ( ( seconds % 3600 ) / 60 ) ;
348+
349+ if ( days > 0 ) {
350+ return `${ days } d ${ hours } h ${ minutes } m` ;
351+ } else if ( hours > 0 ) {
352+ return `${ hours } h ${ minutes } m` ;
353+ } else {
354+ return `${ minutes } m` ;
355+ }
356+ } ;
357+
358+ const formatStartTime = ( timestamp : number ) : string => {
359+ if ( ! timestamp || timestamp <= 0 ) return 'N/A' ;
360+ try {
361+ const date = new Date ( timestamp * 1000 ) ;
362+ return date . toLocaleString ( ) ;
363+ } catch {
364+ return 'N/A' ;
365+ }
366+ } ;
367+
368+ const formatSyncTime = ( timestamp : number ) : string => {
369+ if ( ! timestamp || timestamp <= 0 ) return 'N/A' ;
370+ try {
371+ const date = new Date ( timestamp * 1000 ) ;
372+ const now = new Date ( ) ;
373+ const diffInSeconds = Math . floor ( ( now . getTime ( ) - date . getTime ( ) ) / 1000 ) ;
374+
375+ if ( diffInSeconds < 60 ) return `${ diffInSeconds } s ago` ;
376+ if ( diffInSeconds < 3600 ) return `${ Math . floor ( diffInSeconds / 60 ) } m ago` ;
377+ if ( diffInSeconds < 86400 ) return `${ Math . floor ( diffInSeconds / 3600 ) } h ago` ;
378+ return `${ Math . floor ( diffInSeconds / 86400 ) } d ago` ;
379+ } catch {
380+ return 'N/A' ;
381+ }
382+ } ;
383+
384+ const getFSMStateColor = ( state : string ) : string => {
385+ const stateColors : Record < string , string > = {
386+ 'running' : 'text-green-600' ,
387+ 'syncing' : 'text-blue-600' ,
388+ 'stopped' : 'text-red-600' ,
389+ 'paused' : 'text-yellow-600' ,
390+ 'idle' : 'text-gray-600'
391+ } ;
392+ return stateColors [ state ?. toLowerCase ( ) ] || 'text-gray-600' ;
393+ } ;
394+
395+ return (
396+ < MessageCardBase
397+ icon = "🖥️"
398+ title = { `Node Status - ${ nodeStatus . Type || 'UNKNOWN' } ` }
399+ network = { nodeStatus . Network }
400+ receivedAt = { nodeStatus . ReceivedAt }
401+ isNew = { isNew }
402+ className = "border-l-4 border-l-blue-400"
403+ sentFromPeer = { nodeStatus . sentFromPeer }
404+ peerID = { nodeStatus . PeerID }
405+ >
406+ < div className = "grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4" >
407+ < div >
408+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > Client Name</ span >
409+ < p className = "text-xs sm:text-sm text-gray-900" > { nodeStatus . ClientName || 'Unknown' } </ p >
410+ </ div >
411+ < div >
412+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > Version</ span >
413+ < p className = "text-xs sm:text-sm text-gray-900" > { nodeStatus . Version || 'N/A' } </ p >
414+ </ div >
415+ </ div >
416+
417+ < div className = "grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4" >
418+ < div >
419+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > Best Height</ span >
420+ < p className = "text-xs sm:text-sm text-gray-900" >
421+ { nodeStatus . BestHeight ?. toLocaleString ( ) || 'N/A' }
422+ </ p >
423+ </ div >
424+ < div >
425+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > FSM State</ span >
426+ < p className = { `text-xs sm:text-sm font-semibold ${ getFSMStateColor ( nodeStatus . FSMState ) } ` } >
427+ { nodeStatus . FSMState || 'Unknown' }
428+ </ p >
429+ </ div >
430+ </ div >
431+
432+ < div >
433+ < span className = "text-sm font-medium text-gray-500" > Best Block Hash</ span >
434+ < p className = "font-mono text-sm text-gray-900 truncate" title = { nodeStatus . BestBlockHash || '' } >
435+ { nodeStatus . BestBlockHash || 'N/A' }
436+ </ p >
437+ </ div >
438+
439+ { nodeStatus . CommitHash && (
440+ < div >
441+ < span className = "text-sm font-medium text-gray-500" > Commit Hash</ span >
442+ < p className = "font-mono text-sm text-gray-900 truncate" title = { nodeStatus . CommitHash } >
443+ { nodeStatus . CommitHash . substring ( 0 , 12 ) }
444+ </ p >
445+ </ div >
446+ ) }
447+
448+ < div className = "grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4" >
449+ < div >
450+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > Uptime</ span >
451+ < p className = "text-xs sm:text-sm text-gray-900" > { formatUptime ( nodeStatus . Uptime ) } </ p >
452+ </ div >
453+ < div >
454+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > Started</ span >
455+ < p className = "text-xs sm:text-sm text-gray-900" > { formatStartTime ( nodeStatus . StartTime ) } </ p >
456+ </ div >
457+ </ div >
458+
459+ { nodeStatus . MinerName && (
460+ < div >
461+ < span className = "text-sm font-medium text-gray-500" > Miner</ span >
462+ < p className = "text-sm text-gray-900" > { nodeStatus . MinerName } </ p >
463+ </ div >
464+ ) }
465+
466+ { nodeStatus . ListenMode && (
467+ < div >
468+ < span className = "text-sm font-medium text-gray-500" > Listen Mode</ span >
469+ < p className = "text-sm text-gray-900" > { nodeStatus . ListenMode } </ p >
470+ </ div >
471+ ) }
472+
473+ { nodeStatus . ChainWork && (
474+ < div >
475+ < span className = "text-sm font-medium text-gray-500" > Chain Work</ span >
476+ < p className = "font-mono text-xs text-gray-900 truncate" title = { nodeStatus . ChainWork } >
477+ { nodeStatus . ChainWork }
478+ </ p >
479+ </ div >
480+ ) }
481+
482+ { nodeStatus . MinMiningTxFee !== undefined && nodeStatus . MinMiningTxFee !== null && (
483+ < div >
484+ < span className = "text-sm font-medium text-gray-500" > Min Mining TX Fee</ span >
485+ < p className = "text-sm text-gray-900" >
486+ { nodeStatus . MinMiningTxFee === 0 ? 'No fee' : nodeStatus . MinMiningTxFee . toFixed ( 8 ) + ' BSV/byte' }
487+ </ p >
488+ </ div >
489+ ) }
490+
491+ { nodeStatus . SyncPeerID && (
492+ < div className = "border-t border-gray-100 pt-3 space-y-2" >
493+ < div className = "font-semibold text-sm text-gray-700" > Sync Information</ div >
494+ < div className = "grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4" >
495+ < div >
496+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > Sync Peer</ span >
497+ < div className = "text-xs sm:text-sm text-gray-900" >
498+ < ClickablePeerNameDisplay
499+ peerID = { nodeStatus . SyncPeerID }
500+ showBoth = { true }
501+ className = "text-gray-700 hover:text-gray-900"
502+ />
503+ </ div >
504+ </ div >
505+ < div >
506+ < span className = "text-xs sm:text-sm font-medium text-gray-500" > Sync Height</ span >
507+ < p className = "text-xs sm:text-sm text-gray-900" >
508+ { nodeStatus . SyncPeerHeight ?. toLocaleString ( ) || 'N/A' }
509+ </ p >
510+ </ div >
511+ </ div >
512+ { nodeStatus . SyncPeerBlockHash && (
513+ < div >
514+ < span className = "text-sm font-medium text-gray-500" > Sync Block Hash</ span >
515+ < p className = "font-mono text-xs text-gray-900 truncate" title = { nodeStatus . SyncPeerBlockHash } >
516+ { nodeStatus . SyncPeerBlockHash }
517+ </ p >
518+ </ div >
519+ ) }
520+ { nodeStatus . SyncConnectedAt && (
521+ < div >
522+ < span className = "text-sm font-medium text-gray-500" > Connected</ span >
523+ < p className = "text-sm text-gray-900" > { formatSyncTime ( nodeStatus . SyncConnectedAt ) } </ p >
524+ </ div >
525+ ) }
526+ </ div >
527+ ) }
528+ </ MessageCardBase >
529+ ) ;
530+ } ;
531+
0 commit comments