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';
 | 
			
		||||
@ -121,3 +122,16 @@ export interface Recommendedfees {
 | 
			
		||||
  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