change disk type, add endpoint, share stats prompt

This commit is contained in:
Matt Hill
2021-10-17 08:18:25 -06:00
committed by Aiden McClelland
parent effcd5ea57
commit c27fd487b9
22 changed files with 350 additions and 148 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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