mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 06:19:44 +00:00
0.2.5 initial commit
Makefile incomplete
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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 { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DevOptionsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [DevOptionsPage],
|
||||
})
|
||||
export class DevOptionsPageModule { }
|
||||
@@ -0,0 +1,29 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Developer Options</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</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 [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-group>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,42 @@
|
||||
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'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-options',
|
||||
templateUrl: './dev-options.page.html',
|
||||
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,
|
||||
) { }
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
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 { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DevSSHKeysPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [DevSSHKeysPage],
|
||||
})
|
||||
export class DevSSHKeysPageModule { }
|
||||
@@ -0,0 +1,51 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>SSH Keys</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</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 *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-divider style="margin-top: 0px;">Description</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="medium">Add SSH keys to your Embassy to gain root access from the command line.</ion-text></h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Saved Keys</ion-item-divider>
|
||||
<ion-item-sliding *ngFor="let fingerprint of server.ssh | async">
|
||||
<ion-item-options side="end">
|
||||
<ion-item-option color="danger" (click)="delete(fingerprint)">
|
||||
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
|
||||
</ion-item-option>
|
||||
</ion-item-options>
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">{{ fingerprint.alg }} {{ fingerprint.hash }} {{ fingerprint.hostname }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-sliding>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
|
||||
<ion-fab-button (click)="presentModalAdd()" class="fab-button">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,56 @@
|
||||
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'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-ssh-keys',
|
||||
templateUrl: 'dev-ssh-keys.page.html',
|
||||
styleUrls: ['dev-ssh-keys.page.scss'],
|
||||
})
|
||||
export class DevSSHKeysPage {
|
||||
server: PropertySubject<S9Server> = { } as any
|
||||
error = ''
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
) { }
|
||||
|
||||
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 presentModalAdd () {
|
||||
await this.serverConfigService.presentModalValueEdit('ssh', true)
|
||||
}
|
||||
|
||||
async delete (fingerprint: SSHFingerprint) {
|
||||
this.loader.of({
|
||||
message: 'Deleting...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(
|
||||
this.apiService.deleteSSHKey(fingerprint).then(() => this.error = ''),
|
||||
).catch(e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./dev-options/dev-options.module').then(m => m.DevOptionsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'ssh-keys',
|
||||
loadChildren: () => import('./dev-ssh-keys/dev-ssh-keys.module').then(m => m.DevSSHKeysPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class DeveloperRoutingModule { }
|
||||
28
ui/src/app/pages/server-routes/lan/lan.module.ts
Normal file
28
ui/src/app/pages/server-routes/lan/lan.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { LANPage } from './lan.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LANPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [LANPage],
|
||||
})
|
||||
export class LANPageModule { }
|
||||
67
ui/src/app/pages/server-routes/lan/lan.page.html
Normal file
67
ui/src/app/pages/server-routes/lan/lan.page.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Secure LAN Setup</ion-title>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group>
|
||||
<ion-item lines="none" style="font-size: small; --background: var(--ion-background-color);">
|
||||
<ion-label size="small" class="ion-text-wrap">
|
||||
<ion-text color="medium">For a <ion-text style="font-style: italic;">faster</ion-text> experience, you can also securely communicate with your Embassy by visiting its Local Area Network (LAN) address.</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- info -->
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="warning">Instructions</ion-text></h2>
|
||||
<ng-container *ngIf="!lanDisabled">
|
||||
<ul style="font-size: smaller">
|
||||
<li>Download your Embassy's SSL Certificate Authority by clicking the download button below.</li>
|
||||
<li>Install and trust the CA.</li>
|
||||
<li>Connect this device to the same network as the Embassy. This should be your private home network.</li>
|
||||
<li>Navigate to your Embassy LAN address, indicated below.</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<div *ngIf="lanDisabled" class="ion-padding-top ion-padding-bottom">
|
||||
<p [innerHtml]="lanDisabledExplanation[lanDisabled]"></p>
|
||||
</div>
|
||||
<a *ngIf="!isConsulate" [href]="fullDocumentationLink" target="_blank">full documentation</a>
|
||||
<ion-button *ngIf="isConsulate" fill="outline" (click)="copyDocumentation()">full documentation</ion-button>
|
||||
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider style="margin-top: 0px"></ion-item-divider>
|
||||
<!-- Certificate -->
|
||||
<ion-item [disabled]="!!lanDisabled">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>SSL Certificate</h2>
|
||||
<p>Embassy Local CA</p>
|
||||
</ion-label>
|
||||
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="installCert()">
|
||||
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<!-- URL -->
|
||||
<ion-item [disabled]="!!lanDisabled" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<a [href]="lanAddress" target="_blank">{{ lanAddress }}</a>
|
||||
</ion-label>
|
||||
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="copyLAN()">
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- hidden element for downloading cert -->
|
||||
<a id="install-cert" href="/api/v0/certificate" download="Embassy Local CA.crt"></a>
|
||||
|
||||
</ion-content>
|
||||
0
ui/src/app/pages/server-routes/lan/lan.page.scss
Normal file
0
ui/src/app/pages/server-routes/lan/lan.page.scss
Normal file
82
ui/src/app/pages/server-routes/lan/lan.page.ts
Normal file
82
ui/src/app/pages/server-routes/lan/lan.page.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
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'
|
||||
|
||||
@Component({
|
||||
selector: 'lan',
|
||||
templateUrl: './lan.page.html',
|
||||
styleUrls: ['./lan.page.scss'],
|
||||
})
|
||||
export class LANPage {
|
||||
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manuals/embassyos/general/secure-lan'
|
||||
lanDocs = 'docs.start9labs.com/user-manuals/embassyos/general/secure-lan'
|
||||
|
||||
lanAddress: string
|
||||
isTor: boolean
|
||||
fullDocumentationLink: string
|
||||
isConsulate: boolean
|
||||
lanDisabled: LanSetupIssue = undefined
|
||||
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.<br /><br/>Navigate to your Embassy Tor Address and try again.`,
|
||||
}
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly config: ConfigService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
if (isPlatform('ios') || isPlatform('android')) {
|
||||
this.lanDisabled = 'NotDesktop'
|
||||
} else if (!this.config.isTor()) {
|
||||
this.lanDisabled = 'NotTor'
|
||||
}
|
||||
|
||||
this.isConsulate = this.config.isConsulateIos || this.config.isConsulateAndroid
|
||||
|
||||
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 copyLAN (): Promise < void > {
|
||||
const message = await copyToClipboard(this.lanAddress).then(success => success ? 'copied to clipboard!' : 'failed to copy')
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async copyDocumentation (): Promise < void > {
|
||||
const message = await copyToClipboard(this.fullDocumentationLink).then(
|
||||
success => success ? 'copied documentation 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'
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ServerConfigPage } from './server-config.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 { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ServerConfigPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [ServerConfigPage],
|
||||
})
|
||||
export class ServerConfigPageModule { }
|
||||
@@ -0,0 +1,29 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</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>
|
||||
<!-- <ion-item style="word-break: break-all;" button (click)="presentModalValueEdit('password', true)">
|
||||
<ion-label>Change Password</ion-label>
|
||||
<ion-note slot="end">********</ion-note>
|
||||
</ion-item> -->
|
||||
</ion-item-group>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,43 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { ServerMetricsPage } from './server-metrics.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ServerMetricsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [ServerMetricsPage],
|
||||
})
|
||||
export class ServerMetricsPageModule { }
|
||||
@@ -0,0 +1,32 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Metrics</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<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" 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>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,3 @@
|
||||
.metric-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ServerMetrics } from 'src/app/models/server-model'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
|
||||
@Component({
|
||||
selector: 'server-metrics',
|
||||
templateUrl: './server-metrics.page.html',
|
||||
styleUrls: ['./server-metrics.page.scss'],
|
||||
})
|
||||
export class ServerMetricsPage {
|
||||
error = ''
|
||||
loading = true
|
||||
going = false
|
||||
metrics: ServerMetrics = { }
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
await Promise.all([
|
||||
this.getMetrics(),
|
||||
pauseFor(600),
|
||||
])
|
||||
|
||||
this.loading = false
|
||||
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.stopDaemon()
|
||||
}
|
||||
|
||||
async startDaemon (): Promise<void> {
|
||||
this.going = true
|
||||
while (this.going) {
|
||||
await pauseFor(250)
|
||||
await this.getMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
stopDaemon () {
|
||||
this.going = false
|
||||
}
|
||||
|
||||
async getMetrics (): Promise<void> {
|
||||
try {
|
||||
const metrics = await this.apiService.getServerMetrics()
|
||||
Object.keys(metrics).forEach(outerKey => {
|
||||
if (!this.metrics[outerKey]) {
|
||||
this.metrics[outerKey] = metrics[outerKey]
|
||||
} else {
|
||||
Object.entries(metrics[outerKey]).forEach(([key, value]) => {
|
||||
this.metrics[outerKey][key] = value
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
this.stopDaemon()
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
47
ui/src/app/pages/server-routes/server-routing.module.ts
Normal file
47
ui/src/app/pages/server-routes/server-routing.module.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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: '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: 'config',
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: () => import('./server-config/server-config.module').then(m => m.ServerConfigPageModule),
|
||||
},
|
||||
{
|
||||
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({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ServerRoutingModule { }
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { ServerShowPage } from './server-show.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ServerShowPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
StatusComponentModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharingModule,
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [ServerShowPage],
|
||||
})
|
||||
export class ServerShowPageModule { }
|
||||
@@ -0,0 +1,86 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ server.name | async }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="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-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>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
|
||||
<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]="['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 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-divider></ion-item-divider>
|
||||
|
||||
<ion-item lines="none" button (click)="checkForUpdates()">
|
||||
<ion-icon slot="start" name="refresh-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text style="font-weight: bold;" color="primary">Check for Updates</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">Secure LAN Setup</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 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-content>
|
||||
@@ -0,0 +1,12 @@
|
||||
.notification-button {
|
||||
ion-badge {
|
||||
position: absolute;
|
||||
font-size: 8px;
|
||||
bottom: .7rem;
|
||||
left: .8rem;
|
||||
}
|
||||
}
|
||||
|
||||
ion-item-divider {
|
||||
margin-top: 0px;
|
||||
}
|
||||
222
ui/src/app/pages/server-routes/server-show/server-show.page.ts
Normal file
222
ui/src/app/pages/server-routes/server-show/server-show.page.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
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 { 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 { Emver } from 'src/app/services/emver.service'
|
||||
|
||||
@Component({
|
||||
selector: 'server-show',
|
||||
templateUrl: 'server-show.page.html',
|
||||
styleUrls: ['server-show.page.scss'],
|
||||
})
|
||||
export class ServerShowPage {
|
||||
error = ''
|
||||
s9Host$: Observable<string>
|
||||
|
||||
server: PropertySubject<S9Server>
|
||||
currentServer: S9Server
|
||||
|
||||
subsToTearDown: Subscription[] = []
|
||||
|
||||
updatingFreeze = false
|
||||
updating = false
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly syncDaemon: SyncDaemon,
|
||||
private readonly emver: Emver,
|
||||
) { }
|
||||
|
||||
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 checkForUpdates (): Promise<void> {
|
||||
const loader = await this.loader.ctrl.create(LoadingSpinner('Checking for updates...'))
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const { versionLatest } = await this.apiService.getVersionLatest()
|
||||
if (this.emver.compare(this.server.versionInstalled.getValue(), versionLatest) === -1) {
|
||||
this.presentAlertUpdate(versionLatest)
|
||||
} else {
|
||||
this.presentAlertUpToDate()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async presentAlertUpToDate () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Up To Date',
|
||||
message: `You are running the latest version of EmbassyOS!`,
|
||||
buttons: ['OK'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertUpdate (versionLatest: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Confirm',
|
||||
message: `Update EmbassyOS to ${versionLatest}?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Update',
|
||||
handler: () => {
|
||||
this.updateEmbassyOS(versionLatest)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertRestart () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you want to restart your Embassy?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Restart',
|
||||
cssClass: 'alert-danger',
|
||||
handler: () => {
|
||||
this.restart()
|
||||
},
|
||||
},
|
||||
]},
|
||||
)
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertShutdown () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you shut down your Embassy? To turn it back on, you will need to physically unplug the device and plug it back in.`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Shutdown',
|
||||
cssClass: 'alert-danger',
|
||||
handler: () => {
|
||||
this.shutdown()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async updateEmbassyOS (versionLatest: string) {
|
||||
this.loader
|
||||
.displayDuringAsync(async () => {
|
||||
await this.apiService.updateAgent(versionLatest)
|
||||
this.serverModel.update({ status: ServerStatus.UPDATING })
|
||||
// hides the "Update Embassy to..." button for this intance of the component
|
||||
this.updatingFreeze = true
|
||||
this.updating = true
|
||||
setTimeout(() => this.updatingFreeze = false, 8000)
|
||||
})
|
||||
.catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
private async restart () {
|
||||
this.loader
|
||||
.of(LoadingSpinner(`Restarting ${this.currentServer.name}...`))
|
||||
.displayDuringAsync( async () => {
|
||||
this.serverModel.markUnreachable()
|
||||
await this.apiService.restartServer()
|
||||
})
|
||||
.catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
private async shutdown () {
|
||||
this.loader
|
||||
.of(LoadingSpinner(`Shutting down ${this.currentServer.name}...`))
|
||||
.displayDuringAsync( async () => {
|
||||
this.serverModel.markUnreachable()
|
||||
await this.apiService.shutdownServer()
|
||||
})
|
||||
.catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
setError (e: Error) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
const LoadingSpinner: (m?: string) => LoadingOptions = (m) => {
|
||||
const toMergeIn = m ? { message: m } : { }
|
||||
return {
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
...toMergeIn,
|
||||
} as LoadingOptions
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { ServerSpecsPage } from './server-specs.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ServerSpecsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [ServerSpecsPage],
|
||||
})
|
||||
export class ServerSpecsPageModule { }
|
||||
@@ -0,0 +1,33 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>About</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</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>
|
||||
|
||||
<ion-item-group>
|
||||
<!-- TODO: Tor address needs a copy button. -->
|
||||
<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-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,53 @@
|
||||
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 { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'server-specs',
|
||||
templateUrl: './server-specs.page.html',
|
||||
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,
|
||||
) { }
|
||||
|
||||
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 () {
|
||||
let message = ''
|
||||
await copyToClipboard((this.server.specs.getValue()['Tor Address'] as string).trim() || '')
|
||||
.then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { WifiAddPage } from './wifi-add.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: WifiAddPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [WifiAddPage],
|
||||
})
|
||||
export class WifiAddPageModule { }
|
||||
@@ -0,0 +1,52 @@
|
||||
<ion-header>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-toolbar>
|
||||
<ion-title>Add Network</ion-title>
|
||||
</ion-toolbar>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-item *ngIf="error">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>Select Country</ion-label>
|
||||
<ion-select slot="end" placeholder="Select" [(ngModel)]="countryCode" [selectedText]="countryCode">
|
||||
<ion-select-option *ngFor="let country of countries | keyvalue : asIsOrder" [value]="country.key">
|
||||
{{ country.key }} - {{ country.value }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item-divider>Network and Password</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-input placeholder="Network Name (SSID)" [(ngModel)]="ssid"></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-input type="password" placeholder="Password" [(ngModel)]="password"></ion-input>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-grid style="margin-top: 40px;">
|
||||
<ion-row>
|
||||
<ion-col size="6">
|
||||
<ion-button [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="add()">
|
||||
Add
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col size="6">
|
||||
<ion-button [disabled]="!ssid" expand="block" fill="outline" color="success" (click)="addAndConnect()">
|
||||
Add and Connect
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Component } from '@angular/core'
|
||||
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'
|
||||
|
||||
@Component({
|
||||
selector: 'wifi-add',
|
||||
templateUrl: 'wifi-add.page.html',
|
||||
styleUrls: ['wifi-add.page.scss'],
|
||||
})
|
||||
export class WifiAddPage {
|
||||
countries = require('../../../../util/countries.json')
|
||||
countryCode = 'US'
|
||||
ssid = ''
|
||||
password = ''
|
||||
error = ''
|
||||
|
||||
constructor (
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly wifiService: WifiService,
|
||||
) { }
|
||||
|
||||
async add (): Promise<void> {
|
||||
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)
|
||||
this.ssid = ''
|
||||
this.password = ''
|
||||
this.error = ''
|
||||
this.navCtrl.back()
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
})
|
||||
}
|
||||
|
||||
async addAndConnect (): Promise<void> {
|
||||
this.loader.of({
|
||||
message: 'Connecting. This could take while...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync( async () => {
|
||||
await this.apiService.addWifi(this.ssid, this.password, this.countryCode, true)
|
||||
const success = await this.wifiService.confirmWifi(this.ssid)
|
||||
if (!success) { return }
|
||||
this.wifiService.addWifi(this.ssid)
|
||||
this.ssid = ''
|
||||
this.password = ''
|
||||
this.error = ''
|
||||
this.navCtrl.back()
|
||||
}).catch (e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
})
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
30
ui/src/app/pages/server-routes/wifi/wifi.module.ts
Normal file
30
ui/src/app/pages/server-routes/wifi/wifi.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
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 { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: WifiListPage,
|
||||
},
|
||||
{
|
||||
path: 'add',
|
||||
loadChildren: () => import('./wifi-add/wifi-add.module').then(m => m.WifiAddPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [WifiListPage],
|
||||
})
|
||||
export class WifiListPageModule { }
|
||||
47
ui/src/app/pages/server-routes/wifi/wifi.page.html
Normal file
47
ui/src/app/pages/server-routes/wifi/wifi.page.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Wifi</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</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 *ngIf="error">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>
|
||||
Add WiFi credentials to your Embassy so it can connect to the Internet without an ethernet cable.
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider class="borderless"></ion-item-divider>
|
||||
|
||||
<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>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
|
||||
<ion-fab-button [routerLink]="['add']" class="fab-button">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
|
||||
</ion-content>
|
||||
0
ui/src/app/pages/server-routes/wifi/wifi.page.scss
Normal file
0
ui/src/app/pages/server-routes/wifi/wifi.page.scss
Normal file
102
ui/src/app/pages/server-routes/wifi/wifi.page.ts
Normal file
102
ui/src/app/pages/server-routes/wifi/wifi.page.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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'
|
||||
|
||||
@Component({
|
||||
selector: 'wifi',
|
||||
templateUrl: 'wifi.page.html',
|
||||
styleUrls: ['wifi.page.scss'],
|
||||
})
|
||||
export class WifiListPage {
|
||||
server: PropertySubject<S9Server> = { } as any
|
||||
error: string
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly actionCtrl: ActionSheetController,
|
||||
private readonly wifiService: WifiService,
|
||||
private readonly preload: ModelPreload,
|
||||
) { }
|
||||
|
||||
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) {
|
||||
const buttons: ActionSheetButton[] = [
|
||||
{
|
||||
text: 'Forget',
|
||||
cssClass: 'alert-danger',
|
||||
handler: () => {
|
||||
this.delete(ssid)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (ssid !== this.server.wifi.getValue().current) {
|
||||
buttons.unshift(
|
||||
{
|
||||
text: 'Connect',
|
||||
handler: () => {
|
||||
this.connect(ssid)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const action = await this.actionCtrl.create({
|
||||
buttons,
|
||||
})
|
||||
|
||||
await action.present()
|
||||
}
|
||||
|
||||
// Let's add country code here.
|
||||
async connect (ssid: string): Promise<void> {
|
||||
this.loader.of({
|
||||
message: 'Connecting. This could take while...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync(async () => {
|
||||
await this.apiService.connectWifi(ssid)
|
||||
await this.wifiService.confirmWifi(ssid)
|
||||
this.error = ''
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
})
|
||||
}
|
||||
|
||||
async delete (ssid: string): Promise<void> {
|
||||
this.loader.of({
|
||||
message: 'Deleting...',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync( async () => {
|
||||
await this.apiService.deleteWifi(ssid)
|
||||
this.wifiService.removeWifi(ssid)
|
||||
this.error = ''
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
})
|
||||
}
|
||||
}
|
||||
80
ui/src/app/pages/server-routes/wifi/wifi.service.ts
Normal file
80
ui/src/app/pages/server-routes/wifi/wifi.service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { 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'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class WifiService {
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly serverModel: ServerModel,
|
||||
) { }
|
||||
|
||||
addWifi (ssid: string): void {
|
||||
const wifi = this.serverModel.peek().wifi
|
||||
this.serverModel.update({ wifi: { ...wifi, ssids: [...new Set([ssid, ...wifi.ssids])] } })
|
||||
}
|
||||
|
||||
removeWifi (ssid: string): void {
|
||||
const wifi = this.serverModel.peek().wifi
|
||||
this.serverModel.update({ wifi: { ...wifi, ssids: wifi.ssids.filter(s => s !== ssid) } })
|
||||
}
|
||||
|
||||
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(0, timeout - diff))
|
||||
if (attempts === maxAttempts) {
|
||||
this.serverModel.update({ wifi: { current, ssids } })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
attempts++
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.serverModel.peek().wifi.current === ssid) {
|
||||
return true
|
||||
} else {
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: 'Failed to connect:',
|
||||
message: `Check credentials and try again`,
|
||||
position: 'bottom',
|
||||
duration: 4000,
|
||||
buttons: [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
|
||||
setTimeout(() => toast.present(), 300)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user