diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index e123a1525..18949876e 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -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), diff --git a/frontend/src/app/components/server-health/server-health.component.html b/frontend/src/app/components/server-health/server-health.component.html new file mode 100644 index 000000000..dd8d61444 --- /dev/null +++ b/frontend/src/app/components/server-health/server-health.component.html @@ -0,0 +1,29 @@ +
+ +
+ + + + + + + + + + + + + + + +
HostRTTHeight
⭐️{{ host.host }}{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '✅')) }}
+
+
+ + + + + +
diff --git a/frontend/src/app/components/server-health/server-health.component.scss b/frontend/src/app/components/server-health/server-health.component.scss new file mode 100644 index 000000000..e403e5824 --- /dev/null +++ b/frontend/src/app/components/server-health/server-health.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/server-health/server-health.component.ts b/frontend/src/app/components/server-health/server-health.component.ts new file mode 100644 index 000000000..bd7cc57e8 --- /dev/null +++ b/frontend/src/app/components/server-health/server-health.component.ts @@ -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; + tip$: Subject; + 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 = []; + } +} diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index ff5977332..72fd8c419 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -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; } \ No newline at end of file diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index dc1365baa..83cd449c8 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -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(1); recommendedFees$ = new ReplaySubject(1); chainTip$ = new ReplaySubject(-1); + serverHealth$ = new Subject(); live2Chart$ = new Subject(); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 11e24ef71..f4dcc4037 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -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']); } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 36e7e79b8..6c43a019f 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -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,