diff --git a/ui/src/app/modals/app-restore/app-restore.component.html b/ui/src/app/modals/app-restore/app-restore.component.html index 879824983..c68af91f9 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.html +++ b/ui/src/app/modals/app-restore/app-restore.component.html @@ -5,9 +5,13 @@ - - Restore From Backup - + Restore From Backup + + + Refresh + + + @@ -17,11 +21,6 @@ Select the drive containing the backup you would like to restore. - - - - Warning! All current data for {{ patch.data['package-data'][pkgId].manifest.title }} will be overwritten by the backup. - @@ -56,22 +55,41 @@ - - - {{ disk.logicalname }} - {{ disk.capacity | convertBytes }} - - {{ disk.vendor }} - - - {{ disk.model }} - - - - - {{ partition.label || partition.logicalname }} - {{ partition.capacity | convertBytes }} - - - - + + + + + + + No drives found. Insert a backup drive into your Embassy and click "Refresh" above. + + + + + + + + {{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }} + + + + + {{ partition.label || partition.logicalname }} + {{ partition.capacity | convertBytes }} + + + Embassy backups detected + + + + + + + + + + + + diff --git a/ui/src/app/modals/app-restore/app-restore.component.ts b/ui/src/app/modals/app-restore/app-restore.component.ts index 1d29308d0..5a6d7d023 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.ts +++ b/ui/src/app/modals/app-restore/app-restore.component.ts @@ -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 { 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 { 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 { + private async restore (logicalname: string, password: string, loader: HTMLIonLoadingElement): Promise { + 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 { + 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() + }) + } } diff --git a/ui/src/app/modals/generic-input/generic-input.component.html b/ui/src/app/modals/generic-input/generic-input.component.html index 5df9aa9af..6230304d4 100644 --- a/ui/src/app/modals/generic-input/generic-input.component.html +++ b/ui/src/app/modals/generic-input/generic-input.component.html @@ -6,6 +6,12 @@ {{ title }} {{ message }} + + + + {{ warning }} + + diff --git a/ui/src/app/modals/generic-input/generic-input.component.ts b/ui/src/app/modals/generic-input/generic-input.component.ts index 46958a37d..a71d13b60 100644 --- a/ui/src/app/modals/generic-input/generic-input.component.ts +++ b/ui/src/app/modals/generic-input/generic-input.component.ts @@ -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 + @Input() submitFn: (value: string, loader?: HTMLIonLoadingElement) => Promise 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) diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index 5c1e59586..e3251c422 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -10,7 +10,7 @@ - + Standard Actions @@ -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()"> @@ -40,7 +40,7 @@ description: action.value.description, icon: 'play-circle-outline' }" - (click)="handleAction(pkg, action)"> + (click)="handleAction(action)"> \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index 1c85b2405..68517a820 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -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 { 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 { + private async executeAction (actionId: string, input?: object): Promise { 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, }) diff --git a/ui/src/app/pages/server-routes/server-backup/server-backup.page.html b/ui/src/app/pages/server-routes/server-backup/server-backup.page.html index e916d25e4..4bdd519c5 100644 --- a/ui/src/app/pages/server-routes/server-backup/server-backup.page.html +++ b/ui/src/app/pages/server-routes/server-backup/server-backup.page.html @@ -4,6 +4,12 @@ Create Backup + + + Refresh + + + @@ -47,23 +53,36 @@ - - - {{ disk.logicalname }} - {{ disk.capacity | convertBytes }} - - {{ disk.vendor }} - - - {{ disk.model }} - - - - - {{ partition.label || partition.logicalname }} - {{ partition.capacity | convertBytes }} - - - - + + + + + + No drives found. Insert a backup drive into your Embassy and click "Refresh" above. + + + + + + + + {{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }} + + + + + {{ partition.label || partition.logicalname }} + {{ partition.capacity | convertBytes }} + + + Embassy backups detected + + + + + + + diff --git a/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index bbda1965b..0099538fa 100644 --- a/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -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 { 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', diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.html b/ui/src/app/pages/server-routes/server-show/server-show.page.html index 1a7ff122b..f661bebd0 100644 --- a/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -16,6 +16,11 @@ {{ button.title }} {{ button.description }} + + + Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }} + + diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.html b/ui/src/app/pages/server-routes/wifi/wifi.page.html index b3b54106c..979b440e5 100644 --- a/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -53,7 +53,17 @@ {{ ssid }} - + + + + + 33 && strength <= 66"> + + + 66"> + + + diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/ui/src/app/pages/server-routes/wifi/wifi.page.ts index 44c80e800..134138d00 100644 --- a/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -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 { const loader = await this.loadingCtrl.create({ spinner: 'lines', diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index ef3a7436d..feaa85dfd 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -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: { diff --git a/ui/src/app/services/api/api.types.ts b/ui/src/app/services/api/api.types.ts index b41975a85..6c63312a8 100644 --- a/ui/src/app/services/api/api.types.ts +++ b/ui/src/app/services/api/api.types.ts @@ -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 = { 'expire-id'?: string } & T export type WithRevision = { 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 { diff --git a/ui/src/app/services/api/embassy-api.service.ts b/ui/src/app/services/api/embassy-api.service.ts index 6cc9c53e6..448914ba2 100644 --- a/ui/src/app/services/api/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy-api.service.ts @@ -130,7 +130,7 @@ export abstract class ApiService implements Source, Http { abstract getDisks (params: RR.GetDisksReq): Promise - abstract ejectDisk (params: RR.EjectDisksReq): Promise + abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise // package diff --git a/ui/src/app/services/api/embassy-live-api.service.ts b/ui/src/app/services/api/embassy-live-api.service.ts index 547882113..158f3b516 100644 --- a/ui/src/app/services/api/embassy-live-api.service.ts +++ b/ui/src/app/services/api/embassy-live-api.service.ts @@ -197,8 +197,8 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'disk.list', params }) } - ejectDisk (params: RR.EjectDisksReq): Promise { - return this.http.rpcRequest({ method: 'disk.eject', params }) + getBackupInfo (params: RR.GetBackupInfoReq): Promise { + return this.http.rpcRequest({ method: 'disk.backup-info', params }) } // package diff --git a/ui/src/app/services/api/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy-mock-api.service.ts index 3c7650e7a..b59cb534e 100644 --- a/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy-mock-api.service.ts @@ -291,9 +291,9 @@ export class MockApiService extends ApiService { return Mock.Disks } - async ejectDisk (params: RR.EjectDisksReq): Promise { + async getBackupInfo (params: RR.GetBackupInfoReq): Promise { 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>({ method: 'db.patch', params: { patch } }) @@ -440,14 +441,12 @@ export class MockApiService extends ApiService { async stopPackageRaw (params: RR.StopPackageReq): Promise { 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>({ 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, }, ] diff --git a/ui/src/app/services/emver.service.ts b/ui/src/app/services/emver.service.ts index 6ad0dd0e9..84fb48c51 100644 --- a/ui/src/app/services/emver.service.ts +++ b/ui/src/app/services/emver.service.ts @@ -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 { diff --git a/ui/src/app/services/patch-db/data-model.ts b/ui/src/app/services/patch-db/data-model.ts index 9107d5168..ef1fde6e7 100644 --- a/ui/src/app/services/patch-db/data-model.ts +++ b/ui/src/app/services/patch-db/data-model.ts @@ -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 } diff --git a/ui/src/app/services/server-config.service.ts b/ui/src/app/services/server-config.service.ts index 0c3d5c280..7a53fbaef 100644 --- a/ui/src/app/services/server-config.service.ts +++ b/ui/src/app/services/server-config.service.ts @@ -17,7 +17,7 @@ export class ServerConfigService { private readonly embassyApi: ApiService, ) { } - async presentAlert (key: string, current?: any): Promise { + async presentAlert (key: string, current?: any): Promise { const spec = serverConfig[key] let inputs: AlertInput[] @@ -78,6 +78,7 @@ export class ServerConfigService { buttons, }) await alert.present() + return alert } // async presentModalForm (key: string) { diff --git a/ui/src/app/services/startup-alerts.service.ts b/ui/src/app/services/startup-alerts.service.ts index e3765fd84..b8cfbd494 100644 --- a/ui/src/app/services/startup-alerts.service.ts +++ b/ui/src/app/services/startup-alerts.service.ts @@ -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 = { name: 'osWelcome', @@ -39,6 +41,12 @@ export class StartupAlertsService { check: async () => true, display: () => this.displayOsWelcome(), } + const shareStats: Check = { + name: 'shareStats', + shouldRun: () => this.shouldRunShareStats(), + check: async () => true, + display: () => this.displayShareStats(), + } const osUpdate: Check = { 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 { const res = await this.api.getEos({ }) @@ -114,6 +129,8 @@ export class StartupAlertsService { return !!updates.length } + // ** display ** + private async displayOsWelcome (): Promise { 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 { + 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 { 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({ diff --git a/ui/src/app/util/misc.util.ts b/ui/src/app/util/misc.util.ts index ca1576f32..f9a1d9828 100644 --- a/ui/src/app/util/misc.util.ts +++ b/ui/src/app/util/misc.util.ts @@ -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 = Pick> export type PromiseRes = { 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 { 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 } \ No newline at end of file diff --git a/ui/src/global.scss b/ui/src/global.scss index 25ad70771..061902667 100644 --- a/ui/src/global.scss +++ b/ui/src/global.scss @@ -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; } } }
- {{ disk.vendor }} - - - {{ disk.model }} -
+ + Embassy backups detected + +
{{ message }}
+ {{ warning }} +
{{ button.description }}
+ + Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }} + +