statically type server metrics and use websocket (#2124)

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Aiden McClelland
2023-05-09 15:05:42 -06:00
committed by Aiden McClelland
parent 873f2b2814
commit 8313dfaeb9
14 changed files with 303 additions and 405 deletions

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppMetricsPage } from './app-metrics.page'
import { SharedPipesModule } from '@start9labs/shared'
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
const routes: Routes = [
{
path: '',
component: AppMetricsPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
SharedPipesModule,
SkeletonListComponentModule,
],
declarations: [AppMetricsPage],
})
export class AppMetricsPageModule {}

View File

@@ -1,25 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
</ion-buttons>
<ion-title>Monitor</ion-title>
<ion-title slot="end"
><ion-spinner name="dots" class="fader"></ion-spinner
></ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding with-widgets">
<skeleton-list *ngIf="loading"></skeleton-list>
<ion-item-group *ngIf="!loading">
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
<ion-label>{{ metric.key }}</ion-label>
<ion-note *ngIf="metric.value" slot="end" class="metric-note">
<ion-text style="color: white"
>{{ metric.value.value }} {{ metric.value.unit }}</ion-text
>
</ion-note>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -1,3 +0,0 @@
.metric-note {
font-size: 16px;
}

View File

@@ -1,59 +0,0 @@
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { Metric } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared'
@Component({
selector: 'app-metrics',
templateUrl: './app-metrics.page.html',
styleUrls: ['./app-metrics.page.scss'],
})
export class AppMetricsPage {
loading = true
readonly pkgId = getPkgId(this.route)
going = false
metrics?: Metric
constructor(
private readonly route: ActivatedRoute,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
) {}
ngOnInit() {
this.startDaemon()
}
ngOnDestroy() {
this.stopDaemon()
}
async startDaemon(): Promise<void> {
this.going = true
while (this.going) {
const startTime = Date.now()
await this.getMetrics()
await pauseFor(Math.max(4000 - (Date.now() - startTime), 0))
}
}
stopDaemon() {
this.going = false
}
async getMetrics(): Promise<void> {
try {
this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId })
} catch (e: any) {
this.errToast.present(e)
this.stopDaemon()
} finally {
this.loading = false
}
}
asIsOrder(a: any, b: any) {
return 0
}
}

View File

