diff --git a/diagnosticUI/.gitignore b/diagnosticUI/.gitignore index 416fc4c1f..cad308928 100644 --- a/diagnosticUI/.gitignore +++ b/diagnosticUI/.gitignore @@ -29,3 +29,5 @@ npm-debug.log* /platforms /plugins /www + +config.json diff --git a/diagnosticUI/config-sample.json b/diagnosticUI/config-sample.json new file mode 100644 index 000000000..fdf4ba828 --- /dev/null +++ b/diagnosticUI/config-sample.json @@ -0,0 +1,3 @@ +{ + "useMocks": true +} \ No newline at end of file diff --git a/diagnosticUI/src/app/app-routing.module.ts b/diagnosticUI/src/app/app-routing.module.ts index 01ebf61c3..4c88ac1dc 100644 --- a/diagnosticUI/src/app/app-routing.module.ts +++ b/diagnosticUI/src/app/app-routing.module.ts @@ -4,13 +4,17 @@ import { PreloadAllModules, RouterModule, Routes } from '@angular/router' const routes: Routes = [ { path: '', - loadChildren: () => import('./home/home.module').then( m => m.HomePageModule) + loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule) }, ] @NgModule({ imports: [ - RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) + RouterModule.forRoot(routes, { + scrollPositionRestoration: 'enabled', + preloadingStrategy: PreloadAllModules, + useHash: true, + }) ], exports: [RouterModule] }) diff --git a/diagnosticUI/src/app/app.module.ts b/diagnosticUI/src/app/app.module.ts index 6ed93cff9..f4b18151f 100644 --- a/diagnosticUI/src/app/app.module.ts +++ b/diagnosticUI/src/app/app.module.ts @@ -4,12 +4,37 @@ import { RouteReuseStrategy } from '@angular/router' import { IonicModule, IonicRouteStrategy } from '@ionic/angular' import { AppComponent } from './app.component' import { AppRoutingModule } from './app-routing.module' +import { HttpClientModule } from '@angular/common/http' +import { ApiService } from './services/api/api.service' +import { MockApiService } from './services/api/mock-api.service' +import { LiveApiService } from './services/api/live-api.service' +import { HttpService } from './services/http.service' + +const useMocks = require('../../config.json').useMocks @NgModule({ declarations: [AppComponent], entryComponents: [], - imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], - providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], + imports: [ + HttpClientModule, + BrowserModule, + IonicModule.forRoot(), + AppRoutingModule, + ], + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + { + provide: ApiService, + useFactory: (http: HttpService) => { + if (useMocks) { + return new MockApiService() + } else { + return new LiveApiService(http) + } + }, + deps: [HttpService] + }, + ], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/diagnosticUI/src/app/home/home.page.html b/diagnosticUI/src/app/home/home.page.html deleted file mode 100644 index d2ef01fff..000000000 --- a/diagnosticUI/src/app/home/home.page.html +++ /dev/null @@ -1,11 +0,0 @@ - -
-

EmbassyOS - Diagnostic Mode

-

EmbassyOS failed to launch due to the following error:

- jwenfjnwc wekjdnwm ckjwenm kjhe xkjew nj wehjd -

Possible Solutions

- -
-
diff --git a/diagnosticUI/src/app/home/home.page.scss b/diagnosticUI/src/app/home/home.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/diagnosticUI/src/app/home/home.page.ts b/diagnosticUI/src/app/home/home.page.ts deleted file mode 100644 index 76509c019..000000000 --- a/diagnosticUI/src/app/home/home.page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - selector: 'app-home', - templateUrl: 'home.page.html', - styleUrls: ['home.page.scss'], -}) -export class HomePage { - - constructor() {} - -} diff --git a/diagnosticUI/src/app/home/home-routing.module.ts b/diagnosticUI/src/app/pages/home/home-routing.module.ts similarity index 100% rename from diagnosticUI/src/app/home/home-routing.module.ts rename to diagnosticUI/src/app/pages/home/home-routing.module.ts diff --git a/diagnosticUI/src/app/home/home.module.ts b/diagnosticUI/src/app/pages/home/home.module.ts similarity index 100% rename from diagnosticUI/src/app/home/home.module.ts rename to diagnosticUI/src/app/pages/home/home.module.ts diff --git a/diagnosticUI/src/app/pages/home/home.page.html b/diagnosticUI/src/app/pages/home/home.page.html new file mode 100644 index 000000000..7e6bad01a --- /dev/null +++ b/diagnosticUI/src/app/pages/home/home.page.html @@ -0,0 +1,31 @@ + +
+ +

EmbassyOS - Diagnostic Mode

+

EmbassyOS launch error:

+
+ {{ error.problem }} +
+

Possible solution

