diff --git a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html b/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html index 037215e54..ae93a6bb9 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html +++ b/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html @@ -49,6 +49,12 @@ }} +
+ {{ 'Repair Drive' }} +
diff --git a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.ts b/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.ts index 84aa11027..a4b319318 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.ts +++ b/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.ts @@ -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 { + 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( + `Warning: 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() } diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts b/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts index 3fb0595f8..c11881e5e 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts @@ -1,22 +1,31 @@ export abstract class ApiService { - abstract getError (): Promise - abstract restart (): Promise - abstract forgetDrive (): Promise - abstract getLogs (params: GetLogsReq): Promise + abstract getError(): Promise + abstract restart(): Promise + abstract forgetDrive(): Promise + abstract repairDisk(): Promise + abstract getLogs(params: GetLogsReq): Promise } 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 -} \ No newline at end of file +} diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts b/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts index 948e19730..e4e613ffa 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts @@ -24,7 +24,14 @@ export class LiveApiService extends ApiService { forgetDrive(): Promise { return this.http.rpcRequest({ - method: 'diagnostic.forget-disk', + method: 'diagnostic.disk.forget', + params: {}, + }) + } + + repairDisk(): Promise { + return this.http.rpcRequest({ + method: 'diagnostic.disk.repair', params: {}, }) } diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts b/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts index 959a2493d..c1a772450 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts @@ -33,6 +33,11 @@ export class MockApiService extends ApiService { return null } + async repairDisk(): Promise { + await pauseFor(1000) + return null + } + async getLogs(params: GetLogsReq): Promise { await pauseFor(1000) let entries: Log[] diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html index e3796e034..ba9552876 100644 --- a/frontend/projects/ui/src/app/app.component.html +++ b/frontend/projects/ui/src/app/app.component.html @@ -162,6 +162,7 @@ + diff --git a/frontend/projects/ui/src/app/app.component.ts b/frontend/projects/ui/src/app/app.component.ts index e5350cf48..e2d45f1ba 100644 --- a/frontend/projects/ui/src/app/app.component.ts +++ b/frontend/projects/ui/src/app/app.component.ts @@ -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.', diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts b/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts index c50dbb5bf..05041af99 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -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 } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index 8c0a87054..db5f35281 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -24,73 +24,83 @@
- {{ cat.key }} - - - -

{{ button.title }}

-

{{ button.description }}

+ + {{ cat.key }} + {{ cat.key }} + + + + + +

{{ button.title }}

+

{{ button.description }}

- -

- - - Last Backup: {{ patch.data['server-info']['last-backup'] ? - (patch.data['server-info']['last-backup'] | date: 'short') : - 'never' }} - - - - Backing up - - -

- -

- - - Update Complete, Restart to apply changes - - - + +

- - - Update Available + + Last Backup: {{ patch.data['server-info']['last-backup'] ? + (patch.data['server-info']['last-backup'] | date: 'short') : + 'never' }} + + + + Backing up + + +

+ +

+ + + Update Complete, Restart to apply changes - - - - Check for updates - + + + + + Update Available + + + + + + Check for updates + + - -

-
-
+

+
+
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 8e074e28c..3508d1abc 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -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( + `Warning:

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.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 { diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index e24af70dc..91210738e 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -93,6 +93,8 @@ export abstract class ApiService params: RR.SystemRebuildReq, ): Promise + abstract repairDisk(params: RR.SystemRebuildReq): Promise + // marketplace URLs abstract marketplaceProxy( diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 2c5145314..7058fa105 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -99,6 +99,10 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'server.rebuild', params }) } + async repairDisk(params: RR.RestartServerReq): Promise { + return this.http.rpcRequest({ method: 'disk.repair', params }) + } + // marketplace URLs async marketplaceProxy(path: string, params: {}, url: string): Promise { diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 01b59d921..05d49685e 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -193,6 +193,11 @@ export class MockApiService extends ApiService { return null } + async repairDisk(params: RR.RestartServerReq): Promise { + await pauseFor(2000) + return null + } + // marketplace URLs async marketplaceProxy(path: string, params: {}, url: string): Promise { diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 94a504bce..2fbff01df 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -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': { diff --git a/frontend/projects/ui/src/app/services/local-storage.service.ts b/frontend/projects/ui/src/app/services/local-storage.service.ts index 1e3b70aad..0b728ead4 100644 --- a/frontend/projects/ui/src/app/services/local-storage.service.ts +++ b/frontend/projects/ui/src/app/services/local-storage.service.ts @@ -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 = new BehaviorSubject(false) + showDiskRepair$: BehaviorSubject = 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 { @@ -21,4 +25,11 @@ export class LocalStorageService { this.showDevTools$.next(newVal) return newVal } + + async toggleShowDiskRepair(): Promise { + const newVal = !(await this.storage.get(SHOW_DISK_REPAIR)) + await this.storage.set(SHOW_DISK_REPAIR, newVal) + this.showDiskRepair$.next(newVal) + return newVal + } }