@@ -36,13 +36,6 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
}, },
{
path: ':pkgId/metrics',
loadChildren: () =>
import('./app-metrics/app-metrics.module').then(
m => m.AppMetricsPageModule,
),
},
{ {
path: ':pkgId/properties', path: ':pkgId/properties',
loadChildren: () => loadChildren: () =>

View File

@@ -4,58 +4,121 @@
<ion-back-button defaultHref="system"></ion-back-button> <ion-back-button defaultHref="system"></ion-back-button>
</ion-buttons> </ion-buttons>
<ion-title>Monitor</ion-title> <ion-title>Monitor</ion-title>
<ion-title slot="end"
><ion-spinner name="dots" class="fader"></ion-spinner
></ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding with-widgets"> <ion-content class="ion-padding with-widgets">
<skeleton-list *ngIf="loading" [groups]="2"></skeleton-list> <ion-item-group *ngIf="serverData$ | async as serverData; else loading">
<p *ngIf="websocketFail" class="loading-dots ion-text-center">
<ion-text color="warning">Websocket Failed. Reconnecting</ion-text>
</p>
<div id="metricSection"> <ng-container *ngIf="serverData[0] as timeInfo">
<ng-container *ngIf="!loading"> <ion-item-divider>System Time</ion-item-divider>
<ion-item-group> <ion-item>
<ion-item-divider>Time</ion-item-divider> <ion-label>Current Time (UTC)</ion-label>
<ion-item> <h6 slot="end">
<ion-label>System Time</ion-label> {{ timeInfo.systemCurrentTime | date : 'MMMM d, y, h:mm:ss a' : 'UTC'
<ion-note slot="end" class="metric-note"> }}
<ion-text style="color: white" </h6>
>{{ systemTime$ | async | date:'MMMM d, y, h:mm a z':'UTC' </ion-item>
}}</ion-text <ion-item>
> <ion-label>Start Time (UTC)</ion-label>
</ion-note> <h6 slot="end">
</ion-item> {{ timeInfo.systemStartTime | date : 'MMMM d, y, h:mm:ss a' : 'UTC' }}
<ion-item> </h6>
<ion-label>System Uptime</ion-label> </ion-item>
<ion-note <ion-item>
*ngIf="systemUptime$ | async as uptime" <ion-label>Uptime</ion-label>
slot="end" <h6 *ngIf="timeInfo.systemUptime as uptime" slot="end">
class="metric-note" <b>{{ uptime.days }}</b> Days, <b>{{ uptime.hours }}</b> Hours,
> <b>{{ uptime.minutes }}</b> Minutes,
<ion-text style="color: white"> <b>{{ uptime.seconds }}</b> Seconds
<b>{{ uptime.days }}</b> Days, <b>{{ uptime.hours }}</b> Hours, </h6>
<b>{{ uptime.minutes }}</b> Minutes </ion-item>
</ion-text>
</ion-note>
</ion-item>
</ion-item-group>
<ion-item-group
*ngFor="let metricGroup of metrics | keyvalue : asIsOrder"
>
<ion-item-divider>{{ metricGroup.key }}</ion-item-divider>
<ion-item
*ngFor="let metric of metricGroup.value | keyvalue : asIsOrder"
>
<ion-label>{{ metric.key }}</ion-label>
<ion-note *ngIf="metric.value" slot="end" class="metric-note">
<ion-text style="color: white"
>{{ metric.value.value }} {{ metric.value.unit }}</ion-text
>
</ion-note>
</ion-item>
</ion-item-group>
</ng-container> </ng-container>
</div>
<ng-container *ngIf="serverData[1] as metrics">
<!-- General -->
<ion-item-divider>General</ion-item-divider>
<ion-item>
<ion-label>Temperature</ion-label>
<h6 slot="end">{{ metrics.general.temperature }} °C</h6>
</ion-item>
<!-- Memory -->
<ion-item-divider>Memory</ion-item-divider>
<ion-item>
<ion-label>Percentage Used</ion-label>
<h6 slot="end">{{ metrics.memory['percentage-used'] }} %</h6>
</ion-item>
<ion-item>
<ion-label>Total</ion-label>
<h6 slot="end">{{ metrics.memory.total }} MiB</h6>
</ion-item>
<ion-item>
<ion-label>Available</ion-label>
<h6 slot="end">{{ metrics.memory.available }} MiB</h6>
</ion-item>
<ion-item>
<ion-label>Used</ion-label>
<h6 slot="end">{{ metrics.memory.used }} MiB</h6>
</ion-item>
<ion-item>
<ion-label>Swap Total</ion-label>
<h6 slot="end">{{ metrics.memory['swap-total'] }} MiB</h6>
</ion-item>
<ion-item>
<ion-label>Swap Free</ion-label>
<h6 slot="end">{{ metrics.memory['swap-free'] }} MiB</h6>
</ion-item>
<ion-item>
<ion-label>Swap Used</ion-label>
<h6 slot="end">{{ metrics.memory['swap-used'] }} MiB</h6>
</ion-item>
<!-- CPU -->
<ion-item-divider>CPU</ion-item-divider>
<ion-item>
<ion-label>User Space</ion-label>
<h6 slot="end">{{ metrics.cpu['user-space'] }} %</h6>
</ion-item>
<ion-item>
<ion-label>Kernel Space</ion-label>
<h6 slot="end">{{ metrics.cpu['kernel-space'] }} %</h6>
</ion-item>
<ion-item>
<ion-label>I/O Wait</ion-label>
<h6 slot="end">{{ metrics.cpu['io-wait'] }} %</h6>
</ion-item>
<ion-item>
<ion-label>Idle</ion-label>
<h6 slot="end">{{ metrics.cpu.idle }} %</h6>
</ion-item>
<ion-item>
<ion-label>Usage</ion-label>
<h6 slot="end">{{ metrics.cpu.usage }} %</h6>
</ion-item>
<!-- Disk -->
<ion-item-divider>Disk</ion-item-divider>
<ion-item>
<ion-label>Size</ion-label>
<h6 slot="end">{{ metrics.disk.size }} GB</h6>
</ion-item>
<ion-item>
<ion-label>Used</ion-label>
<h6 slot="end">{{ metrics.disk.used }} GB</h6>
</ion-item>
<ion-item>
<ion-label>Percentage Used</ion-label>
<h6 slot="end">{{ metrics.disk['percentage-used'] }} %</h6>
</ion-item>
<ion-item>
<ion-label>Available</ion-label>
<h6 slot="end">{{ metrics.disk.available }} GB</h6>
</ion-item>
</ng-container>
</ion-item-group>
<ng-template #loading>
<skeleton-list [groups]="3"></skeleton-list>
</ng-template>
</ion-content> </ion-content>

View File

@@ -1,3 +1,3 @@
.metric-note { ion-note {
font-size: 16px; font-size: medium;
} }

View File

@@ -1,8 +1,17 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { Metrics } from 'src/app/services/api/api.types' import { Metrics } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { TimeService } from 'src/app/services/time-service' import { TimeInfo, TimeService } from 'src/app/services/time-service'
import { pauseFor, ErrorToastService } from '@start9labs/shared' import {
catchError,
combineLatest,
filter,
from,
Observable,
startWith,
switchMap,
} from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
@Component({ @Component({
selector: 'server-metrics', selector: 'server-metrics',
@@ -10,63 +19,43 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
styleUrls: ['./server-metrics.page.scss'], styleUrls: ['./server-metrics.page.scss'],
}) })
export class ServerMetricsPage { export class ServerMetricsPage {
loading = true websocketFail = false
going = false
metrics: Metrics = {}
readonly systemTime$ = this.timeService.systemTime$ readonly serverData$ = this.getServerData$()
readonly systemUptime$ = this.timeService.systemUptime$
constructor( constructor(
private readonly errToast: ErrorToastService, private readonly api: ApiService,
private readonly embassyApi: ApiService, readonly timeService: TimeService,
private readonly timeService: TimeService, private readonly connectionService: ConnectionService,
) {} ) {}
async ngOnInit() { private getServerData$(): Observable<[TimeInfo, Metrics]> {
await this.getMetrics() return combineLatest([
let headersCount = 0 this.timeService.getTimeInfo$(),
let rowsCount = 0 this.getMetrics$(),
Object.values(this.metrics).forEach(groupVal => { ]).pipe(
headersCount++ catchError(() => {
Object.keys(groupVal).forEach(_ => { this.websocketFail = true
rowsCount++ return this.connectionService.connected$.pipe(
}) filter(Boolean),
}) switchMap(() => this.getServerData$()),
const height = headersCount * 54 + rowsCount * 50 + 24 // extra 24 for room at the bottom )
const elem = document.getElementById('metricSection') }),
if (elem) elem.style.height = `${height}px` )
this.startDaemon()
this.loading = false
} }
ngOnDestroy() { private getMetrics$(): Observable<Metrics> {
this.stopDaemon() return from(this.api.getServerMetrics({})).pipe(
} switchMap(({ metrics, guid }) =>
this.api
private async startDaemon(): Promise<void> { .openMetricsWebsocket$({
this.going = true url: `/rpc/${guid}`,
while (this.going) { openObserver: {
const startTime = Date.now() next: () => (this.websocketFail = false),
await this.getMetrics() },
await pauseFor(4000 - Math.max(Date.now() - startTime, 0)) })
} .pipe(startWith(metrics)),
} ),
)
private stopDaemon() {
this.going = false
}
private async getMetrics(): Promise<void> {
try {
this.metrics = await this.embassyApi.getServerMetrics({})
} catch (e: any) {
this.errToast.present(e)
this.stopDaemon()
}
}
asIsOrder(a: any, b: any) {
return 0
} }
} }

View File

@@ -5,7 +5,13 @@ import {
PackageState, PackageState,
ServerStatusInfo, ServerStatusInfo,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types' import {
RR,
NotificationLevel,
ServerNotifications,
Metrics,
} from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
import { import {
DependencyMetadata, DependencyMetadata,
@@ -365,112 +371,36 @@ export module Mock {
}, },
] ]
export function getServerMetrics() { export function getMetrics(): Metrics {
return { return {
Group1: { general: {
Metric1: { temperature: (Math.random() * 100).toFixed(1),
value: Math.random(),
unit: 'mi/b',
},
Metric2: {
value: Math.random(),
unit: '%',
},
Metric3: {
value: 10.1,
unit: '%',
},
}, },
Group2: { memory: {
Hmmmm1: { 'percentage-used': '20',
value: 22.2, total: (Math.random() * 100).toFixed(2),
unit: 'mi/b', available: '18000',
}, used: '4000',
Hmmmm2: { 'swap-total': '1000',
value: 50, 'swap-free': Math.random().toFixed(2),
unit: '%', 'swap-used': '0',
},
Hmmmm3: {
value: 10.1,
unit: '%',
},
}, },
Group3: { cpu: {
Hmmmm1: { 'user-space': '100',
value: Math.random(), 'kernel-space': '50',
unit: 'mi/b', 'io-wait': String(Math.random() * 50),
}, idle: '80',
Hmmmm2: { usage: '30',
value: 50,
unit: '%',
},
Hmmmm3: {
value: 10.1,
unit: '%',
},
}, },
Group4: { disk: {
Hmmmm1: { size: '1000',
value: Math.random(), used: '900',
unit: 'mi/b', available: '100',
}, 'percentage-used': '90',
Hmmmm2: {
value: 50,
unit: '%',
},
Hmmmm3: {
value: 10.1,
unit: '%',
},
},
Group5: {
Hmmmm1: {
value: Math.random(),
unit: 'mi/b',
},
Hmmmm2: {
value: 50,
unit: '%',
},
Hmmmm3: {
value: 10.1,
unit: '%',
},
Hmmmm4: {
value: Math.random(),
unit: 'mi/b',
},
Hmmmm5: {
value: 50,
unit: '%',
},
Hmmmm6: {
value: 10.1,
unit: '%',
},
}, },
} }
} }
export function getAppMetrics() {
const metr: Metric = {
Metric1: {
value: Math.random(),
unit: 'mi/b',
},
Metric2: {
value: Math.random(),
unit: '%',
},
Metric3: {
value: 10.1,
unit: '%',
},
}
return metr
}
export const ServerLogs: Log[] = [ export const ServerLogs: Log[] = [
{ {
timestamp: '2022-07-28T03:52:54.808769Z', timestamp: '2022-07-28T03:52:54.808769Z',

View File

@@ -47,7 +47,10 @@ export module RR {
} }
export type GetServerMetricsReq = {} // server.metrics export type GetServerMetricsReq = {} // server.metrics
export type GetServerMetricsRes = Metrics export type GetServerMetricsRes = {
guid: string
metrics: Metrics
}
export type UpdateServerReq = { 'marketplace-url': string } // server.update export type UpdateServerReq = { 'marketplace-url': string } // server.update
export type UpdateServerRes = 'updating' | 'no-updates' export type UpdateServerRes = 'updating' | 'no-updates'
@@ -233,9 +236,6 @@ export module RR {
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
export type FollowPackageLogsRes = FollowServerLogsRes export type FollowPackageLogsRes = FollowServerLogsRes
export type GetPackageMetricsReq = { id: string } // package.metrics
export type GetPackageMetricsRes = Metric
export type InstallPackageReq = { export type InstallPackageReq = {
id: string id: string
'version-spec'?: string 'version-spec'?: string
@@ -350,18 +350,30 @@ export interface ActionResponse {
} }
export interface Metrics { export interface Metrics {
[key: string]: { general: {
[key: string]: { temperature: string
value: string | number | null
unit?: string
}
} }
} memory: {
'percentage-used': string
export interface Metric { total: string
[key: string]: { available: string
value: string | number | null used: string
unit?: string 'swap-total': string
'swap-free': string
'swap-used': string
}
cpu: {
'user-space': string
'kernel-space': string
'io-wait': string
idle: string
usage: string
}
disk: {
size: string
used: string
available: string
'percentage-used': string
} }
} }

View File

@@ -1,6 +1,6 @@
import { BehaviorSubject, Observable } from 'rxjs' import { BehaviorSubject, Observable } from 'rxjs'
import { Update } from 'patch-db-client' import { Update } from 'patch-db-client'
import { RR, Encrypted, BackupTargetType } from './api.types' import { RR, Encrypted, BackupTargetType, Metrics } from './api.types'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { Log } from '@start9labs/shared' import { Log } from '@start9labs/shared'
import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { WebSocketSubjectConfig } from 'rxjs/webSocket'
@@ -63,6 +63,10 @@ export abstract class ApiService {
config: WebSocketSubjectConfig<Log>, config: WebSocketSubjectConfig<Log>,
): Observable<Log> ): Observable<Log>
abstract openMetricsWebsocket$(
config: WebSocketSubjectConfig<Metrics>,
): Observable<Metrics>
abstract getSystemTime( abstract getSystemTime(
params: RR.GetSystemTimeReq, params: RR.GetSystemTimeReq,
): Promise<RR.GetSystemTimeRes> ): Promise<RR.GetSystemTimeRes>
@@ -93,10 +97,6 @@ export abstract class ApiService {
params: RR.GetServerMetricsReq, params: RR.GetServerMetricsReq,
): Promise<RR.GetServerMetricsRes> ): Promise<RR.GetServerMetricsRes>
abstract getPkgMetrics(
params: RR.GetPackageMetricsReq,
): Promise<RR.GetPackageMetricsRes>
abstract updateServer(url?: string): Promise<RR.UpdateServerRes> abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
abstract restartServer( abstract restartServer(

View File

@@ -10,7 +10,7 @@ import {
RPCOptions, RPCOptions,
} from '@start9labs/shared' } from '@start9labs/shared'
import { ApiService } from './embassy-api.service' import { ApiService } from './embassy-api.service'
import { BackupTargetType, RR } from './api.types' import { BackupTargetType, Metrics, RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { ConfigService } from '../config.service' import { ConfigService } from '../config.service'
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
@@ -126,6 +126,12 @@ export class LiveApiService extends ApiService {
return this.openWebsocket(config) return this.openWebsocket(config)
} }
openMetricsWebsocket$(
config: WebSocketSubjectConfig<Metrics>,
): Observable<Metrics> {
return this.openWebsocket(config)
}
async getSystemTime( async getSystemTime(
params: RR.GetSystemTimeReq, params: RR.GetSystemTimeReq,
): Promise<RR.GetSystemTimeRes> { ): Promise<RR.GetSystemTimeRes> {
@@ -400,12 +406,6 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.logs.follow', params }) return this.rpcRequest({ method: 'package.logs.follow', params })
} }
async getPkgMetrics(
params: RR.GetPackageMetricsReq,
): Promise<RR.GetPackageMetricsRes> {
return this.rpcRequest({ method: 'package.metrics', params })
}
async installPackage( async installPackage(
params: RR.InstallPackageReq, params: RR.InstallPackageReq,
): Promise<RR.InstallPackageRes> { ): Promise<RR.InstallPackageRes> {

View File

@@ -16,7 +16,7 @@ import {
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { BackupTargetType, CifsBackupTarget, RR } from './api.types' import { BackupTargetType, Metrics, RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { Mock } from './api.fixures' import { Mock } from './api.fixures'
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
@@ -181,6 +181,19 @@ export class MockApiService extends ApiService {
) )
} }
openMetricsWebsocket$(
config: WebSocketSubjectConfig<Metrics>,
): Observable<Metrics> {
return interval(2000).pipe(
map((_, index) => {
// mock fire open observer
if (index === 0) config.openObserver?.next(new Event(''))
if (index === 4) throw new Error('HAHAHA')
return Mock.getMetrics()
}),
)
}
async getSystemTime( async getSystemTime(
params: RR.GetSystemTimeReq, params: RR.GetSystemTimeReq,
): Promise<RR.GetSystemTimeRes> { ): Promise<RR.GetSystemTimeRes> {
@@ -268,14 +281,10 @@ export class MockApiService extends ApiService {
params: RR.GetServerMetricsReq, params: RR.GetServerMetricsReq,
): Promise<RR.GetServerMetricsRes> { ): Promise<RR.GetServerMetricsRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.getServerMetrics() return {
} guid: 'iqudh37um-i38u3-34-a51b-jkhd783ein',
metrics: Mock.getMetrics(),
async getPkgMetrics( }
params: RR.GetServerMetricsReq,
): Promise<RR.GetPackageMetricsRes> {
await pauseFor(2000)
return Mock.getAppMetrics()
} }
async updateServer(url?: string): Promise<RR.UpdateServerRes> { async updateServer(url?: string): Promise<RR.UpdateServerRes> {

View File

@@ -1,63 +1,78 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { import { map, startWith, switchMap } from 'rxjs/operators'
map,
shareReplay,
startWith,
switchMap,
take,
tap,
} from 'rxjs/operators'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { DataModel } from './patch-db/data-model' import { DataModel } from './patch-db/data-model'
import { ApiService } from './api/embassy-api.service' import { ApiService } from './api/embassy-api.service'
import { combineLatest, from, timer } from 'rxjs' import { combineLatest, from, Observable, timer } from 'rxjs'
export interface TimeInfo {
systemStartTime: number
systemCurrentTime: number
systemUptime: {
days: number
hours: number
minutes: number
seconds: number
}
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class TimeService { export class TimeService {
private readonly startTimeMs$ = this.patch private readonly systemStartTime$ = this.patch
.watch$('server-info', 'system-start-time') .watch$('server-info', 'system-start-time')
.pipe( .pipe(map(startTime => new Date(startTime).valueOf()))
take(1),
map(startTime => new Date(startTime).valueOf()),
shareReplay(),
)
readonly systemTime$ = from(this.apiService.getSystemTime({})).pipe(
switchMap(utcStr => {
const dateObj = new Date(utcStr)
const msRemaining = (60 - dateObj.getSeconds()) * 1000
dateObj.setSeconds(0)
const current = dateObj.valueOf()
return timer(msRemaining, 60000).pipe(
map(index => {
const incremented = index + 1
const msToAdd = 60000 * incremented
return current + msToAdd
}),
startWith(current),
)
}),
)
readonly systemUptime$ = combineLatest([
this.startTimeMs$,
this.systemTime$,
]).pipe(
map(([startTime, currentTime]) => {
const ms = currentTime - startTime
const days = Math.floor(ms / (24 * 60 * 60 * 1000))
const daysms = ms % (24 * 60 * 60 * 1000)
const hours = Math.floor(daysms / (60 * 60 * 1000))
const hoursms = ms % (60 * 60 * 1000)
const minutes = Math.floor(hoursms / (60 * 1000))
return { days, hours, minutes }
}),
)
constructor( constructor(
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
private readonly apiService: ApiService, private readonly apiService: ApiService,
) {} ) {}
getTimeInfo$(): Observable<TimeInfo> {
return combineLatest([
this.systemStartTime$.pipe(),
this.getSystemCurrentTime$(),
]).pipe(
map(([systemStartTime, systemCurrentTime]) => ({
systemStartTime,
systemCurrentTime,
systemUptime: this.getSystemUptime(systemStartTime, systemCurrentTime),
})),
)
}
private getSystemCurrentTime$() {
return from(this.apiService.getSystemTime({})).pipe(
switchMap(utcStr => {
const dateObj = new Date(utcStr)
const current = dateObj.valueOf()
return timer(0, 1000).pipe(
map(index => {
const incremented = index + 1
const msToAdd = 1000 * incremented
return current + msToAdd
}),
startWith(current),
)
}),
)
}
private getSystemUptime(systemStartTime: number, systemCurrentTime: number) {
const ms = systemCurrentTime - systemStartTime
const days = Math.floor(ms / (24 * 60 * 60 * 1000))
const daysms = ms % (24 * 60 * 60 * 1000)
const hours = Math.floor(daysms / (60 * 60 * 1000))
const hoursms = ms % (60 * 60 * 1000)
const minutes = Math.floor(hoursms / (60 * 1000))
const minutesms = ms % (60 * 1000)
const seconds = Math.floor(minutesms / 1000)
return { days, hours, minutes, seconds }
}
} }