mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
0.3.0 refactor
ui: adds overlay layer to patch-db-client ui: getting towards mocks ui: cleans up factory init ui: nice type hack ui: live api for patch ui: api service source + http starts up ui: api source + http ui: rework patchdb config, pass stashTimeout into patchDbModel wires in temp patching into api service ui: example of wiring patchdbmodel into page begin integration remove unnecessary method linting first data rendering rework app initialization http source working for ssh delete call temp patches working entire Embassy tab complete not in kansas anymore ripping, saving progress progress for API request response types and endoint defs Update data-model.ts shambles, but in a good way progress big progress progress installed list working big progress progress progress begin marketplace redesign Update api-types.ts Update api-types.ts marketplace improvements cosmetic dependencies and recommendations begin nym auth approach install wizard restore flow and donations
This commit is contained in:
committed by
Aiden McClelland
parent
fd685ae32c
commit
594d93eb3b
@@ -5,6 +5,7 @@ import { DevOptionsPage } from './dev-options.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,7 +21,10 @@ const routes: Routes = [
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [
|
||||
DevOptionsPage,
|
||||
],
|
||||
declarations: [DevOptionsPage],
|
||||
})
|
||||
export class DevOptionsPageModule { }
|
||||
|
||||
@@ -9,18 +9,14 @@
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item button [routerLink]="['ssh-keys']">
|
||||
<ion-item-group *ngrxLet="patch.watch$('server-info') as server">
|
||||
<ion-item detail="true" button [routerLink]="['ssh-keys']">
|
||||
<ion-label>SSH Keys</ion-label>
|
||||
</ion-item>
|
||||
<!-- <ion-item button (click)="presentModalValueEdit('alternativeRegistryUrl')">
|
||||
<ion-label>Alt Marketplace</ion-label>
|
||||
<ion-note slot="end">{{ server.alternativeRegistryUrl | async }}</ion-note>
|
||||
</ion-item> -->
|
||||
<ion-item button (click)="presentModalValueEdit('registry', server.registry)">
|
||||
<ion-label>Marketplace URL</ion-label>
|
||||
<ion-note slot="end">{{ server.registry }}</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
</ion-content>
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { S9Server } from 'src/app/models/server-model'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-options',
|
||||
@@ -13,30 +8,13 @@ import { ModelPreload } from 'src/app/models/model-preload'
|
||||
styleUrls: ['./dev-options.page.scss'],
|
||||
})
|
||||
export class DevOptionsPage {
|
||||
server: PropertySubject<S9Server> = { } as any
|
||||
|
||||
constructor (
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly preload: ModelPreload,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.loader.displayDuring$(
|
||||
this.preload.server(),
|
||||
).subscribe(s => this.server = s)
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.apiService.getServer(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async presentModalValueEdit (key: string): Promise<void> {
|
||||
await this.serverConfigService.presentModalValueEdit(key)
|
||||
async presentModalValueEdit (key: string, current?: any): Promise<void> {
|
||||
await this.serverConfigService.presentModalValueEdit(key, current)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { DevSSHKeysPage } from './dev-ssh-keys.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -18,6 +19,7 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [DevSSHKeysPage],
|
||||
})
|
||||
|
||||
@@ -8,22 +8,19 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-item *ngIf="error" class="ion-margin-bottom">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-spinner *ngIf="loading" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-divider>Saved Keys</ion-item-divider>
|
||||
<ion-item *ngFor="let fingerprint of server.ssh | async">
|
||||
<ion-item *ngFor="let ssh of sshService.watch$() | ngrxPush | keyvalue : asIsOrder">
|
||||
<ion-label class="ion-text-wrap">
|
||||
{{ fingerprint.alg }} {{ fingerprint.hash }} {{ fingerprint.hostname }}
|
||||
{{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }}
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(fingerprint)">
|
||||
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(ssh.key)">
|
||||
<ion-icon slot="icon-only" name="close-outline" color="medium"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { SSHFingerprint, S9Server } from 'src/app/models/server-model'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { SSHService } from './ssh.service'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-ssh-keys',
|
||||
@@ -14,40 +10,31 @@ import { AlertController } from '@ionic/angular'
|
||||
styleUrls: ['dev-ssh-keys.page.scss'],
|
||||
})
|
||||
export class DevSSHKeysPage {
|
||||
server: PropertySubject<S9Server> = { } as any
|
||||
error = ''
|
||||
loading = true
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
public readonly sshService: SSHService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.loader.displayDuring$(
|
||||
this.preload.server(),
|
||||
).subscribe(s => this.server = s)
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.apiService.getServer(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
this.sshService.getKeys().then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
async presentModalAdd () {
|
||||
await this.serverConfigService.presentModalValueEdit('ssh', true)
|
||||
await this.serverConfigService.presentModalValueEdit('ssh')
|
||||
}
|
||||
|
||||
async presentAlertDelete (fingerprint: SSHFingerprint) {
|
||||
async presentAlertDelete (hash: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Caution',
|
||||
message: `Are you sure you want to delete this SSH key?`,
|
||||
message: `Are you sure you want to delete this key?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
@@ -57,7 +44,7 @@ export class DevSSHKeysPage {
|
||||
text: 'Delete',
|
||||
cssClass: 'alert-danger',
|
||||
handler: () => {
|
||||
this.delete(fingerprint)
|
||||
this.delete(hash)
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -65,16 +52,21 @@ export class DevSSHKeysPage {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async delete (fingerprint: SSHFingerprint) {
|
||||
async delete (hash: string): Promise<void> {
|
||||
this.error = ''
|
||||
this.loader.of({
|
||||
message: 'Deleting...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(
|
||||
this.apiService.deleteSSHKey(fingerprint).then(() => this.error = ''),
|
||||
).catch(e => {
|
||||
}).displayDuringAsync(async () => {
|
||||
await this.sshService.delete(hash)
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
this.error = ''
|
||||
})
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { SSHKeys } from 'src/app/services/api/api-types'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SSHService {
|
||||
private readonly keys$ = new BehaviorSubject<SSHKeys>({ })
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
) { }
|
||||
|
||||
watch$ () {
|
||||
return this.keys$.asObservable()
|
||||
}
|
||||
|
||||
async getKeys (): Promise<void> {
|
||||
const keys = await this.apiService.getSshKeys({ })
|
||||
this.keys$.next(keys)
|
||||
}
|
||||
|
||||
async add (pubkey: string): Promise<void> {
|
||||
const key = await this.apiService.addSshKey({ pubkey })
|
||||
const keys = this.keys$.getValue()
|
||||
this.keys$.next({ ...keys, ...key })
|
||||
}
|
||||
|
||||
async delete (hash: string): Promise<void> {
|
||||
await this.apiService.deleteSshKey({ hash })
|
||||
const keys = this.keys$.getValue()
|
||||
|
||||
const filtered = Object.keys(keys)
|
||||
.filter(h => h !== hash)
|
||||
.reduce((res, h) => {
|
||||
res[h] = keys[h]
|
||||
return res
|
||||
}, { })
|
||||
this.keys$.next(filtered)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ExternalDrivesPage } from './external-drives.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
// TODO: EJECT-DISKS
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ExternalDrivesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
],
|
||||
declarations: [ExternalDrivesPage],
|
||||
})
|
||||
export class ExternalDrivesPageModule { }
|
||||
@@ -1,30 +0,0 @@
|
||||
<!-- TODO: EJECT-DISKS -->
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Backup drives</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content *ngIf="!($loading$ | async)" class="ion-padding-top">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let d of disks; let i = index">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
<ion-label>{{d.logicalname}} ({{ d.size }})</ion-label>
|
||||
<ion-button *ngIf="!(d.$ejecting$ | async)" slot="end" fill="clear" color="medium" (click)="ejectDisk(i)">
|
||||
<ion-icon color="primary" class="icon" src="/assets/icon/eject.svg"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-spinner *ngIf="d.$ejecting$ | async" name="lines" color="medium"></ion-spinner>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
<ion-content *ngIf="$loading$ | async" class="ion-padding-top">
|
||||
<ion-spinner class="center" name="lines" color="warning"></ion-spinner>
|
||||
</ion-content>
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { DiskInfo } from 'src/app/models/server-model'
|
||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
// TODO: EJECT-DISKS
|
||||
|
||||
type Ejectable<T> = T & { $ejecting$: BehaviorSubject<boolean> }
|
||||
|
||||
@Component({
|
||||
selector: 'external-drives',
|
||||
templateUrl: './external-drives.page.html',
|
||||
styleUrls: ['./external-drives.page.scss'],
|
||||
})
|
||||
export class ExternalDrivesPage {
|
||||
disks: Ejectable<DiskInfo>[] = []
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
markAsLoadingDuringP(this.$loading$, this.fetchDisks())
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.fetchDisks(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async fetchDisks () {
|
||||
return this.apiService.getExternalDisks().then(ds => {
|
||||
this.disks = ds
|
||||
.filter(d => !!d.partitions.find(p => !p.isMounted))
|
||||
.map(d => ({ ...d, $ejecting$: new BehaviorSubject(false)}))
|
||||
.sort( (a, b) => a.logicalname < b.logicalname ? -1 : 1 )
|
||||
})
|
||||
}
|
||||
|
||||
async ejectDisk (diskIndex: number) {
|
||||
const d = this.disks[diskIndex]
|
||||
markAsLoadingDuringP(d.$ejecting$, this.apiService.ejectExternalDisk(d.logicalname))
|
||||
.then(() => this.disks.splice(diskIndex, 1))
|
||||
.catch((e: Error) => {
|
||||
this.alertError(`Could not eject ${d.logicalname}: ${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
async alertError (desc: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: true,
|
||||
message: desc,
|
||||
cssClass: 'alert-error-message',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ServerConfigPage } from './server-config.page'
|
||||
import { GeneralSettingsPage } from './general-settings.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ServerConfigPage,
|
||||
component: GeneralSettingsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -19,10 +18,11 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
],
|
||||
declarations: [ServerConfigPage],
|
||||
declarations: [
|
||||
GeneralSettingsPage,
|
||||
],
|
||||
})
|
||||
export class ServerConfigPageModule { }
|
||||
export class GeneralSettingsPageModule { }
|
||||
@@ -3,25 +3,22 @@
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-title>General Settings</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item button (click)="presentModalValueEdit('name')">
|
||||
<ion-label>Device Name</ion-label>
|
||||
<ion-note slot="end">{{ server.name | async }}</ion-note>
|
||||
<ion-item-group *ngrxLet="patch.watch$('ui') as ui">
|
||||
<ion-item button (click)="presentModalValueEdit('name', ui['server-name'])">
|
||||
<ion-label>Embassy Name</ion-label>
|
||||
<ion-note slot="end">{{ ui['server-name'] }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item button (click)="presentModalValueEdit('autoCheckUpdates')">
|
||||
<ion-item button (click)="presentModalValueEdit('autoCheckUpdates', ui['auto-check-updates'])">
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-note slot="end">{{ server.autoCheckUpdates | async }}</ion-note>
|
||||
<ion-note slot="end">{{ ui['auto-check-updates'] }}</ion-note>
|
||||
</ion-item>
|
||||
<!-- <ion-item style="word-break: break-all;" button (click)="presentModalValueEdit('password', true)">
|
||||
<!-- <ion-item style="word-break: break-all;" button (click)="presentModalValueEdit('password')">
|
||||
<ion-label>Change Password</ion-label>
|
||||
<ion-note slot="end">********</ion-note>
|
||||
</ion-item> -->
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'general-settings',
|
||||
templateUrl: './general-settings.page.html',
|
||||
styleUrls: ['./general-settings.page.scss'],
|
||||
})
|
||||
export class GeneralSettingsPage {
|
||||
constructor (
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async presentModalValueEdit (key: string, current?: string): Promise<void> {
|
||||
await this.serverConfigService.presentModalValueEdit(key, current)
|
||||
}
|
||||
}
|
||||
@@ -10,26 +10,33 @@
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item lines="none">
|
||||
<!-- about -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
You can connect to your Embassy over your Local Area Network (LAN). This can be useful for achieving a faster experience, as well as a fallback in case the Tor network is experiencing issues.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="lanDisabled">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<ion-text color="warning" [innerHtml]="lanDisabledExplanation[lanDisabled]"></ion-text>
|
||||
<p style="padding-bottom: 6px;">About</p>
|
||||
<h2>You can connect to your Embassy over your Local Area Network (LAN). This can be useful for achieving a faster experience, as well as a fallback in case the Tor network is experiencing issues.</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-button slot="start" fill="clear" color="primary" (click)="viewInstructions()">View Instructions</ion-button>
|
||||
<ion-button slot="start" fill="clear" color="primary" [href]="docsUrl" target="_blank">View Instructions</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="lanDisabled">
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p style="padding-bottom: 4px;">Setup</p>
|
||||
<ion-text color="warning" [innerHtml]="lanDisabledExplanation[lanDisabled]"></ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- Refresh Network -->
|
||||
<ion-item-divider></ion-item-divider>
|
||||
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
If you are having issues connecting to your Embassy or services over LAN, you can try refreshing the network by clicking the button below.
|
||||
<p style="padding-bottom: 6px;">Troubleshooting</p>
|
||||
<h2>If you are having issues connecting to your Embassy over LAN, try refreshing the network by clicking the button below.</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
@@ -39,10 +46,10 @@
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider></ion-item-divider>
|
||||
|
||||
<!-- Certificate and Lan Address -->
|
||||
<ng-container *ngIf="!lanDisabled">
|
||||
<ion-item-divider class="borderless"></ion-item-divider>
|
||||
<ion-item-divider>Certificate and Address</ion-item-divider>
|
||||
<!-- Certificate -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Root Certificate Authority</h2>
|
||||
@@ -52,20 +59,21 @@
|
||||
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<!-- URL -->
|
||||
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p>{{ lanAddress }}</p>
|
||||
<p>https://{{ patch.watch$('server-info', 'lan-address') | ngrxPush }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copyLAN()">
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- hidden element for downloading cert -->
|
||||
<a id="install-cert" href="/api/v0/certificate" download="Embassy Local CA.crt"></a>
|
||||
<a id="install-cert" href="/public/local.crt" download="Embassy Local CA.crt"></a>
|
||||
|
||||
</ion-content>
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { isPlatform, ToastController } from '@ionic/angular'
|
||||
import { ServerModel } from 'src/app/models/server-model'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'lan',
|
||||
@@ -12,40 +12,28 @@ import { ApiService } from 'src/app/services/api/api.service'
|
||||
styleUrls: ['./lan.page.scss'],
|
||||
})
|
||||
export class LANPage {
|
||||
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manual/general/lan-setup'
|
||||
lanDocs = 'docs.start9labs.com/user-manual/general/lan-setup'
|
||||
|
||||
lanAddress: string
|
||||
fullDocumentationLink: string
|
||||
lanDisabled: LanSetupIssue
|
||||
readonly lanDisabledExplanation: { [k in LanSetupIssue]: string } = {
|
||||
NotDesktop: `We have detected you are on a mobile device. To setup LAN on a mobile device, use the Start9 Setup App.`,
|
||||
NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection. Please navigate to your Embassy Tor Address and try again.`,
|
||||
NotDesktop: `You are using a mobile device. To setup LAN on a mobile device, please use the Start9 Setup App.`,
|
||||
NotTor: `For security reasons, you must setup LAN over a Tor connection. Please navigate to your Embassy Tor Address and try again.`,
|
||||
}
|
||||
readonly docsUrl = 'https://docs.start9.com/user-manual/general/lan-setup'
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly config: ConfigService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly apiService: ApiService,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
if (isPlatform('ios') || isPlatform('android')) {
|
||||
this.lanDisabled = 'NotDesktop'
|
||||
this.lanDisabled = LanSetupIssue.NOT_DESKTOP
|
||||
} else if (!this.config.isTor()) {
|
||||
this.lanDisabled = 'NotTor'
|
||||
this.lanDisabled = LanSetupIssue.NOT_TOR
|
||||
}
|
||||
|
||||
if (this.config.isTor()) {
|
||||
this.fullDocumentationLink = `http://${this.torDocs}`
|
||||
} else {
|
||||
this.fullDocumentationLink = `https://${this.lanDocs}`
|
||||
}
|
||||
|
||||
const server = this.serverModel.peek()
|
||||
this.lanAddress = `https://${server.serverId}.local`
|
||||
}
|
||||
|
||||
async refreshLAN (): Promise<void> {
|
||||
@@ -54,20 +42,12 @@ export class LANPage {
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync( async () => {
|
||||
await this.apiService.refreshLAN()
|
||||
await this.apiService.refreshLan({ })
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
|
||||
viewInstructions (): void {
|
||||
if (this.config.isConsulate) {
|
||||
this.copyInstructions()
|
||||
} else {
|
||||
window.open(this.fullDocumentationLink, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
async copyLAN (): Promise <void> {
|
||||
const message = await copyToClipboard(this.lanAddress).then(success => success ? 'copied to clipboard!' : 'failed to copy')
|
||||
|
||||
@@ -80,23 +60,12 @@ export class LANPage {
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async copyInstructions (): Promise < void > {
|
||||
const message = await copyToClipboard(this.fullDocumentationLink).then(
|
||||
success => success ? 'copied link to clipboard!' : 'failed to copy',
|
||||
)
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
installCert (): void {
|
||||
document.getElementById('install-cert').click()
|
||||
}
|
||||
}
|
||||
|
||||
type LanSetupIssue = 'NotTor' | 'NotDesktop'
|
||||
enum LanSetupIssue {
|
||||
NOT_TOR = 'NotTor',
|
||||
NOT_DESKTOP = 'NotDesktop',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ServerBackupPage } from './server-backup.page'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { BackupConfirmationComponentModule } from 'src/app/modals/backup-confirmation/backup-confirmation.component.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ServerBackupPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
BackupConfirmationComponentModule,
|
||||
PwaBackComponentModule,
|
||||
],
|
||||
declarations: [
|
||||
ServerBackupPage,
|
||||
],
|
||||
})
|
||||
export class ServerBackupPageModule { }
|
||||
@@ -0,0 +1,69 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Create Backup</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="doRefresh()" color="primary">
|
||||
<ion-icon slot="icon-only" name="reload-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p><ion-text color="dark">About</ion-text></p>
|
||||
<p>
|
||||
Select a location to back up your Embassy. Because are diff-based, so your first backup will likely take much longer than subsequent backups.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
During backup, your Embassy will be unusable.
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-spinner *ngIf="loading; else loaded" name="lines" color="warning" class="center"></ion-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
|
||||
<ion-item *ngIf="allPartitionsMounted">
|
||||
<ion-text *ngIf="type === 'create'" class="ion-text-wrap" color="warning">No partitions available. To begin a backup, insert a storage device into your Embassy.</ion-text>
|
||||
<ion-text *ngIf="type === 'restore'" class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you wish to restore.</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-card *ngFor="let disk of disks | keyvalue">
|
||||
<ion-card-header>
|
||||
<ion-card-title>
|
||||
{{ disk.value.size }}
|
||||
</ion-card-title>
|
||||
<ion-card-subtitle>
|
||||
{{ disk.key }}
|
||||
</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-item-group>
|
||||
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key, partition.value)">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
|
||||
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
|
||||
<ng-template #unavailable>
|
||||
<p><ion-text color="danger">Unavailable</ion-text></p>
|
||||
</ng-template>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ng-template>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component'
|
||||
import { DiskInfo, PartitionInfoEntry } from 'src/app/services/api/api-types'
|
||||
|
||||
@Component({
|
||||
selector: 'server-backup',
|
||||
templateUrl: './server-backup.page.html',
|
||||
styleUrls: ['./server-backup.page.scss'],
|
||||
})
|
||||
export class ServerBackupPage {
|
||||
disks: DiskInfo
|
||||
loading = true
|
||||
error: string
|
||||
allPartitionsMounted: boolean
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.getExternalDisks()
|
||||
}
|
||||
|
||||
async doRefresh () {
|
||||
this.loading = true
|
||||
await this.getExternalDisks()
|
||||
}
|
||||
|
||||
async getExternalDisks (): Promise<void> {
|
||||
try {
|
||||
this.disks = await this.apiService.getDisks({ })
|
||||
this.allPartitionsMounted = Object.values(this.disks).every(d => Object.values(d.partitions).every(p => p['is-mounted']))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async presentModal (logicalname: string, partition: PartitionInfoEntry): Promise<void> {
|
||||
const m = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
name: partition.label || logicalname,
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
component: BackupConfirmationComponent,
|
||||
backdropDismiss: false,
|
||||
})
|
||||
|
||||
m.onWillDismiss().then(res => {
|
||||
const data = res.data
|
||||
if (data.cancel) return
|
||||
this.create(logicalname, data.password)
|
||||
})
|
||||
|
||||
return await m.present()
|
||||
}
|
||||
|
||||
private async create (logicalname: string, password: string): Promise<void> {
|
||||
this.error = ''
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.createBackup({ logicalname, password })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { S9Server, ServerModel } from 'src/app/models/server-model'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'server-config',
|
||||
templateUrl: './server-config.page.html',
|
||||
styleUrls: ['./server-config.page.scss'],
|
||||
})
|
||||
export class ServerConfigPage {
|
||||
server: PropertySubject<S9Server>
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navController: NavController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.server = this.serverModel.watch()
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.apiService.getServer(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async presentModalValueEdit (key: string, add = false): Promise<void> {
|
||||
await this.serverConfigService.presentModalValueEdit(key, add)
|
||||
}
|
||||
|
||||
navigateBack () {
|
||||
this.navController.back()
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,10 @@
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
<p style="white-space: pre-line;">{{ logs }}</p>
|
||||
|
||||
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
<p style="white-space: pre-line;">{{ logs }}</p>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'server-logs',
|
||||
@@ -12,7 +9,7 @@ import { BehaviorSubject } from 'rxjs'
|
||||
})
|
||||
export class ServerLogsPage {
|
||||
@ViewChild(IonContent, { static: false }) private content: IonContent
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
loading = true
|
||||
error = ''
|
||||
logs: string
|
||||
|
||||
@@ -20,25 +17,23 @@ export class ServerLogsPage {
|
||||
private readonly apiService: ApiService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
markAsLoadingDuringP(this.$loading$, Promise.all([
|
||||
this.getLogs(),
|
||||
pauseFor(600),
|
||||
]))
|
||||
ngOnInit () {
|
||||
this.getLogs()
|
||||
}
|
||||
|
||||
async getLogs () {
|
||||
this.logs = ''
|
||||
this.$loading$.next(true)
|
||||
this.loading = true
|
||||
try {
|
||||
this.logs = (await this.apiService.getServerLogs()).join('\n')
|
||||
const logs = await this.apiService.getServerLogs({ })
|
||||
this.logs = logs.map(l => `${l.timestamp} ${l.log}`).join('\n\n')
|
||||
this.error = ''
|
||||
setTimeout(async () => await this.content.scrollToBottom(100), 200)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.$loading$.next(false)
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Metrics</ion-title>
|
||||
<ion-title>Monitor</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -12,18 +12,20 @@
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-spinner *ngIf="loading" class="center" name="lines" color="warning"></ion-spinner>
|
||||
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ion-item-group *ngFor="let metricGroup of metrics | keyvalue : asIsOrder">
|
||||
<ion-item-divider class="divider">{{ metricGroup.key }}</ion-item-divider>
|
||||
<ion-item *ngFor="let metric of metricGroup.value | keyvalue : asIsOrder">
|
||||
<ion-label>
|
||||
<ion-text color="medium">{{ metric.key }}</ion-text>
|
||||
</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-template #loaded>
|
||||
<ion-item-group *ngFor="let metricGroup of metrics | keyvalue : asIsOrder">
|
||||
<ion-item-divider class="divider">{{ metricGroup.key }}</ion-item-divider>
|
||||
<ion-item *ngFor="let metric of metricGroup.value | keyvalue : asIsOrder">
|
||||
<ion-label>
|
||||
<ion-text color="medium">{{ metric.key }}</ion-text>
|
||||
</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-template>
|
||||
|
||||
</ion-content>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ServerMetrics } from 'src/app/models/server-model'
|
||||
import { ServerMetrics } from 'src/app/services/api/api-types'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
|
||||
@@ -18,15 +18,11 @@ export class ServerMetricsPage {
|
||||
private readonly apiService: ApiService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
await Promise.all([
|
||||
this.getMetrics(),
|
||||
pauseFor(600),
|
||||
])
|
||||
|
||||
this.loading = false
|
||||
|
||||
this.startDaemon()
|
||||
ngOnInit () {
|
||||
this.getMetrics().then(() => {
|
||||
this.loading = false
|
||||
this.startDaemon()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
@@ -47,7 +43,7 @@ export class ServerMetricsPage {
|
||||
|
||||
async getMetrics (): Promise<void> {
|
||||
try {
|
||||
const metrics = await this.apiService.getServerMetrics()
|
||||
const metrics = await this.apiService.getServerMetrics({ })
|
||||
Object.keys(metrics).forEach(outerKey => {
|
||||
if (!this.metrics[outerKey]) {
|
||||
this.metrics[outerKey] = metrics[outerKey]
|
||||
|
||||
@@ -1,48 +1,43 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { AuthGuard } from '../../guards/auth.guard'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./server-show/server-show.module').then(m => m.ServerShowPageModule),
|
||||
},
|
||||
{
|
||||
path: 'backup',
|
||||
loadChildren: () => import('./server-backup/server-backup.module').then(m => m.ServerBackupPageModule),
|
||||
},
|
||||
{
|
||||
path: 'specs',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./server-specs/server-specs.module').then(m => m.ServerSpecsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'metrics',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./server-metrics/server-metrics.module').then(m => m.ServerMetricsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./server-logs/server-logs.module').then(m => m.ServerLogsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./server-config/server-config.module').then(m => m.ServerConfigPageModule),
|
||||
path: 'settings',
|
||||
loadChildren: () => import('./general-settings/general-settings.module').then(m => m.GeneralSettingsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'wifi',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiListPageModule),
|
||||
},
|
||||
{
|
||||
path: 'lan',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./lan/lan.module').then(m => m.LANPageModule),
|
||||
},
|
||||
{
|
||||
path: 'developer',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./developer-routes/developer-routing.module').then( m => m.DeveloperRoutingModule),
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ server.name | async }}</ion-title>
|
||||
<ion-title>{{ patch.watch$('ui', 'server-name') | ngrxPush }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
@@ -8,78 +8,64 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top ion-padding-bottom">
|
||||
<ng-container *ngIf="updating">
|
||||
<ion-item class="ion-text-center">
|
||||
<div style="display: flex; justify-content: center; width: 100%;">
|
||||
<ion-text class="ion-text-wrap" style="margin-right: 5px; margin-top: 5px" color="primary">Server Updating</ion-text>
|
||||
<ion-spinner style="margin-left: 5px" name="lines"></ion-spinner>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!updating">
|
||||
<ion-item-divider>Backups</ion-item-divider>
|
||||
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item *ngIf="error">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
<ion-item [routerLink]="['backup']">
|
||||
<ion-icon slot="start" name="save-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Create Backup</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Insights</ion-item-divider>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item [routerLink]="['specs']">
|
||||
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">About</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item [routerLink]="['metrics']">
|
||||
<ion-icon slot="start" name="pulse" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Monitor</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item [routerLink]="['specs']">
|
||||
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">About</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item [routerLink]="['logs']">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item [routerLink]="['metrics']">
|
||||
<ion-icon slot="start" name="pulse" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Monitor</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider>Settings</ion-item-divider>
|
||||
|
||||
<ion-item [routerLink]="['logs']">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item lines="none" [routerLink]="['settings']">
|
||||
<ion-icon slot="start" name="cog-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">General</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item [routerLink]="['lan']">
|
||||
<ion-icon slot="start" name="home-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">LAN</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item lines="none" [routerLink]="['config']">
|
||||
<ion-icon slot="start" name="cog-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Config</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item [routerLink]="['wifi']">
|
||||
<ion-icon slot="start" name="wifi" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">WiFi</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item [routerLink]="['lan']">
|
||||
<ion-icon slot="start" name="home-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">LAN Settings</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item lines="none" [routerLink]="['developer']">
|
||||
<ion-icon slot="start" name="terminal-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Developer Options</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item [routerLink]="['wifi']">
|
||||
<ion-icon slot="start" name="wifi" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">WiFi Settings</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider>Power</ion-item-divider>
|
||||
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item button (click)="presentAlertRestart()">
|
||||
<ion-icon slot="start" name="reload-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Restart</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item lines="none" [routerLink]="['developer']">
|
||||
<ion-icon slot="start" name="terminal-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Developer Options</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider></ion-item-divider>
|
||||
|
||||
<ion-item button (click)="presentAlertRestart()">
|
||||
<ion-icon slot="start" name="reload-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Restart</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button lines="none" (click)="presentAlertShutdown()">
|
||||
<ion-icon slot="start" name="power" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Shutdown</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
<ion-item button lines="none" (click)="presentAlertShutdown()">
|
||||
<ion-icon slot="start" name="power" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Shutdown</ion-text></ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { LoadingOptions } from '@ionic/core'
|
||||
import { ServerModel, ServerStatus } from 'src/app/models/server-model'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { S9Server } from 'src/app/models/server-model'
|
||||
import { AlertController, ModalController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { SyncDaemon } from 'src/app/services/sync.service'
|
||||
import { Subscription, Observable } from 'rxjs'
|
||||
import { PropertySubject, toObservable } from 'src/app/util/property-subject.util'
|
||||
import { doForAtLeast } from 'src/app/util/misc.util'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { ServerStatus } from 'src/app/models/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'server-show',
|
||||
@@ -16,66 +12,16 @@ import { LoaderService } from 'src/app/services/loader.service'
|
||||
styleUrls: ['server-show.page.scss'],
|
||||
})
|
||||
export class ServerShowPage {
|
||||
error = ''
|
||||
s9Host$: Observable<string>
|
||||
|
||||
server: PropertySubject<S9Server>
|
||||
currentServer: S9Server
|
||||
|
||||
subsToTearDown: Subscription[] = []
|
||||
|
||||
updatingFreeze = false
|
||||
updating = false
|
||||
ServerStatus = ServerStatus
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly syncDaemon: SyncDaemon,
|
||||
private readonly modalCtrl: ModalController,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.server = this.serverModel.watch()
|
||||
this.subsToTearDown.push(
|
||||
// serverUpdateSubscription
|
||||
this.server.status.subscribe(status => {
|
||||
if (status === ServerStatus.UPDATING) {
|
||||
this.updating = true
|
||||
} else {
|
||||
if (!this.updatingFreeze) { this.updating = false }
|
||||
}
|
||||
}),
|
||||
// currentServerSubscription
|
||||
toObservable(this.server).subscribe(currentServerProperties => {
|
||||
this.currentServer = currentServerProperties
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
ionViewDidEnter () {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subsToTearDown.forEach(s => s.unsubscribe())
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await doForAtLeast([this.getServerAndApps()], 600)
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async getServerAndApps (): Promise<void> {
|
||||
try {
|
||||
this.syncDaemon.sync()
|
||||
this.error = ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async presentAlertRestart () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
@@ -122,27 +68,26 @@ export class ServerShowPage {
|
||||
|
||||
private async restart () {
|
||||
this.loader
|
||||
.of(LoadingSpinner(`Restarting ${this.currentServer.name}...`))
|
||||
.of(LoadingSpinner(`Restarting...`))
|
||||
.displayDuringAsync( async () => {
|
||||
this.serverModel.markUnreachable()
|
||||
await this.apiService.restartServer()
|
||||
// this.serverModel.markUnreachable()
|
||||
await this.apiService.restartServer({ })
|
||||
})
|
||||
.catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
private async shutdown () {
|
||||
this.loader
|
||||
.of(LoadingSpinner(`Shutting down ${this.currentServer.name}...`))
|
||||
.of(LoadingSpinner(`Shutting down...`))
|
||||
.displayDuringAsync( async () => {
|
||||
this.serverModel.markUnreachable()
|
||||
await this.apiService.shutdownServer()
|
||||
// this.serverModel.markUnreachable()
|
||||
await this.apiService.shutdownServer({ })
|
||||
})
|
||||
.catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
setError (e: Error) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,25 +8,46 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
<ion-item-divider>Basic</ion-item-divider>
|
||||
|
||||
<ion-item-group *ngIf="patch.watch$('server-info') | ngrxPush as server">
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>Version</h2>
|
||||
<p>{{ server.version | displayEmver }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let spec of (server.specs | async) | keyvalue : asIsOrder" [class.break-all]="spec.key === 'Tor Address'">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>{{ spec.key }}</h2>
|
||||
<p *ngIf="spec.value | isValidEmver">{{ spec.value | displayEmver }}</p>
|
||||
<p *ngIf="!(spec.value | isValidEmver)">{{ spec.value }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" *ngIf="spec.key === 'Tor Address'" fill="clear" (click)="copyTor()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
<ion-item-divider>Addresses</ion-item-divider>
|
||||
|
||||
<ion-item>
|
||||
<ion-label class="break-all">
|
||||
<h2>Tor Address</h2>
|
||||
<p>http://{{ server['tor-address'] }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copy('http://' + server['tor-address'])">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label class="break-all">
|
||||
<h2>LAN Address</h2>
|
||||
<p>https://{{ server['lan-address'] }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copy('https://' + server['lan-address'])">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Specs</ion-item-divider>
|
||||
|
||||
<ion-item *ngFor="let spec of server.specs | keyvalue : asIsOrder">
|
||||
<ion-label>
|
||||
<h2>{{ spec.key }}</h2>
|
||||
<p>{{ spec.value }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
</ion-content>
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { S9Server } from 'src/app/models/server-model'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'server-specs',
|
||||
@@ -13,28 +9,15 @@ import { BehaviorSubject } from 'rxjs'
|
||||
styleUrls: ['./server-specs.page.scss'],
|
||||
})
|
||||
export class ServerSpecsPage {
|
||||
server: PropertySubject<S9Server> = { } as any
|
||||
error = ''
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly preload: ModelPreload,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.server()).subscribe({
|
||||
next: s => this.server = s,
|
||||
error: e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async copyTor () {
|
||||
async copy (address: string) {
|
||||
let message = ''
|
||||
await copyToClipboard((this.server.specs.getValue()['Tor Address'] as string).trim() || '')
|
||||
await copyToClipboard(address || '')
|
||||
.then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
<ion-grid style="margin-top: 40px;">
|
||||
<ion-row>
|
||||
<ion-col size="6">
|
||||
<ion-button [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="add()">
|
||||
Save for Later
|
||||
<ion-button class="ion-text-wrap" [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="save()">
|
||||
<p>Save</p>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col size="6">
|
||||
<ion-button [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="addAndConnect()">
|
||||
Save and Connect Now
|
||||
<ion-button class="ion-text-wrap" [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="saveAndConnect()">
|
||||
<p>Save & Connect</p>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NavController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { WifiService } from '../wifi.service'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { ServerModel } from 'src/app/models/server-model'
|
||||
|
||||
@Component({
|
||||
selector: 'wifi-add',
|
||||
@@ -22,18 +21,22 @@ export class WifiAddPage {
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly wifiService: WifiService,
|
||||
private readonly serverModel: ServerModel,
|
||||
) { }
|
||||
|
||||
async add (): Promise<void> {
|
||||
async save (): Promise<void> {
|
||||
this.error = ''
|
||||
this.loader.of({
|
||||
message: 'Saving...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync( async () => {
|
||||
await this.apiService.addWifi(this.ssid, this.password, this.countryCode, false)
|
||||
this.wifiService.addWifi(this.ssid)
|
||||
}).displayDuringAsync(async () => {
|
||||
await this.apiService.addWifi({
|
||||
ssid: this.ssid,
|
||||
password: this.password,
|
||||
country: this.countryCode,
|
||||
priority: 0,
|
||||
connect: false,
|
||||
})
|
||||
this.navCtrl.back()
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
@@ -41,23 +44,25 @@ export class WifiAddPage {
|
||||
})
|
||||
}
|
||||
|
||||
async addAndConnect (): Promise<void> {
|
||||
async saveAndConnect (): Promise<void> {
|
||||
this.error = ''
|
||||
this.loader.of({
|
||||
message: 'Connecting. This could take while...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync( async () => {
|
||||
const current = this.serverModel.peek().wifi.current
|
||||
await this.apiService.addWifi(this.ssid, this.password, this.countryCode, true)
|
||||
const success = await this.wifiService.confirmWifi(this.ssid)
|
||||
if (success) {
|
||||
this.navCtrl.back()
|
||||
this.wifiService.presentAlertSuccess(this.ssid, current)
|
||||
} else {
|
||||
this.wifiService.presentToastFail()
|
||||
}
|
||||
}).catch (e => {
|
||||
}).displayDuringAsync(async () => {
|
||||
await this.apiService.addWifi({
|
||||
ssid: this.ssid,
|
||||
password: this.password,
|
||||
country: this.countryCode,
|
||||
priority: 0,
|
||||
connect: true,
|
||||
})
|
||||
const success = this.wifiService.confirmWifi(this.ssid)
|
||||
if (success) {
|
||||
this.navCtrl.back()
|
||||
}
|
||||
}).catch (e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { WifiListPage } from './wifi.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -22,6 +23,7 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [WifiListPage],
|
||||
})
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item *ngIf="error">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
@@ -20,21 +15,20 @@
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<ion-text color="dark">By providing your Embassy with WiFi credentials for one or more networks, you can remove the Ethernet cable and place your Embassy anywhere.</ion-text>
|
||||
<p style="padding-bottom: 6px;">About</p>
|
||||
<h2>Embassy will automatically connect to available networks, allowing you to remove the Ethernet cable.</h2>
|
||||
<br />
|
||||
<br />
|
||||
<ion-text color="warning">Warning!</ion-text>
|
||||
<br />
|
||||
<br />
|
||||
<ion-text color="dark">Connecting, disconnecting, or changing WiFi networks can cause your Embassy and its services to become unreachable for up to an hour. Please be patient.</ion-text>
|
||||
<h2>Connecting, disconnecting, or changing WiFi networks can cause your Embassy and its services to become unreachable for up to an hour. Please be patient.</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Saved Networks</ion-item-divider>
|
||||
<ion-item button detail="false" *ngFor="let ssid of (server.wifi | async)?.ssids" (click)="presentAction(ssid)">
|
||||
<ion-label>{{ ssid }}</ion-label>
|
||||
<ion-icon *ngIf="ssid === (server.wifi | async).current" name="wifi" color="success"></ion-icon>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="patch.watch$('server-info', 'wifi') | ngrxPush as wifi">
|
||||
<ion-item button detail="false" *ngFor="let ssid of wifi.ssids" (click)="presentAction(ssid, wifi)">
|
||||
<ion-label>{{ ssid }}</ion-label>
|
||||
<ion-icon *ngIf="ssid === wifi.connected" name="wifi" color="success"></ion-icon>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
|
||||
|
||||
@@ -2,12 +2,10 @@ import { Component } from '@angular/core'
|
||||
import { ActionSheetController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { ActionSheetButton } from '@ionic/core'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { WifiService } from './wifi.service'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { S9Server } from 'src/app/models/server-model'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { WiFiInfo } from 'src/app/models/patch-db/data-model'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'wifi',
|
||||
@@ -15,32 +13,17 @@ import { ModelPreload } from 'src/app/models/model-preload'
|
||||
styleUrls: ['wifi.page.scss'],
|
||||
})
|
||||
export class WifiListPage {
|
||||
server: PropertySubject<S9Server> = { } as any
|
||||
error: string
|
||||
error = ''
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly actionCtrl: ActionSheetController,
|
||||
private readonly wifiService: WifiService,
|
||||
private readonly preload: ModelPreload,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.loader.displayDuring$(
|
||||
this.preload.server(),
|
||||
).subscribe(s => this.server = s)
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.apiService.getServer(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async presentAction (ssid: string) {
|
||||
async presentAction (ssid: string, wifi: WiFiInfo) {
|
||||
const buttons: ActionSheetButton[] = [
|
||||
{
|
||||
text: 'Forget',
|
||||
@@ -51,7 +34,7 @@ export class WifiListPage {
|
||||
},
|
||||
]
|
||||
|
||||
if (ssid !== this.server.wifi.getValue().current) {
|
||||
if (ssid !== wifi.connected) {
|
||||
buttons.unshift(
|
||||
{
|
||||
text: 'Connect',
|
||||
@@ -69,7 +52,7 @@ export class WifiListPage {
|
||||
await action.present()
|
||||
}
|
||||
|
||||
// Let's add country code here.
|
||||
// Let's add country code here
|
||||
async connect (ssid: string): Promise<void> {
|
||||
this.error = ''
|
||||
this.loader.of({
|
||||
@@ -77,17 +60,11 @@ export class WifiListPage {
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync(async () => {
|
||||
const current = this.server.wifi.getValue().current
|
||||
await this.apiService.connectWifi(ssid)
|
||||
const success = await this.wifiService.confirmWifi(ssid)
|
||||
if (success) {
|
||||
this.wifiService.presentAlertSuccess(ssid, current)
|
||||
} else {
|
||||
this.wifiService.presentToastFail()
|
||||
}
|
||||
await this.apiService.connectWifi({ ssid })
|
||||
this.wifiService.confirmWifi(ssid)
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
this.error = ''
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,13 +74,12 @@ export class WifiListPage {
|
||||
message: 'Deleting...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync( async () => {
|
||||
await this.apiService.deleteWifi(ssid)
|
||||
this.wifiService.removeWifi(ssid)
|
||||
}).displayDuringAsync(async () => {
|
||||
await this.apiService.deleteWifi({ ssid })
|
||||
this.error = ''
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
this.error = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertController, ToastController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { ServerModel } from 'src/app/models/server-model'
|
||||
import { merge, Observable, timer } from 'rxjs'
|
||||
import { filter, map, take, tap } from 'rxjs/operators'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -10,53 +10,39 @@ import { ServerModel } from 'src/app/models/server-model'
|
||||
export class WifiService {
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
addWifi (ssid: string): void {
|
||||
const wifi = this.serverModel.peek().wifi
|
||||
this.serverModel.update({ wifi: { ...wifi, ssids: [...new Set([ssid, ...wifi.ssids])] } })
|
||||
confirmWifi (ssid: string): Observable<boolean> {
|
||||
const success$ = this.patch.watch$('server-info', 'wifi', 'connected')
|
||||
.pipe(
|
||||
filter(connected => connected === ssid),
|
||||
tap(connected => this.presentAlertSuccess(connected)),
|
||||
map(_ => true),
|
||||
)
|
||||
|
||||
const timer$ = timer(20000)
|
||||
.pipe(
|
||||
map(_ => false),
|
||||
tap(_ => this.presentToastFail()),
|
||||
)
|
||||
|
||||
return merge(success$, timer$).pipe(take(1))
|
||||
}
|
||||
|
||||
removeWifi (ssid: string): void {
|
||||
const wifi = this.serverModel.peek().wifi
|
||||
this.serverModel.update({ wifi: { ...wifi, ssids: wifi.ssids.filter(s => s !== ssid) } })
|
||||
private async presentAlertSuccess (ssid: string): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: `Connected to "${ssid}"`,
|
||||
message: 'Note. It may take several minutes to an hour for your Embassy to reconnect over Tor.',
|
||||
buttons: ['OK'],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async confirmWifi (ssid: string): Promise<boolean> {
|
||||
const timeout = 4000
|
||||
const maxAttempts = 5
|
||||
let attempts = 0
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const start = new Date().valueOf()
|
||||
const { current, ssids } = (await this.apiService.getServer(timeout)).wifi
|
||||
const end = new Date().valueOf()
|
||||
if (current === ssid) {
|
||||
this.serverModel.update({ wifi: { current, ssids } })
|
||||
break
|
||||
} else {
|
||||
attempts++
|
||||
const diff = end - start
|
||||
await pauseFor(Math.max(2000, timeout - diff))
|
||||
if (attempts === maxAttempts) {
|
||||
this.serverModel.update({ wifi: { current, ssids } })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
attempts++
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return this.serverModel.peek().wifi.current === ssid
|
||||
}
|
||||
|
||||
async presentToastFail (): Promise<void> {
|
||||
private async presentToastFail (): Promise<void> {
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: 'Failed to connect:',
|
||||
message: `Check credentials and try again`,
|
||||
@@ -71,20 +57,9 @@ export class WifiService {
|
||||
},
|
||||
},
|
||||
],
|
||||
cssClass: 'notification-toast',
|
||||
cssClass: 'notification-toast-error',
|
||||
})
|
||||
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async presentAlertSuccess (current: string, old?: string): Promise<void> {
|
||||
let message = 'Note. It may take a while for your Embassy to reconnect over Tor, upward of a few hours. Unplugging the device and plugging it back in may help, but it may also just need time. You may also need to hard refresh your browser cache.'
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: `Connected to "${current}"`,
|
||||
message: old ? message : 'You may now unplug your Embassy from Ethernet.<br /></br />' + message,
|
||||
buttons: ['OK'],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user