+
+ {{ error.solution }} +
+ + Restart Embassy + +
+ + {{ error.code === 15 ? 'Use Current Drive' : 'Enter Recovery Mode' }} + +
+
+ + +

Embassy is restarting

+

Wait for Embassy restart, then refresh this page or click REFRESH below.

+ + Refresh + +
+
+
diff --git a/diagnosticUI/src/app/pages/home/home.page.scss b/diagnosticUI/src/app/pages/home/home.page.scss new file mode 100644 index 000000000..214e26874 --- /dev/null +++ b/diagnosticUI/src/app/pages/home/home.page.scss @@ -0,0 +1,5 @@ +.code-block { + background-color: rgb(69, 69, 69); + padding: 12px; + margin-bottom: 32px; +} \ No newline at end of file diff --git a/diagnosticUI/src/app/pages/home/home.page.ts b/diagnosticUI/src/app/pages/home/home.page.ts new file mode 100644 index 000000000..9b2fe2788 --- /dev/null +++ b/diagnosticUI/src/app/pages/home/home.page.ts @@ -0,0 +1,92 @@ +import { Component } from '@angular/core' +import { LoadingController } from '@ionic/angular' +import { ApiService } from 'src/app/services/api/api.service' + +@Component({ + selector: 'app-home', + templateUrl: 'home.page.html', + styleUrls: ['home.page.scss'], +}) +export class HomePage { + error: { + code: number + problem: string + solution: string + } = { } as any + solutions: string[] = [] + restarted = false + forgotten = false + + constructor ( + private readonly loadingCtrl: LoadingController, + private readonly api: ApiService, + ) { } + + async ngOnInit () { + try { + const error = await this.api.getError() + // incorrect drive + if (error.code === 15) { + this.error = { + code: 15, + problem: 'Unknown storage drive detected', + solution: 'To use a different storage drive, replace the current one and click RESTART EMBASSY below. To use the current storage drive, click USE CURRENT DRIVE below, then follow instructions. No data will be erased during this process.' + } + // no drive + } else if (error.code === 20) { + this.error = { + code: 20, + problem: 'Storage drive not found', + solution: 'Insert your EmbassyOS storage drive and click RESTART EMBASSY below.' + } + // drive corrupted + } else if (error.code === 25) { + this.error = { + code: 25, + problem: 'Storage drive corrupted. This could be the result of data corruption or a 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.' + } + } + } catch (e) { + console.error(e) + } + } + + async restart (): Promise { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + cssClass: 'loader', + }) + await loader.present() + + try { + await this.api.restart() + this.restarted = true + } catch (e) { + console.error(e) + } finally { + loader.dismiss() + } + } + + async forgetDrive (): Promise { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + cssClass: 'loader', + }) + await loader.present() + + try { + await this.api.forgetDrive() + this.forgotten = true + } catch (e) { + console.error(e) + } finally { + loader.dismiss() + } + } + + refreshPage (): void { + window.location.reload() + } +} diff --git a/diagnosticUI/src/app/services/api/api.service.ts b/diagnosticUI/src/app/services/api/api.service.ts new file mode 100644 index 000000000..3fb0595f8 --- /dev/null +++ b/diagnosticUI/src/app/services/api/api.service.ts @@ -0,0 +1,22 @@ +export abstract class ApiService { + abstract getError (): Promise + abstract restart (): Promise + abstract forgetDrive (): Promise + abstract getLogs (params: GetLogsReq): Promise +} + +export interface GetErrorRes { + code: number, + message: string, + data: { details: string } +} + +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 interface Log { + timestamp: string + message: string +} \ No newline at end of file diff --git a/diagnosticUI/src/app/services/api/live-api.service.ts b/diagnosticUI/src/app/services/api/live-api.service.ts new file mode 100644 index 000000000..38948992a --- /dev/null +++ b/diagnosticUI/src/app/services/api/live-api.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from "@angular/core" +import { HttpService } from "../http.service" +import { ApiService, GetErrorRes, GetLogsReq, GetLogsRes } from "./api.service" + +@Injectable() +export class LiveApiService extends ApiService { + + constructor ( + private readonly http: HttpService, + ) { super() } + + getError (): Promise { + return this.http.rpcRequest({ + method: 'diagnostic.error', + params: { }, + }) + } + + restart (): Promise { + return this.http.rpcRequest({ + method: 'diagnostic.restart', + params: { }, + }) + } + + forgetDrive (): Promise { + return this.http.rpcRequest({ + method: 'diagnostic.forget-disk', + params: { }, + }) + } + + getLogs (params: GetLogsReq): Promise { + return this.http.rpcRequest({ + method: 'diagnostic.logs', + params, + }) + } +} \ No newline at end of file diff --git a/diagnosticUI/src/app/services/api/mock-api.service.ts b/diagnosticUI/src/app/services/api/mock-api.service.ts new file mode 100644 index 000000000..f7b3efd6d --- /dev/null +++ b/diagnosticUI/src/app/services/api/mock-api.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from "@angular/core" +import { pauseFor } from "../../util/misc.util" +import { ApiService, GetErrorRes, GetLogsReq, GetLogsRes, Log } from "./api.service" + +@Injectable() +export class MockApiService extends ApiService { + + constructor () { super() } + + async getError (): Promise { + await pauseFor(1000) + return { + code: 15, + message: 'Unknown Embassy', + data: { details: 'Some details about the error here' } + } + } + + async restart (): Promise { + await pauseFor(1000) + return null + } + + async forgetDrive (): Promise { + await pauseFor(1000) + return null + } + + async getLogs (params: GetLogsReq): Promise { + await pauseFor(1000) + let entries: Log[] + if (Math.random() < .2) { + entries = packageLogs + } else { + const arrLength = params.limit ? Math.ceil(params.limit / packageLogs.length) : 10 + entries = new Array(arrLength).fill(packageLogs).reduce((acc, val) => acc.concat(val), []) + } + return { + entries, + 'start-cursor': 'startCursor', + 'end-cursor': 'endCursor', + } + } +} + +const packageLogs = [ + { + timestamp: '2019-12-26T14:20:30.872Z', + message: '****** START *****', + }, + { + timestamp: '2019-12-26T14:21:30.872Z', + message: 'ServerLogs ServerLogs ServerLogs ServerLogs ServerLogs', + }, + { + timestamp: '2019-12-26T14:22:30.872Z', + message: '****** FINISH *****', + }, +] \ No newline at end of file diff --git a/diagnosticUI/src/app/services/http.service.ts b/diagnosticUI/src/app/services/http.service.ts new file mode 100644 index 000000000..d95937399 --- /dev/null +++ b/diagnosticUI/src/app/services/http.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core' +import { HttpClient, HttpErrorResponse } from '@angular/common/http' + +@Injectable({ + providedIn: 'root', +}) +export class HttpService { + + constructor ( + private readonly http: HttpClient, + ) { } + + async rpcRequest (options: RPCOptions): Promise { + const res = await this.httpRequest>(options) + + if (isRpcError(res)) throw new RpcError(res.error) + + if (isRpcSuccess(res)) return res.result + } + + async httpRequest (body: RPCOptions): Promise { + const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/rpc/v1` + return this.http.post(url, body) + .toPromise() + .then((res: any) => res.body) + .catch(e => { throw new HttpError(e) }) + } +} + +function RpcError (e: RPCError['error']): void { + const { code, message, data } = e + + this.code = code + this.message = message + + if (typeof data === 'string') { + this.details = e.data + this.revision = null + } else { + this.details = data.details + } +} + +function HttpError (e: HttpErrorResponse): void { + const { status, statusText } = e + + this.code = status + this.message = statusText + this.details = null + this.revision = null +} + +function isRpcError (arg: { error: Error } | { result: Result}): arg is { error: Error } { + return !!(arg as any).error +} + +function isRpcSuccess (arg: { error: Error } | { result: Result}): arg is { result: Result } { + return !!(arg as any).result +} + +export interface RPCOptions { + method: string + params: { [param: string]: Params } +} + +export interface RequestError { + code: number + message: string + details: string +} + +export type Params = string | number | boolean | object | string[] | number[] + +interface RPCBase { + jsonrpc: '2.0' + id: string +} + +export interface RPCRequest extends RPCBase { + method: string + params?: T +} + +export interface RPCSuccess extends RPCBase { + result: T +} + +export interface RPCError extends RPCBase { + error: { + code: number, + message: string + data?: { + details: string + } | string + } +} + +export type RPCResponse = RPCSuccess | RPCError + +type HttpError = HttpErrorResponse & { error: { code: string, message: string } } diff --git a/diagnosticUI/src/app/util/misc.util.ts b/diagnosticUI/src/app/util/misc.util.ts new file mode 100644 index 000000000..f898a9339 --- /dev/null +++ b/diagnosticUI/src/app/util/misc.util.ts @@ -0,0 +1,3 @@ +export function pauseFor (ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} \ No newline at end of file diff --git a/diagnosticUI/src/global.scss b/diagnosticUI/src/global.scss index d854de84a..0d9b8e8b4 100644 --- a/diagnosticUI/src/global.scss +++ b/diagnosticUI/src/global.scss @@ -24,3 +24,8 @@ @import "~@ionic/angular/css/text-alignment.css"; @import "~@ionic/angular/css/text-transformation.css"; @import "~@ionic/angular/css/flex-utils.css"; + +.loader { + --spinner-color: var(--ion-color-warning) !important; + z-index: 40000 !important; +} \ No newline at end of file