Feature/diagnostic repair disk (#1358)

* add disk repair actions to diagnostic ui and server menu

* only display repair disk button when activated

* fix typo

* add repairDrive fn with restart to diagnostic ui

* fix copy

* add alert before repairing disk in diagnostic ui

* fix repair disk message spacing and hidden display

* fix version comparisons and enable dismissable refresh modal

* eager load medkit and fix storefront to outline icon
This commit is contained in:
Lucy C
2022-03-28 17:31:32 -06:00
committed by GitHub
parent 8ef1584a4d
commit e53bf81cbc
15 changed files with 260 additions and 82 deletions

View File

@@ -49,6 +49,12 @@
}}
</ion-button>
</div>
<div
*ngIf="error.code === 2 || error.code === 48"
class="ion-padding-top"
>
<ion-button (click)="repairDrive()"> {{ 'Repair Drive' }} </ion-button>
</div>
</ng-container>
<ng-template #refresh>

View File

@@ -1,5 +1,9 @@
import { Component } from '@angular/core'
import { LoadingController } from '@ionic/angular'
import {
AlertController,
IonicSafeString,
LoadingController,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
@Component({
@@ -20,6 +24,7 @@ export class HomePage {
constructor(
private readonly loadingCtrl: LoadingController,
private readonly api: ApiService,
private readonly alertCtrl: AlertController,
) {}
async ngOnInit() {
@@ -48,16 +53,33 @@ export class HomePage {
this.error = {
code: 25,
problem:
'Storage drive corrupted. This could be the result of data corruption or a physical damage.',
'Storage drive corrupted. This could be the result of data corruption or physical damage.',
solution:
'It may or may not be possible to re-use this drive by reformatting and recovering from backup. To enter recovery mode, click ENTER RECOVERY MODE below, then follow instructions. No data will be erased during this step.',
details: error.data?.details,
}
// filesystem I/O error - disk needs repair
} else if (error.code === 2) {
this.error = {
code: 2,
problem: 'Filesystem I/O error.',
solution: '',
details: error.data?.details,
}
// disk management error - disk needs repair
} else if (error.code === 48) {
this.error = {
code: 48,
problem: 'Disk management error.',
solution:
'Repairing the disk could help resolve this issue. This will occur on a restart between the bep and chime. Please DO NOT unplug the drive or Embassy during this time or the situation will become worse.',
details: error.data?.details,
}
} else {
this.error = {
code: error.code,
problem: error.message,
solution: 'Please conact support.',
solution: 'Please contact support.',
details: error.data?.details,
}
}
@@ -101,6 +123,53 @@ export class HomePage {
}
}
async repairDrive(): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.repairDisk()
await this.api.restart()
this.restarted = true
} catch (e) {
console.error(e)
} finally {
loader.dismiss()
}
}
async presentAlertRepairDisk() {
const alert = await this.alertCtrl.create({
header: 'RepairDisk',
message: new IonicSafeString(
`<ion-text color="warning">Warning:</ion-text> This action will attempt to preform a disk repair operation and system reboot. No data will be deleted. This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action. If anything happens to the device during the reboot (between the bep and chime), such as loosing power, a power surge, unplugging the drive, or unplugging the Embassy, the filesystem *will* be in an unrecoverable state. Please proceed with caution.`,
),
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Repair',
handler: () => {
try {
this.api.repairDisk().then(_ => {
this.restart()
})
} catch (e) {
console.error(e)
}
},
cssClass: 'enter-click',
},
],
})
await alert.present()
}
refreshPage(): void {
window.location.reload()
}

View File

