Electrs server status page
This commit is contained in:
		
							parent
							
								
									f63f1b1773
								
							
						
					
					
						commit
						5898143f66
					
				| @ -6,6 +6,7 @@ import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.com | ||||
| import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; | ||||
| import { ClockComponent } from './components/clock/clock.component'; | ||||
| import { StatusViewComponent } from './components/status-view/status-view.component'; | ||||
| import { ServerHealthComponent } from './components/server-health/server-health.component'; | ||||
| 
 | ||||
| const browserWindow = window || {}; | ||||
| // @ts-ignore
 | ||||
| @ -31,6 +32,11 @@ let routes: Routes = [ | ||||
|         data: { networks: ['bitcoin', 'liquid'] }, | ||||
|         component: StatusViewComponent | ||||
|       }, | ||||
|       { | ||||
|         path: 'nodes', | ||||
|         data: { networks: ['bitcoin', 'liquid'] }, | ||||
|         component: ServerHealthComponent | ||||
|       }, | ||||
|       { | ||||
|         path: '', | ||||
|         loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||
| @ -66,6 +72,11 @@ let routes: Routes = [ | ||||
|         data: { networks: ['bitcoin', 'liquid'] }, | ||||
|         component: StatusViewComponent | ||||
|       }, | ||||
|       { | ||||
|         path: 'nodes', | ||||
|         data: { networks: ['bitcoin', 'liquid'] }, | ||||
|         component: ServerHealthComponent | ||||
|       }, | ||||
|       { | ||||
|         path: '', | ||||
|         loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||
| @ -134,6 +145,11 @@ let routes: Routes = [ | ||||
|     data: { networks: ['bitcoin', 'liquid'] }, | ||||
|     component: StatusViewComponent | ||||
|   }, | ||||
|   { | ||||
|     path: 'nodes', | ||||
|     data: { networks: ['bitcoin', 'liquid'] }, | ||||
|     component: ServerHealthComponent | ||||
|   }, | ||||
|   { | ||||
|     path: '', | ||||
|     loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||
| @ -173,6 +189,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | ||||
|           data: { networks: ['bitcoin', 'liquid'] }, | ||||
|           component: StatusViewComponent | ||||
|         }, | ||||
|         { | ||||
|           path: 'nodes', | ||||
|           data: { networks: ['bitcoin', 'liquid'] }, | ||||
|           component: ServerHealthComponent | ||||
|         }, | ||||
|         { | ||||
|           path: '', | ||||
|           loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), | ||||
| @ -213,6 +234,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | ||||
|       data: { networks: ['bitcoin', 'liquid']}, | ||||
|       component: StatusViewComponent | ||||
|     }, | ||||
|     { | ||||
|       path: 'nodes', | ||||
|       data: { networks: ['bitcoin', 'liquid'] }, | ||||
|       component: ServerHealthComponent | ||||
|     }, | ||||
|     { | ||||
|       path: '', | ||||
|       loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), | ||||
|  | ||||
| @ -0,0 +1,29 @@ | ||||
| <div class="tomahawk"> | ||||
|   <ng-container *ngIf="(hosts$ | async) as hosts"> | ||||
|     <div class="status-panel"> | ||||
|       <table class="status-table table table-borderless table-striped" *ngIf="(tip$ | async) as tip"> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|             <th class="active"></th> | ||||
|             <th class="host">Host</th> | ||||
|             <th class="rtt">RTT</th> | ||||
|             <th class="height">Height</th> | ||||
|           </tr> | ||||
|           <tr *ngFor="let host of hosts;" (click)="scrollTo(host)"> | ||||
|             <td class="active"><span *ngIf="host.active">⭐️</span></td> | ||||
|             <td class="host">{{ host.host }}</td> | ||||
|             <td class="rtt">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td> | ||||
|             <td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '✅')) }}</td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </div> | ||||
|   </ng-container> | ||||
| 
 | ||||
