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:
Aaron Greenspan
2021-02-16 13:45:09 -07:00
committed by Aiden McClelland
parent fd685ae32c
commit 594d93eb3b
238 changed files with 15137 additions and 21331 deletions

View File

@@ -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 { }

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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],
})

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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 { }

View File

@@ -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>

View File

@@ -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()
}
}

View File

@@ -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 { }

View File

@@ -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> -->

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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',
}

View File

@@ -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 { }

View File

@@ -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>

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -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>

View File

@@ -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]

View File

@@ -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({

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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({

View File

@@ -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>

View File

@@ -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
})

View File

@@ -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],
})

View File

@@ -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">

View File

@@ -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 = ''
})
}
}

View File

@@ -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()
}
}