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> </ion-button>
</div> </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-container>
<ng-template #refresh> <ng-template #refresh>

View File

@@ -1,5 +1,9 @@
import { Component } from '@angular/core' 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' import { ApiService } from 'src/app/services/api/api.service'
@Component({ @Component({
@@ -20,6 +24,7 @@ export class HomePage {
constructor( constructor(
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly api: ApiService, private readonly api: ApiService,
private readonly alertCtrl: AlertController,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -48,16 +53,33 @@ export class HomePage {
this.error = { this.error = {
code: 25, code: 25,
problem: 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: 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.', '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, 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 { } else {
this.error = { this.error = {
code: error.code, code: error.code,
problem: error.message, problem: error.message,
solution: 'Please conact support.', solution: 'Please contact support.',
details: error.data?.details, 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 { refreshPage(): void {
window.location.reload() window.location.reload()
} }

View File

@@ -1,22 +1,31 @@
export abstract class ApiService { export abstract class ApiService {
abstract getError (): Promise<GetErrorRes> abstract getError(): Promise<GetErrorRes>
abstract restart (): Promise<void> abstract restart(): Promise<void>
abstract forgetDrive (): Promise<void> abstract forgetDrive(): Promise<void>
abstract getLogs (params: GetLogsReq): Promise<GetLogsRes> abstract repairDisk(): Promise<void>
abstract getLogs(params: GetLogsReq): Promise<GetLogsRes>
} }
export interface GetErrorRes { export interface GetErrorRes {
code: number, code: number
message: string, message: string
data: { details: 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 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 { export interface Log {
timestamp: string timestamp: string
message: string message: string
} }

View File

@@ -24,7 +24,14 @@ export class LiveApiService extends ApiService {
forgetDrive(): Promise<void> { forgetDrive(): Promise<void> {
return this.http.rpcRequest<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: {}, params: {},
}) })
} }

View File

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

View File

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

View File

@@ -396,7 +396,7 @@ export class AppComponent {
private async presentAlertRefreshNeeded() { private async presentAlertRefreshNeeded() {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
backdropDismiss: false, backdropDismiss: true,
header: 'Refresh Needed', header: 'Refresh Needed',
message: message:
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.', '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 { hasValidBackup(target: BackupTarget): boolean {
return [0, 1].includes( return this.emver.compare(target['embassy-os']?.version, '0.3.0') !== -1
this.emver.compare(target['embassy-os']?.version, '0.3.0'),
)
} }
} }

View File

@@ -24,73 +24,83 @@
<ng-template #data> <ng-template #data>
<ion-item-group> <ion-item-group>
<div *ngFor="let cat of settings | keyvalue : asIsOrder"> <div *ngFor="let cat of settings | keyvalue : asIsOrder">
<ion-item-divider <ion-item-divider>
><ion-text color="dark">{{ cat.key }}</ion-text></ion-item-divider <ion-text color="dark" *ngIf="cat.key !== 'Power'"
> >{{ cat.key }}</ion-text
<ion-item >
button <ion-text
*ngFor="let button of cat.value" color="dark"
[detail]="button.detail" *ngIf="cat.key === 'Power'"
[disabled]="button.disabled | async" (click)="addClick()"
(click)="button.action()" >{{ cat.key }}</ion-text
> >
<ion-icon slot="start" [name]="button.icon"></ion-icon> </ion-item-divider>
<ion-label> <ng-container *ngFor="let button of cat.value">
<h2>{{ button.title }}</h2> <ion-item
<p *ngIf="button.description">{{ button.description }}</p> 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 --> <!-- "Create Backup" button only -->
<p *ngIf="button.title === 'Create Backup'"> <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>
<ng-container <ng-container
*ngIf="eosService.updateAvailable$ | async; else check" *ngIf="patch.data['server-info']['status-info'] as statusInfo"
> >
<ion-text class="inline" color="success"> <ion-text
<ion-icon name="rocket-outline"></ion-icon> color="warning"
Update Available *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> </ion-text>
</ng-container> </ng-container>
<ng-template #check> <ng-template #enabled>
<ion-text class="inline" color="dark"> <ng-container
<ion-icon name="refresh"></ion-icon> *ngIf="eosService.updateAvailable$ | async; else check"
Check for updates >
</ion-text> <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>
</ng-template> </p>
</p> </ion-label>
</ion-label> </ion-item>
</ion-item> </ng-container>
</div> </div>
</ion-item-group> </ion-item-group>
</ng-template> </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 { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared'
import { EOSService } from 'src/app/services/eos.service' import { EOSService } from 'src/app/services/eos.service'
import { ServerStatus } from 'src/app/services/patch-db/data-model' import { ServerStatus } from 'src/app/services/patch-db/data-model'
import { LocalStorageService } from 'src/app/services/local-storage.service'
@Component({ @Component({
selector: 'server-show', selector: 'server-show',
@@ -25,6 +26,7 @@ import { ServerStatus } from 'src/app/services/patch-db/data-model'
export class ServerShowPage { export class ServerShowPage {
ServerStatus = ServerStatus ServerStatus = ServerStatus
hasRecoveredPackage: boolean hasRecoveredPackage: boolean
clicks = 0
constructor( constructor(
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
@@ -37,6 +39,7 @@ export class ServerShowPage {
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
public readonly eosService: EOSService, public readonly eosService: EOSService,
public readonly patch: PatchDbService, public readonly patch: PatchDbService,
public readonly localStorageService: LocalStorageService,
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -143,6 +146,35 @@ export class ServerShowPage {
await alert.present() 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() { private async restart() {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
@@ -305,7 +337,7 @@ export class ServerShowPage {
{ {
title: 'Marketplace Settings', title: 'Marketplace Settings',
description: 'Add or remove marketplaces', description: 'Add or remove marketplaces',
icon: 'storefront', icon: 'storefront-outline',
action: () => action: () =>
this.navCtrl.navigateForward(['marketplaces'], { this.navCtrl.navigateForward(['marketplaces'], {
relativeTo: this.route, relativeTo: this.route,
@@ -418,12 +450,31 @@ export class ServerShowPage {
detail: false, detail: false,
disabled: of(false), disabled: of(false),
}, },
{
title: 'Repair Disk',
description: '',
icon: 'medkit-outline',
action: () => this.presentAlertRepairDisk(),
detail: false,
disabled: of(false),
},
], ],
} }
asIsOrder() { asIsOrder() {
return 0 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 { interface ServerSettings {

View File

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

View File

@@ -99,6 +99,10 @@ export class LiveApiService extends ApiService {
return this.http.rpcRequest({ method: 'server.rebuild', params }) 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 // marketplace URLs
async marketplaceProxy<T>(path: string, params: {}, url: string): Promise<T> { async marketplaceProxy<T>(path: string, params: {}, url: string): Promise<T> {

View File

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

View File

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

View File

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