@@ -1,22 +1,31 @@
export abstract class ApiService {
abstract getError (): Promise<GetErrorRes>
abstract restart (): Promise<void>
abstract forgetDrive (): Promise<void>
abstract getLogs (params: GetLogsReq): Promise<GetLogsRes>
abstract getError(): Promise<GetErrorRes>
abstract restart(): Promise<void>
abstract forgetDrive(): Promise<void>
abstract repairDisk(): Promise<void>
abstract getLogs(params: GetLogsReq): Promise<GetLogsRes>
}
export interface GetErrorRes {
code: number,
message: string,
code: number
message: string
data: { details: string }
}
export type GetLogsReq = { cursor?: string, before_flag?: boolean, limit?: number }
export type GetLogsReq = {
cursor?: string
before_flag?: boolean
limit?: number
}
export type GetLogsRes = LogsRes
export type LogsRes = { entries: Log[], 'start-cursor'?: string, 'end-cursor'?: string }
export type LogsRes = {
entries: Log[]
'start-cursor'?: string
'end-cursor'?: string
}
export interface Log {
timestamp: string
message: string
}
}

View File

@@ -24,7 +24,14 @@ export class LiveApiService extends ApiService {
forgetDrive(): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.forget-disk',
method: 'diagnostic.disk.forget',
params: {},
})
}
repairDisk(): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.disk.repair',
params: {},
})
}

View File