|   <ng-container *ngFor="let host of hosts; trackBy: trackByFn"> | ||||
|     <h5 [id]="host.host" class="hostLink"> | ||||
|       <a [href]="host.link">{{ host.link }}</a> | ||||
|     </h5> | ||||
|     <iframe class="mempoolStatus" [src]="host.statusPage"></iframe> | ||||
|   </ng-container> | ||||
| </div> | ||||
| @ -0,0 +1,35 @@ | ||||
| .tomahawk { | ||||
|   .status-panel { | ||||
|     max-width: 720px; | ||||
|     margin: auto; | ||||
|     margin-top: 2em; | ||||
|     padding: 1em; | ||||
|     background: #24273e; | ||||
|   } | ||||
| 
 | ||||
|   .status-table { | ||||
|     width: 100%; | ||||
| 
 | ||||
|     td, th { | ||||
|       padding: 0.25em; | ||||
|       &.rtt, &.height { | ||||
|         text-align: right; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     td { | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .mempoolStatus { | ||||
|     width: 100%; | ||||
|     height: 270px; | ||||
|   } | ||||
| 
 | ||||
|   .hostLink { | ||||
|     text-align: center; | ||||
|     margin: auto; | ||||
|     margin-top: 1em; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,68 @@ | ||||
| import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, OnDestroy } from '@angular/core'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { Observable, Subject, map, tap } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { HealthCheckHost } from '../../interfaces/websocket.interface'; | ||||
| import { DomSanitizer } from '@angular/platform-browser'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-server-health', | ||||
|   templateUrl: './server-health.component.html', | ||||
|   styleUrls: ['./server-health.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class ServerHealthComponent implements OnInit, OnDestroy { | ||||
|   hosts$: Observable<HealthCheckHost[]>; | ||||
|   tip$: Subject<number>; | ||||
|   hosts: HealthCheckHost[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     private websocketService: WebsocketService, | ||||
|     private stateService: StateService, | ||||
|     public sanitizer: DomSanitizer, | ||||
|   ) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.hosts$ = this.stateService.serverHealth$.pipe( | ||||
|       map((hosts) => { | ||||
|         const subpath = window.location.pathname.slice(0, -6); | ||||
|         for (const host of hosts) { | ||||
|           let statusUrl = ''; | ||||
|           let linkHost = ''; | ||||
|           if (host.socket) { | ||||
|             statusUrl = window.location.host + subpath + '/status'; | ||||
|             linkHost = window.location.host + subpath; | ||||
|           } else { | ||||
|             statusUrl = host.host.slice(0, -4) + subpath + '/status'; | ||||
|             linkHost = host.host.slice(0, -4) + subpath; | ||||
|           } | ||||
|           host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl)); | ||||
|           host.link = linkHost; | ||||
|         } | ||||
|         return hosts; | ||||
|       }), | ||||
|       tap((hosts) => { | ||||
|         if (this.hosts.length !== hosts.length) { | ||||
|           this.hosts = hosts; | ||||
|         } | ||||
|       }) | ||||
|     ); | ||||
|     this.tip$ = this.stateService.chainTip$; | ||||
|     this.websocketService.want(['blocks', 'tomahawk']); | ||||
|   } | ||||
| 
 | ||||
|   scrollTo(host: HealthCheckHost): void { | ||||
|     const el = document.getElementById(host.host); | ||||
|     if (el) { | ||||
|       el.scrollIntoView(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   trackByFn(index: number, host: HealthCheckHost): string { | ||||
|     return host.host; | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.hosts = []; | ||||
|   } | ||||
| } | ||||
| @ -1,3 +1,4 @@ | ||||
| import { SafeResourceUrl } from '@angular/platform-browser'; | ||||
| import { ILoadingIndicators } from '../services/state.service'; | ||||
| import { Transaction } from './electrs.interface'; | ||||
| import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface'; | ||||
| @ -120,4 +121,17 @@ export interface Recommendedfees { | ||||
|   hourFee: number; | ||||
|   minimumFee: number; | ||||
|   economyFee: number; | ||||
| } | ||||
| 
 | ||||
| export interface HealthCheckHost { | ||||
|   host: string; | ||||
|   active: boolean; | ||||
|   rtt: number; | ||||
|   latestHeight: number; | ||||
|   socket: boolean; | ||||
|   outOfSync: boolean; | ||||
|   unreachable: boolean; | ||||
|   checked: boolean; | ||||
|   link?: string; | ||||
|   statusPage?: SafeResourceUrl; | ||||
| } | ||||
| @ -1,14 +1,13 @@ | ||||
| import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; | ||||
| import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionCompressed, TransactionStripped } from '../interfaces/websocket.interface'; | ||||
| import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface'; | ||||
| import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; | ||||
| import { Router, NavigationStart } from '@angular/router'; | ||||
| import { isPlatformBrowser } from '@angular/common'; | ||||
| import { filter, map, scan, shareReplay } from 'rxjs/operators'; | ||||
| import { StorageService } from './storage.service'; | ||||
| import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; | ||||
| import { ApiService } from './api.service'; | ||||
| import { ActiveFilter } from '../shared/filters.utils'; | ||||
| 
 | ||||
| export interface MarkBlockState { | ||||
| @ -129,6 +128,7 @@ export class StateService { | ||||
|   loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1); | ||||
|   recommendedFees$ = new ReplaySubject<Recommendedfees>(1); | ||||
|   chainTip$ = new ReplaySubject<number>(-1); | ||||
|   serverHealth$ = new Subject<HealthCheckHost[]>(); | ||||
| 
 | ||||
|   live2Chart$ = new Subject<OptimizedMempoolStats>(); | ||||
| 
 | ||||
|  | ||||
| @ -415,6 +415,10 @@ export class WebsocketService { | ||||
|       this.stateService.previousRetarget$.next(response.previousRetarget); | ||||
|     } | ||||
| 
 | ||||
|     if (response['tomahawk']) { | ||||
|       this.stateService.serverHealth$.next(response['tomahawk']); | ||||
|     } | ||||
| 
 | ||||
|     if (response['git-commit']) { | ||||
|       this.stateService.backendInfo$.next(response['git-commit']); | ||||
|     } | ||||
|  | ||||
| @ -53,6 +53,7 @@ import { AssetComponent } from '../components/asset/asset.component'; | ||||
| import { AssetsComponent } from '../components/assets/assets.component'; | ||||
| import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; | ||||
| import { StatusViewComponent } from '../components/status-view/status-view.component'; | ||||
| import { ServerHealthComponent } from '../components/server-health/server-health.component'; | ||||
| import { FeesBoxComponent } from '../components/fees-box/fees-box.component'; | ||||
| import { DifficultyComponent } from '../components/difficulty/difficulty.component'; | ||||
| import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component'; | ||||
| @ -151,6 +152,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AssetComponent, | ||||
|     AssetsComponent, | ||||
|     StatusViewComponent, | ||||
|     ServerHealthComponent, | ||||
|     FeesBoxComponent, | ||||
|     DifficultyComponent, | ||||
|     DifficultyMiningComponent, | ||||
| @ -277,6 +279,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AssetComponent, | ||||
|     AssetsComponent, | ||||
|     StatusViewComponent, | ||||
|     ServerHealthComponent, | ||||
|     FeesBoxComponent, | ||||
|     DifficultyComponent, | ||||
|     DifficultyMiningComponent, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user