better network monitoring

This commit is contained in:
Matt Hill
2021-07-01 16:18:49 -06:00
committed by Aiden McClelland
parent 80db9b71b9
commit d8ef531721
14 changed files with 295 additions and 244 deletions

View File

@@ -12,7 +12,7 @@ import { LoadingOptions } from '@ionic/core'
import { PatchDbModel } from './models/patch-db/patch-db-model' import { PatchDbModel } from './models/patch-db/patch-db-model'
import { HttpService } from './services/http.service' import { HttpService } from './services/http.service'
import { ServerStatus } from './models/patch-db/data-model' import { ServerStatus } from './models/patch-db/data-model'
import { ConnectionService } from './services/connection.service' import { ConnectionFailure, ConnectionService } from './services/connection.service'
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -83,6 +83,7 @@ export class AppComponent {
this.http.authReqEnabled = true this.http.authReqEnabled = true
this.showMenu = true this.showMenu = true
this.patch.start() this.patch.start()
this.connectionService.start()
// watch network // watch network
this.watchConnection(auth) this.watchConnection(auth)
// watch router to highlight selected menu item // watch router to highlight selected menu item
@@ -95,6 +96,7 @@ export class AppComponent {
} else if (auth === AuthState.UNVERIFIED) { } else if (auth === AuthState.UNVERIFIED) {
this.http.authReqEnabled = false this.http.authReqEnabled = false
this.showMenu = false this.showMenu = false
this.connectionService.stop()
this.patch.stop() this.patch.stop()
this.storage.clear() this.storage.clear()
this.router.navigate(['/login'], { replaceUrl: true }) this.router.navigate(['/login'], { replaceUrl: true })
@@ -107,13 +109,13 @@ export class AppComponent {
} }
private watchConnection (auth: AuthState): void { private watchConnection (auth: AuthState): void {
this.connectionService.monitor$() this.connectionService.watch$()
.pipe( .pipe(
distinctUntilChanged(), distinctUntilChanged(),
takeWhile(() => auth === AuthState.VERIFIED), takeWhile(() => auth === AuthState.VERIFIED),
) )
.subscribe(internet => { .subscribe(connectionFailure => {
if (!internet) { if (connectionFailure !== ConnectionFailure.None) {
this.presentToastOffline() this.presentToastOffline()
} else { } else {
if (this.offlineToast) { if (this.offlineToast) {
@@ -121,7 +123,6 @@ export class AppComponent {
this.offlineToast = undefined this.offlineToast = undefined
} }
} }
console.log('INTERNET CONNECTION', internet)
}) })
} }

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { PackageDataEntry } from 'src/app/models/patch-db/data-model' import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
import { ConnectionState } from 'src/app/services/connection.service'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { ConnectionStatus } from 'patch-db-client'
@Component({ @Component({
selector: 'status', selector: 'status',
@@ -10,7 +10,7 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
}) })
export class StatusComponent { export class StatusComponent {
@Input() pkg: PackageDataEntry @Input() pkg: PackageDataEntry
@Input() connection: ConnectionState @Input() connected: boolean
@Input() size?: 'small' | 'medium' | 'large' = 'large' @Input() size?: 'small' | 'medium' | 'large' = 'large'
@Input() style?: string = 'regular' @Input() style?: string = 'regular'
@Input() weight?: string = 'normal' @Input() weight?: string = 'normal'
@@ -19,7 +19,7 @@ export class StatusComponent {
showDots = false showDots = false
ngOnChanges () { ngOnChanges () {
const { display, color, showDots } = renderPkgStatus(this.pkg, this.connection) const { display, color, showDots } = renderPkgStatus(this.pkg)
this.display = display this.display = display
this.color = color this.color = color
this.showDots = showDots this.showDots = showDots

View File

@@ -16,7 +16,8 @@ export interface ServerInfo {
'lan-address': URL 'lan-address': URL
'tor-address': URL 'tor-address': URL
status: ServerStatus status: ServerStatus
registry: URL 'package-registry': URL
'system-registry': URL
wifi: WiFiInfo wifi: WiFiInfo
'unread-notification-count': number 'unread-notification-count': number
specs: { specs: {
@@ -24,6 +25,10 @@ export interface ServerInfo {
Disk: string Disk: string
Memory: string Memory: string
} }
'connection-addresses': {
tor: string[]
clearnet: string[]
}
} }
export enum ServerStatus { export enum ServerStatus {

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable, InjectionToken } from '@angular/core' import { Inject, Injectable, InjectionToken } from '@angular/core'
import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client' import { Bootstrapper, ConnectionStatus, PatchDB, Source, Store } from 'patch-db-client'
import { Observable, of, Subscription } from 'rxjs' import { Observable, of, Subscription } from 'rxjs'
import { catchError, debounceTime } from 'rxjs/operators' import { catchError, debounceTime, map } from 'rxjs/operators'
import { DataModel } from './data-model' import { DataModel } from './data-model'
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('app.config') export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('app.config')
@@ -37,6 +37,7 @@ export class PatchDbModel {
}, },
error: e => { error: e => {
console.error('patch-db-sync sub ERROR', e) console.error('patch-db-sync sub ERROR', e)
this.start()
}, },
complete: () => { complete: () => {
console.error('patch-db-sync sub COMPLETE') console.error('patch-db-sync sub COMPLETE')
@@ -54,7 +55,18 @@ export class PatchDbModel {
} }
} }
watch$: Store < DataModel > ['watch$'] = (...args: (string | number)[]): Observable<DataModel> => { connected$ (): Observable<boolean> {
return this.patchDb.connectionStatus$
.pipe(
map(status => status === ConnectionStatus.Connected),
)
}
connectionStatus$ (): Observable<ConnectionStatus> {
return this.patchDb.connectionStatus$.asObservable()
}
watch$: Store<DataModel> ['watch$'] = (...args: (string | number)[]): Observable<DataModel> => {
// console.log('WATCHING') // console.log('WATCHING')
return this.patchDb.store.watch$(...(args as [])).pipe( return this.patchDb.store.watch$(...(args as [])).pipe(
catchError(e => { catchError(e => {

View File

@@ -8,45 +8,43 @@
</ion-header> </ion-header>
<ion-content style="position: relative"> <ion-content style="position: relative">
<ng-container *ngIf="patch.watch$('package-data') | ngrxPush as pkgs">
<div *ngIf="pkgs | empty; else list" class="ion-text-center ion-padding"> <div *ngIf="pkgs | empty; else list" class="ion-text-center ion-padding">
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh"> <div style="display: flex; flex-direction: column; justify-content: center; height: 40vh">
<h2>Welcome to your <span style="font-style: italic; color: var(--ion-color-start9)">Embassy</span></h2> <h2>Welcome to your <span style="font-style: italic; color: var(--ion-color-start9)">Embassy</span></h2>
<p class="ion-text-wrap">Get started by installing your first service.</p> <p class="ion-text-wrap">Get started by installing your first service.</p>
</div>
<ion-button [routerLink]="['/marketplace']" style="width: 50%;" fill="outline">
<ion-icon slot="start" name="storefront-outline"></ion-icon>
Marketplace
</ion-button>
</div> </div>
<ion-button [routerLink]="['/marketplace']" style="width: 50%;" fill="outline">
<ion-icon slot="start" name="storefront-outline"></ion-icon>
Marketplace
</ion-button>
</div>
<ng-template #list> <ng-template #list>
<ion-grid> <ion-grid>
<ion-row *ngIf="connectionService.monitor$() | ngrxPush as connection"> <ion-row>
<ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeLg="3" sizeXl="2"> <ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeLg="3" sizeXl="2">
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', (pkg.value | manifest).id]"> <ion-card class="installed-card" style="position:relative" [routerLink]="['/services', (pkg.value | manifest).id]">
<div class="launch-container" *ngIf="pkg.value | hasUi"> <div class="launch-container" *ngIf="pkg.value | hasUi">
<div class="launch-button-triangle" (click)="launchUi(pkg.value, $event)" [class.launch-disabled]="!(pkg.value | isLaunchable)"> <div class="launch-button-triangle" (click)="launchUi(pkg.value, $event)" [class.launch-disabled]="!(pkg.value | isLaunchable)">
<ion-icon name="rocket-outline"></ion-icon> <ion-icon name="rocket-outline"></ion-icon>
</div>
</div> </div>
</div>
<img style="position: absolute" class="main-img" [src]="pkg.value['static-files'].icon" alt="icon" />
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="> <img style="position: absolute" class="main-img" [src]="pkg.value['static-files'].icon" alt="icon" />
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'green' : connection" src="assets/img/running-bulb.png"/> <img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'red' : connection" src="assets/img/issue-bulb.png"/> <img class="bulb-on" *ngIf="pkg.value | displayBulb: 'green' : connected" src="assets/img/running-bulb.png"/>
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'yellow' : connection" src="assets/img/warning-bulb.png"/> <img class="bulb-on" *ngIf="pkg.value | displayBulb: 'red' : connected" src="assets/img/issue-bulb.png"/>
<img class="bulb-off" *ngIf="pkg.value | displayBulb: 'off' : connection" src="assets/img/off-bulb.png"/> <img class="bulb-on" *ngIf="pkg.value | displayBulb: 'yellow' : connected" src="assets/img/warning-bulb.png"/>
<img class="bulb-off" *ngIf="pkg.value | displayBulb: 'off' : connected" src="assets/img/off-bulb.png"/>
<ion-card-header> <ion-card-header>
<status [pkg]="pkg.value" [connection]="connection" size="calc(4px + .7vw)" weight="bold"></status> <status *ngIf="connected" [pkg]="pkg.value" size="calc(4px + .7vw)" weight="bold"></status>
<ion-card-title>{{ (pkg.value | manifest).title }}</ion-card-title> <ion-card-title>{{ (pkg.value | manifest).title }}</ion-card-title>
</ion-card-header> </ion-card-header>
</ion-card> </ion-card>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>
</ng-template> </ng-template>
</ng-container>
</ion-content> </ion-content>

View File

@@ -3,6 +3,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model' import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { PackageDataEntry } from 'src/app/models/patch-db/data-model' import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
import { Subscription } from 'rxjs'
@Component({ @Component({
selector: 'app-list', selector: 'app-list',
@@ -10,7 +11,9 @@ import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
styleUrls: ['./app-list.page.scss'], styleUrls: ['./app-list.page.scss'],
}) })
export class AppListPage { export class AppListPage {
pkgs: PackageDataEntry[] = [] pkgs: { [id: string]: PackageDataEntry } = { }
connected: boolean
subs: Subscription[] = []
constructor ( constructor (
private readonly config: ConfigService, private readonly config: ConfigService,
@@ -18,6 +21,19 @@ export class AppListPage {
public readonly patch: PatchDbModel, public readonly patch: PatchDbModel,
) { } ) { }
ngOnInit () {
this.subs = [
this.patch.watch$('package-data').subscribe(pkgs => {
this.pkgs = pkgs
}),
this.patch.connected$().subscribe(c => this.connected = c),
]
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
launchUi (pkg: PackageDataEntry, event: Event): void { launchUi (pkg: PackageDataEntry, event: Event): void {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()

View File

@@ -16,122 +16,120 @@
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text> <ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item> </ion-item>
<ng-container *ngrxLet="connectionService.monitor$() as connection"> <ng-container *ngIf="pkg">
<ng-container *ngIf="pkg | status : connection as status"> <!-- top plate -->
<!-- top plate --> <div class="top-plate">
<div class="top-plate"> <ion-item class="no-cushion-item" lines="none">
<ion-item class="no-cushion-item" lines="none"> <ion-label class="ion-text-wrap" style="
<ion-label class="ion-text-wrap" style=" display: grid;
display: grid; grid-template-columns: 80px auto;
grid-template-columns: 80px auto; margin: 0px;
margin: 0px; margin-top: 15px;"
margin-top: 15px;" >
> <ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start">
<ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start"> <img [src]="pkg['static-files'].icon" />
<img [src]="pkg['static-files'].icon" /> </ion-avatar>
</ion-avatar> <div style="display: flex; flex-direction: column;">
<div style="display: flex; flex-direction: column;"> <ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="manifest.title.length > 20">
<ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="manifest.title.length > 20"> {{ manifest.title }}
{{ manifest.title }} </ion-text>
</ion-text> <ion-text style="margin-top: -5px; margin-left: 2px;">
<ion-text style="margin-top: -5px; margin-left: 2px;"> {{ manifest.version | displayEmver }}
{{ manifest.version | displayEmver }} </ion-text>
</ion-text> </div>
</div> </ion-label>
</ion-label> </ion-item>
</ion-item>
<div class="status-readout">
<div class="status-readout"> <status *ngIf="connected" size="large" weight="bold" [pkg]="pkg"></status>
<status size="large" weight="bold" [pkg]="pkg" [connection]="connection"></status> <ion-button *ngIf="pkg.status === FeStatus.NeedsConfig" expand="block" fill="outline" [routerLink]="['config']">
<ion-button *ngIf="status === FeStatus.NeedsConfig" expand="block" fill="outline" [routerLink]="['config']"> Configure
Configure </ion-button>
</ion-button> <ion-button *ngIf="[FeStatus.Running, FeStatus.StartingUp, FeStatus.NeedsAttention] | includes : pkg.status" expand="block" fill="outline" color="danger" (click)="stop()">
<ion-button *ngIf="[FeStatus.Running, FeStatus.StartingUp, FeStatus.NeedsAttention] | includes : status" expand="block" fill="outline" color="danger" (click)="stop()"> Stop
Stop </ion-button>
</ion-button> <ion-button *ngIf="pkg.status === FeStatus.DependencyIssue" expand="block" fill="outline" (click)="scrollToRequirements()">
<ion-button *ngIf="status === FeStatus.DependencyIssue" expand="block" fill="outline" (click)="scrollToRequirements()"> Fix
Fix </ion-button>
</ion-button> <ion-button *ngIf="pkg.status === FeStatus.Stopped" expand="block" fill="outline" color="success" (click)="tryStart()">
<ion-button *ngIf="status === FeStatus.Stopped" expand="block" fill="outline" color="success" (click)="tryStart()"> Start
Start
</ion-button>
</div>
<ion-button size="small" *ngIf="pkg | hasUi" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()">
Launch Web Interface
<ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button> </ion-button>
</div> </div>
<ion-button size="small" *ngIf="pkg | hasUi" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()">
Launch Web Interface
<ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button>
</div>
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : status)"> <ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : pkg.status)">
<ion-grid class="ion-text-center" style="margin: 0 6px;"> <ion-grid class="ion-text-center" style="margin: 0 6px;">
<ion-row> <ion-row>
<ion-col *ngFor="let button of buttons" sizeMd="4" sizeSm="6" sizeXs="6"> <ion-col *ngFor="let button of buttons" sizeMd="4" sizeSm="6" sizeXs="6">
<ion-button style="width: 100%; min-height: 120px;" color="light" [disabled]="button.disabled | includes : status" (click)="button.action()"> <ion-button style="width: 100%; min-height: 120px;" color="light" [disabled]="button.disabled | includes : pkg.status" (click)="button.action()">
<div> <div>
<ion-icon size="large" [name]="button.icon"></ion-icon> <ion-icon size="large" [name]="button.icon"></ion-icon>
<br/><br/> <br/><br/>
{{ button.title }} {{ button.title }}
</div> </div>
</ion-button> </ion-button>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>
<ion-item-group class="ion-padding-bottom"> <ion-item-group class="ion-padding-bottom">
<!-- dependencies --> <!-- dependencies -->
<ng-container *ngIf="!(pkg.installed['current-dependencies'] | empty)"> <ng-container *ngIf="!(pkg.installed['current-dependencies'] | empty)">
<ion-card id="dependencies" class="dep-card"> <ion-card id="dependencies" class="dep-card">
<ion-card-header> <ion-card-header>
<ion-card-title>Dependencies</ion-card-title> <ion-card-title>Dependencies</ion-card-title>
</ion-card-header> </ion-card-header>
<ion-card-content> <ion-card-content>
<!-- A current-dependency is a subset of the manifest.dependencies that is currently required as determined by the service config. --> <!-- A current-dependency is a subset of the manifest.dependencies that is currently required as determined by the service config. -->
<div *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue"> <div *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue">
<ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep" class="dependency-item"> <ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep" class="dependency-item">
<ion-avatar slot="start" style="position: relative; height: 5vh; width: 5vh; margin: 0px;"> <ion-avatar slot="start" style="position: relative; height: 5vh; width: 5vh; margin: 0px;">
<div class="dep-badge" [class]="pkg.installed.status['dependency-errors'][dep.key] ? 'dep-issue' : 'dep-sat'"></div> <div class="dep-badge" [class]="pkg.installed.status['dependency-errors'][dep.key] ? 'dep-issue' : 'dep-sat'"></div>
<img [src]="localDep ? localDep['static-files'].icon : pkg.installed.status['dependency-errors'][dep.key]?.icon" /> <img [src]="localDep ? localDep['static-files'].icon : pkg.installed.status['dependency-errors'][dep.key]?.icon" />
</ion-avatar> </ion-avatar>
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh"> <ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
<h4 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h4> <h4 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h4>
<p style="font-size: small">{{ manifest.dependencies[dep.key].version | displayEmver }}</p> <p style="font-size: small">{{ manifest.dependencies[dep.key].version | displayEmver }}</p>
<p style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p> <p style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
</ion-label> </ion-label>
<ion-button *ngIf="!pkg.installed.status['dependency-errors'][dep.key] || (pkg.installed.status['dependency-errors'][dep.key] && [DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed] | includes : pkg.installed.status['dependency-errors'][dep.key].type)" slot="end" size="small" [routerLink]="['/services', dep.key]" color="primary" fill="outline" style="font-size: x-small"> <ion-button *ngIf="!pkg.installed.status['dependency-errors'][dep.key] || (pkg.installed.status['dependency-errors'][dep.key] && [DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed] | includes : pkg.installed.status['dependency-errors'][dep.key].type)" slot="end" size="small" [routerLink]="['/services', dep.key]" color="primary" fill="outline" style="font-size: x-small">
View View
</ion-button>
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]">
<ion-button *ngIf="!localDep" slot="end" size="small" (click)="fixDep('install', dep.key)" color="primary" fill="outline" style="font-size: x-small">
Install
</ion-button> </ion-button>
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]"> <ng-container *ngIf="localDep && localDep.state === PackageState.Installed">
<ion-button *ngIf="!localDep" slot="end" size="small" (click)="fixDep('install', dep.key)" color="primary" fill="outline" style="font-size: x-small"> <ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', dep.key]" color="primary" fill="outline" style="font-size: x-small">
Install Start
</ion-button> </ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" (click)="fixDep('update', dep.key)" color="primary" fill="outline" style="font-size: x-small">
Update
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.ConfigUnsatisfied" slot="end" size="small" (click)="fixDep('configure', dep.key)" color="primary" fill="outline" style="font-size: x-small">
Configure
</ion-button>
</ng-container>
<ng-container *ngIf="localDep && localDep.state === PackageState.Installed"> <div *ngIf="localDep && localDep.state !== PackageState.Installed" slot="end" class="spinner">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', dep.key]" color="primary" fill="outline" style="font-size: x-small"> <ion-spinner [color]="localDep.state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
Start </div>
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" (click)="fixDep('update', dep.key)" color="primary" fill="outline" style="font-size: x-small">
Update
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.ConfigUnsatisfied" slot="end" size="small" (click)="fixDep('configure', dep.key)" color="primary" fill="outline" style="font-size: x-small">
Configure
</ion-button>
</ng-container>
<div *ngIf="localDep && localDep.state !== PackageState.Installed" slot="end" class="spinner"> </ng-container>
<ion-spinner [color]="localDep.state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner> </ion-item>
</div> </div>
</ion-card-content>
</ng-container> </ion-card>
</ion-item> </ng-container>
</div> </ion-item-group>
</ion-card-content>
</ion-card>
</ng-container>
</ion-item-group>
</ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ion-content> </ion-content>

View File

@@ -23,10 +23,12 @@ export class AppShowPage {
error: string error: string
pkgId: string pkgId: string
pkg: PackageDataEntry pkg: PackageDataEntry
pkgSub: Subscription
hideLAN: boolean hideLAN: boolean
buttons: Button[] = [] buttons: Button[] = []
manifest: Manifest = { } as Manifest manifest: Manifest = { } as Manifest
connected: boolean
subs: Subscription[] = []
FeStatus = FEStatus FeStatus = FEStatus
PackageState = PackageState PackageState = PackageState
@@ -49,10 +51,13 @@ export class AppShowPage {
async ngOnInit () { async ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId') this.pkgId = this.route.snapshot.paramMap.get('pkgId')
this.pkgSub = this.patch.watch$('package-data', this.pkgId).subscribe(pkg => { this.subs = [
this.pkg = pkg this.patch.watch$('package-data', this.pkgId).subscribe(pkg => {
this.manifest = getManifest(this.pkg) this.pkg = pkg
}) this.manifest = getManifest(this.pkg)
}),
this.patch.connected$().subscribe(c => this.connected = c),
]
this.setButtons() this.setButtons()
} }
@@ -61,7 +66,7 @@ export class AppShowPage {
} }
async ngOnDestroy () { async ngOnDestroy () {
this.pkgSub.unsubscribe() this.subs.forEach(sub => sub.unsubscribe())
} }
launchUiTab (): void { launchUiTab (): void {

View File

@@ -1,6 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { PackageDataEntry } from '../models/patch-db/data-model' import { PackageDataEntry } from '../models/patch-db/data-model'
import { ConnectionState } from '../services/connection.service'
import { renderPkgStatus } from '../services/pkg-status-rendering.service' import { renderPkgStatus } from '../services/pkg-status-rendering.service'
@Pipe({ @Pipe({
@@ -8,8 +7,9 @@ import { renderPkgStatus } from '../services/pkg-status-rendering.service'
}) })
export class DisplayBulbPipe implements PipeTransform { export class DisplayBulbPipe implements PipeTransform {
transform (pkg: PackageDataEntry, bulb: DisplayBulb, connection: ConnectionState): boolean { transform (pkg: PackageDataEntry, bulb: DisplayBulb, connected: boolean): boolean {
const { color } = renderPkgStatus(pkg, connection) if (!connected) return bulb === 'off'
const { color } = renderPkgStatus(pkg)
switch (color) { switch (color) {
case 'danger': return bulb === 'red' case 'danger': return bulb === 'red'
case 'success': return bulb === 'green' case 'success': return bulb === 'green'

View File

@@ -1,13 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { PackageDataEntry } from '../models/patch-db/data-model' import { PackageDataEntry } from '../models/patch-db/data-model'
import { ConnectionState } from '../services/connection.service'
import { FEStatus, renderPkgStatus } from '../services/pkg-status-rendering.service' import { FEStatus, renderPkgStatus } from '../services/pkg-status-rendering.service'
@Pipe({ @Pipe({
name: 'status', name: 'status',
}) })
export class StatusPipe implements PipeTransform { export class StatusPipe implements PipeTransform {
transform (pkg: PackageDataEntry, connection: ConnectionState): FEStatus { transform (pkg: PackageDataEntry): FEStatus {
return renderPkgStatus(pkg, connection).feStatus return renderPkgStatus(pkg).feStatus
} }
} }

View File

@@ -1,11 +1,10 @@
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { Http, Source, Update, Operation, Revision } from 'patch-db-client' import { Http, Update, Operation, Revision } from 'patch-db-client'
import { RR } from './api-types' import { RR } from './api-types'
import { DataModel } from 'src/app/models/patch-db/data-model' import { DataModel } from 'src/app/models/patch-db/data-model'
import { filter } from 'rxjs/operators' import { filter } from 'rxjs/operators'
import * as uuid from 'uuid'
export abstract class ApiService implements Source<DataModel>, Http<DataModel> { export abstract class ApiService implements Http<DataModel> {
protected readonly sync = new Subject<Update<DataModel>>() protected readonly sync = new Subject<Update<DataModel>>()
private syncing = true private syncing = true

View File

@@ -719,13 +719,18 @@ export module Mock {
selected: 'Goosers5G', selected: 'Goosers5G',
connected: 'Goosers5G', connected: 'Goosers5G',
}, },
registry: 'https://registry.start9.com', 'package-registry': 'https://registry.start9.com',
'system-registry': 'https://registry.start9.com',
'unread-notification-count': 4, 'unread-notification-count': 4,
specs: { specs: {
CPU: 'Cortex-A72: 4 Cores @1500MHz', CPU: 'Cortex-A72: 4 Cores @1500MHz',
Disk: '1TB SSD', Disk: '1TB SSD',
Memory: '8GB', Memory: '8GB',
}, },
'connection-addresses': {
tor: [],
clearnet: [],
},
}, },
'package-data': { 'package-data': {
'bitcoind': bitcoind, 'bitcoind': bitcoind,

View File

@@ -1,80 +1,98 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { BehaviorSubject, fromEvent, merge, Observable, Subscription, timer } from 'rxjs' import { BehaviorSubject, combineLatest, fromEvent, merge, Observable, Subscription } from 'rxjs'
import { delay, retryWhen, switchMap, tap } from 'rxjs/operators' import { ConnectionStatus } from '../../../../../patch-db/client/dist'
import { ApiService } from './api/api.service' import { DataModel } from '../models/patch-db/data-model'
import { PatchDbModel } from '../models/patch-db/patch-db-model'
import { HttpService, Method } from './http.service'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class ConnectionService { export class ConnectionService {
private httpSubscription$: Subscription private addrs: DataModel['server-info']['connection-addresses']
private readonly networkState$ = new BehaviorSubject<boolean>(navigator.onLine) private readonly networkState$ = new BehaviorSubject<boolean>(true)
private readonly internetState$ = new BehaviorSubject<boolean | null>(null) private readonly connectionFailure$ = new BehaviorSubject<ConnectionFailure>(ConnectionFailure.None)
private subs: Subscription[] = []
constructor ( constructor (
private readonly apiService: ApiService, private readonly httpService: HttpService,
) { private readonly patch: PatchDbModel,
merge(fromEvent(window, 'online'), fromEvent(window, 'offline')) ) { }
.subscribe(event => {
this.networkState$.next(event.type === 'online')
})
this.networkState$ watch$ () {
.subscribe(online => { return this.connectionFailure$.asObservable()
if (online) { }
this.testInternet()
} else { start () {
this.killHttp() this.subs = [
this.internetState$.next(false) this.patch.watch$('server-info')
.subscribe(data => {
if (!data) return
this.addrs = data['connection-addresses'] || {
tor: [],
clearnet: [],
}
}),
merge(fromEvent(window, 'online'), fromEvent(window, 'offline'))
.subscribe(event => {
this.networkState$.next(event.type === 'online')
}),
combineLatest([this.networkState$, this.patch.connectionStatus$()])
.subscribe(async ([network, connectionStatus]) => {
if (connectionStatus !== ConnectionStatus.Disconnected) {
this.connectionFailure$.next(ConnectionFailure.None)
} else if (!network) {
this.connectionFailure$.next(ConnectionFailure.Network)
} else {
this.connectionFailure$.next(ConnectionFailure.Diagnosing)
const torSuccess = await this.testAddrs(this.addrs.tor)
if (torSuccess) {
this.connectionFailure$.next(ConnectionFailure.Embassy)
} else {
const clearnetSuccess = await this.testAddrs(this.addrs.clearnet)
if (clearnetSuccess) {
this.connectionFailure$.next(ConnectionFailure.Tor)
} else {
this.connectionFailure$.next(ConnectionFailure.Internet)
}
}
}
}),
]
}
stop () {
this.subs.forEach(sub => {
sub.unsubscribe()
})
this.subs = []
}
private async testAddrs (addrs: string[]): Promise<boolean> {
if (!addrs.length) return true
const results = await Promise.all(addrs.map(async addr => {
try {
await this.httpService.httpRequest({
method: Method.GET,
url: addr,
})
return true
} catch (e) {
return false
} }
}) }))
} return results.includes(true)
monitor$ (): Observable<boolean> {
return this.internetState$.asObservable()
}
private testInternet (): void {
this.killHttp()
// ping server every 10 seconds
this.httpSubscription$ = timer(0, 10000)
.pipe(
switchMap(() => this.apiService.echo()),
retryWhen(errors =>
errors.pipe(
tap(val => {
console.error('Echo error: ', val)
this.internetState$.next(false)
}),
// restart after 2 seconds
delay(2000),
),
),
)
.subscribe(() => {
this.internetState$.next(true)
})
}
private killHttp () {
if (this.httpSubscription$) {
this.httpSubscription$.unsubscribe()
this.httpSubscription$ = undefined
}
} }
} }
/** export enum ConnectionFailure {
* Instance of this interface is used to report current connection status. None = 'none',
*/ Diagnosing = 'diagnosing',
export interface ConnectionState { Network = 'network',
/** Embassy = 'embassy',
* "True" if browser has network connection. Determined by Window objects "online" / "offline" events. Tor = 'tor',
*/ Internet = 'internet',
network: boolean }
/**
* "True" if browser has Internet access. Determined by heartbeat system which periodically makes request to heartbeat Url.
*/
internet: boolean | null
}

View File

@@ -1,11 +1,6 @@
import { HealthCheckResultLoading, MainStatusRunning, PackageDataEntry, PackageMainStatus, PackageState, Status } from '../models/patch-db/data-model' import { HealthCheckResultLoading, MainStatusRunning, PackageDataEntry, PackageMainStatus, PackageState, Status } from '../models/patch-db/data-model'
import { ConnectionState } from './connection.service'
export function renderPkgStatus (pkg: PackageDataEntry, connection: ConnectionState): PkgStatusRendering {
if (!connection.network || !connection.internet) {
return { display: 'Connecting', color: 'medium', showDots: true, feStatus: FEStatus.Connecting }
}
export function renderPkgStatus (pkg: PackageDataEntry): PkgStatusRendering {
switch (pkg.state) { switch (pkg.state) {
case PackageState.Installing: return { display: 'Installing', color: 'primary', showDots: true, feStatus: FEStatus.Installing } case PackageState.Installing: return { display: 'Installing', color: 'primary', showDots: true, feStatus: FEStatus.Installing }
case PackageState.Updating: return { display: 'Updating', color: 'primary', showDots: true, feStatus: FEStatus.Updating } case PackageState.Updating: return { display: 'Updating', color: 'primary', showDots: true, feStatus: FEStatus.Updating }