mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
change disk type, add endpoint, share stats prompt
This commit is contained in:
committed by
Aiden McClelland
parent
effcd5ea57
commit
c27fd487b9
@@ -5,9 +5,13 @@
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
Restore From Backup
|
||||
</ion-title>
|
||||
<ion-title>Restore From Backup</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
Refresh
|
||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -17,11 +21,6 @@
|
||||
<ion-label>
|
||||
<h2>
|
||||
Select the drive containing the backup you would like to restore.
|
||||
<br />
|
||||
<br />
|
||||
<ion-text color="warning">
|
||||
Warning! All current data for {{ patch.data['package-data'][pkgId].manifest.title }} will be overwritten by the backup.
|
||||
</ion-text>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
@@ -56,22 +55,41 @@
|
||||
</ion-item>
|
||||
|
||||
<!-- no error -->
|
||||
<ion-item-group *ngIf="!loadingError">
|
||||
<div *ngFor="let disk of disks">
|
||||
<ion-item-divider>{{ disk.logicalname }} - {{ disk.capacity | convertBytes }}</ion-item-divider>
|
||||
<p class="item-subdivider" *ngIf="disk.vendor || disk.model">
|
||||
{{ disk.vendor }}
|
||||
<span *ngIf="disk.vendor && disk.model"> - </span>
|
||||
{{ disk.model }}
|
||||
</p>
|
||||
<ion-item button *ngFor="let partition of disk.partitions" (click)="presentModal(partition.logicalname)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
|
||||
<ng-container *ngIf="!loadingError">
|
||||
|
||||
<ion-item *ngIf="!disks.length">
|
||||
<ion-label>
|
||||
<ion-text color="warning">
|
||||
No drives found. Insert a backup drive into your Embassy and click "Refresh" above.
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group *ngIf="disks.length">
|
||||
<div *ngFor="let disk of disks">
|
||||
<ion-item-divider>
|
||||
{{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }}
|
||||
</ion-item-divider>
|
||||
<ion-item button *ngFor="let partition of disk.partitions" [disabled]="!partition.hasBackup" (click)="presentModal(partition.logicalname)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
<p *ngIf="partition.hasBackup">
|
||||
<ion-text color="success">
|
||||
Embassy backups detected
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="partition.hasBackup">
|
||||
<ion-icon *ngIf="!partition.backupInfo" slot="end" color="danger" name="lock-closed-outline"></ion-icon>
|
||||
<ion-icon *ngIf="partition.backupInfo" slot="end" color="success" name="lock-open-outline"></ion-icon>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, IonicSafeString } from '@ionic/angular'
|
||||
import { ModalController, IonicSafeString, AlertController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { DiskInfo } from 'src/app/services/api/api.types'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { MappedDiskInfo, MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-restore',
|
||||
@@ -12,17 +14,18 @@ import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
styleUrls: ['./app-restore.component.scss'],
|
||||
})
|
||||
export class AppRestoreComponent {
|
||||
@Input() pkgId: string
|
||||
disks: DiskInfo[]
|
||||
@Input() pkg: PackageDataEntry
|
||||
disks: MappedDiskInfo[]
|
||||
loading = true
|
||||
allPartitionsMounted: boolean
|
||||
modal: HTMLIonModalElement
|
||||
loadingError: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly embassyApi: ApiService,
|
||||
public readonly patch: PatchDbService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly emver: Emver,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -37,7 +40,20 @@ export class AppRestoreComponent {
|
||||
|
||||
async getExternalDisks (): Promise<void> {
|
||||
try {
|
||||
this.disks = await this.embassyApi.getDisks({ })
|
||||
const disks = await this.embassyApi.getDisks({ })
|
||||
this.disks = disks.map(d => {
|
||||
const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => {
|
||||
return {
|
||||
...p,
|
||||
hasBackup: [0, 1].includes(this.emver.compare(p['embassy-os']?.version, '0.3.0')),
|
||||
backupInfo: null,
|
||||
}
|
||||
})
|
||||
return {
|
||||
...d,
|
||||
partitions: partionInfo,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
@@ -48,12 +64,15 @@ export class AppRestoreComponent {
|
||||
async presentModal (logicalname: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title: 'Enter Password',
|
||||
message: 'Backup encrypted. Enter the password that was originally used to encrypt this backup.',
|
||||
title: 'Decryption Required',
|
||||
message: 'Enter the password that was originally used to encrypt this backup.',
|
||||
warning: `Warning! All current data for ${this.pkg.manifest.title} will be overwritten.`,
|
||||
label: 'Password',
|
||||
placeholder: 'Enter password',
|
||||
useMask: true,
|
||||
buttonText: 'Restore',
|
||||
submitFn: (value: string) => this.restore(logicalname, value),
|
||||
loadingText: 'Decrypting drive...',
|
||||
submitFn: (value: string, loader: HTMLIonLoadingElement) => this.restore(logicalname, value, loader),
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
@@ -71,11 +90,60 @@ export class AppRestoreComponent {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
private async restore (logicalname: string, password: string): Promise<void> {
|
||||
private async restore (logicalname: string, password: string, loader: HTMLIonLoadingElement): Promise<void> {
|
||||
const { id, title } = this.pkg.manifest
|
||||
|
||||
const backupInfo = await this.embassyApi.getBackupInfo({
|
||||
logicalname,
|
||||
password,
|
||||
})
|
||||
const pkgBackupInfo = backupInfo['package-backups'][id]
|
||||
|
||||
if (!pkgBackupInfo) {
|
||||
throw new Error(`Disk does not contain a backup of ${title}`)
|
||||
}
|
||||
|
||||
if (this.emver.compare(pkgBackupInfo['os-version'], this.config.version) === 1) {
|
||||
throw new Error(`The backup of ${title} you are attempting to restore was made on a newer version of EmbassyOS. Update EmbassyOS and try again.`)
|
||||
}
|
||||
|
||||
const timestamp = new Date(pkgBackupInfo.timestamp).getTime()
|
||||
const lastBackup = new Date(this.pkg.installed['last-backup']).getTime() // ok if last-backup is null
|
||||
if (timestamp < lastBackup) {
|
||||
const proceed = await this.presentAlertNewerBackup()
|
||||
if (!proceed) {
|
||||
throw new Error('Action cancelled')
|
||||
}
|
||||
}
|
||||
|
||||
loader.message = `Beginning Restore of ${title}`
|
||||
|
||||
await this.embassyApi.restorePackage({
|
||||
id: this.pkgId,
|
||||
id,
|
||||
logicalname,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
private async presentAlertNewerBackup (): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Outdated Backup',
|
||||
message: `The backup you are attempting to restore is older than your most recent backup of ${this.pkg.manifest.title}. Are you sure you want to continue?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
handler: () => resolve(false),
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => resolve(true),
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
<h1>{{ title }}</h1>
|
||||
<br />
|
||||
<p>{{ message }}</p>
|
||||
<ng-container *ngIf="warning">
|
||||
<br />
|
||||
<p>
|
||||
<ion-text color="warning">{{ warning }}</ion-text>
|
||||
</p>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
export class GenericInputComponent {
|
||||
@Input() title: string
|
||||
@Input() message: string
|
||||
@Input() warning: string
|
||||
@Input() label: string
|
||||
@Input() buttonText = 'Submit'
|
||||
@Input() placeholder = 'Enter Value'
|
||||
@@ -17,7 +18,7 @@ export class GenericInputComponent {
|
||||
@Input() useMask = false
|
||||
@Input() value = ''
|
||||
@Input() loadingText = ''
|
||||
@Input() submitFn: (value: string) => Promise<any>
|
||||
@Input() submitFn: (value: string, loader?: HTMLIonLoadingElement) => Promise<any>
|
||||
unmasked = false
|
||||
error: string | IonicSafeString
|
||||
|
||||
@@ -45,7 +46,7 @@ export class GenericInputComponent {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.submitFn(this.value)
|
||||
await this.submitFn(this.value, loader)
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
} catch (e) {
|
||||
this.error = getErrorMessage(e)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ion-item-group *ngIf="pkg">
|
||||
|
||||
<!-- ** standard actions ** -->
|
||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
||||
@@ -28,7 +28,7 @@
|
||||
description: 'This will uninstall the service from your Embassy and delete all data permanently.',
|
||||
icon: 'trash-outline'
|
||||
}"
|
||||
(click)="uninstall(pkg.manifest)">
|
||||
(click)="uninstall()">
|
||||
</app-actions-item>
|
||||
|
||||
<!-- ** specific actions ** -->
|
||||
@@ -40,7 +40,7 @@
|
||||
description: action.value.description,
|
||||
icon: 'play-circle-outline'
|
||||
}"
|
||||
(click)="handleAction(pkg, action)">
|
||||
(click)="handleAction(action)">
|
||||
</app-actions-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -19,10 +19,10 @@ import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.
|
||||
styleUrls: ['./app-actions.page.scss'],
|
||||
})
|
||||
export class AppActionsPage {
|
||||
subs: Subscription[] = []
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
pkgId: string
|
||||
pkg: PackageDataEntry
|
||||
subs: Subscription[]
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
@@ -33,11 +33,17 @@ export class AppActionsPage {
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly navCtrl: NavController,
|
||||
public readonly patch: PatchDbService,
|
||||
private readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.subs = [
|
||||
this.patch.watch$('package-data', this.pkgId)
|
||||
.subscribe(pkg => {
|
||||
this.pkg = pkg
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
@@ -48,8 +54,9 @@ export class AppActionsPage {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
async handleAction (pkg: PackageDataEntry, action: { key: string, value: Action }) {
|
||||
if (!pkg.installed.status.configured) {
|
||||
async handleAction (action: { key: string, value: Action }) {
|
||||
const status = this.pkg.installed.status
|
||||
if (!status.configured) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Forbidden',
|
||||
message: `Service must be properly configured in order to run "${action.value.name}"`,
|
||||
@@ -57,7 +64,7 @@ export class AppActionsPage {
|
||||
cssClass: 'alert-error-message enter-click',
|
||||
})
|
||||
await alert.present()
|
||||
} else if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.installed.status.main.status)) {
|
||||
} else if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(status.main.status)) {
|
||||
if (!isEmptyObject(action.value['input-spec'])) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
@@ -68,7 +75,7 @@ export class AppActionsPage {
|
||||
{
|
||||
text: 'Execute',
|
||||
handler: (value: any) => {
|
||||
return this.executeAction(pkg.manifest.id, action.key, value)
|
||||
return this.executeAction(action.key, value)
|
||||
},
|
||||
isSubmit: true,
|
||||
},
|
||||
@@ -88,7 +95,7 @@ export class AppActionsPage {
|
||||
{
|
||||
text: 'Execute',
|
||||
handler: () => {
|
||||
this.executeAction(pkg.manifest.id, action.key)
|
||||
this.executeAction(action.key)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
@@ -124,7 +131,7 @@ export class AppActionsPage {
|
||||
async restore (): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
pkgId: this.pkgId,
|
||||
pkg: this.pkg,
|
||||
},
|
||||
component: AppRestoreComponent,
|
||||
})
|
||||
@@ -136,8 +143,8 @@ export class AppActionsPage {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async uninstall (manifest: Manifest) {
|
||||
const { id, title, version, alerts } = manifest
|
||||
async uninstall () {
|
||||
const { id, title, version, alerts } = this.pkg.manifest
|
||||
const data = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.uninstall({
|
||||
@@ -152,7 +159,7 @@ export class AppActionsPage {
|
||||
return this.navCtrl.navigateRoot('/services')
|
||||
}
|
||||
|
||||
private async executeAction (pkgId: string, actionId: string, input?: object): Promise<boolean> {
|
||||
private async executeAction (actionId: string, input?: object): Promise<boolean> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Executing action...',
|
||||
@@ -162,7 +169,7 @@ export class AppActionsPage {
|
||||
|
||||
try {
|
||||
const res = await this.embassyApi.executePackageAction({
|
||||
id: pkgId,
|
||||
id: this.pkgId,
|
||||
'action-id': actionId,
|
||||
input,
|
||||
})
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Create Backup</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
Refresh
|
||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -47,23 +53,36 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group *ngIf="!loadingError">
|
||||
<div *ngFor="let disk of disks">
|
||||
<ion-item-divider>{{ disk.logicalname }} - {{ disk.capacity | convertBytes }}</ion-item-divider>
|
||||
<p class="item-subdivider" *ngIf="disk.vendor || disk.model">
|
||||
{{ disk.vendor }}
|
||||
<span *ngIf="disk.vendor && disk.model"> - </span>
|
||||
{{ disk.model }}
|
||||
</p>
|
||||
<ion-item button *ngFor="let partition of disk.partitions" (click)="presentModal(partition.logicalname)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
<ng-container *ngIf="!loadingError">
|
||||
|
||||
<ion-item *ngIf="!disks.length">
|
||||
<ion-label>
|
||||
<ion-text color="warning">
|
||||
No drives found. Insert a backup drive into your Embassy and click "Refresh" above.
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group *ngIf="disks.length">
|
||||
<div *ngFor="let disk of disks">
|
||||
<ion-item-divider>
|
||||
{{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }}
|
||||
</ion-item-divider>
|
||||
<ion-item button *ngFor="let partition of disk.partitions" (click)="presentModal(partition.logicalname)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
<p *ngIf="partition.hasBackup">
|
||||
<ion-text color="success">
|
||||
Embassy backups detected
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Component } from '@angular/core'
|
||||
import { ModalController, IonicSafeString } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { DiskInfo } from 'src/app/services/api/api.types'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { MappedDiskInfo, MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
|
||||
@Component({
|
||||
selector: 'server-backup',
|
||||
@@ -11,13 +12,13 @@ import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
styleUrls: ['./server-backup.page.scss'],
|
||||
})
|
||||
export class ServerBackupPage {
|
||||
disks: DiskInfo[]
|
||||
disks: MappedDiskInfo[]
|
||||
loading = true
|
||||
allPartitionsMounted: boolean
|
||||
loadingError: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly emver: Emver,
|
||||
private readonly embassyApi: ApiService,
|
||||
) { }
|
||||
|
||||
@@ -25,14 +26,27 @@ export class ServerBackupPage {
|
||||
this.getExternalDisks()
|
||||
}
|
||||
|
||||
async doRefresh () {
|
||||
async refresh () {
|
||||
this.loading = true
|
||||
await this.getExternalDisks()
|
||||
}
|
||||
|
||||
async getExternalDisks (): Promise<void> {
|
||||
try {
|
||||
this.disks = await this.embassyApi.getDisks({ })
|
||||
const disks = await this.embassyApi.getDisks({ })
|
||||
this.disks = disks.map(d => {
|
||||
const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => {
|
||||
return {
|
||||
...p,
|
||||
hasBackup: [0, 1].includes(this.emver.compare(p['embassy-os']?.version, '0.3.0')),
|
||||
backupInfo: null,
|
||||
}
|
||||
})
|
||||
return {
|
||||
...d,
|
||||
partitions: partionInfo,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
@@ -46,8 +60,10 @@ export class ServerBackupPage {
|
||||
title: 'Create Backup',
|
||||
message: `Enter your master password to create an encrypted backup of your Embassy and all its installed services.`,
|
||||
label: 'Password',
|
||||
placeholder: 'Enter password',
|
||||
useMask: true,
|
||||
buttonText: 'Create Backup',
|
||||
loadingText: 'Beginning backup...',
|
||||
submitFn: async (value: string) => await this.create(logicalname, value),
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
<p *ngIf="button.title === 'Create Backup'">
|
||||
<ion-text color="warning">
|
||||
Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }}
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,17 @@
|
||||
<div *ngIf="ssid !== wifi.connected" slot="start" style="padding-right: 32px;"></div>
|
||||
<ion-icon *ngIf="ssid === wifi.connected" slot="start" size="large" name="checkmark" color="success"></ion-icon>
|
||||
<ion-label>{{ ssid }}</ion-label>
|
||||
<img *ngIf="ssid === wifi.connected" slot="end" [src]="getWifiIcon()" style="max-width: 32px;" />
|
||||
<ng-container *ngIf="ssid === wifi.connected && wifi['signal-strength'] as strength">
|
||||
<ng-container *ngIf="strength < 33">
|
||||
<img slot="end" src="assets/img/icons/wifi-1.png" style="max-width: 32px;" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="strength > 33 && strength <= 66">
|
||||
<img slot="end" src="assets/img/icons/wifi-2.png" style="max-width: 32px;" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="strength > 66">
|
||||
<img slot="end" src="assets/img/icons/wifi-3.png" style="max-width: 32px;" />
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
@@ -137,27 +137,6 @@ export class WifiPage {
|
||||
await action.present()
|
||||
}
|
||||
|
||||
getWifiIcon (): string {
|
||||
const strength = this.wifi['signal-strength']
|
||||
if (!strength) return
|
||||
|
||||
let path = 'assets/img/icons/wifi-'
|
||||
|
||||
switch (true) {
|
||||
case strength > 66:
|
||||
path = path + '3'
|
||||
break
|
||||
case strength > 33 || strength <= 66:
|
||||
path = path + '2'
|
||||
break
|
||||
case strength < 33:
|
||||
path = path + '1'
|
||||
break
|
||||
}
|
||||
|
||||
return path + '.png'
|
||||
}
|
||||
|
||||
private async setCountry (country: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
|
||||
@@ -985,12 +985,10 @@ export module Mock {
|
||||
label: 'Matt Stuff',
|
||||
capacity: 1000000000000,
|
||||
used: 0,
|
||||
'embassy-os': null,
|
||||
},
|
||||
],
|
||||
capacity: 1000000000000,
|
||||
'embassy-os': {
|
||||
version: '0.3.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalname: '/dev/sdb',
|
||||
@@ -1002,21 +1000,35 @@ export module Mock {
|
||||
label: 'Partition 1',
|
||||
capacity: 1000000000,
|
||||
used: 1000000000,
|
||||
'embassy-os': {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalname: 'sdba2',
|
||||
label: 'Partition 2',
|
||||
capacity: 900000000,
|
||||
used: 300000000,
|
||||
'embassy-os': null,
|
||||
},
|
||||
],
|
||||
capacity: 10000000000,
|
||||
'embassy-os': {
|
||||
version: '0.3.0',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const BackupInfo: RR.GetBackupInfoRes = {
|
||||
version: '0.3.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
'package-backups': {
|
||||
bitcoind: {
|
||||
version: '0.21.0',
|
||||
'os-version': '0.3.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const PackageProperties: RR.GetPackagePropertiesRes<2> = {
|
||||
version: 2,
|
||||
data: {
|
||||
@@ -1534,6 +1546,7 @@ export module Mock {
|
||||
manifest: MockManifestBitcoind,
|
||||
installed: {
|
||||
manifest: MockManifestBitcoind,
|
||||
'last-backup': null,
|
||||
status: {
|
||||
configured: true,
|
||||
main: {
|
||||
@@ -1571,6 +1584,7 @@ export module Mock {
|
||||
},
|
||||
manifest: MockManifestBitcoinProxy,
|
||||
installed: {
|
||||
'last-backup': null,
|
||||
status: {
|
||||
configured: true,
|
||||
main: {
|
||||
@@ -1623,6 +1637,7 @@ export module Mock {
|
||||
},
|
||||
manifest: MockManifestLnd,
|
||||
installed: {
|
||||
'last-backup': null,
|
||||
status: {
|
||||
configured: true,
|
||||
main: {
|
||||
|
||||
@@ -46,7 +46,7 @@ export module RR {
|
||||
|
||||
export type GetSessionsReq = { } // sessions.list
|
||||
export type GetSessionsRes = {
|
||||
current: string,
|
||||
current: string
|
||||
sessions: { [hash: string]: Session }
|
||||
}
|
||||
|
||||
@@ -126,8 +126,8 @@ export module RR {
|
||||
export type GetDisksReq = { } // disk.list
|
||||
export type GetDisksRes = DiskInfo[]
|
||||
|
||||
export type EjectDisksReq = { logicalname: string } // disk.eject
|
||||
export type EjectDisksRes = null
|
||||
export type GetBackupInfoReq = { logicalname: string, password: string } // disk.backup-info
|
||||
export type GetBackupInfoRes = BackupInfo
|
||||
|
||||
// package
|
||||
|
||||
@@ -214,7 +214,7 @@ export type WithExpire<T> = { 'expire-id'?: string } & T
|
||||
export type WithRevision<T> = { response: T, revision?: Revision }
|
||||
|
||||
export interface MarketplaceData {
|
||||
categories: string[],
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
export interface MarketplaceEOS {
|
||||
@@ -243,8 +243,8 @@ export interface Breakages {
|
||||
}
|
||||
|
||||
export interface TaggedDependencyError {
|
||||
dependency: string,
|
||||
error: DependencyError,
|
||||
dependency: string
|
||||
error: DependencyError
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
@@ -289,22 +289,35 @@ export type PlatformType = 'cli' | 'ios' | 'ipad' | 'iphone' | 'android' | 'phab
|
||||
|
||||
export interface DiskInfo {
|
||||
logicalname: string
|
||||
vendor: string | null,
|
||||
model: string | null,
|
||||
partitions: PartitionInfo[],
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
partitions: PartitionInfo[]
|
||||
capacity: number
|
||||
'embassy-os': EmbassyOsDiskInfo | null
|
||||
}
|
||||
|
||||
export interface PartitionInfo {
|
||||
logicalname: string,
|
||||
label: string | null,
|
||||
capacity: number,
|
||||
used: number | null,
|
||||
logicalname: string
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
'embassy-os': EmbassyOsDiskInfo | null
|
||||
}
|
||||
|
||||
export interface EmbassyOsDiskInfo {
|
||||
version: string
|
||||
full: boolean
|
||||
}
|
||||
|
||||
export interface BackupInfo {
|
||||
version: string,
|
||||
timestamp: string,
|
||||
'package-backups': {
|
||||
[id: string]: {
|
||||
version: string
|
||||
'os-version': string
|
||||
timestamp: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerSpecs {
|
||||
|
||||
@@ -130,7 +130,7 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
|
||||
abstract getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes>
|
||||
|
||||
abstract ejectDisk (params: RR.EjectDisksReq): Promise<RR.EjectDisksRes>
|
||||
abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes>
|
||||
|
||||
// package
|
||||
|
||||
|
||||
@@ -197,8 +197,8 @@ export class LiveApiService extends ApiService {
|
||||
return this.http.rpcRequest({ method: 'disk.list', params })
|
||||
}
|
||||
|
||||
ejectDisk (params: RR.EjectDisksReq): Promise <RR.EjectDisksRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.eject', params })
|
||||
getBackupInfo (params: RR.GetBackupInfoReq): Promise <RR.GetBackupInfoRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.backup-info', params })
|
||||
}
|
||||
|
||||
// package
|
||||
|
||||
@@ -291,9 +291,9 @@ export class MockApiService extends ApiService {
|
||||
return Mock.Disks
|
||||
}
|
||||
|
||||
async ejectDisk (params: RR.EjectDisksReq): Promise<RR.EjectDisksRes> {
|
||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
return Mock.BackupInfo
|
||||
}
|
||||
|
||||
// package
|
||||
@@ -415,12 +415,13 @@ export class MockApiService extends ApiService {
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path,
|
||||
value: {
|
||||
status: PackageMainStatus.Running,
|
||||
started: new Date().toISOString(), // UTC date string
|
||||
health: { },
|
||||
},
|
||||
path: path + '/status',
|
||||
value: PackageMainStatus.Running,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/started',
|
||||
value: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
@@ -440,14 +441,12 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async stopPackageRaw (params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const path = `/package-data/${params.id}/installed/status/main`
|
||||
const path = `/package-data/${params.id}/installed/status/main/status`
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path,
|
||||
value: {
|
||||
status: PackageMainStatus.Stopping,
|
||||
},
|
||||
value: PackageMainStatus.Stopping,
|
||||
},
|
||||
]
|
||||
const res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
@@ -455,7 +454,7 @@ export class MockApiService extends ApiService {
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
path,
|
||||
value: PackageMainStatus.Stopped,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -9,8 +9,8 @@ export class Emver {
|
||||
constructor () { }
|
||||
|
||||
compare (lhs: string, rhs: string): number {
|
||||
const compare = emver.compare(lhs, rhs)
|
||||
return compare
|
||||
if (!lhs || !rhs) return null
|
||||
return emver.compare(lhs, rhs)
|
||||
}
|
||||
|
||||
satisfies (version: string, range: string): boolean {
|
||||
|
||||
@@ -9,19 +9,22 @@ export interface DataModel {
|
||||
|
||||
export interface UIData {
|
||||
name: string
|
||||
'welcome-ack': string
|
||||
'auto-check-updates': boolean
|
||||
'pkg-order': string[]
|
||||
'ack-welcome': string // EOS version
|
||||
'ack-share-stats': boolean
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
id: string
|
||||
version: string
|
||||
'last-backup': string | null
|
||||
'lan-address': URL
|
||||
'tor-address': URL
|
||||
status: ServerStatus
|
||||
'eos-marketplace': URL
|
||||
'package-marketplace': URL | null // uses EOS marketplace if null
|
||||
'share-stats': boolean
|
||||
'unread-notification-count': number
|
||||
'update-progress'?: {
|
||||
size: number
|
||||
@@ -66,6 +69,7 @@ export interface InstallProgress {
|
||||
export interface InstalledPackageDataEntry {
|
||||
status: Status
|
||||
manifest: Manifest,
|
||||
'last-backup': string | null
|
||||
'system-pointers': any[]
|
||||
'current-dependents': { [id: string]: CurrentDependencyInfo }
|
||||
'current-dependencies': { [id: string]: CurrentDependencyInfo }
|
||||
|
||||
@@ -17,7 +17,7 @@ export class ServerConfigService {
|
||||
private readonly embassyApi: ApiService,
|
||||
) { }
|
||||
|
||||
async presentAlert (key: string, current?: any): Promise<void> {
|
||||
async presentAlert (key: string, current?: any): Promise<HTMLIonAlertElement> {
|
||||
const spec = serverConfig[key]
|
||||
|
||||
let inputs: AlertInput[]
|
||||
@@ -78,6 +78,7 @@ export class ServerConfigService {
|
||||
buttons,
|
||||
})
|
||||
await alert.present()
|
||||
return alert
|
||||
}
|
||||
|
||||
// async presentModalForm (key: string) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { filter, take } from 'rxjs/operators'
|
||||
import { isEmptyObject } from '../util/misc.util'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ServerConfigService } from './server-config.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -32,6 +33,7 @@ export class StartupAlertsService {
|
||||
private readonly emver: Emver,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly serverConfig: ServerConfigService,
|
||||
) {
|
||||
const osWelcome: Check<boolean> = {
|
||||
name: 'osWelcome',
|
||||
@@ -39,6 +41,12 @@ export class StartupAlertsService {
|
||||
check: async () => true,
|
||||
display: () => this.displayOsWelcome(),
|
||||
}
|
||||
const shareStats: Check<boolean> = {
|
||||
name: 'shareStats',
|
||||
shouldRun: () => this.shouldRunShareStats(),
|
||||
check: async () => true,
|
||||
display: () => this.displayShareStats(),
|
||||
}
|
||||
const osUpdate: Check<RR.GetMarketplaceEOSRes | undefined> = {
|
||||
name: 'osUpdate',
|
||||
shouldRun: () => this.shouldRunOsUpdateCheck(),
|
||||
@@ -51,7 +59,7 @@ export class StartupAlertsService {
|
||||
check: () => this.appsCheck(),
|
||||
display: () => this.displayAppsCheck(),
|
||||
}
|
||||
this.checks = [osWelcome, osUpdate, pkgsUpdate]
|
||||
this.checks = [osWelcome, shareStats, osUpdate, pkgsUpdate]
|
||||
}
|
||||
|
||||
// This takes our three checks and filters down to those that should run.
|
||||
@@ -87,8 +95,13 @@ export class StartupAlertsService {
|
||||
})
|
||||
}
|
||||
|
||||
// ** should run **
|
||||
|
||||
private shouldRunOsWelcome (): boolean {
|
||||
return this.data.ui['welcome-ack'] !== this.config.version
|
||||
return this.data.ui['ack-welcome'] !== this.config.version
|
||||
}
|
||||
private shouldRunShareStats (): boolean {
|
||||
return !this.data.ui['ack-share-stats']
|
||||
}
|
||||
|
||||
private shouldRunOsUpdateCheck (): boolean {
|
||||
@@ -99,6 +112,8 @@ export class StartupAlertsService {
|
||||
return this.data.ui['auto-check-updates']
|
||||
}
|
||||
|
||||
// ** check **
|
||||
|
||||
private async osUpdateCheck (): Promise<RR.GetMarketplaceEOSRes | undefined> {
|
||||
const res = await this.api.getEos({ })
|
||||
|
||||
@@ -114,6 +129,8 @@ export class StartupAlertsService {
|
||||
return !!updates.length
|
||||
}
|
||||
|
||||
// ** display **
|
||||
|
||||
private async displayOsWelcome (): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const modal = await this.modalCtrl.create({
|
||||
@@ -124,7 +141,7 @@ export class StartupAlertsService {
|
||||
},
|
||||
})
|
||||
modal.onWillDismiss().then(() => {
|
||||
this.api.setDbValue({ pointer: '/welcome-ack', value: this.config.version })
|
||||
this.api.setDbValue({ pointer: '/ack-welcome', value: this.config.version })
|
||||
.catch()
|
||||
return resolve(true)
|
||||
})
|
||||
@@ -132,6 +149,18 @@ export class StartupAlertsService {
|
||||
})
|
||||
}
|
||||
|
||||
private async displayShareStats (): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.serverConfig.presentAlert('share-stats', this.data['server-info']['share-stats'])
|
||||
|
||||
alert.onDidDismiss().then(() => {
|
||||
this.api.setDbValue({ pointer: '/ack-share-stats', value: this.config.version })
|
||||
.catch()
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async displayOsUpdateCheck (eos: RR.GetMarketplaceEOSRes): Promise<boolean> {
|
||||
const { update } = await this.presentAlertNewOS(eos.version)
|
||||
if (update) {
|
||||
@@ -180,6 +209,8 @@ export class StartupAlertsService {
|
||||
})
|
||||
}
|
||||
|
||||
// more
|
||||
|
||||
private async presentAlertNewOS (versionLatest: string): Promise<{ cancel?: true, update?: true }> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { OperatorFunction } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { BackupInfo, DiskInfo, PartitionInfo } from '../services/api/api.types'
|
||||
|
||||
export type Omit<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>
|
||||
export type PromiseRes<T> = { result: 'resolve', value: T } | { result: 'reject', value: Error }
|
||||
|
||||
@@ -9,9 +13,6 @@ export type Recommendation = {
|
||||
version?: string
|
||||
}
|
||||
|
||||
import { OperatorFunction } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
export function trace<T> (t: T): T {
|
||||
console.log(`TRACE`, t)
|
||||
return t
|
||||
@@ -190,4 +191,13 @@ export function debounce (delay: number = 300): MethodDecorator {
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
export interface MappedDiskInfo extends DiskInfo {
|
||||
partitions: MappedPartitionInfo[]
|
||||
}
|
||||
|
||||
export interface MappedPartitionInfo extends PartitionInfo {
|
||||
hasBackup: boolean
|
||||
backupInfo: BackupInfo | null
|
||||
}
|
||||
@@ -220,8 +220,8 @@ ion-button {
|
||||
@media (min-width:1000px) {
|
||||
.alertlike-modal {
|
||||
.modal-wrapper {
|
||||
width: 40% !important;
|
||||
left: 30% !important;
|
||||
width: 60% !important;
|
||||
left: 20% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user