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 @@
+
+
+
+
+
+
+ |
+ Host |
+ RTT |
+ Height |
+
+
+ ⭐️ |
+ {{ 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,