Merge pull request #4744 from mempool/mononaut/send-nodes

More status page polish
This commit is contained in:
wiz 2024-03-07 18:06:05 +09:00 committed by GitHub
commit 1d877a746f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 65 additions and 33 deletions

View File

@ -49,4 +49,5 @@ export interface HealthCheckHost {
outOfSync: boolean; outOfSync: boolean;
unreachable: boolean; unreachable: boolean;
checked: boolean; checked: boolean;
lastChecked: number;
} }

View File

@ -18,6 +18,7 @@ interface FailoverHost {
unreachable?: boolean, unreachable?: boolean,
preferred?: boolean, preferred?: boolean,
checked: boolean, checked: boolean,
lastChecked?: number,
} }
class FailoverRouter { class FailoverRouter {
@ -122,7 +123,7 @@ class FailoverRouter {
} }
} }
host.checked = true; host.checked = true;
host.lastChecked = Date.now();
// switch if the current host is out of sync or significantly slower than the next best alternative // switch if the current host is out of sync or significantly slower than the next best alternative
const rankOrder = this.sortHosts(); const rankOrder = this.sortHosts();
@ -361,6 +362,7 @@ class ElectrsApi implements AbstractBitcoinApi {
outOfSync: !!host.outOfSync, outOfSync: !!host.outOfSync,
unreachable: !!host.unreachable, unreachable: !!host.unreachable,
checked: !!host.checked, checked: !!host.checked,
lastChecked: host.lastChecked || 0,
})); }));
} else { } else {
return []; return [];

View File

@ -1,4 +1,4 @@
<footer class="footer"> <footer class="footer" [class.inline-footer]="inline">
<div class="container-xl"> <div class="container-xl">
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData"> <div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
<div class="col d-none d-sm-block"> <div class="col d-none d-sm-block">

View File

@ -6,6 +6,12 @@
background-color: #1d1f31; background-color: #1d1f31;
box-shadow: 15px 15px 15px 15px #000; box-shadow: 15px 15px 15px 15px #000;
z-index: 10; z-index: 10;
&.inline-footer {
position: relative;
bottom: unset;
top: -44px;
}
} }
.sub-text { .sub-text {

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Observable, combineLatest } from 'rxjs'; import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -23,6 +23,8 @@ interface MempoolInfoData {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FooterComponent implements OnInit { export class FooterComponent implements OnInit {
@Input() inline = false;
mempoolBlocksData$: Observable<MempoolBlocksData>; mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>; mempoolInfoData$: Observable<MempoolInfoData>;
vBytesPerSecondLimit = 1667; vBytesPerSecondLimit = 1667;

View File

@ -1,9 +1,11 @@
<div class="tomahawk container-xl dashboard-container"> <div class="tomahawk">
<div class="links"> <div class="links">
<span>Status</span> <span>Monitoring</span>
<a [routerLink]='"/network"'>Live</a> <a [routerLink]='"/nodes"'>Nodes</a>
</div> </div>
<h1 class="dashboard-title">Node Status</h1>
<app-start [showLoadingIndicator]="true"></app-start>
<app-footer [inline]="true"></app-footer>
<ng-container *ngIf="(hosts$ | async) as hosts"> <ng-container *ngIf="(hosts$ | async) as hosts">
<div class="status-panel"> <div class="status-panel">
@ -13,6 +15,7 @@
<th class="rank"></th> <th class="rank"></th>
<th class="flag"></th> <th class="flag"></th>
<th class="host">Host</th> <th class="host">Host</th>
<th class="updated">Last checked</th>
<th class="rtt only-small">RTT</th> <th class="rtt only-small">RTT</th>
<th class="rtt only-large">RTT</th> <th class="rtt only-large">RTT</th>
<th class="height">Height</th> <th class="height">Height</th>
@ -21,6 +24,7 @@
<td class="rank">{{ i + 1 }}</td> <td class="rank">{{ i + 1 }}</td>
<td class="flag">{{ host.active ? '⭐️' : host.flag }}</td> <td class="flag">{{ host.active ? '⭐️' : host.flag }}</td>
<td class="host">{{ host.link }}</td> <td class="host">{{ host.link }}</td>
<td class="updated">{{ getLastUpdateSeconds(host) }}</td>
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td> <td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td> <td class="rtt only-large">{{ 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> <td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '')) }}</td>

View File

@ -1,22 +1,16 @@
.tomahawk { .tomahawk {
.links { .links {
float: right;
text-align: right; text-align: right;
margin-top: 1em; margin-inline-end: 1em;
a, span { a, span {
margin-left: 1em; margin-left: 1em;
} }
} }
.dashboard-title {
text-align: left;
}
.status-panel { .status-panel {
max-width: 720px; max-width: 720px;
margin: auto; margin: auto;
margin-top: 2em;
padding: 1em; padding: 1em;
background: #24273e; background: #24273e;
} }
@ -31,6 +25,12 @@
width: 28px; width: 28px;
text-align: right; text-align: right;
} }
&.updated {
display: none;
width: 130px;
text-align: right;
white-space: pre-wrap;
}
&.rtt, &.height { &.rtt, &.height {
width: 92px; width: 92px;
text-align: right; text-align: right;
@ -57,6 +57,9 @@
&.rank, &.flag { &.rank, &.flag {
width: 32px; width: 32px;
} }
&.updated {
display: table-cell;
}
&.rtt, &.height { &.rtt, &.height {
width: 96px; width: 96px;
} }

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { Observable, Subject, map } from 'rxjs'; import { Observable, Subject, map } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@ -14,17 +14,20 @@ import { DomSanitizer } from '@angular/platform-browser';
export class ServerHealthComponent implements OnInit { export class ServerHealthComponent implements OnInit {
hosts$: Observable<HealthCheckHost[]>; hosts$: Observable<HealthCheckHost[]>;
tip$: Subject<number>; tip$: Subject<number>;
interval: number;
now: number = Date.now();
constructor( constructor(
private websocketService: WebsocketService, private websocketService: WebsocketService,
private stateService: StateService, private stateService: StateService,
private cd: ChangeDetectorRef,
public sanitizer: DomSanitizer, public sanitizer: DomSanitizer,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.hosts$ = this.stateService.serverHealth$.pipe( this.hosts$ = this.stateService.serverHealth$.pipe(
map((hosts) => { map((hosts) => {
const subpath = window.location.pathname.slice(0, -6); const subpath = window.location.pathname.slice(0, -11);
for (const host of hosts) { for (const host of hosts) {
let statusUrl = ''; let statusUrl = '';
let linkHost = ''; let linkHost = '';
@ -44,13 +47,27 @@ export class ServerHealthComponent implements OnInit {
}) })
); );
this.tip$ = this.stateService.chainTip$; this.tip$ = this.stateService.chainTip$;
this.websocketService.want(['blocks', 'tomahawk']); this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
this.interval = window.setInterval(() => {
this.now = Date.now();
this.cd.markForCheck();
}, 1000);
} }
trackByFn(index: number, host: HealthCheckHost): string { trackByFn(index: number, host: HealthCheckHost): string {
return host.host; return host.host;
} }
getLastUpdateSeconds(host: HealthCheckHost): string {
if (host.lastChecked) {
const seconds = Math.ceil((this.now - host.lastChecked) / 1000);
return `${seconds} second${seconds > 1 ? 's' : ' '} ago`;
} else {
return '~';
}
}
private parseFlag(host: string): string { private parseFlag(host: string): string {
if (host.includes('.fra.')) { if (host.includes('.fra.')) {
return '🇩🇪'; return '🇩🇪';

View File

@ -1,12 +1,12 @@
<div class="tomahawk"> <div class="tomahawk">
<div class="container-xl dashboard-container"> <div class="links">
<div class="links"> <a [routerLink]='"/monitoring"'>Monitoring</a>
<a [routerLink]='"/nodes"'>Status</a> <span>Nodes</span>
<span>Live</span>
</div>
<h1 class="dashboard-title">Live Network</h1>
</div> </div>
<app-start [showLoadingIndicator]="true"></app-start>
<app-footer [inline]="true"></app-footer>
<ng-container *ngFor="let host of hosts; trackBy: trackByFn"> <ng-container *ngFor="let host of hosts; trackBy: trackByFn">
<h5 [id]="host.host" class="hostLink"> <h5 [id]="host.host" class="hostLink">
<a [href]="'https://' + host.link">{{ host.link }}</a> <a [href]="'https://' + host.link">{{ host.link }}</a>

View File

@ -1,21 +1,17 @@
.tomahawk { .tomahawk {
.links { .links {
float: right;
text-align: right; text-align: right;
margin-top: 1em; margin-inline-end: 1em;
a, span { a, span {
margin-left: 1em; margin-left: 1em;
} }
} }
.dashboard-title {
text-align: left;
}
.mempoolStatus { .mempoolStatus {
width: 100%; width: 100%;
height: 270px; height: 270px;
border: none;
} }
.hostLink { .hostLink {

View File

@ -26,7 +26,7 @@ export class ServerStatusComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.hostSubscription = this.stateService.serverHealth$.pipe( this.hostSubscription = this.stateService.serverHealth$.pipe(
map((hosts) => { map((hosts) => {
const subpath = window.location.pathname.slice(0, -8); const subpath = window.location.pathname.slice(0, -6);
for (const host of hosts) { for (const host of hosts) {
let statusUrl = ''; let statusUrl = '';
let linkHost = ''; let linkHost = '';
@ -66,7 +66,7 @@ export class ServerStatusComponent implements OnInit, OnDestroy {
}) })
).subscribe(); ).subscribe();
this.tip$ = this.stateService.chainTip$; this.tip$ = this.stateService.chainTip$;
this.websocketService.want(['blocks', 'tomahawk']); this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
} }
trackByFn(index: number, host: HealthCheckHost): string { trackByFn(index: number, host: HealthCheckHost): string {

View File

@ -132,6 +132,7 @@ export interface HealthCheckHost {
outOfSync: boolean; outOfSync: boolean;
unreachable: boolean; unreachable: boolean;
checked: boolean; checked: boolean;
lastChecked: number;
link?: string; link?: string;
statusPage?: SafeResourceUrl; statusPage?: SafeResourceUrl;
flag?: string; flag?: string;

View File

@ -101,12 +101,12 @@ const routes: Routes = [
if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) { if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
routes[0].children.push({ routes[0].children.push({
path: 'nodes', path: 'monitoring',
data: { networks: ['bitcoin', 'liquid'] }, data: { networks: ['bitcoin', 'liquid'] },
component: ServerHealthComponent component: ServerHealthComponent
}); });
routes[0].children.push({ routes[0].children.push({
path: 'network', path: 'nodes',
data: { networks: ['bitcoin', 'liquid'] }, data: { networks: ['bitcoin', 'liquid'] },
component: ServerStatusComponent component: ServerStatusComponent
}); });