@@ -33,6 +33,11 @@ export class MockApiService extends ApiService {
return null
}
async repairDisk(): Promise<void> {
await pauseFor(1000)
return null
}
async getLogs(params: GetLogsReq): Promise<GetLogsRes> {
await pauseFor(1000)
let entries: Log[]

View File

@@ -162,6 +162,7 @@
<ion-icon name="logo-bitcoin"></ion-icon>
<ion-icon name="mail-outline"></ion-icon>
<ion-icon name="map-outline"></ion-icon>
<ion-icon name="medkit-outline"></ion-icon>
<ion-icon name="newspaper-outline"></ion-icon>
<ion-icon name="notifications-outline"></ion-icon>
<ion-icon name="options-outline"></ion-icon>

View File

@@ -396,7 +396,7 @@ export class AppComponent {
private async presentAlertRefreshNeeded() {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
backdropDismiss: true,
header: 'Refresh Needed',
message:
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.',

View File

@@ -56,8 +56,6 @@ export class BackupService {
}
hasValidBackup(target: BackupTarget): boolean {
return [0, 1].includes(
this.emver.compare(target['embassy-os']?.version, '0.3.0'),
)
return this.emver.compare(target['embassy-os']?.version, '0.3.0') !== -1
}
}

View File

@@ -24,73 +24,83 @@
<ng-template #data>
<ion-item-group>
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
<ion-item-divider
><ion-text color="dark">{{ cat.key }}</ion-text></ion-item-divider
>
<ion-item
button
*ngFor="let button of cat.value"
[detail]="button.detail"
[disabled]="button.disabled | async"
(click)="button.action()"
>
<ion-icon slot="start" [name]="button.icon"></ion-icon>
<ion-label>
<h2>{{ button.title }}</h2>
<p *ngIf="button.description">{{ button.description }}</p>
<ion-item-divider>
<ion-text color="dark" *ngIf="cat.key !== 'Power'"
>{{ cat.key }}</ion-text
>
<ion-text
color="dark"
*ngIf="cat.key === 'Power'"
(click)="addClick()"
>{{ cat.key }}</ion-text
>
</ion-item-divider>
<ng-container *ngFor="let button of cat.value">
<ion-item
button
[style.display]="(button.title === 'Repair Disk' && !(localStorageService.showDiskRepair$ | async)) ? 'none' : 'block'"
[detail]="button.detail"
[disabled]="button.disabled | async"
(click)="button.action()"
>
<ion-icon slot="start" [name]="button.icon"></ion-icon>
<ion-label>
<h2>{{ button.title }}</h2>
<p *ngIf="button.description">{{ button.description }}</p>
<!-- "Create Backup" button only -->
<p *ngIf="button.title === 'Create Backup'">
<ng-container
*ngIf="patch.data['server-info']['status-info'] as statusInfo"
>
<ion-text
color="warning"
*ngIf="!statusInfo['backing-up'] && !statusInfo['update-progress']"
>
Last Backup: {{ patch.data['server-info']['last-backup'] ?
(patch.data['server-info']['last-backup'] | date: 'short') :
'never' }}
</ion-text>
<span *ngIf="!!statusInfo['backing-up']" class="inline">
<ion-spinner
color="success"
style="height: 12px; width: 12px; margin-right: 6px"
></ion-spinner>
<ion-text color="success"> Backing up </ion-text>
</span>
</ng-container>
</p>
<!-- "Software Update" button only -->
<p *ngIf="button.title === 'Software Update'">
<ng-container *ngIf="button.disabled | async; else enabled">
<ion-text
*ngIf="patch.data['server-info']['status-info'].updated"
class="inline"
color="warning"
>
Update Complete, Restart to apply changes
</ion-text>
</ng-container>
<ng-template #enabled>
<!-- "Create Backup" button only -->
<p *ngIf="button.title === 'Create Backup'">
<ng-container
*ngIf="eosService.updateAvailable$ | async; else check"
*ngIf="patch.data['server-info']['status-info'] as statusInfo"
>
<ion-text class="inline" color="success">
<ion-icon name="rocket-outline"></ion-icon>
Update Available
<ion-text
color="warning"
*ngIf="!statusInfo['backing-up'] && !statusInfo['update-progress']"
>
Last Backup: {{ patch.data['server-info']['last-backup'] ?
(patch.data['server-info']['last-backup'] | date: 'short') :
'never' }}
</ion-text>
<span *ngIf="!!statusInfo['backing-up']" class="inline">
<ion-spinner
color="success"
style="height: 12px; width: 12px; margin-right: 6px"
></ion-spinner>
<ion-text color="success"> Backing up </ion-text>
</span>
</ng-container>
</p>
<!-- "Software Update" button only -->
<p *ngIf="button.title === 'Software Update'">
<ng-container *ngIf="button.disabled | async; else enabled">
<ion-text
*ngIf="patch.data['server-info']['status-info'].updated"
class="inline"
color="warning"
>
Update Complete, Restart to apply changes
</ion-text>
</ng-container>
<ng-template #check>
<ion-text class="inline" color="dark">
<ion-icon name="refresh"></ion-icon>
Check for updates
</ion-text>
<ng-template #enabled>
<ng-container
*ngIf="eosService.updateAvailable$ | async; else check"
>
<ion-text class="inline" color="success">
<ion-icon name="rocket-outline"></ion-icon>
Update Available
</ion-text>
</ng-container>
<ng-template #check>
<ion-text class="inline" color="dark">
<ion-icon name="refresh"></ion-icon>
Check for updates
</ion-text>
</ng-template>
</ng-template>
</ng-template>
</p>
</ion-label>
</ion-item>
</p>
</ion-label>
</ion-item>
</ng-container>
</div>
</ion-item-group>
</ng-template>

View File

@@ -16,6 +16,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared'
import { EOSService } from 'src/app/services/eos.service'
import { ServerStatus } from 'src/app/services/patch-db/data-model'
import { LocalStorageService } from 'src/app/services/local-storage.service'
@Component({
selector: 'server-show',
@@ -25,6 +26,7 @@ import { ServerStatus } from 'src/app/services/patch-db/data-model'
export class ServerShowPage {
ServerStatus = ServerStatus
hasRecoveredPackage: boolean
clicks = 0
constructor(
private readonly alertCtrl: AlertController,
@@ -37,6 +39,7 @@ export class ServerShowPage {
private readonly route: ActivatedRoute,
public readonly eosService: EOSService,
public readonly patch: PatchDbService,
public readonly localStorageService: LocalStorageService,
) {}
ngOnInit() {
@@ -143,6 +146,35 @@ export class ServerShowPage {
await alert.present()
}
async presentAlertRepairDisk() {
const alert = await this.alertCtrl.create({
header: 'Repair Disk',
message: new IonicSafeString(
`<ion-text color="warning">Warning:</ion-text> <p>This action will attempt to preform a disk repair operation and system reboot. No data will be deleted. This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action.</p><p>If anything happens to the device during the reboot (between the bep and chime), such as loosing power, a power surge, unplugging the drive, or unplugging the Embassy, the filesystem *will* be in an unrecoverable state. Please proceed with caution.</p>`,
),
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Repair',
handler: () => {
try {
this.embassyApi.repairDisk({}).then(_ => {
this.restart()
})
} catch (e) {
this.errToast.present(e)
}
},
cssClass: 'enter-click',
},
],
})
await alert.present()
}
private async restart() {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
@@ -305,7 +337,7 @@ export class ServerShowPage {
{
title: 'Marketplace Settings',
description: 'Add or remove marketplaces',
icon: 'storefront',
icon: 'storefront-outline',
action: () =>
this.navCtrl.navigateForward(['marketplaces'], {
relativeTo: this.route,
@@ -418,12 +450,31 @@ export class ServerShowPage {
detail: false,
disabled: of(false),
},
{
title: 'Repair Disk',
description: '',
icon: 'medkit-outline',
action: () => this.presentAlertRepairDisk(),
detail: false,
disabled: of(false),
},
],
}
asIsOrder() {
return 0
}
async addClick() {
this.clicks++
if (this.clicks >= 5) {
this.clicks = 0
const newVal = await this.localStorageService.toggleShowDiskRepair()
}
setTimeout(() => {
this.clicks = Math.max(this.clicks - 1, 0)
}, 10000)
}
}
interface ServerSettings {

View File

@@ -93,6 +93,8 @@ export abstract class ApiService
params: RR.SystemRebuildReq,
): Promise<RR.SystemRebuildRes>
abstract repairDisk(params: RR.SystemRebuildReq): Promise<RR.SystemRebuildRes>
// marketplace URLs
abstract marketplaceProxy<T>(

View File

@@ -99,6 +99,10 @@ export class LiveApiService extends ApiService {
return this.http.rpcRequest({ method: 'server.rebuild', params })
}
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
return this.http.rpcRequest({ method: 'disk.repair', params })
}
// marketplace URLs
async marketplaceProxy<T>(path: string, params: {}, url: string): Promise<T> {

View File

@@ -193,6 +193,11 @@ export class MockApiService extends ApiService {
return null
}
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
await pauseFor(2000)
return null
}
// marketplace URLs
async marketplaceProxy(path: string, params: {}, url: string): Promise<any> {

View File

@@ -27,7 +27,7 @@ export const mockPatchData: DataModel = {
'unread-notification-count': 4,
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'eos-version-compat': '>=0.3.0',
'eos-version-compat': '>=0.3.0 <=0.3.0.1',
'status-info': null,
},
'recovered-packages': {

View File

@@ -2,17 +2,21 @@ import { Injectable } from '@angular/core'
import { Storage } from '@ionic/storage-angular'
import { BehaviorSubject } from 'rxjs'
const SHOW_DEV_TOOLS = 'SHOW_DEV_TOOLS'
const SHOW_DISK_REPAIR = 'SHOW_DISK_REPAIR'
@Injectable({
providedIn: 'root',
})
export class LocalStorageService {
showDevTools$: BehaviorSubject<boolean> = new BehaviorSubject(false)
showDiskRepair$: BehaviorSubject<boolean> = new BehaviorSubject(false)
constructor(private readonly storage: Storage) {}
async init() {
const val = await this.storage.get(SHOW_DEV_TOOLS)
this.showDevTools$.next(!!val)
const devTools = await this.storage.get(SHOW_DEV_TOOLS)
this.showDevTools$.next(!!devTools)
const diskRepair = await this.storage.get(SHOW_DISK_REPAIR)
this.showDiskRepair$.next(!!diskRepair)
}
async toggleShowDevTools(): Promise<boolean> {
@@ -21,4 +25,11 @@ export class LocalStorageService {
this.showDevTools$.next(newVal)
return newVal
}
async toggleShowDiskRepair(): Promise<boolean> {
const newVal = !(await this.storage.get(SHOW_DISK_REPAIR))
await this.storage.set(SHOW_DISK_REPAIR, newVal)
this.showDiskRepair$.next(newVal)
return newVal
}
}