This commit is contained in:
Matt Hill
2021-09-15 16:06:33 -06:00
committed by Aiden McClelland
parent 29b4c27812
commit 2d301e84ea
18 changed files with 394 additions and 27 deletions

View File

@@ -29,3 +29,5 @@ npm-debug.log*
/platforms
/plugins
/www
config.json

View File

@@ -0,0 +1,3 @@
{
"useMocks": true
}

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
<ion-content>
<div style="padding: 48px">
<h1 class="ion-text-center" style="padding-bottom: 36px;">EmbassyOS - Diagnostic Mode</h1>
<h2 style="padding-bottom: 24px;">EmbassyOS failed to launch due to the following error:</h2>
<code><ion-text color="warning">jwenfjnwc wekjdnwm ckjwenm kjhe xkjew nj wehjd </ion-text></code>
<h2>Possible Solutions</h2>
<ul>
<li></li>
</ul>
</div>
</ion-content>

View File

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

View File

@@ -0,0 +1,31 @@
<ion-content>
<div style="padding: 48px">
<ng-container *ngIf="!restarted && !forgotten; else refresh">
<h1 class="ion-text-center" style="padding-bottom: 36px; font-size: calc(2vw + 12px);">EmbassyOS - Diagnostic Mode</h1>
<h2 style="padding-bottom: 16px; font-size: calc(1vw + 12px);">EmbassyOS launch error:</h2>
<div class="code-block">
<code><ion-text color="warning">{{ error.problem }}</ion-text></code>
</div>
<h2 style="padding-bottom: 16px; font-size: calc(1vw + 12px);">Possible solution</h2>
<div class="code-block">
<code><ion-text color="success">{{ error.solution }}</ion-text></code>
</div>
<ion-button (click)="restart()">
Restart Embassy
</ion-button>
<div *ngIf="error.code === 15 || error.code === 25" class="ion-padding-top">
<ion-button *ngIf="error.code === 15" (click)="forgetDrive()">
{{ error.code === 15 ? 'Use Current Drive' : 'Enter Recovery Mode' }}
</ion-button>
</div>
</ng-container>
<ng-template #refresh>
<h1 class="ion-text-center" style="padding-bottom: 36px; font-size: calc(2vw + 12px);">Embassy is restarting</h1>
<h2 style="padding-bottom: 16px; font-size: calc(1vw + 12px);">Wait for Embassy restart, then refresh this page or click REFRESH below.</h2>
<ion-button (click)="refreshPage()">
Refresh
</ion-button>
</ng-template>
</div>
</ion-content>

View File

@@ -0,0 +1,5 @@
.code-block {
background-color: rgb(69, 69, 69);
padding: 12px;
margin-bottom: 32px;
}

View File

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

View File

@@ -0,0 +1,22 @@
export abstract class ApiService {
abstract getError (): Promise<GetErrorRes>
abstract restart (): Promise<void>
abstract forgetDrive (): Promise<void>
abstract getLogs (params: GetLogsReq): Promise<GetLogsRes>
}
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
}

View File

@@ -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<GetErrorRes> {
return this.http.rpcRequest<GetErrorRes>({
method: 'diagnostic.error',
params: { },
})
}
restart (): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.restart',
params: { },
})
}
forgetDrive (): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.forget-disk',
params: { },
})
}
getLogs (params: GetLogsReq): Promise<GetLogsRes> {
return this.http.rpcRequest<GetLogsRes>({
method: 'diagnostic.logs',
params,
})
}
}

View File

@@ -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<GetErrorRes> {
await pauseFor(1000)
return {
code: 15,
message: 'Unknown Embassy',
data: { details: 'Some details about the error here' }
}
}
async restart (): Promise<void> {
await pauseFor(1000)
return null
}
async forgetDrive (): Promise<void> {
await pauseFor(1000)
return null
}
async getLogs (params: GetLogsReq): Promise<GetLogsRes> {
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 *****',
},
]

View File

@@ -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<T> (options: RPCOptions): Promise<T> {
const res = await this.httpRequest<RPCResponse<T>>(options)
if (isRpcError(res)) throw new RpcError(res.error)
if (isRpcSuccess(res)) return res.result
}
async httpRequest<T> (body: RPCOptions): Promise<T> {
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<Error, Result> (arg: { error: Error } | { result: Result}): arg is { error: Error } {
return !!(arg as any).error
}
function isRpcSuccess<Error, Result> (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<T> extends RPCBase {
method: string
params?: T
}
export interface RPCSuccess<T> extends RPCBase {
result: T
}
export interface RPCError extends RPCBase {
error: {
code: number,
message: string
data?: {
details: string
} | string
}
}
export type RPCResponse<T> = RPCSuccess<T> | RPCError
type HttpError = HttpErrorResponse & { error: { code: string, message: string } }

View File

@@ -0,0 +1,3 @@
export function pauseFor (ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}

View File

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